vue3-yuanma/packages/runtime-core/__tests__/hydration.spec.ts
2022-06-06 16:45:24 +08:00

1076 lines
32 KiB
TypeScript

import {
createSSRApp,
h,
ref,
nextTick,
VNode,
Teleport,
createStaticVNode,
Suspense,
onMounted,
defineAsyncComponent,
defineComponent,
createTextVNode,
createVNode,
withDirectives,
vModelCheckbox,
renderSlot
} from '@vue/runtime-dom'
import { renderToString, SSRContext } from '@vue/server-renderer'
import { PatchFlags } from '../../shared/src'
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<Node, Element> & {
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('<div></div>', () =>
h('div', createTextVNode(''))
)
expect(container.textContent).toBe('')
expect(`Hydration children mismatch in <div>`).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 = '<div><span>hello</span></div>'
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 = '<div></div><span>hello</span>'
const html = `<div><div>hi</div>` + staticContent + `<div>ho</div></div>`
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 <span>foo</span> B`
const { vnode, container } = mountWithHydration(html, () =>
createStaticVNode(` A <span>foo</span> 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 <span>foo</span> 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(
'<div class="foo">foo</div>',
() => 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(`<div class="bar">bar</div>`)
})
test('element with elements children', async () => {
const msg = ref('foo')
const fn = jest.fn()
const { vnode, container } = mountWithHydration(
'<div><span>foo</span><span class="foo"></span></div>',
() =>
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(`<span>bar</span><span class="bar"></span>`)
})
test('element with ref', () => {
const el = ref()
const { vnode, container } = mountWithHydration('<div></div>', () =>
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 = jest.fn()
const { vnode, container } = mountWithHydration(
'<div><!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]--></div>',
() =>
h('div', [
[h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]]
])
)
expect(vnode.el).toBe(container.firstChild)
expect(vnode.el.innerHTML).toBe(
`<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`
)
// 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 <span>
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 <span>
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(
`<!--[--><span>bar</span><!--[--><span class="bar"></span><!--]--><!--]-->`
)
})
test('Teleport', async () => {
const msg = ref('foo')
const fn = jest.fn()
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport'
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(
'<!--teleport start--><!--teleport end-->',
() =>
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.children as VNode[])[0].el).toBe(
teleportContainer.childNodes[0]
)
expect((vnode.children as VNode[])[1].el).toBe(
teleportContainer.childNodes[1]
)
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
// event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
expect(fn).toHaveBeenCalled()
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toBe(
`<span>bar</span><span class="bar"></span><!--teleport anchor-->`
)
})
test('Teleport (multiple + integration)', async () => {
const msg = ref('foo')
const fn1 = jest.fn()
const fn2 = jest.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(
`"<!--[--><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--><!--]-->"`
)
const teleportHtml = ctx.teleports!['#teleport2']
expect(teleportHtml).toMatchInlineSnapshot(
`"<span>foo</span><span class=\\"foo\\"></span><!--teleport anchor--><span>foo2</span><span class=\\"foo2\\"></span><!--teleport anchor-->"`
)
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 as any).children[0].el).toBe(
teleportContainer.childNodes[0]
)
expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
expect(teleportVnode2.target).toBe(teleportContainer)
expect((teleportVnode2 as any).children[0].el).toBe(
teleportContainer.childNodes[3]
)
expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
// // 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(
`"<span>bar</span><span class=\\"bar\\"></span><!--teleport anchor--><span>bar2</span><span class=\\"bar2\\"></span><!--teleport anchor-->"`
)
})
test('Teleport (disabled)', async () => {
const msg = ref('foo')
const fn1 = jest.fn()
const fn2 = jest.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(
`"<!--[--><div>foo</div><!--teleport start--><span>foo</span><span class=\\"foo\\"></span><!--teleport end--><div class=\\"foo2\\">bar</div><!--]-->"`
)
const teleportHtml = ctx.teleports!['#teleport3']
expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
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.targetAnchor).toBe(teleportContainer.childNodes[0])
// // 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(
`"<!--[--><div>foo</div><!--teleport start--><span>bar</span><span class=\\"bar\\"></span><!--teleport end--><div class=\\"bar2\\">bar</div><!--]-->"`
)
})
test('Teleport (as component root)', () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport4'
teleportContainer.innerHTML = `hello<!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const wrapper = {
render() {
return h(Teleport, { to: '#teleport4' }, ['hello'])
}
}
const { vnode, container } = mountWithHydration(
'<div><!--teleport start--><!--teleport end--><div></div></div>',
() => 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 = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(
'<!--teleport start--><!--teleport end-->',
() =>
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.firstChild
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
)
})
// compile SSR + client render fn from the same template & hydrate
test('full compiler integration', async () => {
const mounted: string[] = []
const log = jest.fn()
const toggle = ref(true)
const Child = {
data() {
return {
count: 0,
text: 'hello',
style: {
color: 'red'
}
}
},
mounted() {
mounted.push('child')
},
template: `
<div>
<span class="count" :style="style">{{ count }}</span>
<button class="inc" @click="count++">inc</button>
<button class="change" @click="style.color = 'green'" >change color</button>
<button class="emit" @click="$emit('foo')">emit</button>
<span class="text">{{ text }}</span>
<input v-model="text">
</div>
`
}
const App = {
setup() {
return { toggle }
},
mounted() {
mounted.push('parent')
},
template: `
<div>
<span>hello</span>
<template v-if="toggle">
<Child @foo="log('child')"/>
<template v-if="true">
<button class="parent-click" @click="log('click')">click me</button>
</template>
</template>
<span>hello</span>
</div>`,
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: `
<div>
<button class="parent-click" @click="throwError">click me</button>
</div>`
}
const container = document.createElement('div')
// server render
container.innerHTML = await renderToString(h(App))
// hydrate
const app = createSSRApp(App)
const handler = (app.config.errorHandler = jest.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: `
<div>
<input class="parent-click" @blur="throwError"/>
</div>`
}
const container = document.createElement('div')
// server render
container.innerHTML = await renderToString(h(App))
// hydrate
const app = createSSRApp(App)
const handler = (app.config.errorHandler = jest.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('<span>0</span>', () =>
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(`<span>1</span>`)
})
test('Suspense (full integration)', async () => {
const mountedCalls: number[] = []
const asyncDeps: Promise<any>[] = []
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 = jest.fn()
const App = {
template: `
<Suspense @resolve="done">
<div>
<AsyncChild :n="1" />
<AsyncChild :n="2" />
</div>
</Suspense>`,
components: {
AsyncChild
},
methods: {
done
}
}
const container = document.createElement('div')
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span>1</span><span>2</span></div>"`
)
// 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(
`<div><span>1</span><span>2</span></div>`
)
const span1 = container.querySelector('span')!
triggerEvent('click', span1)
await nextTick()
expect(container.innerHTML).toMatch(
`<div><span>2</span><span>2</span></div>`
)
const span2 = span1.nextSibling as Element
triggerEvent('click', span2)
await nextTick()
expect(container.innerHTML).toMatch(
`<div><span>2</span><span>3</span></div>`
)
})
test('async component', async () => {
const spy = jest.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(
`"<!--[-->hello<button>hello!</button>world<!--]-->"`
)
// 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 bol = ref(true)
const App = {
setup() {
onMounted(() => {
// change state, this makes updateComponent(AsyncComp) execute before
// the async component is resolved
bol.value = false
})
return () => {
return [bol.value ? 'hello' : 'world', h(AsyncComp)]
}
}
}
// server render
const htmlPromise = renderToString(h(App))
serverResolve(Comp)
const html = await htmlPromise
expect(html).toMatchInlineSnapshot(
`"<!--[-->hello<h1>Async component</h1><!--]-->"`
)
// 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<h1>Async component</h1><!--]-->"`
)
})
// #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 = '<div><div>async</div></div>'
createSSRApp({
render() {
return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
}
}).mount(root)
show.value = false
await nextTick()
expect(root.innerHTML).toBe('<div><div>hi</div></div>')
resolve({})
})
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 = '<div><!--[-->async<!--]--></div>'
createSSRApp({
render() {
return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
}
}).mount(root)
show.value = false
await nextTick()
expect(root.innerHTML).toBe('<div><div>hi</div></div>')
resolve({})
})
test('elements with camel-case in svg ', () => {
const { vnode, container } = mountWithHydration(
'<animateTransform></animateTransform>',
() => 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 = '<g></g>'
const app = createSSRApp({
render: () => h('g')
})
expect(
(
app.mount(svgContainer).$.subTree as VNode<Node, Element> & {
el: Element
}
).el instanceof SVGElement
)
})
test('force hydrate input v-model with non-string value bindings', () => {
const { container } = mountWithHydration(
'<input type="checkbox" value="true">',
() =>
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 select option with non-string value bindings', () => {
const { container } = mountWithHydration(
'<select><option :value="true">ok</option></select>',
() =>
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)
})
// #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)
})
test('app.unmount()', async () => {
const container = document.createElement('DIV')
container.innerHTML = '<button></button>'
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)
})
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(`<div>foo</div>`, () =>
h('div', 'bar')
)
expect(container.innerHTML).toBe('<div>bar</div>')
expect(`Hydration text content mismatch in <div>`).toHaveBeenWarned()
})
test('not enough children', () => {
const { container } = mountWithHydration(`<div></div>`, () =>
h('div', [h('span', 'foo'), h('span', 'bar')])
)
expect(container.innerHTML).toBe(
'<div><span>foo</span><span>bar</span></div>'
)
expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
})
test('too many children', () => {
const { container } = mountWithHydration(
`<div><span>foo</span><span>bar</span></div>`,
() => h('div', [h('span', 'foo')])
)
expect(container.innerHTML).toBe('<div><span>foo</span></div>')
expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
})
test('complete mismatch', () => {
const { container } = mountWithHydration(
`<div><span>foo</span><span>bar</span></div>`,
() => h('div', [h('div', 'foo'), h('p', 'bar')])
)
expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
})
test('fragment mismatch removal', () => {
const { container } = mountWithHydration(
`<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
() => h('div', [h('span', 'replaced')])
)
expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
expect(`Hydration node mismatch`).toHaveBeenWarned()
})
test('fragment not enough children', () => {
const { container } = mountWithHydration(
`<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
() => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')])
)
expect(container.innerHTML).toBe(
'<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>'
)
expect(`Hydration node mismatch`).toHaveBeenWarned()
})
test('fragment too many children', () => {
const { container } = mountWithHydration(
`<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
() => h('div', [[h('div', 'foo')], h('div', 'baz')])
)
expect(container.innerHTML).toBe(
'<div><!--[--><div>foo</div><!--]--><div>baz</div></div>'
)
// fragment ends early and attempts to hydrate the extra <div>bar</div>
// 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('<!--teleport start--><!--teleport end-->', () =>
h(Teleport, { to: '#teleport' }, [h('span', 'value')])
)
expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
expect(`Hydration children mismatch`).toHaveBeenWarned()
})
})
})