test(ssr): hydratioon tests (wip)
This commit is contained in:
parent
91269da52c
commit
fb4856b363
167
packages/compiler-core/__tests__/hydration.spec.ts
Normal file
167
packages/compiler-core/__tests__/hydration.spec.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { createSSRApp, h, ref, nextTick, VNode, Portal } from '@vue/runtime-dom'
|
||||
|
||||
function mountWithHydration(html: string, render: () => any) {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = html
|
||||
const app = createSSRApp({
|
||||
render
|
||||
})
|
||||
return {
|
||||
vnode: app.mount(container).$.subTree,
|
||||
container
|
||||
}
|
||||
}
|
||||
|
||||
const triggerEvent = (type: string, el: Element) => {
|
||||
const event = new Event(type)
|
||||
el.dispatchEvent(event)
|
||||
}
|
||||
|
||||
describe('SSR hydration', () => {
|
||||
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('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('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)
|
||||
|
||||
// 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('portal', async () => {
|
||||
const msg = ref('foo')
|
||||
const fn = jest.fn()
|
||||
const portalContainer = document.createElement('div')
|
||||
portalContainer.id = 'portal'
|
||||
portalContainer.innerHTML = `<span>foo</span><span class="foo"></span>`
|
||||
document.body.appendChild(portalContainer)
|
||||
|
||||
const { vnode, container } = mountWithHydration('<!--portal-->', () =>
|
||||
h(Portal, { target: '#portal' }, [
|
||||
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(
|
||||
portalContainer.childNodes[0]
|
||||
)
|
||||
expect((vnode.children as VNode[])[1].el).toBe(
|
||||
portalContainer.childNodes[1]
|
||||
)
|
||||
|
||||
// event handler
|
||||
triggerEvent('click', portalContainer.querySelector('.foo')!)
|
||||
expect(fn).toHaveBeenCalled()
|
||||
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
`<span>bar</span><span class="bar"></span>`
|
||||
)
|
||||
})
|
||||
|
||||
test('comment', () => {})
|
||||
|
||||
test('static', () => {})
|
||||
|
||||
// compile SSR + client render fn from the same template & hydrate
|
||||
test('full compiler integration', () => {})
|
||||
|
||||
describe('mismatch handling', () => {
|
||||
test('text', () => {})
|
||||
|
||||
test('not enough children', () => {})
|
||||
|
||||
test('too many children', () => {})
|
||||
|
||||
test('complete mismatch', () => {})
|
||||
})
|
||||
})
|
@ -23,7 +23,7 @@ const enum DOMNodeTypes {
|
||||
COMMENT = 8
|
||||
}
|
||||
|
||||
let hasHydrationMismatch = false
|
||||
let hasMismatch = false
|
||||
|
||||
// Note: hydration is DOM-specific
|
||||
// But we have to place it in core due to tight coupling with core - splitting
|
||||
@ -44,10 +44,10 @@ export function createHydrationFunctions({
|
||||
patch(null, vnode, container)
|
||||
return
|
||||
}
|
||||
hasHydrationMismatch = false
|
||||
hasMismatch = false
|
||||
hydrateNode(container.firstChild!, vnode)
|
||||
flushPostFlushCbs()
|
||||
if (hasHydrationMismatch) {
|
||||
if (hasMismatch && !__TEST__) {
|
||||
// this error should show up in production
|
||||
console.error(`Hydration completed but contains mismatches.`)
|
||||
}
|
||||
@ -70,7 +70,7 @@ export function createHydrationFunctions({
|
||||
return handleMismtach(node, vnode, parentComponent)
|
||||
}
|
||||
if ((node as Text).data !== vnode.children) {
|
||||
hasHydrationMismatch = true
|
||||
hasMismatch = true
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Hydration text mismatch:` +
|
||||
@ -114,12 +114,7 @@ export function createHydrationFunctions({
|
||||
if (domType !== DOMNodeTypes.COMMENT) {
|
||||
return handleMismtach(node, vnode, parentComponent)
|
||||
}
|
||||
hydratePortal(
|
||||
vnode,
|
||||
node.parentNode as Element,
|
||||
parentComponent,
|
||||
optimized
|
||||
)
|
||||
hydratePortal(vnode, parentComponent, optimized)
|
||||
return node.nextSibling
|
||||
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
|
||||
// TODO Suspense
|
||||
@ -180,7 +175,7 @@ export function createHydrationFunctions({
|
||||
optimized || vnode.dynamicChildren !== null
|
||||
)
|
||||
while (next) {
|
||||
hasHydrationMismatch = true
|
||||
hasMismatch = true
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Hydration children mismatch: ` +
|
||||
@ -205,16 +200,17 @@ export function createHydrationFunctions({
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
optimized: boolean
|
||||
): Node | null => {
|
||||
const children = vnode.children as VNode[]
|
||||
optimized = optimized || vnode.dynamicChildren !== null
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const children = vnode.children as VNode[]
|
||||
const l = children.length
|
||||
for (let i = 0; i < l; i++) {
|
||||
const vnode = optimized
|
||||
? children[i]
|
||||
: (children[i] = normalizeVNode(children[i]))
|
||||
if (node) {
|
||||
node = hydrateNode(node, vnode, parentComponent, optimized)
|
||||
} else {
|
||||
hasHydrationMismatch = true
|
||||
hasMismatch = true
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Hydration children mismatch: ` +
|
||||
@ -248,7 +244,6 @@ export function createHydrationFunctions({
|
||||
|
||||
const hydratePortal = (
|
||||
vnode: VNode,
|
||||
container: Element,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
optimized: boolean
|
||||
) => {
|
||||
@ -260,10 +255,15 @@ export function createHydrationFunctions({
|
||||
hydrateChildren(
|
||||
target.firstChild,
|
||||
vnode,
|
||||
container,
|
||||
target,
|
||||
parentComponent,
|
||||
optimized
|
||||
)
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
`Attempting to hydrate portal but target ${targetSelector} does not ` +
|
||||
`exist in server-rendered markup.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,7 +272,7 @@ export function createHydrationFunctions({
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null
|
||||
) => {
|
||||
hasHydrationMismatch = true
|
||||
hasMismatch = true
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Hydration node mismatch:\n- Client vnode:`,
|
||||
|
Loading…
Reference in New Issue
Block a user