/** * @vitest-environment jsdom */ import { type ObjectDirective, Suspense, Teleport, Transition, type VNode, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSSRApp, createStaticVNode, createTextVNode, createVNode, defineAsyncComponent, defineComponent, h, nextTick, onMounted, onServerPrefetch, openBlock, reactive, ref, renderSlot, useCssVars, vModelCheckbox, vShow, withCtx, withDirectives, } from '@vue/runtime-dom' import type { HMRRuntime } from '../src/hmr' import { type SSRContext, renderToString } from '@vue/server-renderer' import { PatchFlags, normalizeStyle } from '@vue/shared' import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow' declare var __VUE_HMR_RUNTIME__: HMRRuntime const { createRecord, reload } = __VUE_HMR_RUNTIME__ function mountWithHydration(html: string, render: () => any) { const container = document.createElement('div') container.innerHTML = html const app = createSSRApp({ render, }) return { vnode: app.mount(container).$.subTree as VNode & { el: Element }, container, } } const triggerEvent = (type: string, el: Element) => { const event = new Event(type) el.dispatchEvent(event) } describe('SSR hydration', () => { beforeEach(() => { document.body.innerHTML = '' }) test('text', async () => { const msg = ref('foo') const { vnode, container } = mountWithHydration('foo', () => msg.value) expect(vnode.el).toBe(container.firstChild) expect(container.textContent).toBe('foo') msg.value = 'bar' await nextTick() expect(container.textContent).toBe('bar') }) test('empty text', async () => { const { container } = mountWithHydration('
', () => h('div', createTextVNode('')), ) expect(container.textContent).toBe('') expect(`Hydration children mismatch in
`).not.toHaveBeenWarned() }) test('text w/ newlines', async () => { mountWithHydration('
1\n2\n3
', () => h('div', '1\r\n2\r3')) expect(`Hydration text mismatch`).not.toHaveBeenWarned() }) test('comment', () => { const { vnode, container } = mountWithHydration('', () => null) expect(vnode.el).toBe(container.firstChild) expect(vnode.el.nodeType).toBe(8) // comment }) test('static', () => { const html = '
hello
' const { vnode, container } = mountWithHydration(html, () => createStaticVNode('', 1), ) expect(vnode.el).toBe(container.firstChild) expect(vnode.el.outerHTML).toBe(html) expect(vnode.anchor).toBe(container.firstChild) expect(vnode.children).toBe(html) }) test('static (multiple elements)', () => { const staticContent = '
hello' const html = `
hi
` + staticContent + `
ho
` const n1 = h('div', 'hi') const s = createStaticVNode('', 2) const n2 = h('div', 'ho') const { container } = mountWithHydration(html, () => h('div', [n1, s, n2])) const div = container.firstChild! expect(n1.el).toBe(div.firstChild) expect(n2.el).toBe(div.lastChild) expect(s.el).toBe(div.childNodes[1]) expect(s.anchor).toBe(div.childNodes[2]) expect(s.children).toBe(staticContent) }) // #6008 test('static (with text node as starting node)', () => { const html = ` A foo B` const { vnode, container } = mountWithHydration(html, () => createStaticVNode(` A foo B`, 3), ) expect(vnode.el).toBe(container.firstChild) expect(vnode.anchor).toBe(container.lastChild) expect(`Hydration node mismatch`).not.toHaveBeenWarned() }) test('static with content adoption', () => { const html = ` A foo B` const { vnode, container } = mountWithHydration(html, () => createStaticVNode(``, 3), ) expect(vnode.el).toBe(container.firstChild) expect(vnode.anchor).toBe(container.lastChild) expect(vnode.children).toBe(html) expect(`Hydration node mismatch`).not.toHaveBeenWarned() }) test('element with text children', async () => { const msg = ref('foo') const { vnode, container } = mountWithHydration( '
foo
', () => h('div', { class: msg.value }, msg.value), ) expect(vnode.el).toBe(container.firstChild) expect(container.firstChild!.textContent).toBe('foo') msg.value = 'bar' await nextTick() expect(container.innerHTML).toBe(`
bar
`) }) // #7285 test('element with multiple continuous text vnodes', async () => { // should no mismatch warning const { container } = mountWithHydration('
foo0o
', () => h('div', ['fo', createTextVNode('o'), 0, 'o']), ) expect(container.textContent).toBe('foo0o') }) test('element with elements children', async () => { const msg = ref('foo') const fn = vi.fn() const { vnode, container } = mountWithHydration( '
foo
', () => h('div', [ h('span', msg.value), h('span', { class: msg.value, onClick: fn }), ]), ) expect(vnode.el).toBe(container.firstChild) expect((vnode.children as VNode[])[0].el).toBe( container.firstChild!.childNodes[0], ) expect((vnode.children as VNode[])[1].el).toBe( container.firstChild!.childNodes[1], ) // event handler triggerEvent('click', vnode.el.querySelector('.foo')!) expect(fn).toHaveBeenCalled() msg.value = 'bar' await nextTick() expect(vnode.el.innerHTML).toBe(`bar`) }) test('element with ref', () => { const el = ref() const { vnode, container } = mountWithHydration('
', () => h('div', { ref: el }), ) expect(vnode.el).toBe(container.firstChild) expect(el.value).toBe(vnode.el) }) test('Fragment', async () => { const msg = ref('foo') const fn = vi.fn() const { vnode, container } = mountWithHydration( '
foo
', () => h('div', [ [ h('span', msg.value), [h('span', { class: msg.value, onClick: fn })], ], ]), ) expect(vnode.el).toBe(container.firstChild) expect(vnode.el.innerHTML).toBe( `foo`, ) // start fragment 1 const fragment1 = (vnode.children as VNode[])[0] expect(fragment1.el).toBe(vnode.el.childNodes[0]) const fragment1Children = fragment1.children as VNode[] // first expect(fragment1Children[0].el!.tagName).toBe('SPAN') expect(fragment1Children[0].el).toBe(vnode.el.childNodes[1]) // start fragment 2 const fragment2 = fragment1Children[1] expect(fragment2.el).toBe(vnode.el.childNodes[2]) const fragment2Children = fragment2.children as VNode[] // second expect(fragment2Children[0].el!.tagName).toBe('SPAN') expect(fragment2Children[0].el).toBe(vnode.el.childNodes[3]) // end fragment 2 expect(fragment2.anchor).toBe(vnode.el.childNodes[4]) // end fragment 1 expect(fragment1.anchor).toBe(vnode.el.childNodes[5]) // event handler triggerEvent('click', vnode.el.querySelector('.foo')!) expect(fn).toHaveBeenCalled() msg.value = 'bar' await nextTick() expect(vnode.el.innerHTML).toBe( `bar`, ) }) // #7285 test('Fragment (multiple continuous text vnodes)', async () => { // should no mismatch warning const { container } = mountWithHydration('fooo', () => [ 'fo', createTextVNode('o'), 'o', ]) expect(container.textContent).toBe('fooo') }) test('Teleport', async () => { const msg = ref('foo') const fn = vi.fn() const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport' teleportContainer.innerHTML = `foo` document.body.appendChild(teleportContainer) const { vnode, container } = mountWithHydration( '', () => h(Teleport, { to: '#teleport' }, [ h('span', msg.value), h('span', { class: msg.value, onClick: fn }), ]), ) expect(vnode.el).toBe(container.firstChild) expect(vnode.anchor).toBe(container.lastChild) expect(vnode.target).toBe(teleportContainer) expect(vnode.targetStart).toBe(teleportContainer.childNodes[0]) expect((vnode.children as VNode[])[0].el).toBe( teleportContainer.childNodes[1], ) expect((vnode.children as VNode[])[1].el).toBe( teleportContainer.childNodes[2], ) expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[3]) // event handler triggerEvent('click', teleportContainer.querySelector('.foo')!) expect(fn).toHaveBeenCalled() msg.value = 'bar' await nextTick() expect(teleportContainer.innerHTML).toBe( `bar`, ) }) test('Teleport (multiple + integration)', async () => { const msg = ref('foo') const fn1 = vi.fn() const fn2 = vi.fn() const Comp = () => [ h(Teleport, { to: '#teleport2' }, [ h('span', msg.value), h('span', { class: msg.value, onClick: fn1 }), ]), h(Teleport, { to: '#teleport2' }, [ h('span', msg.value + '2'), h('span', { class: msg.value + '2', onClick: fn2 }), ]), ] const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport2' const ctx: SSRContext = {} const mainHtml = await renderToString(h(Comp), ctx) expect(mainHtml).toMatchInlineSnapshot( `""`, ) const teleportHtml = ctx.teleports!['#teleport2'] expect(teleportHtml).toMatchInlineSnapshot( `"foofoo2"`, ) teleportContainer.innerHTML = teleportHtml document.body.appendChild(teleportContainer) const { vnode, container } = mountWithHydration(mainHtml, Comp) expect(vnode.el).toBe(container.firstChild) const teleportVnode1 = (vnode.children as VNode[])[0] const teleportVnode2 = (vnode.children as VNode[])[1] expect(teleportVnode1.el).toBe(container.childNodes[1]) expect(teleportVnode1.anchor).toBe(container.childNodes[2]) expect(teleportVnode2.el).toBe(container.childNodes[3]) expect(teleportVnode2.anchor).toBe(container.childNodes[4]) expect(teleportVnode1.target).toBe(teleportContainer) expect(teleportVnode1.targetStart).toBe(teleportContainer.childNodes[0]) expect((teleportVnode1 as any).children[0].el).toBe( teleportContainer.childNodes[1], ) expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[3]) expect(teleportVnode2.target).toBe(teleportContainer) expect(teleportVnode2.targetStart).toBe(teleportContainer.childNodes[4]) expect((teleportVnode2 as any).children[0].el).toBe( teleportContainer.childNodes[5], ) expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[7]) // // event handler triggerEvent('click', teleportContainer.querySelector('.foo')!) expect(fn1).toHaveBeenCalled() triggerEvent('click', teleportContainer.querySelector('.foo2')!) expect(fn2).toHaveBeenCalled() msg.value = 'bar' await nextTick() expect(teleportContainer.innerHTML).toMatchInlineSnapshot( `"barbar2"`, ) }) test('Teleport (disabled)', async () => { const msg = ref('foo') const fn1 = vi.fn() const fn2 = vi.fn() const Comp = () => [ h('div', 'foo'), h(Teleport, { to: '#teleport3', disabled: true }, [ h('span', msg.value), h('span', { class: msg.value, onClick: fn1 }), ]), h('div', { class: msg.value + '2', onClick: fn2 }, 'bar'), ] const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport3' const ctx: SSRContext = {} const mainHtml = await renderToString(h(Comp), ctx) expect(mainHtml).toMatchInlineSnapshot( `"
foo
foo
bar
"`, ) const teleportHtml = ctx.teleports!['#teleport3'] expect(teleportHtml).toMatchInlineSnapshot( `""`, ) teleportContainer.innerHTML = teleportHtml document.body.appendChild(teleportContainer) const { vnode, container } = mountWithHydration(mainHtml, Comp) expect(vnode.el).toBe(container.firstChild) const children = vnode.children as VNode[] expect(children[0].el).toBe(container.childNodes[1]) const teleportVnode = children[1] expect(teleportVnode.el).toBe(container.childNodes[2]) expect((teleportVnode.children as VNode[])[0].el).toBe( container.childNodes[3], ) expect((teleportVnode.children as VNode[])[1].el).toBe( container.childNodes[4], ) expect(teleportVnode.anchor).toBe(container.childNodes[5]) expect(children[2].el).toBe(container.childNodes[6]) expect(teleportVnode.target).toBe(teleportContainer) expect(teleportVnode.targetStart).toBe(teleportContainer.childNodes[0]) expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[1]) // // event handler triggerEvent('click', container.querySelector('.foo')!) expect(fn1).toHaveBeenCalled() triggerEvent('click', container.querySelector('.foo2')!) expect(fn2).toHaveBeenCalled() msg.value = 'bar' await nextTick() expect(container.innerHTML).toMatchInlineSnapshot( `"
foo
bar
bar
"`, ) }) // #6152 test('Teleport (disabled + as component root)', () => { const { container } = mountWithHydration( '
Parent fragment
Teleport content
', () => [ h('div', 'Parent fragment'), h(() => h(Teleport, { to: 'body', disabled: true }, [ h('div', 'Teleport content'), ]), ), ], ) expect(document.body.innerHTML).toBe('') expect(container.innerHTML).toBe( '
Parent fragment
Teleport content
', ) expect( `Hydration completed but contains mismatches.`, ).not.toHaveBeenWarned() }) test('Teleport (as component root)', () => { const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport4' teleportContainer.innerHTML = `hello` document.body.appendChild(teleportContainer) const wrapper = { render() { return h(Teleport, { to: '#teleport4' }, ['hello']) }, } const { vnode, container } = mountWithHydration( '
', () => h('div', [h(wrapper), h('div')]), ) expect(vnode.el).toBe(container.firstChild) // component el const wrapperVNode = (vnode as any).children[0] const tpStart = container.firstChild?.firstChild const tpEnd = tpStart?.nextSibling expect(wrapperVNode.el).toBe(tpStart) expect(wrapperVNode.component.subTree.el).toBe(tpStart) expect(wrapperVNode.component.subTree.anchor).toBe(tpEnd) // next node hydrate properly const nextVNode = (vnode as any).children[1] expect(nextVNode.el).toBe(container.firstChild?.lastChild) }) test('Teleport (nested)', () => { const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport5' teleportContainer.innerHTML = `
child
` document.body.appendChild(teleportContainer) const { vnode, container } = mountWithHydration( '', () => h(Teleport, { to: '#teleport5' }, [ h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])]), ]), ) expect(vnode.el).toBe(container.firstChild) expect(vnode.anchor).toBe(container.lastChild) const childDivVNode = (vnode as any).children[0] const div = teleportContainer.childNodes[1] expect(childDivVNode.el).toBe(div) expect(vnode.targetAnchor).toBe(div?.nextSibling) const childTeleportVNode = childDivVNode.children[0] expect(childTeleportVNode.el).toBe(div?.firstChild) expect(childTeleportVNode.anchor).toBe(div?.lastChild) expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild) expect(childTeleportVNode.children[0].el).toBe( teleportContainer.lastChild?.previousSibling, ) }) test('with data-allow-mismatch component when using onServerPrefetch', async () => { const Comp = { template: `
Comp2
`, } let foo: any const App = { setup() { const flag = ref(true) foo = () => { flag.value = false } onServerPrefetch(() => (flag.value = false)) return { flag } }, components: { Comp, }, template: ` `, } // hydrate const container = document.createElement('div') container.innerHTML = await renderToString(h(App)) createSSRApp(App).mount(container) expect(container.innerHTML).toBe( '
Comp2
', ) foo() await nextTick() expect(container.innerHTML).toBe( '', ) }) test('Teleport unmount (full integration)', async () => { const Comp1 = { template: ` Teleported Comp1 `, } const Comp2 = { template: `
Comp2
`, } const toggle = ref(true) const App = { template: `
`, components: { Comp1, Comp2, }, setup() { return { toggle } }, } const container = document.createElement('div') const teleportContainer = document.createElement('div') teleportContainer.id = 'target' document.body.appendChild(teleportContainer) // server render const ctx: SSRContext = {} container.innerHTML = await renderToString(h(App), ctx) expect(container.innerHTML).toBe( '
', ) teleportContainer.innerHTML = ctx.teleports!['#target'] // hydrate createSSRApp(App).mount(container) expect(container.innerHTML).toBe( '
', ) expect(teleportContainer.innerHTML).toBe( 'Teleported Comp1', ) expect(`Hydration children mismatch`).not.toHaveBeenWarned() toggle.value = false await nextTick() expect(container.innerHTML).toBe('
Comp2
') expect(teleportContainer.innerHTML).toBe('') }) test('Teleport unmount (mismatch + full integration)', async () => { const Comp1 = { template: ` Teleported Comp1 `, } const Comp2 = { template: `
Comp2
`, } const toggle = ref(true) const App = { template: `
`, components: { Comp1, Comp2, }, setup() { return { toggle } }, } const container = document.createElement('div') const teleportContainer = document.createElement('div') teleportContainer.id = 'target' document.body.appendChild(teleportContainer) // server render container.innerHTML = await renderToString(h(App)) expect(container.innerHTML).toBe( '
', ) expect(teleportContainer.innerHTML).toBe('') // hydrate createSSRApp(App).mount(container) expect(container.innerHTML).toBe( '
', ) expect(teleportContainer.innerHTML).toBe('Teleported Comp1') expect(`Hydration children mismatch`).toHaveBeenWarned() toggle.value = false await nextTick() expect(container.innerHTML).toBe('
Comp2
') expect(teleportContainer.innerHTML).toBe('') }) test('Teleport target change (mismatch + full integration)', async () => { const target = ref('#target1') const Comp = { template: ` Teleported `, setup() { return { target } }, } const App = { template: `
`, components: { Comp, }, } const container = document.createElement('div') const teleportContainer1 = document.createElement('div') teleportContainer1.id = 'target1' const teleportContainer2 = document.createElement('div') teleportContainer2.id = 'target2' document.body.appendChild(teleportContainer1) document.body.appendChild(teleportContainer2) // server render container.innerHTML = await renderToString(h(App)) expect(container.innerHTML).toBe( '
', ) expect(teleportContainer1.innerHTML).toBe('') expect(teleportContainer2.innerHTML).toBe('') // hydrate createSSRApp(App).mount(container) expect(container.innerHTML).toBe( '
', ) expect(teleportContainer1.innerHTML).toBe('Teleported') expect(teleportContainer2.innerHTML).toBe('') expect(`Hydration children mismatch`).toHaveBeenWarned() target.value = '#target2' await nextTick() expect(teleportContainer1.innerHTML).toBe('') expect(teleportContainer2.innerHTML).toBe('Teleported') }) // compile SSR + client render fn from the same template & hydrate test('full compiler integration', async () => { const mounted: string[] = [] const log = vi.fn() const toggle = ref(true) const Child = { data() { return { count: 0, text: 'hello', style: { color: 'red', }, } }, mounted() { mounted.push('child') }, template: `
{{ count }} {{ text }}
`, } const App = { setup() { return { toggle } }, mounted() { mounted.push('parent') }, template: `
hello hello
`, components: { Child, }, methods: { log, }, } const container = document.createElement('div') // server render container.innerHTML = await renderToString(h(App)) // hydrate createSSRApp(App).mount(container) // assert interactions // 1. parent button click triggerEvent('click', container.querySelector('.parent-click')!) expect(log).toHaveBeenCalledWith('click') // 2. child inc click + text interpolation const count = container.querySelector('.count') as HTMLElement expect(count.textContent).toBe(`0`) triggerEvent('click', container.querySelector('.inc')!) await nextTick() expect(count.textContent).toBe(`1`) // 3. child color click + style binding expect(count.style.color).toBe('red') triggerEvent('click', container.querySelector('.change')!) await nextTick() expect(count.style.color).toBe('green') // 4. child event emit triggerEvent('click', container.querySelector('.emit')!) expect(log).toHaveBeenCalledWith('child') // 5. child v-model const text = container.querySelector('.text')! const input = container.querySelector('input')! expect(text.textContent).toBe('hello') input.value = 'bye' triggerEvent('input', input) await nextTick() expect(text.textContent).toBe('bye') }) test('handle click error in ssr mode', async () => { const App = { setup() { const throwError = () => { throw new Error('Sentry Error') } return { throwError } }, template: `
`, } const container = document.createElement('div') // server render container.innerHTML = await renderToString(h(App)) // hydrate const app = createSSRApp(App) const handler = (app.config.errorHandler = vi.fn()) app.mount(container) // assert interactions // parent button click triggerEvent('click', container.querySelector('.parent-click')!) expect(handler).toHaveBeenCalled() }) test('handle blur error in ssr mode', async () => { const App = { setup() { const throwError = () => { throw new Error('Sentry Error') } return { throwError } }, template: `
`, } const container = document.createElement('div') // server render container.innerHTML = await renderToString(h(App)) // hydrate const app = createSSRApp(App) const handler = (app.config.errorHandler = vi.fn()) app.mount(container) // assert interactions // parent blur event triggerEvent('blur', container.querySelector('.parent-click')!) expect(handler).toHaveBeenCalled() }) test('Suspense', async () => { const AsyncChild = { async setup() { const count = ref(0) return () => h( 'span', { onClick: () => { count.value++ }, }, count.value, ) }, } const { vnode, container } = mountWithHydration('0', () => h(Suspense, () => h(AsyncChild)), ) expect(vnode.el).toBe(container.firstChild) // wait for hydration to finish await new Promise(r => setTimeout(r)) triggerEvent('click', container.querySelector('span')!) await nextTick() expect(container.innerHTML).toBe(`1`) }) // #6638 test('Suspense + async component', async () => { let isSuspenseResolved = false let isSuspenseResolvedInChild: any const AsyncChild = defineAsyncComponent(() => Promise.resolve( defineComponent({ setup() { isSuspenseResolvedInChild = isSuspenseResolved const count = ref(0) return () => h( 'span', { onClick: () => { count.value++ }, }, count.value, ) }, }), ), ) const { vnode, container } = mountWithHydration('0', () => h( Suspense, { onResolve() { isSuspenseResolved = true }, }, () => h(AsyncChild), ), ) expect(vnode.el).toBe(container.firstChild) // wait for hydration to finish await new Promise(r => setTimeout(r)) expect(isSuspenseResolvedInChild).toBe(false) expect(isSuspenseResolved).toBe(true) // assert interaction triggerEvent('click', container.querySelector('span')!) await nextTick() expect(container.innerHTML).toBe(`1`) }) test('Suspense (full integration)', async () => { const mountedCalls: number[] = [] const asyncDeps: Promise[] = [] const AsyncChild = defineComponent({ props: ['n'], async setup(props) { const count = ref(props.n) onMounted(() => { mountedCalls.push(props.n) }) const p = new Promise(r => setTimeout(r, props.n * 10)) asyncDeps.push(p) await p return () => h( 'span', { onClick: () => { count.value++ }, }, count.value, ) }, }) const done = vi.fn() const App = { template: `
`, components: { AsyncChild, }, methods: { done, }, } const container = document.createElement('div') // server render container.innerHTML = await renderToString(h(App)) expect(container.innerHTML).toMatchInlineSnapshot( `"
12
"`, ) // reset asyncDeps from ssr asyncDeps.length = 0 // hydrate createSSRApp(App).mount(container) expect(mountedCalls.length).toBe(0) expect(asyncDeps.length).toBe(2) // wait for hydration to complete await Promise.all(asyncDeps) await new Promise(r => setTimeout(r)) // should flush buffered effects expect(mountedCalls).toMatchObject([1, 2]) expect(container.innerHTML).toMatch( `
12
`, ) const span1 = container.querySelector('span')! triggerEvent('click', span1) await nextTick() expect(container.innerHTML).toMatch( `
22
`, ) const span2 = span1.nextSibling as Element triggerEvent('click', span2) await nextTick() expect(container.innerHTML).toMatch( `
23
`, ) }) test('async component', async () => { const spy = vi.fn() const Comp = () => h( 'button', { onClick: spy, }, 'hello!', ) let serverResolve: any let AsyncComp = defineAsyncComponent( () => new Promise(r => { serverResolve = r }), ) const App = { render() { return ['hello', h(AsyncComp), 'world'] }, } // server render const htmlPromise = renderToString(h(App)) serverResolve(Comp) const html = await htmlPromise expect(html).toMatchInlineSnapshot( `"helloworld"`, ) // hydration let clientResolve: any AsyncComp = defineAsyncComponent( () => new Promise(r => { clientResolve = r }), ) const container = document.createElement('div') container.innerHTML = html createSSRApp(App).mount(container) // hydration not complete yet triggerEvent('click', container.querySelector('button')!) expect(spy).not.toHaveBeenCalled() // resolve clientResolve(Comp) await new Promise(r => setTimeout(r)) // should be hydrated now triggerEvent('click', container.querySelector('button')!) expect(spy).toHaveBeenCalled() }) test('update async wrapper before resolve', async () => { const Comp = { render() { return h('h1', 'Async component') }, } let serverResolve: any let AsyncComp = defineAsyncComponent( () => new Promise(r => { serverResolve = r }), ) const toggle = ref(true) const App = { setup() { onMounted(() => { // change state, this makes updateComponent(AsyncComp) execute before // the async component is resolved toggle.value = false }) return () => { return [toggle.value ? 'hello' : 'world', h(AsyncComp)] } }, } // server render const htmlPromise = renderToString(h(App)) serverResolve(Comp) const html = await htmlPromise expect(html).toMatchInlineSnapshot( `"hello

Async component

"`, ) // hydration let clientResolve: any AsyncComp = defineAsyncComponent( () => new Promise(r => { clientResolve = r }), ) const container = document.createElement('div') container.innerHTML = html createSSRApp(App).mount(container) // resolve clientResolve(Comp) await new Promise(r => setTimeout(r)) // should be hydrated now expect(`Hydration node mismatch`).not.toHaveBeenWarned() expect(container.innerHTML).toMatchInlineSnapshot( `"world

Async component

"`, ) }) // #13510 test('update async component after parent mount before async component resolve', async () => { const Comp = { props: ['toggle'], render(this: any) { return h('h1', [ this.toggle ? 'Async component' : 'Updated async component', ]) }, } let serverResolve: any let AsyncComp = defineAsyncComponent( () => new Promise(r => { serverResolve = r }), ) const toggle = ref(true) const App = { setup() { onMounted(() => { // change state, after mount and before async component resolve nextTick(() => (toggle.value = false)) }) return () => { return h(AsyncComp, { toggle: toggle.value }) } }, } // server render const htmlPromise = renderToString(h(App)) serverResolve(Comp) const html = await htmlPromise expect(html).toMatchInlineSnapshot(`"

Async component

"`) // hydration let clientResolve: any AsyncComp = defineAsyncComponent( () => new Promise(r => { clientResolve = r }), ) const container = document.createElement('div') container.innerHTML = html createSSRApp(App).mount(container) // resolve clientResolve(Comp) await new Promise(r => setTimeout(r)) // prevent lazy hydration since the component has been patched expect('Skipping lazy hydration for component').toHaveBeenWarned() expect(`Hydration node mismatch`).not.toHaveBeenWarned() expect(container.innerHTML).toMatchInlineSnapshot( `"

Updated async component

"`, ) }) test('hydrate safely when property used by async setup changed before render', async () => { const toggle = ref(true) const AsyncComp = { async setup() { await new Promise(r => setTimeout(r, 10)) return () => h('h1', 'Async component') }, } const AsyncWrapper = { render() { return h(AsyncComp) }, } const SiblingComp = { setup() { toggle.value = false return () => h('span') }, } const App = { setup() { return () => h( Suspense, {}, { default: () => [ h('main', {}, [ h(AsyncWrapper, { prop: toggle.value ? 'hello' : 'world', }), h(SiblingComp), ]), ], }, ) }, } // server render const html = await renderToString(h(App)) expect(html).toMatchInlineSnapshot( `"

Async component

"`, ) expect(toggle.value).toBe(false) // hydration // reset the value toggle.value = true expect(toggle.value).toBe(true) const container = document.createElement('div') container.innerHTML = html createSSRApp(App).mount(container) await new Promise(r => setTimeout(r, 10)) expect(toggle.value).toBe(false) // should be hydrated now expect(container.innerHTML).toMatchInlineSnapshot( `"

Async component

"`, ) }) test('hydrate safely when property used by deep nested async setup changed before render', async () => { const toggle = ref(true) const AsyncComp = { async setup() { await new Promise(r => setTimeout(r, 10)) return () => h('h1', 'Async component') }, } const AsyncWrapper = { render: () => h(AsyncComp) } const AsyncWrapperWrapper = { render: () => h(AsyncWrapper) } const SiblingComp = { setup() { toggle.value = false return () => h('span') }, } const App = { setup() { return () => h( Suspense, {}, { default: () => [ h('main', {}, [ h(AsyncWrapperWrapper, { prop: toggle.value ? 'hello' : 'world', }), h(SiblingComp), ]), ], }, ) }, } // server render const html = await renderToString(h(App)) expect(html).toMatchInlineSnapshot( `"

Async component

"`, ) expect(toggle.value).toBe(false) // hydration // reset the value toggle.value = true expect(toggle.value).toBe(true) const container = document.createElement('div') container.innerHTML = html createSSRApp(App).mount(container) await new Promise(r => setTimeout(r, 10)) expect(toggle.value).toBe(false) // should be hydrated now expect(container.innerHTML).toMatchInlineSnapshot( `"

Async component

"`, ) }) // #3787 test('unmount async wrapper before load', async () => { let resolve: any const AsyncComp = defineAsyncComponent( () => new Promise(r => { resolve = r }), ) const show = ref(true) const root = document.createElement('div') root.innerHTML = '
async
' createSSRApp({ render() { return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')]) }, }).mount(root) show.value = false await nextTick() expect(root.innerHTML).toBe('
hi
') resolve({}) }) //#12362 test('nested async wrapper', async () => { const Toggle = defineAsyncComponent( () => new Promise(r => { r( defineComponent({ setup(_, { slots }) { const show = ref(false) onMounted(() => { nextTick(() => { show.value = true }) }) return () => withDirectives( h('div', null, [renderSlot(slots, 'default')]), [[vShow, show.value]], ) }, }) as any, ) }), ) const Wrapper = defineAsyncComponent(() => { return new Promise(r => { r( defineComponent({ render(this: any) { return renderSlot(this.$slots, 'default') }, }) as any, ) }) }) const count = ref(0) const fn = vi.fn() const Child = { setup() { onMounted(() => { fn() count.value++ }) return () => h('div', count.value) }, } const App = { render() { return h(Toggle, null, { default: () => h(Wrapper, null, { default: () => h(Wrapper, null, { default: () => h(Child), }), }), }) }, } const root = document.createElement('div') root.innerHTML = await renderToString(h(App)) expect(root.innerHTML).toMatchInlineSnapshot( `"
0
"`, ) createSSRApp(App).mount(root) await nextTick() await nextTick() expect(root.innerHTML).toMatchInlineSnapshot( `"
1
"`, ) expect(fn).toBeCalledTimes(1) }) test('unmount async wrapper before load (fragment)', async () => { let resolve: any const AsyncComp = defineAsyncComponent( () => new Promise(r => { resolve = r }), ) const show = ref(true) const root = document.createElement('div') root.innerHTML = '
async
' createSSRApp({ render() { return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')]) }, }).mount(root) show.value = false await nextTick() expect(root.innerHTML).toBe('
hi
') resolve({}) }) test('elements with camel-case in svg ', () => { const { vnode, container } = mountWithHydration( '', () => h('animateTransform'), ) expect(vnode.el).toBe(container.firstChild) expect(`Hydration node mismatch`).not.toHaveBeenWarned() }) test('SVG as a mount container', () => { const svgContainer = document.createElement('svg') svgContainer.innerHTML = '' const app = createSSRApp({ render: () => h('g'), }) expect( ( app.mount(svgContainer).$.subTree as VNode & { el: Element } ).el instanceof SVGElement, ) }) test('force hydrate prop with `.prop` modifier', () => { const { container } = mountWithHydration('', () => h('input', { type: 'checkbox', '.indeterminate': true, }), ) expect((container.firstChild! as any).indeterminate).toBe(true) }) test('force hydrate input v-model with non-string value bindings', () => { const { container } = mountWithHydration( '', () => withDirectives( createVNode( 'input', { type: 'checkbox', 'true-value': true }, null, PatchFlags.PROPS, ['true-value'], ), [[vModelCheckbox, true]], ), ) expect((container.firstChild as any)._trueValue).toBe(true) }) test('force hydrate checkbox with indeterminate', () => { const { container } = mountWithHydration( '', () => createVNode( 'input', { type: 'checkbox', indeterminate: '' }, null, PatchFlags.CACHED, ), ) expect((container.firstChild as any).indeterminate).toBe(true) }) test('force hydrate select option with non-string value bindings', () => { const { container } = mountWithHydration( '', () => h('select', [ // hoisted because bound value is a constant... createVNode('option', { value: true }, null, -1 /* HOISTED */), ]), ) expect((container.firstChild!.firstChild as any)._value).toBe(true) }) // #7203 test('force hydrate custom element with dynamic props', () => { class MyElement extends HTMLElement { foo = '' constructor() { super() } } customElements.define('my-element-7203', MyElement) const msg = ref('bar') const container = document.createElement('div') container.innerHTML = '' const app = createSSRApp({ render: () => h('my-element-7203', { foo: msg.value }), }) app.mount(container) expect((container.firstChild as any).foo).toBe(msg.value) }) // #14274 test('should not render ref on custom element during hydration', () => { const container = document.createElement('div') container.innerHTML = 'hello' const root = ref() const app = createSSRApp({ render: () => h('my-element', { ref: root, innerHTML: 'hello', }), }) app.mount(container) expect(container.innerHTML).toBe('hello') expect((container.firstChild as Element).hasAttribute('ref')).toBe(false) expect(root.value).toBe(container.firstChild) }) // #5728 test('empty text node in slot', () => { const Comp = { render(this: any) { return renderSlot(this.$slots, 'default', {}, () => [ createTextVNode(''), ]) }, } const { container, vnode } = mountWithHydration('', () => h(Comp), ) expect(container.childNodes.length).toBe(3) const text = container.childNodes[1] expect(text.nodeType).toBe(3) expect(vnode.el).toBe(container.childNodes[0]) // component => slot fragment => text node expect((vnode as any).component?.subTree.children[0].el).toBe(text) }) // #7215 test('empty text node', () => { const Comp = { render(this: any) { return h('p', ['']) }, } const { container } = mountWithHydration('

', () => h(Comp)) expect(container.childNodes.length).toBe(1) const p = container.childNodes[0] expect(p.childNodes.length).toBe(1) const text = p.childNodes[0] expect(text.nodeType).toBe(3) }) // #11372 test('object style value tracking in prod', async () => { __DEV__ = false try { const style = reactive({ color: 'red' }) const Comp = { render(this: any) { return ( openBlock(), createElementBlock( 'div', { style: normalizeStyle(style), }, null, 4 /* STYLE */, ) ) }, } const { container } = mountWithHydration( `
`, () => h(Comp), ) style.color = 'green' await nextTick() expect(container.innerHTML).toBe(`
`) } finally { __DEV__ = true } }) test('app.unmount()', async () => { const container = document.createElement('DIV') container.innerHTML = '' const App = defineComponent({ setup(_, { expose }) { const count = ref(0) expose({ count }) return () => h('button', { onClick: () => count.value++, }) }, }) const app = createSSRApp(App) const vm = app.mount(container) await nextTick() expect((container as any)._vnode).toBeDefined() // @ts-expect-error - expose()'d properties are not available on vm type expect(vm.count).toBe(0) app.unmount() expect((container as any)._vnode).toBe(null) }) // #6637 test('stringified root fragment', () => { mountWithHydration(`
`, () => createStaticVNode(`
`, 1), ) expect(`mismatch`).not.toHaveBeenWarned() }) test('transition appear', () => { const { vnode, container } = mountWithHydration( ``, () => h( Transition, { appear: true }, { default: () => h('div', 'foo'), }, ), ) expect(container.firstChild).toMatchInlineSnapshot(`
foo
`) expect(vnode.el).toBe(container.firstChild) expect(`mismatch`).not.toHaveBeenWarned() }) test('transition appear work with pre-existing class', () => { const { vnode, container } = mountWithHydration( ``, () => h( Transition, { appear: true }, { default: () => h('div', { class: 'foo' }, 'foo'), }, ), ) expect(container.firstChild).toMatchInlineSnapshot(`
foo
`) expect(vnode.el).toBe(container.firstChild) expect(`mismatch`).not.toHaveBeenWarned() }) // #13394 test('transition appear work with empty content', async () => { const show = ref(true) const { vnode, container } = mountWithHydration( ``, function (this: any) { return h( Transition, { appear: true }, { default: () => show.value ? renderSlot(this.$slots, 'default') : createTextVNode('foo'), }, ) }, ) // empty slot render as a comment node expect(container.firstChild!.nodeType).toBe(Node.COMMENT_NODE) expect(vnode.el).toBe(container.firstChild) expect(`mismatch`).not.toHaveBeenWarned() show.value = false await nextTick() expect(container.innerHTML).toBe('foo') }) test('transition appear with v-if', () => { const show = false const { vnode, container } = mountWithHydration( ``, () => h( Transition, { appear: true }, { default: () => (show ? h('div', 'foo') : createCommentVNode('')), }, ), ) expect(container.firstChild).toMatchInlineSnapshot('') expect(vnode.el).toBe(container.firstChild) expect(`mismatch`).not.toHaveBeenWarned() }) test('transition appear with v-show', () => { const show = false const { vnode, container } = mountWithHydration( ``, () => h( Transition, { appear: true }, { default: () => withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]), }, ), ) expect(container.firstChild).toMatchInlineSnapshot(` `) expect((container.firstChild as any)[vShowOriginalDisplay]).toBe('') expect(vnode.el).toBe(container.firstChild) expect(`mismatch`).not.toHaveBeenWarned() }) test('transition appear w/ event listener', async () => { const container = document.createElement('div') container.innerHTML = `` createSSRApp({ data() { return { count: 0, } }, template: ` `, }).mount(container) expect(container.firstChild).toMatchInlineSnapshot(` `) triggerEvent('click', container.querySelector('button')!) await nextTick() expect(container.firstChild).toMatchInlineSnapshot(` `) }) test('Suspense + transition appear', async () => { const { vnode, container } = mountWithHydration( ``, () => h(Suspense, {}, () => h( Transition, { appear: true }, { default: () => h('div', 'foo'), }, ), ), ) expect(vnode.el).toBe(container.firstChild) // wait for hydration to finish await new Promise(r => setTimeout(r)) expect(container.firstChild).toMatchInlineSnapshot(`
foo
`) await nextTick() expect(vnode.el).toBe(container.firstChild) }) // #10607 test('update component stable slot (prod + optimized mode)', async () => { __DEV__ = false try { const container = document.createElement('div') container.innerHTML = `` const Comp = { render(this: any) { return ( openBlock(), createElementBlock('div', null, [ renderSlot(this.$slots, 'default'), ]) ) }, } const show = ref(false) const clicked = ref(false) const Wrapper = { setup() { const items = ref([]) onMounted(() => { items.value = [1] }) return () => { return ( openBlock(), createBlock(Comp, null, { default: withCtx(() => [ createElementVNode('div', null, [ createElementVNode('div', null, [ clicked.value ? (openBlock(), createElementBlock('div', { key: 0 }, 'foo')) : createCommentVNode('v-if', true), ]), ]), createElementVNode( 'div', null, items.value.length, 1 /* TEXT */, ), ]), _: 1 /* STABLE */, }) ) } }, } createSSRApp({ components: { Wrapper }, data() { return { show } }, template: ``, }).mount(container) await nextTick() expect(container.innerHTML).toBe( `
1
`, ) show.value = true await nextTick() expect(async () => { clicked.value = true await nextTick() }).not.toThrow("Cannot read properties of null (reading 'insertBefore')") await nextTick() expect(container.innerHTML).toBe( `
foo
1
`, ) } catch (e) { throw e } finally { __DEV__ = true } }) test('hmr reload child wrapped in KeepAlive', async () => { const id = 'child-reload' const Child = { __hmrId: id, template: `
foo
`, } createRecord(id, Child) const appId = 'test-app-id' const App = { __hmrId: appId, components: { Child }, template: `
`, } const root = document.createElement('div') root.innerHTML = await renderToString(h(App)) createSSRApp(App).mount(root) expect(root.innerHTML).toBe('
foo
') reload(id, { __hmrId: id, template: `
bar
`, }) await nextTick() expect(root.innerHTML).toBe('
bar
') }) test('hmr root reload', async () => { const appId = 'test-app-id' const App = { __hmrId: appId, template: `
foo
`, } const root = document.createElement('div') root.innerHTML = await renderToString(h(App)) createSSRApp(App).mount(root) expect(root.innerHTML).toBe('
foo
') reload(appId, { __hmrId: appId, template: `
bar
`, }) await nextTick() expect(root.innerHTML).toBe('
bar
') }) describe('mismatch handling', () => { test('text node', () => { const { container } = mountWithHydration(`foo`, () => 'bar') expect(container.textContent).toBe('bar') expect(`Hydration text mismatch`).toHaveBeenWarned() }) test('element text content', () => { const { container } = mountWithHydration(`
foo
`, () => h('div', 'bar'), ) expect(container.innerHTML).toBe('
bar
') expect(`Hydration text content mismatch`).toHaveBeenWarned() }) test('not enough children', () => { const { container } = mountWithHydration(`
`, () => h('div', [h('span', 'foo'), h('span', 'bar')]), ) expect(container.innerHTML).toBe( '
foobar
', ) expect(`Hydration children mismatch`).toHaveBeenWarned() }) test('too many children', () => { const { container } = mountWithHydration( `
foobar
`, () => h('div', [h('span', 'foo')]), ) expect(container.innerHTML).toBe('
foo
') expect(`Hydration children mismatch`).toHaveBeenWarned() }) test('complete mismatch', () => { const { container } = mountWithHydration( `
foobar
`, () => h('div', [h('div', 'foo'), h('p', 'bar')]), ) expect(container.innerHTML).toBe('
foo

bar

') expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2) }) test('fragment mismatch removal', () => { const { container } = mountWithHydration( `
foo
bar
`, () => h('div', [h('span', 'replaced')]), ) expect(container.innerHTML).toBe('
replaced
') expect(`Hydration node mismatch`).toHaveBeenWarned() }) test('fragment not enough children', () => { const { container } = mountWithHydration( `
foo
baz
`, () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]), ) expect(container.innerHTML).toBe( '
foo
bar
baz
', ) expect(`Hydration node mismatch`).toHaveBeenWarned() }) test('fragment too many children', () => { const { container } = mountWithHydration( `
foo
bar
baz
`, () => h('div', [[h('div', 'foo')], h('div', 'baz')]), ) expect(container.innerHTML).toBe( '
foo
baz
', ) // fragment ends early and attempts to hydrate the extra
bar
// as 2nd fragment child. expect(`Hydration text content mismatch`).toHaveBeenWarned() // excessive children removal expect(`Hydration children mismatch`).toHaveBeenWarned() }) test('Teleport target has empty children', () => { const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport' document.body.appendChild(teleportContainer) mountWithHydration('', () => h(Teleport, { to: '#teleport' }, [h('span', 'value')]), ) expect(teleportContainer.innerHTML).toBe(`value`) expect(`Hydration children mismatch`).toHaveBeenWarned() }) test('comment mismatch (element)', () => { const { container } = mountWithHydration(`
`, () => h('div', [createCommentVNode('hi')]), ) expect(container.innerHTML).toBe('
') expect(`Hydration node mismatch`).toHaveBeenWarned() }) test('comment mismatch (text)', () => { const { container } = mountWithHydration(`
foobar
`, () => h('div', [createCommentVNode('hi')]), ) expect(container.innerHTML).toBe('
') expect(`Hydration node mismatch`).toHaveBeenWarned() }) test('class mismatch', () => { mountWithHydration(`
`, () => h('div', { class: ['foo', 'bar'] }), ) mountWithHydration(`
`, () => h('div', { class: { foo: true, bar: true } }), ) mountWithHydration(`
`, () => h('div', { class: 'foo bar' }), ) // SVG classes mountWithHydration(``, () => h('svg', { class: 'foo bar' }), ) // class with different order mountWithHydration(`
`, () => h('div', { class: 'bar foo' }), ) expect(`Hydration class mismatch`).not.toHaveBeenWarned() mountWithHydration(`
`, () => h('div', { class: 'foo' }), ) expect(`Hydration class mismatch`).toHaveBeenWarned() }) test('style mismatch', () => { mountWithHydration(`
`, () => h('div', { style: { color: 'red' } }), ) mountWithHydration(`
`, () => h('div', { style: `color:red;` }), ) mountWithHydration( `
`, () => h('div', { style: `font-size: 12px; color:red;` }), ) mountWithHydration(`
`, () => withDirectives(createVNode('div', { style: 'color: red' }, ''), [ [vShow, false], ]), ) expect(`Hydration style mismatch`).not.toHaveBeenWarned() mountWithHydration(`
`, () => h('div', { style: { color: 'green' } }), ) expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1) }) test('style mismatch when no style attribute is present', () => { mountWithHydration(`
`, () => h('div', { style: { color: 'red' } }), ) expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1) }) test('style mismatch w/ v-show', () => { mountWithHydration(`
`, () => withDirectives(createVNode('div', { style: 'color: red' }, ''), [ [vShow, false], ]), ) expect(`Hydration style mismatch`).not.toHaveBeenWarned() mountWithHydration(`
`, () => withDirectives(createVNode('div', { style: 'color: red' }, ''), [ [vShow, false], ]), ) expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1) }) test('attr mismatch', () => { mountWithHydration(`
`, () => h('div', { id: 'foo' })) mountWithHydration(`
`, () => h('div', { spellcheck: '' }), ) mountWithHydration(`
`, () => h('div', { id: undefined })) // boolean mountWithHydration(`
`, () => h('select', { multiple: 'multiple' }), ) expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() mountWithHydration(`
`, () => h('div', { id: 'foo' })) expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1) mountWithHydration(`
`, () => h('div', { id: 'foo' })) expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2) }) test('asset url attrs allow client contains server', () => { try { __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ = true mountWithHydration(``, () => h('img', { src: 'http://localhost:3000/a.png' }), ) mountWithHydration(``, () => h('a', { href: 'http://localhost:3000/a.png' }), ) mountWithHydration(``, () => h('video', { poster: 'http://localhost:3000/a.png' }), ) mountWithHydration(``, () => h('object', { data: 'http://localhost:3000/a.png' }), ) mountWithHydration( ``, () => h('svg', [ h('use', { 'xlink:href': 'http://localhost:3000/sprite.svg#icon', }), ]), ) mountWithHydration(``, () => h('img', { srcset: 'http://localhost:3000/a.png 1x, http://localhost:3000/b.png 2x', }), ) expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() } finally { __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ = false } }) test('asset url attrs still warn when client does not contain server', () => { try { __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ = true mountWithHydration(``, () => h('img', { src: 'http://localhost:3000/b.png' }), ) expect(`Hydration attribute mismatch`).toHaveBeenWarned() } finally { __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ = false } }) test('attr special case: textarea value', () => { mountWithHydration(``, () => h('textarea', { value: 'foo' }), ) mountWithHydration(``, () => h('textarea', { value: '' }), ) expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() mountWithHydration(``, () => h('textarea', { value: 'bar' }), ) expect(`Hydration attribute mismatch`).toHaveBeenWarned() }) // #11873 test('