Browse Source

chore: Merge branch 'main' into minor

daiwei 1 tháng trước cách đây
mục cha
commit
57e6da1505

+ 1 - 1
.github/bug-repro-guidelines.md

@@ -18,7 +18,7 @@ You are already familiar with your codebase, but we are not. It is extremely tim
 
 The problematic behavior may very well be caused by your code rather than by a bug in Vue.
 
-A minimal reproduction means it demonstrates the bug, and the bug only. It should only contain the bare minimum amount of code that can reliably cause the bug. Try your best to get rid of anything that aren't directly related to the problem.
+A minimal reproduction means it demonstrates the bug, and the bug only. It should only contain the bare minimum amount of code that can reliably cause the bug. Try your best to get rid of anything that isn't directly related to the problem.
 
 ### How to create a repro
 

+ 7 - 7
.github/contributing.md

@@ -23,17 +23,17 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
 
 - New feature that addresses a clearly explained and widely applicable use case. **"Widely applicable"** means the new feature should provide non-trivial improvements to the majority of the user base. Vue already has a large API surface so we are quite cautious about adding new features - if the use case is niche and can be addressed via userland implementations, it likely isn't suitable to go into core.
 
-  The feature implementation should also consider the trade-off between the added complexity vs. the benefits gained. For example, if a small feature requires significant changes that spreads across the codebase, it is likely not worth it, or the approach should be reconsidered.
+  The feature implementation should also consider the trade-off between the added complexity vs. the benefits gained. For example, if a small feature requires significant changes that spread across the codebase, it is likely not worth it, or the approach should be reconsidered.
 
   If the feature has a non-trivial API surface addition, or significantly affects the way a common use case is approached by the users, it should go through a discussion first in the [RFC repo](https://github.com/vuejs/rfcs/discussions). PRs of such features without prior discussion make it really difficult to steer / adjust the API design due to coupling with concrete implementations, and can lead to wasted work.
 
 - Chore: typos, comment improvements, build config, CI config, etc. For typos and comment changes, try to combine multiple of them into a single PR.
 
-- **It should be noted that we discourage contributors from submitting code refactors that are largely stylistic.** Code refactors are only accepted if it improves performance, or comes with sufficient explanations on why it objectively improves the code quality (e.g. makes a related feature implementation easier).
+- **It should be noted that we discourage contributors from submitting code refactors that are largely stylistic.** Code refactors are only accepted if they improve performance, or come with sufficient explanations on why they objectively improve the code quality (e.g. makes a related feature implementation easier).
 
   The reason is that code readability is subjective. The maintainers of this project have chosen to write the code in its current style based on our preferences, and we do not want to spend time explaining our stylistic preferences. Contributors should just respect the established conventions when contributing code.
 
-  Another aspect of it is that large scale stylistic changes result in massive diffs that touch multiple files, adding noise to the git history and makes tracing behavior changes across commits more cumbersome.
+  Another aspect of it is that large scale stylistic changes result in massive diffs that touch multiple files, adding noise to the git history and make tracing behavior changes across commits more cumbersome.
 
 ### Pull Request Checklist
 
@@ -65,7 +65,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
 
 - The PR should fix the intended bug **only** and not introduce unrelated changes. This includes unnecessary refactors - a PR should focus on the fix and not code style, this makes it easier to trace changes in the future.
 
-- Consider the performance / size impact of the changes, and whether the bug being fixes justifies the cost. If the bug being fixed is a very niche edge case, we should try to minimize the size / perf cost to make it worthwhile.
+- Consider the performance / size impact of the changes, and whether the bug being fixed justifies the cost. If the bug being fixed is a very niche edge case, we should try to minimize the size / perf cost to make it worthwhile.
   - Is the code perf-sensitive (e.g. in "hot paths" like component updates or the vdom patch function?)
     - If the branch is dev-only, performance is less of a concern.
 
@@ -78,7 +78,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
 
 You will need [Node.js](https://nodejs.org) with minimum version as specified in the [`.node-version`](https://github.com/vuejs/core/blob/main/.node-version) file, and [PNPM](https://pnpm.io) with minimum version as specified in the [`"packageManager"` field in `package.json`](https://github.com/vuejs/core/blob/main/package.json#L4).
 
-We also recommend installing [@antfu/ni](https://github.com/antfu/ni) to help switching between repos using different package managers. `ni` also provides the handy `nr` command which running npm scripts easier.
+We also recommend installing [@antfu/ni](https://github.com/antfu/ni) to help switching between repos using different package managers. `ni` also provides the handy `nr` command which makes running npm scripts easier.
 
 After cloning the repo, run:
 
@@ -191,7 +191,7 @@ $ nr dev
 
 - The `dev` script also supports the `-s` flag for generating source maps, but it will make rebuilds slower.
 
-- The `dev` script supports the `-i` flag for inlining all deps. This is useful when debugging `esm-bundler` builds which externalizes deps by default.
+- The `dev` script supports the `-i` flag for inlining all deps. This is useful when debugging `esm-bundler` builds which externalize deps by default.
 
 ### `nr dev-sfc`
 
@@ -336,7 +336,7 @@ Unit tests are collocated with the code being tested in each package, inside dir
 
 - Only use platform-specific runtimes if the test is asserting platform-specific behavior.
 
-Test coverage is continuously deployed at https://coverage.vuejs.org. PRs that improve test coverage are welcome, but in general the test coverage should be used as a guidance for finding API use cases that are not covered by tests. We don't recommend adding tests that only improve coverage but not actually test a meaning use case.
+Test coverage is continuously deployed at https://coverage.vuejs.org. PRs that improve test coverage are welcome, but in general the test coverage should be used as a guidance for finding API use cases that are not covered by tests. We don't recommend adding tests that only improve coverage but not actually test a meaningful use case.
 
 ### Testing Type Definition Correctness
 

+ 2 - 2
.github/maintenance.md

@@ -47,7 +47,7 @@ Depending on the type of the PR, different considerations need to be taken into
 
 - Performance: if a refactor PR claims to improve performance, there should be benchmarks showcasing said performance unless the improvement is self-explanatory.
 
-- Code quality / stylistic PRs: we should be conservative on merging this type PRs because (1) they can be subjective in many cases, and (2) they often come with large git diffs, causing merge conflicts with other pending PRs, and leading to unwanted noise when tracing changes through git history. Use your best judgement on this type of PRs on whether they are worth it.
+- Code quality / stylistic PRs: we should be conservative on merging this type of PRs because (1) they can be subjective in many cases, and (2) they often come with large git diffs, causing merge conflicts with other pending PRs, and leading to unwanted noise when tracing changes through git history. Use your best judgement on this type of PRs on whether they are worth it.
   - For PRs in this category that are approved, do not merge immediately. Group them before releasing a new minor, after all feature-oriented PRs are merged.
 
 ### Reviewing a Feature
@@ -82,7 +82,7 @@ Depending on the type of the PR, different considerations need to be taken into
 - Potential Breakage
   - avoiding runtime behavior breakage is the highest priority
     - if not sure, use `ecosystem-ci` to verify!
-  - some fix inevitably cause behavior change, these must be discussed case-by-case
+  - some fixes inevitably cause behavior change, these must be discussed case-by-case
   - type level breakage (e.g upgrading TS) is possible between minors
 
 ## PR Merge Rules for Team Members

+ 1 - 1
.node-version

@@ -1 +1 @@
-22.14.0
+lts/*

+ 1 - 1
README.md

@@ -6,7 +6,7 @@ Please follow the documentation at [vuejs.org](https://vuejs.org/)!
 
 ## Sponsors
 
-Vue.js is an MIT-licensed open source project with its ongoing development made possible entirely by the support of these awesome [backers](https://github.com/vuejs/core/blob/main/BACKERS.md). If you'd like to join them, please consider [ sponsoring Vue's development](https://vuejs.org/sponsor/).
+Vue.js is an MIT-licensed open source project with its ongoing development made possible entirely by the support of these awesome [backers](https://github.com/vuejs/core/blob/main/BACKERS.md). If you'd like to join them, please consider [sponsoring Vue's development](https://vuejs.org/sponsor/).
 
 <p align="center">
   <h3 align="center">Special Sponsor</h3>

+ 11 - 0
changelogs/CHANGELOG-3.5.md

@@ -1,3 +1,14 @@
+## [3.5.29](https://github.com/vuejs/core/compare/v3.5.28...v3.5.29) (2026-02-24)
+
+
+### Bug Fixes
+
+* **runtime-core:** prevent instance leak in withAsyncContext ([#14445](https://github.com/vuejs/core/issues/14445)) ([702284f](https://github.com/vuejs/core/commit/702284f6a7d0dd6d4e648142e7977a3eb02d77f5)), closes [nuxt/nuxt#33644](https://github.com/nuxt/nuxt/issues/33644)
+* **server-renderer:** render className as escaped string ([#14469](https://github.com/vuejs/core/issues/14469)) ([da6690c](https://github.com/vuejs/core/commit/da6690cae359ec3576403c18040a1a5f017a63b1))
+* **transition:** prevent enter if leave is in progress ([#14443](https://github.com/vuejs/core/issues/14443)) ([df059f8](https://github.com/vuejs/core/commit/df059f890460e4c703b62a54f410627ff29c489b)), closes [#12091](https://github.com/vuejs/core/issues/12091) [#12133](https://github.com/vuejs/core/issues/12133)
+
+
+
 ## [3.5.28](https://github.com/vuejs/core/compare/v3.5.27...v3.5.28) (2026-02-09)
 
 

+ 1 - 1
netlify.toml

@@ -1,3 +1,3 @@
 [build.environment]
-  NODE_VERSION = "22"
+  NODE_VERSION = "24"
   NPM_FLAGS = "--version" # prevent Netlify npm install

+ 3 - 3
package.json

@@ -66,7 +66,7 @@
     "@babel/types": "catalog:",
     "@rolldown/plugin-node-polyfills": "^1.0.3",
     "@types/hash-sum": "^1.0.2",
-    "@types/node": "^24.10.12",
+    "@types/node": "^24.10.13",
     "@types/semver": "^7.7.1",
     "@types/serve-handler": "^6.1.4",
     "@vitest/coverage-v8": "^4.0.18",
@@ -92,8 +92,8 @@
     "picocolors": "^1.1.1",
     "pretty-bytes": "^7.1.0",
     "pug": "^3.0.3",
-    "puppeteer": "~24.37.2",
-    "rimraf": "^6.1.2",
+    "puppeteer": "~24.37.5",
+    "rimraf": "^6.1.3",
     "rolldown": "^1.0.0-rc.2",
     "rolldown-plugin-dts": "^0.21.7",
     "semver": "^7.7.3",

+ 1 - 1
packages/compiler-dom/src/transforms/Transition.ts

@@ -44,7 +44,7 @@ export function postTransformTransition(
       )
     }
 
-    // check if it's s single child w/ v-show
+    // check if it's a single child w/ v-show
     // if yes, inject "persisted: true" to the transition props
     const child = node.children[0]
     if (child.type === NodeTypes.ELEMENT) {

+ 1 - 1
packages/compiler-sfc/README.md

@@ -2,7 +2,7 @@
 
 > Lower level utilities for compiling Vue Single File Components
 
-**Note: as of 3.2.13+, this package is included as a dependency of the main `vue` package and can be accessed as `vue/compiler-sfc`. This means you no longer need to explicitly install this package and ensure its version match that of `vue`'s. Just use the main `vue/compiler-sfc` deep import instead.**
+**Note: as of 3.2.13+, this package is included as a dependency of the main `vue` package and can be accessed as `vue/compiler-sfc`. This means you no longer need to explicitly install this package and ensure its version matches that of `vue`'s. Just use the main `vue/compiler-sfc` deep import instead.**
 
 This package contains lower level utilities that you can use if you are writing a plugin / transform for a bundler or module system that compiles Vue Single File Components (SFCs) into JavaScript. It is used in [vue-loader](https://github.com/vuejs/vue-loader) and [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue).
 

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

@@ -59,7 +59,7 @@
     "hash-sum": "^2.0.0",
     "lru-cache": "10.1.0",
     "merge-source-map": "^1.1.0",
-    "minimatch": "~10.1.2",
+    "minimatch": "~10.2.0",
     "postcss-modules": "^6.0.1",
     "postcss-selector-parser": "^7.1.1",
     "pug": "^3.0.3",

+ 54 - 0
packages/reactivity/__tests__/effectScope.spec.ts

@@ -417,6 +417,60 @@ describe('reactivity/effect/scope', () => {
     expect(rs.value.stage).toBe(1)
     expect(renderCount).toBe(5)
   })
+
+  it('should still trigger updates after stopping scope stored in reactive object', () => {
+    const rs = ref({
+      stage: 0,
+      scope: null as any,
+    })
+
+    let renderCount = 0
+    effect(() => {
+      renderCount++
+      return rs.value.stage
+    })
+
+    const handleBegin = () => {
+      const status = rs.value
+      status.stage = 1
+      status.scope = effectScope()
+      status.scope.run(() => {
+        watch([() => status.stage], () => {})
+      })
+    }
+
+    const handleExit = () => {
+      const status = rs.value
+      status.stage = 0
+      const watchScope = status.scope
+      status.scope = null
+      if (watchScope) {
+        watchScope.stop()
+      }
+    }
+
+    expect(rs.value.stage).toBe(0)
+    expect(renderCount).toBe(1)
+
+    // 1. Click begin
+    handleBegin()
+    expect(rs.value.stage).toBe(1)
+    expect(renderCount).toBe(2)
+
+    // 2. Click add
+    rs.value.stage++
+    expect(rs.value.stage).toBe(2)
+    expect(renderCount).toBe(3)
+
+    // 3. Click end
+    handleExit()
+    expect(rs.value.stage).toBe(0)
+    expect(renderCount).toBe(4)
+
+    handleBegin()
+    expect(rs.value.stage).toBe(1)
+    expect(renderCount).toBe(5)
+  })
 })
 
 function getEffectsCount(scope: EffectScope): number {

+ 80 - 0
packages/runtime-core/__tests__/apiSetupHelpers.spec.ts

@@ -253,6 +253,86 @@ describe('SFC <script setup> helpers', () => {
       expect(serializeInner(root)).toBe('hello')
     })
 
+    test('should not leak instance to user microtasks after restore', async () => {
+      let leakedToUserMicrotask = false
+
+      const Comp = defineComponent({
+        async setup() {
+          let __temp: any, __restore: any
+          ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
+          __temp = await __temp
+          __restore()
+
+          Promise.resolve().then(() => {
+            leakedToUserMicrotask = getCurrentInstance() !== null
+          })
+
+          return () => ''
+        },
+      })
+
+      const root = nodeOps.createElement('div')
+      render(
+        h(() => h(Suspense, () => h(Comp))),
+        root,
+      )
+
+      await new Promise(r => setTimeout(r))
+      expect(leakedToUserMicrotask).toBe(false)
+    })
+
+    test('should not leak sibling instance in concurrent restores', async () => {
+      let resolveOne: () => void
+      let resolveTwo: () => void
+      let done!: () => void
+      let pending = 2
+      const ready = new Promise<void>(r => {
+        done = r
+      })
+      const seenUid: Record<'one' | 'two', number | null> = {
+        one: null,
+        two: null,
+      }
+
+      const makeComp = (name: 'one' | 'two', wait: Promise<void>) =>
+        defineComponent({
+          async setup() {
+            let __temp: any, __restore: any
+            ;[__temp, __restore] = withAsyncContext(() => wait)
+            __temp = await __temp
+            __restore()
+
+            Promise.resolve().then(() => {
+              seenUid[name] = getCurrentInstance()?.uid ?? null
+              if (--pending === 0) done()
+            })
+
+            return () => ''
+          },
+        })
+
+      const oneReady = new Promise<void>(r => {
+        resolveOne = r
+      })
+      const twoReady = new Promise<void>(r => {
+        resolveTwo = r
+      })
+      const CompOne = makeComp('one', oneReady)
+      const CompTwo = makeComp('two', twoReady)
+
+      const root = nodeOps.createElement('div')
+      render(
+        h(() => h(Suspense, () => h('div', [h(CompOne), h(CompTwo)]))),
+        root,
+      )
+
+      resolveOne!()
+      resolveTwo!()
+      await ready
+      expect(seenUid.one).toBeNull()
+      expect(seenUid.two).toBeNull()
+    })
+
     test('error handling', async () => {
       const spy = vi.fn()
 

+ 17 - 1
packages/runtime-core/src/apiSetupHelpers.ts

@@ -520,11 +520,27 @@ export function withAsyncContext(getAwaitable: () => any): [any, () => void] {
   }
   let awaitable = getAwaitable()
   setCurrentInstance(null, undefined)
+
+  // Never restore a captured "prev" instance here: in concurrent async setup
+  // continuations it may belong to a sibling component and cause leaks.
+  // clear global currentInstance for user microtasks.
+  const cleanup = () => setCurrentInstance(null, undefined)
+
   if (isPromise(awaitable)) {
     awaitable = awaitable.catch(e => {
       setCurrentInstance(ctx)
+      // Defer cleanup so the async function's catch continuation
+      // still runs with the restored instance.
+      Promise.resolve().then(() => Promise.resolve().then(cleanup))
       throw e
     })
   }
-  return [awaitable, () => setCurrentInstance(ctx)]
+  return [
+    awaitable,
+    () => {
+      setCurrentInstance(ctx)
+      // Keep instance for the current continuation, then cleanup.
+      Promise.resolve().then(cleanup)
+    },
+  ]
 }

+ 5 - 0
packages/runtime-core/src/components/BaseTransition.ts

@@ -319,6 +319,7 @@ function getLeavingNodesForType(
 }
 
 export interface TransitionHooksContext {
+  isLeaving: () => boolean
   setLeavingNodeCache: (node: any) => void
   unsetLeavingNodeCache: (node: any) => void
   earlyRemove: () => void
@@ -337,6 +338,7 @@ export function resolveTransitionHooks(
   const key = String(vnode.key)
   const leavingVNodesCache = getLeavingNodesForType(state, vnode)
   const context: TransitionHooksContext = {
+    isLeaving: () => leavingVNodesCache[key] === vnode,
     setLeavingNodeCache: () => {
       leavingVNodesCache[key] = vnode
     },
@@ -380,6 +382,7 @@ export function baseResolveTransitionHooks(
   instance: GenericComponentInstance,
 ): TransitionHooks {
   const {
+    isLeaving,
     setLeavingNodeCache,
     unsetLeavingNodeCache,
     earlyRemove,
@@ -449,6 +452,8 @@ export function baseResolveTransitionHooks(
     },
 
     enter(el) {
+      // prevent enter if leave is in progress
+      if (isLeaving()) return
       let hook = onEnter
       let afterHook = onAfterEnter
       let cancelHook = onEnterCancelled

+ 11 - 2
packages/runtime-dom/src/jsx.ts

@@ -275,10 +275,19 @@ export type StyleValue =
   | CSSProperties
   | Array<StyleValue>
 
+// Support for `class` attribute
+export type ClassValue =
+  | false
+  | null
+  | undefined
+  | string
+  | Record<string, any>
+  | Array<ClassValue>
+
 export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> {
   innerHTML?: string | undefined
 
-  class?: any
+  class?: ClassValue | undefined
   style?: StyleValue | undefined
 
   // Standard HTML Attributes
@@ -916,7 +925,7 @@ export interface SVGAttributes extends AriaAttributes, EventHandlers<Events> {
    * SVG Styling Attributes
    * @see https://www.w3.org/TR/SVG/styling.html#ElementSpecificStyling
    */
-  class?: any
+  class?: ClassValue | undefined
   style?: StyleValue | undefined
 
   color?: string | undefined

+ 1 - 0
packages/runtime-vapor/src/components/Transition.ts

@@ -134,6 +134,7 @@ const getTransitionHooksContext = (
 ) => {
   const { leavingNodes } = state
   const context: TransitionHooksContext = {
+    isLeaving: () => leavingNodes.has(key),
     setLeavingNodeCache: el => {
       leavingNodes.set(key, el)
     },

+ 2 - 2
packages/server-renderer/README.md

@@ -1,6 +1,6 @@
 # @vue/server-renderer
 
-**Note: as of 3.2.13+, this package is included as a dependency of the main `vue` package and can be accessed as `vue/server-renderer`. This means you no longer need to explicitly install this package and ensure its version match that of `vue`'s. Just use the `vue/server-renderer` deep import instead.**
+**Note: as of 3.2.13+, this package is included as a dependency of the main `vue` package and can be accessed as `vue/server-renderer`. This means you no longer need to explicitly install this package and ensure its version matches that of `vue`'s. Just use the `vue/server-renderer` deep import instead.**
 
 ## Basic API
 
@@ -165,7 +165,7 @@ renderToSimpleStream(
     push(chunk) {
       if (chunk === null) {
         // done
-        console(`render complete: ${res}`)
+        console.log(`render complete: ${res}`)
       } else {
         res += chunk
       }

+ 1 - 1
packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts

@@ -154,7 +154,7 @@ describe('ssr: renderClass', () => {
       ssrRenderAttrs({
         className: ['foo', 'bar'],
       }),
-    ).toBe(` class="foo bar"`)
+    ).toBe(` class="foo,bar"`)
   })
 })
 

+ 7 - 1
packages/server-renderer/src/helpers/ssrRenderAttrs.ts

@@ -42,10 +42,16 @@ export function ssrRenderAttrs(
     const value = props[key]
     // force as attribute
     if (key.startsWith('^')) key = key.slice(1)
-    if (key === 'class' || key === 'className') {
+    if (key === 'class') {
       ret += ` class="${ssrRenderClass(value)}"`
     } else if (key === 'style') {
       ret += ` style="${ssrRenderStyle(value)}"`
+    } else if (key === 'className') {
+      // className should not go through ssrRenderClass which normalizes non-string
+      // values into strings. it should coerce directly into strings
+      if (value != null) {
+        ret += ` class="${escapeHtml(String(value))}"`
+      }
     } else {
       ret += ssrRenderDynamicAttr(key, value, tag)
     }

+ 1 - 1
packages/vue/README.md

@@ -6,7 +6,7 @@
 
 - **`vue(.runtime).global(.prod).js`**:
   - For direct use via `<script src="...">` in the browser. Exposes the `Vue` global.
-  - Note that global builds are not [UMD](https://github.com/umdjs/umd) builds. They are built as [IIFEs](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) and is only meant for direct use via `<script src="...">`.
+  - Note that global builds are not [UMD](https://github.com/umdjs/umd) builds. They are built as [IIFEs](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) and are only meant for direct use via `<script src="...">`.
   - In-browser template compilation:
     - **`vue.global.js`** is the "full" build that includes both the compiler and the runtime so it supports compiling templates on the fly.
     - **`vue.runtime.global.js`** contains only the runtime and requires templates to be pre-compiled during a build step.

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

@@ -3343,6 +3343,76 @@ describe('e2e: Transition', () => {
     E2E_TIMEOUT,
   )
 
+  // #12091
+  test(
+    'prevent enter when leaving',
+    async () => {
+      const hooks: string[] = []
+      const pushHook = (hook: string) => hooks.push(hook)
+      await page().exposeFunction('pushHook', pushHook)
+      await page().evaluate(() => {
+        const { pushHook } = window as any
+        const { createApp, ref } = (window as any).Vue
+        const visible = ref(true)
+        createApp({
+          components: {
+            Comp: {
+              setup() {
+                visible.value = false
+                return () => null
+              },
+            },
+          },
+          template: `
+            <div id="content" v-if="toggle">
+              <div id="container">
+                <transition
+                  appear
+                  @before-enter="pushHook('beforeEnter')"
+                  @enter="pushHook('enter')"
+                  @enter-cancelled="pushHook('enterCancelled')"
+                  @after-enter="pushHook('afterEnter')"
+                  @before-leave="pushHook('beforeLeave')"
+                  @leave="pushHook('leave')"
+                  @after-leave="pushHook('afterLeave')"
+                >
+                  <div v-if="visible">content</div>
+                </transition>
+              </div>
+              <Comp />
+            </div>
+            <button id="toggleBtn" @click="click">button</button>
+          `,
+          setup: () => {
+            const toggle = ref(false)
+            const click = () => (toggle.value = !toggle.value)
+            return {
+              toggle,
+              click,
+              pushHook,
+              visible,
+            }
+          },
+        }).mount('#app')
+      })
+
+      await click('#toggleBtn')
+      await nextTick()
+      await transitionFinish()
+
+      expect(hooks).toStrictEqual([
+        'beforeEnter',
+        'beforeLeave',
+        'leave',
+        'afterLeave',
+      ])
+      expect(await html('#content')).toBe(
+        '<div id="container"><!--v-if--></div><!---->',
+      )
+    },
+    E2E_TIMEOUT,
+  )
+
   // https://github.com/vuejs/core/issues/12181#issuecomment-2414380955
   describe('not leaking', async () => {
     test('switching VNodes', async () => {

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 294 - 225
pnpm-lock.yaml


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác