Sfoglia il codice sorgente

chore: Merge branch 'main' into minor

Evan You 2 anni fa
parent
commit
801b8dea3b

+ 2 - 1
.github/workflows/release-tag.yml

@@ -24,4 +24,5 @@ jobs:
         with:
           tag_name: ${{ github.ref }}
           body: |
-            Please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/main/CHANGELOG.md) for details.
+            For stable releases, please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/main/CHANGELOG.md) for details.
+            For pre-releases, please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/minor/CHANGELOG.md) of the `minor` branch.

+ 14 - 0
CHANGELOG.md

@@ -1,3 +1,17 @@
+## [3.4.26](https://github.com/vuejs/core/compare/v3.4.25...v3.4.26) (2024-04-29)
+
+
+### Bug Fixes
+
+* **compiler-core:** fix bail constant for globals ([fefce06](https://github.com/vuejs/core/commit/fefce06b41e3b75de3d748dc6399628ec5056e78))
+* **compiler-core:** remove unnecessary constant bail check ([09b4df8](https://github.com/vuejs/core/commit/09b4df809e59ef5f4bc91acfc56dc8f82a8e243a)), closes [#10807](https://github.com/vuejs/core/issues/10807)
+* **runtime-core:** attrs should be readonly in functional components ([#10767](https://github.com/vuejs/core/issues/10767)) ([e8fd644](https://github.com/vuejs/core/commit/e8fd6446d14a6899e5e8ab1ee394d90088e01844))
+* **runtime-core:** ensure slot compiler marker writable ([#10825](https://github.com/vuejs/core/issues/10825)) ([9c2de62](https://github.com/vuejs/core/commit/9c2de6244cd44bc5fbfd82b5850c710ce725044f)), closes [#10818](https://github.com/vuejs/core/issues/10818)
+* **runtime-core:** properly handle inherit transition during clone VNode ([#10809](https://github.com/vuejs/core/issues/10809)) ([638a79f](https://github.com/vuejs/core/commit/638a79f64a7e184f2a2c65e21d764703f4bda561)), closes [#3716](https://github.com/vuejs/core/issues/3716) [#10497](https://github.com/vuejs/core/issues/10497) [#4091](https://github.com/vuejs/core/issues/4091)
+* **Transition:** re-fix [#10620](https://github.com/vuejs/core/issues/10620) ([#10832](https://github.com/vuejs/core/issues/10832)) ([accf839](https://github.com/vuejs/core/commit/accf8396ae1c9dd49759ba0546483f1d2c70c9bc)), closes [#10632](https://github.com/vuejs/core/issues/10632) [#10827](https://github.com/vuejs/core/issues/10827)
+
+
+
 # [3.5.0-alpha.1](https://github.com/vuejs/core/compare/v3.4.25...v3.5.0-alpha.1) (2024-04-29)
 
 

+ 11 - 11
package.json

@@ -1,7 +1,7 @@
 {
   "private": true,
   "version": "3.5.0-alpha.1",
-  "packageManager": "pnpm@9.0.5",
+  "packageManager": "pnpm@9.0.6",
   "type": "module",
   "scripts": {
     "dev": "node scripts/dev.js",
@@ -72,15 +72,15 @@
     "@types/minimist": "^1.2.5",
     "@types/node": "^20.12.7",
     "@types/semver": "^7.5.8",
-    "@vitest/coverage-istanbul": "^1.5.0",
+    "@vitest/coverage-istanbul": "^1.5.2",
     "@vue/consolidate": "1.0.0",
     "conventional-changelog-cli": "^4.1.0",
     "enquirer": "^2.4.1",
     "esbuild": "^0.20.2",
     "esbuild-plugin-polyfill-node": "^0.3.0",
-    "eslint": "^9.0.0",
+    "eslint": "^9.1.1",
     "eslint-plugin-import-x": "^0.5.0",
-    "eslint-plugin-vitest": "^0.5.3",
+    "eslint-plugin-vitest": "^0.5.4",
     "estree-walker": "^2.0.2",
     "execa": "^8.0.1",
     "jsdom": "^24.0.0",
@@ -95,23 +95,23 @@
     "prettier": "^3.2.5",
     "pretty-bytes": "^6.1.1",
     "pug": "^3.0.2",
-    "puppeteer": "~22.6.5",
+    "puppeteer": "~22.7.1",
     "rimraf": "^5.0.5",
-    "rollup": "^4.16.1",
+    "rollup": "^4.17.1",
     "rollup-plugin-dts": "^6.1.0",
     "rollup-plugin-esbuild": "^6.1.1",
     "rollup-plugin-polyfill-node": "^0.13.0",
     "semver": "^7.6.0",
-    "serve": "^14.2.1",
+    "serve": "^14.2.3",
     "simple-git-hooks": "^2.11.1",
-    "terser": "^5.30.3",
+    "terser": "^5.30.4",
     "todomvc-app-css": "^2.4.3",
     "tslib": "^2.6.2",
-    "tsx": "^4.7.2",
+    "tsx": "^4.7.3",
     "typescript": "~5.4.5",
-    "typescript-eslint": "^7.6.0",
+    "typescript-eslint": "^7.7.1",
     "vite": "^5.2.10",
-    "vitest": "^1.5.0"
+    "vitest": "^1.5.2"
   },
   "pnpm": {
     "peerDependencyRules": {

+ 25 - 0
packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts

@@ -421,6 +421,31 @@ describe('compiler: expression transform', () => {
     })
   })
 
+  // #10807
+  test('should not bail constant on strings w/ ()', () => {
+    const node = parseWithExpressionTransform(
+      `{{ { foo: 'ok()' } }}`,
+    ) as InterpolationNode
+    expect(node.content).toMatchObject({
+      constType: ConstantTypes.CAN_STRINGIFY,
+    })
+  })
+
+  test('should bail constant for global identifiers w/ new or call expressions', () => {
+    const node = parseWithExpressionTransform(
+      `{{ new Date().getFullYear() }}`,
+    ) as InterpolationNode
+    expect(node.content).toMatchObject({
+      children: [
+        'new ',
+        { constType: ConstantTypes.NOT_CONSTANT },
+        '().',
+        { constType: ConstantTypes.NOT_CONSTANT },
+        '()',
+      ],
+    })
+  })
+
   describe('ES Proposals support', () => {
     test('bigInt', () => {
       const node = parseWithExpressionTransform(

+ 3 - 0
packages/compiler-core/src/babelUtils.ts

@@ -10,6 +10,9 @@ import type {
 } from '@babel/types'
 import { walk } from 'estree-walker'
 
+/**
+ * Return value indicates whether the AST walked can be a constant
+ */
 export function walkIdentifiers(
   root: Node,
   onIdentifier: (

+ 8 - 7
packages/compiler-core/src/parser.ts

@@ -179,7 +179,7 @@ const tokenizer = new Tokenizer(stack, {
     const name = currentOpenTag!.tag
     currentOpenTag!.isSelfClosing = true
     endOpenTag(end)
-    if (stack[0]?.tag === name) {
+    if (stack[0] && stack[0].tag === name) {
       onCloseTag(stack.shift()!, end)
     }
   },
@@ -587,14 +587,14 @@ function endOpenTag(end: number) {
 
 function onText(content: string, start: number, end: number) {
   if (__BROWSER__) {
-    const tag = stack[0]?.tag
+    const tag = stack[0] && stack[0].tag
     if (tag !== 'script' && tag !== 'style' && content.includes('&')) {
       content = currentOptions.decodeEntities!(content, false)
     }
   }
   const parent = stack[0] || currentRoot
   const lastNode = parent.children[parent.children.length - 1]
-  if (lastNode?.type === NodeTypes.TEXT) {
+  if (lastNode && lastNode.type === NodeTypes.TEXT) {
     // merge
     lastNode.content += content
     setLocEnd(lastNode.loc, end)
@@ -771,7 +771,8 @@ function isComponent({ tag, props }: ElementNode): boolean {
     tag === 'component' ||
     isUpperCase(tag.charCodeAt(0)) ||
     isCoreComponent(tag) ||
-    currentOptions.isBuiltInComponent?.(tag) ||
+    (currentOptions.isBuiltInComponent &&
+      currentOptions.isBuiltInComponent(tag)) ||
     (currentOptions.isNativeTag && !currentOptions.isNativeTag(tag))
   ) {
     return true
@@ -828,8 +829,8 @@ function condenseWhitespace(
     if (node.type === NodeTypes.TEXT) {
       if (!inPre) {
         if (isAllWhitespace(node.content)) {
-          const prev = nodes[i - 1]?.type
-          const next = nodes[i + 1]?.type
+          const prev = nodes[i - 1] && nodes[i - 1].type
+          const next = nodes[i + 1] && nodes[i + 1].type
           // Remove if:
           // - the whitespace is the first or last node, or:
           // - (condense mode) the whitespace is between two comments, or:
@@ -1063,7 +1064,7 @@ export function baseParse(input: string, options?: ParserOptions): RootNode {
     currentOptions.ns === Namespaces.SVG ||
     currentOptions.ns === Namespaces.MATH_ML
 
-  const delimiters = options?.delimiters
+  const delimiters = options && options.delimiters
   if (delimiters) {
     tokenizer.delimiterOpen = toCharCodes(delimiters[0])
     tokenizer.delimiterClose = toCharCodes(delimiters[1])

+ 7 - 10
packages/compiler-core/src/transforms/transformExpression.ts

@@ -46,10 +46,6 @@ import { BindingTypes } from '../options'
 
 const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
 
-// a heuristic safeguard to bail constant expressions on presence of
-// likely function invocation and member access
-const constantBailRE = /\w\s*\(|\.[^\d]/
-
 export const transformExpression: NodeTransform = (node, context) => {
   if (node.type === NodeTypes.INTERPOLATION) {
     node.content = processExpression(
@@ -226,8 +222,6 @@ export function processExpression(
 
   // fast path if expression is a simple identifier.
   const rawExp = node.content
-  // bail constant on parens (function invocation) and dot (member access)
-  const bailConstant = constantBailRE.test(rawExp)
 
   let ast = node.ast
 
@@ -317,7 +311,12 @@ export function processExpression(
       } else {
         // The identifier is considered constant unless it's pointing to a
         // local scope variable (a v-for alias, or a v-slot prop)
-        if (!(needPrefix && isLocal) && !bailConstant) {
+        if (
+          !(needPrefix && isLocal) &&
+          parent.type !== 'CallExpression' &&
+          parent.type !== 'NewExpression' &&
+          parent.type !== 'MemberExpression'
+        ) {
           ;(node as QualifiedId).isConstant = true
         }
         // also generate sub-expressions for other identifiers for better
@@ -371,9 +370,7 @@ export function processExpression(
     ret.ast = ast
   } else {
     ret = node
-    ret.constType = bailConstant
-      ? ConstantTypes.NOT_CONSTANT
-      : ConstantTypes.CAN_STRINGIFY
+    ret.constType = ConstantTypes.CAN_STRINGIFY
   }
   ret.identifiers = Object.keys(knownIds)
   return ret

+ 2 - 3
packages/runtime-core/__tests__/componentProps.spec.ts

@@ -17,7 +17,6 @@ import {
   ref,
   render,
   serializeInner,
-  toRaw,
   toRefs,
   watch,
 } from '@vue/runtime-test'
@@ -129,12 +128,12 @@ describe('component props', () => {
     render(h(Comp, { foo: 1 }), root)
     expect(props).toEqual({ foo: 1 })
     expect(attrs).toEqual({ foo: 1 })
-    expect(toRaw(props)).toBe(attrs)
+    expect(props).toBe(attrs)
 
     render(h(Comp, { bar: 2 }), root)
     expect(props).toEqual({ bar: 2 })
     expect(attrs).toEqual({ bar: 2 })
-    expect(toRaw(props)).toBe(attrs)
+    expect(props).toBe(attrs)
   })
 
   test('boolean casting', () => {

+ 0 - 37
packages/runtime-core/__tests__/components/BaseTransition.spec.ts

@@ -7,7 +7,6 @@ import {
   h,
   nextTick,
   nodeOps,
-  onUnmounted,
   ref,
   render,
   serialize,
@@ -769,42 +768,6 @@ describe('BaseTransition', () => {
     test('w/ KeepAlive', async () => {
       await runTestWithKeepAlive(testOutIn)
     })
-
-    test('w/ KeepAlive + unmount innerChild', async () => {
-      const unmountSpy = vi.fn()
-      const includeRef = ref(['TrueBranch'])
-      const trueComp = {
-        name: 'TrueBranch',
-        setup() {
-          onUnmounted(unmountSpy)
-          const count = ref(0)
-          return () => h('div', count.value)
-        },
-      }
-
-      const toggle = ref(true)
-      const { props } = mockProps({ mode: 'out-in' }, true /*withKeepAlive*/)
-      const root = nodeOps.createElement('div')
-      const App = {
-        render() {
-          return h(BaseTransition, props, () => {
-            return h(
-              KeepAlive,
-              { include: includeRef.value },
-              toggle.value ? h(trueComp) : h('div'),
-            )
-          })
-        },
-      }
-      render(h(App), root)
-
-      // trigger toggle
-      toggle.value = false
-      includeRef.value = []
-
-      await nextTick()
-      expect(unmountSpy).toHaveBeenCalledTimes(1)
-    })
   })
 
   // #6835

+ 1 - 1
packages/runtime-core/__tests__/hmr.spec.ts

@@ -356,7 +356,7 @@ describe('hot module replacement', () => {
     triggerEvent(root.children[1] as TestElement, 'click')
     await nextTick()
     await new Promise(r => setTimeout(r, 0))
-    expect(serializeInner(root)).toBe(`<button></button><!---->`)
+    expect(serializeInner(root)).toBe(`<button></button><!--v-if-->`)
     expect(unmountSpy).toHaveBeenCalledTimes(1)
     expect(mountSpy).toHaveBeenCalledTimes(1)
     expect(activeSpy).toHaveBeenCalledTimes(1)

+ 7 - 14
packages/runtime-core/src/apiWatch.ts

@@ -472,39 +472,32 @@ export function createPathGetter(ctx: any, path: string) {
 
 export function traverse(
   value: unknown,
-  depth?: number,
-  currentDepth = 0,
+  depth = Infinity,
   seen?: Set<unknown>,
 ) {
-  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
+  if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
     return value
   }
 
-  if (depth && depth > 0) {
-    if (currentDepth >= depth) {
-      return value
-    }
-    currentDepth++
-  }
-
   seen = seen || new Set()
   if (seen.has(value)) {
     return value
   }
   seen.add(value)
+  depth--
   if (isRef(value)) {
-    traverse(value.value, depth, currentDepth, seen)
+    traverse(value.value, depth, seen)
   } else if (isArray(value)) {
     for (let i = 0; i < value.length; i++) {
-      traverse(value[i], depth, currentDepth, seen)
+      traverse(value[i], depth, seen)
     }
   } else if (isSet(value) || isMap(value)) {
     value.forEach((v: any) => {
-      traverse(v, depth, currentDepth, seen)
+      traverse(v, depth, seen)
     })
   } else if (isPlainObject(value)) {
     for (const key in value) {
-      traverse(value[key], depth, currentDepth, seen)
+      traverse(value[key], depth, seen)
     }
   }
   return value

+ 11 - 6
packages/runtime-core/src/compat/global.ts

@@ -77,7 +77,12 @@ export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
 
   nextTick: typeof nextTick
 
-  use(plugin: Plugin, ...options: any[]): CompatVue
+  use<Options extends unknown[]>(
+    plugin: Plugin<Options>,
+    ...options: Options
+  ): CompatVue
+  use<Options>(plugin: Plugin<Options>, options: Options): CompatVue
+
   mixin(mixin: ComponentOptions): CompatVue
 
   component(name: string): Component | undefined
@@ -176,11 +181,11 @@ export function createCompatVue(
   Vue.version = `2.6.14-compat:${__VERSION__}`
   Vue.config = singletonApp.config
 
-  Vue.use = (p, ...options) => {
-    if (p && isFunction(p.install)) {
-      p.install(Vue as any, ...options)
-    } else if (isFunction(p)) {
-      p(Vue as any, ...options)
+  Vue.use = (plugin: Plugin, ...options: any[]) => {
+    if (plugin && isFunction(plugin.install)) {
+      plugin.install(Vue as any, ...options)
+    } else if (isFunction(plugin)) {
+      plugin(Vue as any, ...options)
     }
     return Vue
   }

+ 12 - 7
packages/runtime-core/src/componentRenderUtils.ts

@@ -116,7 +116,7 @@ export function renderComponentRoot(
                 ? {
                     get attrs() {
                       markAttrsAccessed()
-                      return attrs
+                      return shallowReadonly(attrs)
                     },
                     slots,
                     emit,
@@ -166,7 +166,7 @@ export function renderComponentRoot(
             propsOptions,
           )
         }
-        root = cloneVNode(root, fallthroughAttrs)
+        root = cloneVNode(root, fallthroughAttrs, false, true)
       } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
         const allAttrs = Object.keys(attrs)
         const eventAttrs: string[] = []
@@ -221,10 +221,15 @@ export function renderComponentRoot(
           getComponentName(instance.type),
         )
       }
-      root = cloneVNode(root, {
-        class: cls,
-        style: style,
-      })
+      root = cloneVNode(
+        root,
+        {
+          class: cls,
+          style: style,
+        },
+        false,
+        true,
+      )
     }
   }
 
@@ -237,7 +242,7 @@ export function renderComponentRoot(
       )
     }
     // clone before mutating since the root may be a hoisted vnode
-    root = cloneVNode(root)
+    root = cloneVNode(root, null, false, true)
     root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
   }
   // inherit transition data

+ 1 - 1
packages/runtime-core/src/componentSlots.ts

@@ -171,7 +171,7 @@ export const initSlots = (
     if (type) {
       extend(slots, children as InternalSlots)
       // make compiler marker non-enumerable
-      def(slots, '_', type)
+      def(slots, '_', type, true)
     } else {
       normalizeObjectSlots(children as RawSlots, slots, instance)
     }

+ 1 - 1
packages/runtime-core/src/components/BaseTransition.ts

@@ -206,7 +206,7 @@ const BaseTransitionImpl: ComponentOptions = {
         // update old tree's hooks in case of dynamic transition
         setTransitionHooks(oldInnerChild, leavingHooks)
         // switching between different views
-        if (mode === 'out-in') {
+        if (mode === 'out-in' && innerChild.type !== Comment) {
           state.isLeaving = true
           // return placeholder node and queue update when leave finishes
           leavingHooks.afterLeave = () => {

+ 1 - 1
packages/runtime-core/src/components/KeepAlive.ts

@@ -254,7 +254,7 @@ const KeepAliveImpl: ComponentOptions = {
       pendingCacheKey = null
 
       if (!slots.default) {
-        return (current = null)
+        return null
       }
 
       const children = slots.default()

+ 3 - 2
packages/runtime-core/src/components/Suspense.ts

@@ -479,7 +479,7 @@ function createSuspenseBoundary(
   let parentSuspenseId: number | undefined
   const isSuspensible = isVNodeSuspensible(vnode)
   if (isSuspensible) {
-    if (parentSuspense?.pendingBranch) {
+    if (parentSuspense && parentSuspense.pendingBranch) {
       parentSuspenseId = parentSuspense.pendingId
       parentSuspense.deps++
     }
@@ -898,5 +898,6 @@ function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
 }
 
 function isVNodeSuspensible(vnode: VNode) {
-  return vnode.props?.suspensible != null && vnode.props.suspensible !== false
+  const suspensible = vnode.props && vnode.props.suspensible
+  return suspensible != null && suspensible !== false
 }

+ 12 - 2
packages/runtime-core/src/vnode.ts

@@ -624,10 +624,11 @@ export function cloneVNode<T, U>(
   vnode: VNode<T, U>,
   extraProps?: (Data & VNodeProps) | null,
   mergeRef = false,
+  cloneTransition = false,
 ): VNode<T, U> {
   // This is intentionally NOT using spread or extend to avoid the runtime
   // key enumeration cost.
-  const { props, ref, patchFlag, children } = vnode
+  const { props, ref, patchFlag, children, transition } = vnode
   const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
   const cloned: VNode<T, U> = {
     __v_isVNode: true,
@@ -670,7 +671,7 @@ export function cloneVNode<T, U>(
     dynamicChildren: vnode.dynamicChildren,
     appContext: vnode.appContext,
     dirs: vnode.dirs,
-    transition: vnode.transition,
+    transition,
 
     // These should technically only be non-null on mounted VNodes. However,
     // they *should* be copied for kept-alive vnodes. So we just always copy
@@ -685,9 +686,18 @@ export function cloneVNode<T, U>(
     ctx: vnode.ctx,
     ce: vnode.ce,
   }
+
+  // if the vnode will be replaced by the cloned one, it is necessary
+  // to clone the transition to ensure that the vnode referenced within
+  // the transition hooks is fresh.
+  if (transition && cloneTransition) {
+    cloned.transition = transition.clone(cloned as VNode)
+  }
+
   if (__COMPAT__) {
     defineLegacyVNodeProperties(cloned as VNode)
   }
+
   return cloned
 }
 

+ 1 - 1
packages/sfc-playground/package.json

@@ -13,7 +13,7 @@
     "vite": "^5.2.10"
   },
   "dependencies": {
-    "@vue/repl": "^4.1.1",
+    "@vue/repl": "^4.1.2",
     "file-saver": "^2.0.5",
     "jszip": "^3.10.1",
     "vue": "workspace:*"

+ 7 - 1
packages/shared/src/general.ts

@@ -139,10 +139,16 @@ export const invokeArrayFns = (fns: Function[], arg?: any) => {
   }
 }
 
-export const def = (obj: object, key: string | symbol, value: any) => {
+export const def = (
+  obj: object,
+  key: string | symbol,
+  value: any,
+  writable = false,
+) => {
   Object.defineProperty(obj, key, {
     configurable: true,
     enumerable: false,
+    writable,
     value,
   })
 }

+ 105 - 0
packages/vue/__tests__/e2e/Transition.spec.ts

@@ -1214,6 +1214,111 @@ describe('e2e: Transition', () => {
       },
       E2E_TIMEOUT,
     )
+
+    // #3716
+    test(
+      'wrapping transition + fallthrough attrs',
+      async () => {
+        await page().goto(baseUrl)
+        await page().waitForSelector('#app')
+        await page().evaluate(() => {
+          const { createApp, ref } = (window as any).Vue
+          createApp({
+            components: {
+              'my-transition': {
+                template: `
+                  <transition foo="1" name="test">
+                    <slot></slot>
+                  </transition>
+                `,
+              },
+            },
+            template: `
+            <div id="container">
+              <my-transition>
+                <div v-if="toggle">content</div>
+              </my-transition>
+            </div>
+            <button id="toggleBtn" @click="click">button</button>
+          `,
+            setup: () => {
+              const toggle = ref(true)
+              const click = () => (toggle.value = !toggle.value)
+              return { toggle, click }
+            },
+          }).mount('#app')
+        })
+        expect(await html('#container')).toBe('<div foo="1">content</div>')
+
+        await click('#toggleBtn')
+        // toggle again before leave finishes
+        await nextTick()
+        await click('#toggleBtn')
+
+        await transitionFinish()
+        expect(await html('#container')).toBe(
+          '<div foo="1" class="">content</div>',
+        )
+      },
+      E2E_TIMEOUT,
+    )
+
+    test(
+      'w/ KeepAlive + unmount innerChild',
+      async () => {
+        const unmountSpy = vi.fn()
+        await page().exposeFunction('unmountSpy', unmountSpy)
+        await page().evaluate(() => {
+          const { unmountSpy } = window as any
+          const { createApp, ref, h, onUnmounted } = (window as any).Vue
+          createApp({
+            template: `
+            <div id="container">
+              <transition mode="out-in">
+                <KeepAlive :include="includeRef">
+                  <TrueBranch v-if="toggle"></TrueBranch>
+                </KeepAlive>
+              </transition>
+            </div>
+            <button id="toggleBtn" @click="click">button</button>
+          `,
+            components: {
+              TrueBranch: {
+                name: 'TrueBranch',
+                setup() {
+                  onUnmounted(unmountSpy)
+                  const count = ref(0)
+                  return () => h('div', count.value)
+                },
+              },
+            },
+            setup: () => {
+              const includeRef = ref(['TrueBranch'])
+              const toggle = ref(true)
+              const click = () => {
+                toggle.value = !toggle.value
+                if (toggle.value) {
+                  includeRef.value = ['TrueBranch']
+                } else {
+                  includeRef.value = []
+                }
+              }
+              return { toggle, click, unmountSpy, includeRef }
+            },
+          }).mount('#app')
+        })
+
+        await transitionFinish()
+        expect(await html('#container')).toBe('<div>0</div>')
+
+        await click('#toggleBtn')
+
+        await transitionFinish()
+        expect(await html('#container')).toBe('<!--v-if-->')
+        expect(unmountSpy).toBeCalledTimes(1)
+      },
+      E2E_TIMEOUT,
+    )
   })
 
   describe('transition with Suspense', () => {

File diff suppressed because it is too large
+ 345 - 143
pnpm-lock.yaml


Some files were not shown because too many files changed in this diff