]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(ssr): hydratioon tests (wip)
authorEvan You <yyx990803@gmail.com>
Wed, 4 Mar 2020 23:06:50 +0000 (17:06 -0600)
committerEvan You <yyx990803@gmail.com>
Wed, 4 Mar 2020 23:06:50 +0000 (17:06 -0600)
packages/compiler-core/__tests__/hydration.spec.ts [new file with mode: 0644]
packages/runtime-core/src/hydration.ts

diff --git a/packages/compiler-core/__tests__/hydration.spec.ts b/packages/compiler-core/__tests__/hydration.spec.ts
new file mode 100644 (file)
index 0000000..af7c632
--- /dev/null
@@ -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', () => {})
+  })
+})
index 061f5e05d966b592daa47fc88d5e64dc843008eb..7091f65db52b7b8f564679287391b77de8d8ff90 100644 (file)
@@ -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:`,