]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(ssr): hydration suspense tests
authorEvan You <yyx990803@gmail.com>
Fri, 13 Mar 2020 17:04:44 +0000 (13:04 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 13 Mar 2020 17:05:05 +0000 (13:05 -0400)
packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/vnode.ts

index af332cd1ebf80f466f0ac17f8a16af6fa312d5a7..5d40b4e5d01c20c451100468f05d2474fd595fd6 100644 (file)
@@ -5,7 +5,9 @@ import {
   nextTick,
   VNode,
   Portal,
-  createStaticVNode
+  createStaticVNode,
+  Suspense,
+  onMounted
 } from '@vue/runtime-dom'
 import { renderToString } from '@vue/server-renderer'
 import { mockWarn } from '@vue/shared'
@@ -28,6 +30,8 @@ const triggerEvent = (type: string, el: Element) => {
 }
 
 describe('SSR hydration', () => {
+  mockWarn()
+
   test('text', async () => {
     const msg = ref('foo')
     const { vnode, container } = mountWithHydration('foo', () => msg.value)
@@ -94,7 +98,7 @@ describe('SSR hydration', () => {
     expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
   })
 
-  test('fragment', async () => {
+  test('Fragment', async () => {
     const msg = ref('foo')
     const fn = jest.fn()
     const { vnode, container } = mountWithHydration(
@@ -142,7 +146,7 @@ describe('SSR hydration', () => {
     expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
   })
 
-  test('portal', async () => {
+  test('Portal', async () => {
     const msg = ref('foo')
     const fn = jest.fn()
     const portalContainer = document.createElement('div')
@@ -271,9 +275,109 @@ describe('SSR hydration', () => {
     expect(text.textContent).toBe('bye')
   })
 
-  describe('mismatch handling', () => {
-    mockWarn()
+  test('Suspense', async () => {
+    const AsyncChild = {
+      async setup() {
+        const count = ref(0)
+        return () =>
+          h(
+            'span',
+            {
+              onClick: () => {
+                count.value++
+              }
+            },
+            count.value
+          )
+      }
+    }
+    const { vnode, container } = mountWithHydration('<span>0</span>', () =>
+      h(Suspense, () => h(AsyncChild))
+    )
+    expect(vnode.el).toBe(container.firstChild)
+    // wait for hydration to finish
+    await new Promise(r => setTimeout(r))
+    triggerEvent('click', container.querySelector('span')!)
+    await nextTick()
+    expect(container.innerHTML).toBe(`<span>1</span>`)
+  })
+
+  test('Suspense (full integration)', async () => {
+    const mountedCalls: number[] = []
+    const asyncDeps: Promise<any>[] = []
+
+    const AsyncChild = {
+      async setup(props: { n: number }) {
+        const count = ref(props.n)
+        onMounted(() => {
+          mountedCalls.push(props.n)
+        })
+        const p = new Promise(r => setTimeout(r, props.n * 10))
+        asyncDeps.push(p)
+        await p
+        return () =>
+          h(
+            'span',
+            {
+              onClick: () => {
+                count.value++
+              }
+            },
+            count.value
+          )
+      }
+    }
 
+    const done = jest.fn()
+    const App = {
+      template: `
+      <Suspense @resolve="done">
+        <AsyncChild :n="1" />
+        <AsyncChild :n="2" />
+      </Suspense>`,
+      components: {
+        AsyncChild
+      },
+      methods: {
+        done
+      }
+    }
+
+    const container = document.createElement('div')
+    // server render
+    container.innerHTML = await renderToString(h(App))
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<!--[--><span>1</span><span>2</span><!--]-->"`
+    )
+    // reset asyncDeps from ssr
+    asyncDeps.length = 0
+    // hydrate
+    createSSRApp(App).mount(container)
+
+    expect(mountedCalls.length).toBe(0)
+    expect(asyncDeps.length).toBe(2)
+
+    // wait for hydration to complete
+    await Promise.all(asyncDeps)
+    await new Promise(r => setTimeout(r))
+
+    // should flush buffered effects
+    expect(mountedCalls).toMatchObject([1, 2])
+    // should have removed fragment markers
+    expect(container.innerHTML).toMatch(`<span>1</span><span>2</span>`)
+
+    const span1 = container.querySelector('span')!
+    triggerEvent('click', span1)
+    await nextTick()
+    expect(container.innerHTML).toMatch(`<span>2</span><span>2</span>`)
+
+    const span2 = span1.nextSibling as Element
+    triggerEvent('click', span2)
+    await nextTick()
+    expect(container.innerHTML).toMatch(`<span>2</span><span>3</span>`)
+  })
+
+  describe('mismatch handling', () => {
     test('text node', () => {
       const { container } = mountWithHydration(`foo`, () => 'bar')
       expect(container.textContent).toBe('bar')
index 5caa282b4012f94d6123e4d022ee0e55f8183706..c0ee245edb1d381d186a2918fffee44bbc3453a5 100644 (file)
@@ -47,7 +47,6 @@ export function createHydrationFunctions(
   const {
     mt: mountComponent,
     p: patch,
-    n: next,
     o: { patchProp, nextSibling, parentNode }
   } = rendererInternals
 
@@ -152,16 +151,12 @@ export function createHydrationFunctions(
             parentSuspense,
             isSVGContainer(container)
           )
-          const subTree = vnode.component!.subTree
-          if (subTree) {
-            return next(subTree)
-          } else {
-            // no subTree means this is an async component
-            // try to locate the ending node
-            return isFragmentStart
-              ? locateClosingAsyncAnchor(node)
-              : nextSibling(node)
-          }
+          // component may be async, so in the case of fragments we cannot rely
+          // on component's rendered output to determine the end of the fragment
+          // instead, we do a lookahead to find the end anchor node.
+          return isFragmentStart
+            ? locateClosingAsyncAnchor(node)
+            : nextSibling(node)
         } else if (shapeFlag & ShapeFlags.PORTAL) {
           if (domType !== DOMNodeTypes.COMMENT) {
             return handleMismtach(node, vnode, parentComponent, parentSuspense)
index 775ca7cc95be6dc345bae1dc35b913f79f6c061a..df54210e34261e49e098471de134b6260183b648 100644 (file)
@@ -212,7 +212,6 @@ export function createVNode(
 ): VNode {
   if (!type) {
     if (__DEV__) {
-      debugger
       warn(`fsef Invalid vnode type when creating vnode: ${type}.`)
     }
     type = Comment