Skip to content

Commit 38053be

Browse files
committed
unwrap render wrapper div element and appendTo app root element
1 parent 9392515 commit 38053be

4 files changed

Lines changed: 90 additions & 9 deletions

File tree

examples/app-vitest-browser/test/nuxt/components/render.spec.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { describe, it, expect } from 'vitest'
1+
import { describe, it, expect, vi } from 'vitest'
22
import { render } from '@nuxt/test-utils/vitest-browser-nuxt'
33
import { MyCounter } from '#components'
44

5-
describe('Render Component (MyCounter)', () => {
5+
describe('Render Component', () => {
66
it('renders', async () => {
77
const { getByText } = await render(MyCounter)
88
expect(getByText('Count: 0')).toBeInTheDocument()
@@ -28,4 +28,46 @@ describe('Render Component (MyCounter)', () => {
2828
expect(config).toBeInTheDocument()
2929
expect(config).toHaveTextContent(/"buildAssetsDir"\s*:\s*"\/_nuxt\/"/)
3030
})
31+
32+
it('locator', async () => {
33+
const screen = await render(defineComponent({
34+
render: () => h('h1', {}, 'Hello Nuxt!'),
35+
}))
36+
37+
expect(screen.locator.getByRole('heading')).toHaveTextContent('Hello Nuxt!')
38+
})
39+
40+
it('baseElement', async () => {
41+
const screen = await render(defineComponent({
42+
render: () => h('h1', {}, 'Hello Nuxt!'),
43+
}))
44+
45+
await expect.element(screen.baseElement).toHaveAttribute('id', 'nuxt-test')
46+
expect(screen.baseElement.children[0]?.outerHTML).toBe('<h1>Hello Nuxt!</h1>')
47+
})
48+
49+
it('container', async () => {
50+
const screen = await render(defineComponent({
51+
render: () => h('h1', {}, 'Hello Nuxt!'),
52+
}))
53+
54+
await expect.element(screen.container).toHaveAttribute('id', 'nuxt-test')
55+
expect(screen.container.children[0]?.outerHTML).toBe('<h1>Hello Nuxt!</h1>')
56+
})
57+
58+
it('umount', async () => {
59+
const onBeforeUnmountFn = vi.fn()
60+
const screen = await render(defineComponent({
61+
setup() {
62+
onBeforeUnmount(onBeforeUnmountFn)
63+
},
64+
render: () => h('h1', {}, 'Hello Nuxt!'),
65+
}))
66+
67+
await screen.unmount()
68+
69+
await expect.element(screen.container).toHaveAttribute('id', 'nuxt-test')
70+
expect(screen.container.children.length).toBe(0)
71+
expect(onBeforeUnmountFn).toHaveBeenCalledOnce()
72+
})
3173
})

examples/app-vitest-browser/test/nuxt/components/render.vue.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'
22
import { render } from 'vitest-browser-vue'
33
import { MyCounter } from '#components'
44

5-
describe('Render(vue) Component (MyCounter)', () => {
5+
describe('Render(vue) Component', () => {
66
it('renders', () => {
77
const { getByText } = render(MyCounter)
88
expect(getByText('Count: 0')).toBeInTheDocument()
@@ -28,4 +28,12 @@ describe('Render(vue) Component (MyCounter)', () => {
2828
expect(config).toBeInTheDocument()
2929
expect(config).toHaveTextContent(/"buildAssetsDir"\s*:\s*"\/_nuxt\/"/)
3030
})
31+
32+
it('locator', async () => {
33+
const screen = await render(defineComponent({
34+
render: () => h('h1', {}, 'Hello Nuxt!'),
35+
}))
36+
37+
expect(screen.locator.getByRole('heading')).toHaveTextContent('Hello Nuxt!')
38+
})
3139
})

src/runtime-utils/utils/suspended.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type WrapperFnComponent<Fn> = Fn extends (c: infer C, o: infer _) => infer _ ? C
1919
type WrapperFnOption<Fn> = Fn extends (c: WrapperFnComponent<Fn>, o: infer O) => infer _ ? O : never
2020
type WrapperFnResult<Fn> = Fn extends (c: WrapperFnComponent<Fn>, o: WrapperFnOption<Fn>) => infer R ? R : never
2121

22+
type VueApp = App<Element> & Record<string, unknown>
23+
2224
export type WrapperSuspendedOptions<Fn> = WrapperFnOption<Fn> & {
2325
route?: RouteLocationRaw | false
2426
scoped?: boolean
@@ -45,18 +47,24 @@ function runEffectScope<T>(fn: () => T) {
4547
return scope.run(fn)
4648
}
4749

48-
export function wrapperSuspended<C, Fn extends WrapperFn<C>>(
50+
export function wrapperSuspended<
51+
C,
52+
Fn extends WrapperFn<C>,
53+
Opts extends WrapperSuspendedOptions<Fn>,
54+
>(
4955
component: C,
50-
options: WrapperSuspendedOptions<Fn>,
56+
options: Opts,
5157
{
5258
wrapperFn,
5359
wrappedRender = fn => fn,
60+
overrideOptionsFn = () => {},
5461
suspendedHelperName,
5562
clonedComponentName,
5663
stubRouterLink = true,
5764
}: {
5865
wrapperFn: NonNullable<Fn>
5966
wrappedRender?: (render: () => VNode) => () => VNode
67+
overrideOptionsFn?: (options: Opts, vueApp: VueApp) => void
6068
suspendedHelperName: string
6169
clonedComponentName: string
6270
stubRouterLink?: boolean
@@ -65,12 +73,15 @@ export function wrapperSuspended<C, Fn extends WrapperFn<C>>(
6573
wrapper: WrapperSuspendedResult<Fn>
6674
setProps: (props: object) => void
6775
}> {
68-
const { props = {}, attrs = {} } = options as ComponentMountingOptions<C>
69-
const { route = '/', scoped = false, ...wrapperFnOptions } = options as ComponentMountingOptions<C>
70-
7176
const vueApp: App<Element> & Record<string, unknown> = tryUseNuxtApp()?.vueApp
7277
// @ts-expect-error untyped global __unctx__
7378
|| globalThis.__unctx__.get('nuxt-app').tryUse().vueApp
79+
80+
overrideOptionsFn(options, vueApp)
81+
82+
const { props = {}, attrs = {} } = options as ComponentMountingOptions<C>
83+
const { route = '/', scoped = false, ...wrapperFnOptions } = options as ComponentMountingOptions<C>
84+
7485
const {
7586
render: componentRender,
7687
setup: componentSetup,

src/vitest-browser-nuxt/pure.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,22 @@ export async function render<T>(
4747

4848
cleanupAll()
4949

50+
const umountedFns: (() => void)[] = []
51+
const wrapperId = window.crypto.randomUUID()
52+
5053
const { wrapper: _wrapper, setProps } = await wrapperSuspended(component, options, {
5154
wrapperFn,
52-
wrappedRender: render => () => h('div', {}, render()),
55+
wrappedRender: render => () => h({
56+
inheritAttrs: false,
57+
unmounted: () => umountedFns.forEach(fn => fn()),
58+
render: () => h('div', { id: wrapperId }, render()),
59+
}),
60+
overrideOptionsFn(options, vueApp) {
61+
const rootId = vueApp._container?.id
62+
if (rootId) {
63+
options.container ??= document.getElementById(rootId) ?? undefined
64+
}
65+
},
5366
suspendedHelperName,
5467
clonedComponentName,
5568
stubRouterLink: false,
@@ -62,5 +75,12 @@ export async function render<T>(
6275
await nextTick()
6376
}
6477

78+
// Unwrap render wrapper div element
79+
const wrapperEl = document.getElementById(wrapperId)!
80+
const unwrappedNodes = [...wrapperEl.childNodes]
81+
wrapperEl.replaceWith(...unwrappedNodes)
82+
83+
umountedFns.push(() => unwrappedNodes.forEach(c => c.remove()))
84+
6585
return wrapper
6686
}

0 commit comments

Comments
 (0)