]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: test hydrate v-if in PROD
authordaiwei <daiwei521@126.com>
Fri, 25 Apr 2025 01:43:15 +0000 (09:43 +0800)
committerdaiwei <daiwei521@126.com>
Fri, 25 Apr 2025 02:26:42 +0000 (10:26 +0800)
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/dom/node.ts
packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts
packages/server-renderer/__tests__/ssrSlot.spec.ts

index f67a4c4202e123575454f84594925b1ecce70649..f1fd4760d4fb55b27f14e0538be9213e010fad6a 100644 (file)
@@ -78,6 +78,15 @@ const triggerEvent = (type: string, el: Element) => {
   el.dispatchEvent(event)
 }
 
+async function runWithEnv(isProd: boolean, fn: () => Promise<void>) {
+  if (isProd) __DEV__ = false
+  try {
+    await fn()
+  } finally {
+    if (isProd) __DEV__ = true
+  }
+}
+
 describe('Vapor Mode hydration', () => {
   delegateEvents('click')
 
@@ -639,188 +648,306 @@ describe('Vapor Mode hydration', () => {
   })
 
   describe('if', () => {
-    test('basic toggle - true -> false', async () => {
-      const data = ref(true)
-      const { container } = await testHydration(
-        `<template>
-          <div v-if="data">foo</div>
-        </template>`,
-        undefined,
-        data,
-      )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div>foo</div><!--if-->"`,
-      )
-
-      data.value = false
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
-    })
-
-    test('basic toggle - false -> true', async () => {
-      const data = ref(false)
-      const { container } = await testHydration(
-        `<template>
-          <div v-if="data">foo</div>
-        </template>`,
-        undefined,
-        data,
-      )
-      expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
-
-      data.value = true
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div>foo</div><!--if-->"`,
-      )
-    })
-
-    test('v-if/else-if/else chain - switch branches', async () => {
-      const data = ref('a')
-      const { container } = await testHydration(
-        `<template>
-            <div v-if="data === 'a'">foo</div>
-            <div v-else-if="data === 'b'">bar</div>
-            <div v-else>baz</div>
-          </template>`,
-        undefined,
-        data,
-      )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div>foo</div><!--if-->"`,
-      )
-
-      data.value = 'b'
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div>bar</div><!--if--><!--if-->"`,
-      )
-
-      data.value = 'c'
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div>baz</div><!--if--><!--if-->"`,
-      )
+    describe('DEV mode', () => {
+      runTests()
     })
 
-    test('nested if', async () => {
-      const data = reactive({ outer: true, inner: true })
-      const { container } = await testHydration(
-        `<template>
-          <div v-if="data.outer">
-            <span>outer</span>
-            <div v-if="data.inner">inner</div>
-          </div>
-        </template>`,
-        undefined,
-        data,
-      )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span>outer</span><div>inner</div><!--if--></div><!--if-->"`,
-      )
-
-      data.inner = false
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span>outer</span><!--if--></div><!--if-->"`,
-      )
-
-      data.outer = false
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
+    describe('PROD mode', () => {
+      runTests(true)
     })
 
-    test('on component', async () => {
-      const data = ref(true)
-      const { container } = await testHydration(
-        `<template>
-          <components.Child v-if="data"/>
-        </template>`,
-        { Child: `<template>foo</template>` },
-        data,
-      )
-      expect(container.innerHTML).toMatchInlineSnapshot(`"foo<!--if-->"`)
-
-      data.value = false
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
-    })
-
-    test('on component with anchor insertion', async () => {
-      const data = ref(true)
-      const { container } = await testHydration(
-        `<template>
-          <div>
-            <span/>
-            <components.Child v-if="data"/>
-            <span/>
-          </div>
-        </template>`,
-        { Child: `<template>foo</template>` },
-        data,
-      )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span>foo<!--if--><span></span></div>"`,
-      )
-
-      data.value = false
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--if--><span></span></div>"`,
-      )
-    })
-
-    test('consecutive v-if on component with anchor insertion', async () => {
-      const data = ref(true)
-      const { container } = await testHydration(
-        `<template>
-          <div>
-            <span/>
-            <components.Child v-if="data"/>
-            <components.Child v-if="data"/>
-            <span/>
-          </div>
-        </template>`,
-        { Child: `<template>foo</template>` },
-        data,
-      )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span>foo<!--if-->foo<!--if--><span></span></div>"`,
-      )
-
-      data.value = false
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--if--><!--if--><span></span></div>"`,
-      )
-    })
-
-    test('consecutive v-if on fragment component with anchor insertion', async () => {
-      const data = ref(true)
-      const { container } = await testHydration(
-        `<template>
-            <div>
-              <span/>
-              <components.Child v-if="data"/>
+    function runTests(isProd: boolean = false) {
+      const anchorLabel = isProd ? '$' : 'if'
+
+      test('basic toggle - true -> false', async () => {
+        runWithEnv(isProd, async () => {
+          const data = ref(true)
+          const { container } = await testHydration(
+            `<template>
+              <div v-if="data">foo</div>
+            </template>`,
+            undefined,
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<div>foo</div><!--${anchorLabel}-->`,
+          )
+
+          data.value = false
+          await nextTick()
+          expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+        })
+      })
+
+      test('basic toggle - false -> true', async () => {
+        runWithEnv(isProd, async () => {
+          // v-if="false" is rendered as <!----> in the server-rendered HTML
+          // it reused as anchor, so the anchor label is empty in PROD
+          let anchorLabel = isProd ? '' : 'if'
+          if (isProd) __DEV__ = false
+          const data = ref(false)
+          const { container } = await testHydration(
+            `<template>
+              <div v-if="data">foo</div>
+            </template>`,
+            undefined,
+            data,
+          )
+          expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+
+          data.value = true
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>foo</div><!--${anchorLabel}-->`,
+          )
+        })
+      })
+
+      test('v-if/else-if/else chain - switch branches', async () => {
+        runWithEnv(isProd, async () => {
+          const data = ref('a')
+          const { container } = await testHydration(
+            `<template>
+              <div v-if="data === 'a'">foo</div>
+              <div v-else-if="data === 'b'">bar</div>
+              <div v-else>baz</div>
+            </template>`,
+            undefined,
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<div>foo</div><!--${anchorLabel}-->`,
+          )
+
+          data.value = 'b'
+          await nextTick()
+          // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node,
+          // so it won't be rendered
+          expect(container.innerHTML).toBe(
+            `<div>bar</div><!--${anchorLabel}-->${isProd ? '' : `<!--${anchorLabel}-->`}`,
+          )
+
+          data.value = 'c'
+          await nextTick()
+          // same as above
+          expect(container.innerHTML).toBe(
+            `<div>baz</div><!--${anchorLabel}-->${isProd ? '' : `<!--${anchorLabel}-->`}`,
+          )
+
+          data.value = 'a'
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>foo</div><!--${anchorLabel}-->`,
+          )
+        })
+      })
+
+      test('nested if', async () => {
+        runWithEnv(isProd, async () => {
+          const data = reactive({ outer: true, inner: true })
+          const { container } = await testHydration(
+            `<template>
+              <div v-if="data.outer">
+                <span>outer</span>
+                <div v-if="data.inner">inner</div>
+              </div>
+            </template>`,
+            undefined,
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span>outer</span>` +
+              `<div>inner</div><!--${anchorLabel}-->` +
+              `</div><!--${anchorLabel}-->`,
+          )
+
+          data.inner = false
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span>outer</span>` +
+              `<!--${anchorLabel}-->` +
+              `</div><!--${anchorLabel}-->`,
+          )
+
+          data.outer = false
+          await nextTick()
+          expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+        })
+      })
+
+      test('on component', async () => {
+        runWithEnv(isProd, async () => {
+          const data = ref(true)
+          const { container } = await testHydration(
+            `<template>
               <components.Child v-if="data"/>
-              <span/>
-            </div>
-          </template>`,
-        {
-          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
-        },
-        data,
-      )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--[--><div>true</div>-true-<!--]--><!--if--><!--[--><div>true</div>-true-<!--]--><!--if--><span></span></div>"`,
-      )
-
-      data.value = false
-      await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--[--><!--]--><!--if--><!--[--><!--]--><!--if--><span></span></div>"`,
-      )
-    })
+            </template>`,
+            { Child: `<template>foo</template>` },
+            data,
+          )
+          expect(container.innerHTML).toBe(`foo<!--${anchorLabel}-->`)
+
+          data.value = false
+          await nextTick()
+          expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+        })
+      })
+
+      test('v-if/else-if/else chain on component - switch branches', async () => {
+        runWithEnv(isProd, async () => {
+          const data = ref('a')
+          const { container } = await testHydration(
+            `<template>
+              <components.Child1 v-if="data === 'a'"/>
+              <components.Child2 v-else-if="data === 'b'"/>
+              <components.Child3 v-else/>
+            </template>`,
+            {
+              Child1: `<template><span>{{data}} child1</span></template>`,
+              Child2: `<template><span>{{data}} child2</span></template>`,
+              Child3: `<template><span>{{data}} child3</span></template>`,
+            },
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<span>a child1</span><!--${anchorLabel}-->`,
+          )
+
+          data.value = 'b'
+          await nextTick()
+          // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node,
+          // so it won't be rendered
+          expect(container.innerHTML).toBe(
+            `<span>b child2</span><!--${anchorLabel}-->${isProd ? '' : `<!--${anchorLabel}-->`}`,
+          )
+
+          data.value = 'c'
+          await nextTick()
+          // same as above
+          expect(container.innerHTML).toBe(
+            `<span>c child3</span><!--${anchorLabel}-->${isProd ? '' : `<!--${anchorLabel}-->`}`,
+          )
+
+          data.value = 'a'
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<span>a child1</span><!--${anchorLabel}-->`,
+          )
+        })
+      })
+
+      test('on component with anchor insertion', async () => {
+        runWithEnv(isProd, async () => {
+          const data = ref(true)
+          const { container } = await testHydration(
+            `<template>
+              <div>
+                <span/>
+                <components.Child v-if="data"/>
+                <span/>
+              </div>
+            </template>`,
+            { Child: `<template>foo</template>` },
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `foo<!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+
+          data.value = false
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+        })
+      })
+
+      test('consecutive v-if on component with anchor insertion', async () => {
+        runWithEnv(isProd, async () => {
+          const data = ref(true)
+          const { container } = await testHydration(
+            `<template>
+              <div>
+                <span/>
+                <components.Child v-if="data"/>
+                <components.Child v-if="data"/>
+                <span/>
+              </div>
+            </template>`,
+            { Child: `<template>foo</template>` },
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `foo<!--${anchorLabel}-->` +
+              `foo<!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+
+          data.value = false
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<!--${anchorLabel}-->` +
+              `<!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+        })
+      })
+
+      test('consecutive v-if on fragment component with anchor insertion', async () => {
+        runWithEnv(isProd, async () => {
+          const data = ref(true)
+          const { container } = await testHydration(
+            `<template>
+              <div>
+                <span/>
+                <components.Child v-if="data"/>
+                <components.Child v-if="data"/>
+                <span/>
+              </div>
+            </template>`,
+            {
+              Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+            },
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<!--[--><div>true</div>-true-<!--]--><!--${anchorLabel}-->` +
+              `<!--[--><div>true</div>-true-<!--]--><!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+
+          data.value = false
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<!--[--><!--]--><!--${anchorLabel}-->` +
+              `<!--[--><!--]--><!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+        })
+      })
+    }
   })
 
   test.todo('for')
index 36d0bc387b7ef29a9990f5d8120cd935459879ca..15128d89df24c46efe30b9ac854dfe22b10b3671 100644 (file)
@@ -5,7 +5,7 @@ import {
   mountComponent,
   unmountComponent,
 } from './component'
-import { createComment, createTextNode, next } from './dom/node'
+import { createComment, createTextNode, nextSiblingAnchor } from './dom/node'
 import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
 import { currentHydrationNode, isComment, isHydrating } from './dom/hydration'
 import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom'
@@ -88,8 +88,9 @@ export class DynamicFragment extends VaporFragment {
     if (isComment(currentHydrationNode!, '')) {
       this.anchor = currentHydrationNode
     } else {
-      const anchor = next(currentHydrationNode!)
-      if (isDynamicFragmentEndAnchor(anchor)) {
+      // find next sibling `<!--$-->` as anchor
+      const anchor = nextSiblingAnchor(currentHydrationNode!, '$')!
+      if (anchor && isDynamicFragmentEndAnchor(anchor)) {
         this.anchor = anchor
       } else if (__DEV__) {
         // TODO warning
index 0740c1f7c9218d6fe214d55c3d56c6d97e0a8ee7..1ea4831f3634e74d3bce89f77f28248b54224e57 100644 (file)
@@ -46,16 +46,8 @@ function _next(node: Node): Node {
 }
 
 /*! #__NO_SIDE_EFFECTS__ */
-function __next(node: Node): Node {
-  // treat dynamic node (<!--[[-->...<!--]]-->) as a single node
-  if (isComment(node, '[[')) {
-    node = locateEndAnchor(node, '[[', ']]')!
-  }
-
-  // treat dynamic node (<!--[-->...<!--]-->) as a single node
-  else if (isComment(node, '[')) {
-    node = locateEndAnchor(node)!
-  }
+export function __next(node: Node): Node {
+  node = handleWrappedNode(node)
 
   let n = node.nextSibling!
   while (n && isNonHydrationNode(n)) {
@@ -109,12 +101,12 @@ export function disableHydrationNodeLookup(): void {
 
 /*! #__NO_SIDE_EFFECTS__ */
 export function prev(node: Node): Node | null {
-  // treat dynamic node (<!--[[-->...<!--]]-->) as a single node
+  // process dynamic node (<!--[[-->...<!--]]-->) as a single one
   if (isComment(node, ']]')) {
     node = locateStartAnchor(node, '[[', ']]')!
   }
 
-  // treat dynamic node (<!--[-->...<!--]-->) as a single node
+  // process fragment node (<!--[-->...<!--]-->) as a single one
   else if (isComment(node, ']')) {
     node = locateStartAnchor(node)!
   }
@@ -134,20 +126,45 @@ function isNonHydrationNode(node: Node) {
     isDynamicAnchor(node) ||
     // fragment end anchor (`<!--]-->`)
     isComment(node, ']') ||
-    isDynamicFragmentAnchor(node)
-  )
-}
-
-function isDynamicFragmentAnchor(node: Node) {
-  return __DEV__
-    ? // v-if anchor (`<!--if-->`)
-      isComment(node, 'if') ||
+    // dynamic fragment anchors
+    (__DEV__
+      ? // v-if anchor (`<!--if-->`)
+        isComment(node, 'if') ||
         // v-for anchor (`<!--for-->`)
         isComment(node, 'for') ||
         // v-slot anchor (`<!--slot-->`)
         isComment(node, 'slot') ||
         // dynamic-component anchor (`<!--dynamic-component-->`)
         isComment(node, 'dynamic-component')
-    : // TODO ?
-      isComment(node, '$')
+      : isComment(node, '$'))
+  )
+}
+
+export function nextSiblingAnchor(
+  node: Node,
+  anchorLabel: string,
+): Comment | null {
+  node = handleWrappedNode(node)
+
+  let n = node.nextSibling
+  while (n) {
+    if (isComment(n, anchorLabel)) return n
+    n = n.nextSibling
+  }
+
+  return null
+}
+
+function handleWrappedNode(node: Node): Node {
+  // process dynamic node (<!--[[-->...<!--]]-->) as a single one
+  if (isComment(node, '[[')) {
+    return locateEndAnchor(node, '[[', ']]')!
+  }
+
+  // process fragment (<!--[-->...<!--]-->) as a single one
+  else if (isComment(node, '[')) {
+    return locateEndAnchor(node)!
+  }
+
+  return node
 }
index e8cfa75e77c58e03435bf9452a4110bbb42199fd..471a48edbdd0eba59dee91c71a51fe4a1bc8cbb8 100644 (file)
@@ -25,7 +25,7 @@ describe('ssr: attr fallthrough', () => {
       template: `<child :ok="ok" class="bar"/>`,
     }
     expect(await renderToString(createApp(Parent, { ok: true }))).toBe(
-      `<div class="foo bar"></div>`,
+      `<div class="foo bar"></div><!--$-->`,
     )
     expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
       `<span class="bar"></span>`,
index 02872274ab60736940175deb199dc03bb25a018e..7f104e4246a3c1bb5585048bbfc5ae2d23fcc961 100644 (file)
@@ -94,7 +94,7 @@ describe('ssr: slot', () => {
           template: `<one><template v-if="true">hello</template></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><!--[-->hello<!--]--><!--]--></div>`)
+    ).toBe(`<div><!--[--><!--[-->hello<!--]--><!--$--><!--]--></div>`)
   })
 
   test('fragment slot (template v-if + multiple elements)', async () => {
@@ -106,7 +106,7 @@ describe('ssr: slot', () => {
         }),
       ),
     ).toBe(
-      `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--]--></div>`,
+      `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--$--><!--]--></div>`,
     )
   })
 
@@ -135,7 +135,7 @@ describe('ssr: slot', () => {
           template: `<one><div v-if="true">foo</div></one>`,
         }),
       ),
-    ).toBe(`<div>foo</div>`)
+    ).toBe(`<div>foo</div><!--$-->`)
   })
 
   // #9933