test(ssr): test for hydration mismatch handling

This commit is contained in:
Evan You 2020-03-06 15:39:54 -05:00
parent f7a026109d
commit dd2d25fee1
2 changed files with 65 additions and 10 deletions

View File

@ -8,6 +8,7 @@ import {
createStaticVNode createStaticVNode
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { renderToString } from '@vue/server-renderer' import { renderToString } from '@vue/server-renderer'
import { mockWarn } from '@vue/shared'
function mountWithHydration(html: string, render: () => any) { function mountWithHydration(html: string, render: () => any) {
const container = document.createElement('div') const container = document.createElement('div')
@ -268,12 +269,48 @@ describe('SSR hydration', () => {
}) })
describe('mismatch handling', () => { describe('mismatch handling', () => {
test('text', () => {}) mockWarn()
test('not enough children', () => {}) test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar')
expect(container.textContent).toBe('bar')
expect(`Hydration text mismatch`).toHaveBeenWarned()
})
test('too many children', () => {}) 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('complete mismatch', () => {}) 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)
})
}) })
}) })

View File

@ -94,7 +94,10 @@ export function createHydrationFunctions({
return hydrateFragment(node, vnode, parentComponent, optimized) return hydrateFragment(node, vnode, parentComponent, optimized)
default: default:
if (shapeFlag & ShapeFlags.ELEMENT) { if (shapeFlag & ShapeFlags.ELEMENT) {
if (domType !== DOMNodeTypes.ELEMENT) { if (
domType !== DOMNodeTypes.ELEMENT ||
vnode.type !== (node as Element).tagName.toLowerCase()
) {
return handleMismtach(node, vnode, parentComponent) return handleMismtach(node, vnode, parentComponent)
} }
return hydrateElement( return hydrateElement(
@ -176,22 +179,34 @@ export function createHydrationFunctions({
parentComponent, parentComponent,
optimized optimized
) )
let hasWarned = false
while (next) { while (next) {
hasMismatch = true hasMismatch = true
__DEV__ && if (__DEV__ && !hasWarned) {
warn( warn(
`Hydration children mismatch: ` + `Hydration children mismatch in <${vnode.type as string}>: ` +
`server rendered element contains more child nodes than client vdom.` `server rendered element contains more child nodes than client vdom.`
) )
hasWarned = true
}
// The SSRed DOM contains more nodes than it should. Remove them. // The SSRed DOM contains more nodes than it should. Remove them.
const cur = next const cur = next
next = next.nextSibling next = next.nextSibling
el.removeChild(cur) el.removeChild(cur)
} }
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (el.textContent !== vnode.children) {
hasMismatch = true
__DEV__ &&
warn(
`Hydration text content mismatch in <${vnode.type as string}>:\n` +
`- Client: ${el.textContent}\n` +
`- Server: ${vnode.children as string}`
)
el.textContent = vnode.children as string el.textContent = vnode.children as string
} }
} }
}
return el.nextSibling return el.nextSibling
} }
@ -205,6 +220,7 @@ export function createHydrationFunctions({
optimized = optimized || vnode.dynamicChildren !== null optimized = optimized || vnode.dynamicChildren !== null
const children = vnode.children as VNode[] const children = vnode.children as VNode[]
const l = children.length const l = children.length
let hasWarned = false
for (let i = 0; i < l; i++) { for (let i = 0; i < l; i++) {
const vnode = optimized const vnode = optimized
? children[i] ? children[i]
@ -213,11 +229,13 @@ export function createHydrationFunctions({
node = hydrateNode(node, vnode, parentComponent, optimized) node = hydrateNode(node, vnode, parentComponent, optimized)
} else { } else {
hasMismatch = true hasMismatch = true
__DEV__ && if (__DEV__ && !hasWarned) {
warn( warn(
`Hydration children mismatch: ` + `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
`server rendered element contains fewer child nodes than client vdom.` `server rendered element contains fewer child nodes than client vdom.`
) )
hasWarned = true
}
// the SSRed DOM didn't contain enough nodes. Mount the missing ones. // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
patch(null, vnode, container) patch(null, vnode, container)
} }