From: Evan You Date: Wed, 4 Mar 2020 23:06:50 +0000 (-0600) Subject: test(ssr): hydratioon tests (wip) X-Git-Tag: v3.0.0-alpha.8~14 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=fb4856b36375fcf3eecaf89f260b272052a0b432;p=thirdparty%2Fvuejs%2Fcore.git test(ssr): hydratioon tests (wip) --- diff --git a/packages/compiler-core/__tests__/hydration.spec.ts b/packages/compiler-core/__tests__/hydration.spec.ts new file mode 100644 index 0000000000..af7c632c37 --- /dev/null +++ b/packages/compiler-core/__tests__/hydration.spec.ts @@ -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( + '
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
`) + }) + + test('element with elements children', async () => { + const msg = ref('foo') + const fn = jest.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('fragment', async () => { + const msg = ref('foo') + const fn = jest.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) + + // 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`) + }) + + test('portal', async () => { + const msg = ref('foo') + const fn = jest.fn() + const portalContainer = document.createElement('div') + portalContainer.id = 'portal' + portalContainer.innerHTML = `foo` + document.body.appendChild(portalContainer) + + const { vnode, container } = mountWithHydration('', () => + 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( + `bar` + ) + }) + + 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', () => {}) + }) +}) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 061f5e05d9..7091f65db5 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -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:`,