--- /dev/null
+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', () => {})
+ })
+})
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
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.`)
}
return handleMismtach(node, vnode, parentComponent)
}
if ((node as Text).data !== vnode.children) {
- hasHydrationMismatch = true
+ hasMismatch = true
__DEV__ &&
warn(
`Hydration text mismatch:` +
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
optimized || vnode.dynamicChildren !== null
)
while (next) {
- hasHydrationMismatch = true
+ hasMismatch = true
__DEV__ &&
warn(
`Hydration children mismatch: ` +
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: ` +
const hydratePortal = (
vnode: VNode,
- container: Element,
parentComponent: ComponentInternalInstance | null,
optimized: boolean
) => {
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.`
+ )
}
}
vnode: VNode,
parentComponent: ComponentInternalInstance | null
) => {
- hasHydrationMismatch = true
+ hasMismatch = true
__DEV__ &&
warn(
`Hydration node mismatch:\n- Client vnode:`,