diff --git a/packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts
new file mode 100644
index 00000000000..ba050f0f263
--- /dev/null
+++ b/packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts
@@ -0,0 +1,406 @@
+import path from 'node:path'
+import {
+ E2E_TIMEOUT,
+ setupPuppeteer,
+} from '../../../packages/vue/__tests__/e2e/e2eUtils'
+import connect from 'connect'
+import sirv from 'sirv'
+import { expect } from 'vitest'
+const { page, nextFrame, timeout, html, transitionStart } = setupPuppeteer()
+
+const duration = process.env.CI ? 200 : 50
+const buffer = process.env.CI ? 50 : 20
+const transitionFinish = (time = duration) => timeout(time + buffer)
+
+describe('vapor transition-group', () => {
+ let server: any
+ const port = '8196'
+ beforeAll(() => {
+ server = connect()
+ .use(sirv(path.resolve(import.meta.dirname, '../dist')))
+ .listen(port)
+ process.on('SIGTERM', () => server && server.close())
+ })
+
+ afterAll(() => {
+ server.close()
+ })
+
+ beforeEach(async () => {
+ const baseUrl = `http://localhost:${port}/transition-group/`
+ await page().goto(baseUrl)
+ await page().waitForSelector('#app')
+ })
+
+ test(
+ 'enter',
+ async () => {
+ const btnSelector = '.enter > button'
+ const containerSelector = '.enter > div'
+
+ expect(await html(containerSelector)).toBe(
+ `
a
` +
+ `b
` +
+ `c
`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
` +
+ `e
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
` +
+ `e
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
` +
+ `e
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'leave',
+ async () => {
+ const btnSelector = '.leave > button'
+ const containerSelector = '.leave > div'
+
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(`b
`)
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'enter + leave',
+ async () => {
+ const btnSelector = '.enter-leave > button'
+ const containerSelector = '.enter-leave > div'
+
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
`,
+ )
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `b
` +
+ `c
` +
+ `d
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'appear',
+ async () => {
+ const btnSelector = '.appear > button'
+ const containerSelector = '.appear > div'
+
+ expect(await html('.appear')).toBe(``)
+
+ await page().evaluate(() => {
+ return (window as any).setAppear()
+ })
+
+ // appear
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
` +
+ `e
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
` +
+ `e
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
` +
+ `e
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'move',
+ async () => {
+ const btnSelector = '.move > button'
+ const containerSelector = '.move > div'
+
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `d
` +
+ `b
` +
+ `a
` +
+ `c
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `d
` +
+ `b
` +
+ `a
` +
+ `c
`,
+ )
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ `d
` +
+ `b
` +
+ `a
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test('dynamic name', async () => {
+ const btnSelector = '.dynamic-name button.toggleBtn'
+ const btnChangeName = '.dynamic-name button.changeNameBtn'
+ const containerSelector = '.dynamic-name > div'
+
+ expect(await html(containerSelector)).toBe(
+ `a
` + `b
` + `c
`,
+ )
+
+ // invalid name
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`b
` + `c
` + `a
`)
+
+ // change name
+ expect(
+ (await transitionStart(btnChangeName, containerSelector)).innerHTML,
+ ).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+ })
+
+ test('events', async () => {
+ const btnSelector = '.events > button'
+ const containerSelector = '.events > div'
+
+ expect(await html('.events')).toBe(``)
+
+ await page().evaluate(() => {
+ return (window as any).setAppear()
+ })
+
+ // appear
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).toContain('beforeAppear')
+ expect(calls).toContain('onAppear')
+ expect(calls).not.toContain('afterAppear')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
`,
+ )
+
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls()
+ }),
+ ).toContain('afterAppear')
+
+ // enter + leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
`,
+ )
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).toContain('beforeLeave')
+ expect(calls).toContain('onLeave')
+ expect(calls).not.toContain('afterLeave')
+ expect(calls).toContain('beforeEnter')
+ expect(calls).toContain('onEnter')
+ expect(calls).not.toContain('afterEnter')
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `a
` +
+ `b
` +
+ `c
` +
+ `d
`,
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).not.toContain('afterLeave')
+ expect(calls).not.toContain('afterEnter')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `b
` +
+ `c
` +
+ `d
`,
+ )
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).toContain('afterLeave')
+ expect(calls).toContain('afterEnter')
+ })
+
+ test('interop: render vdom component', async () => {
+ const btnSelector = '.interop > button'
+ const containerSelector = '.interop > div'
+
+ expect(await html(containerSelector)).toBe(
+ `` +
+ `` +
+ ``,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `` +
+ `` +
+ `` +
+ ``,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `` +
+ `` +
+ `` +
+ ``,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `` +
+ `` +
+ ``,
+ )
+ })
+})
diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts
new file mode 100644
index 00000000000..0bfc30598cc
--- /dev/null
+++ b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts
@@ -0,0 +1,1660 @@
+import path from 'node:path'
+import {
+ E2E_TIMEOUT,
+ setupPuppeteer,
+} from '../../../packages/vue/__tests__/e2e/e2eUtils'
+import connect from 'connect'
+import sirv from 'sirv'
+import { nextTick } from 'vue'
+const {
+ page,
+ classList,
+ text,
+ nextFrame,
+ timeout,
+ isVisible,
+ count,
+ html,
+ transitionStart,
+ waitForElement,
+ click,
+} = setupPuppeteer()
+
+const duration = process.env.CI ? 200 : 50
+const buffer = process.env.CI ? 50 : 20
+const transitionFinish = (time = duration) => timeout(time + buffer)
+
+describe('vapor transition', () => {
+ let server: any
+ const port = '8195'
+ beforeAll(() => {
+ server = connect()
+ .use(sirv(path.resolve(import.meta.dirname, '../dist')))
+ .listen(port)
+ process.on('SIGTERM', () => server && server.close())
+ })
+
+ afterAll(() => {
+ server.close()
+ })
+
+ beforeEach(async () => {
+ const baseUrl = `http://localhost:${port}/transition/`
+ await page().goto(baseUrl)
+ await page().waitForSelector('#app')
+ })
+
+ describe('transition with v-if', () => {
+ test(
+ 'basic transition',
+ async () => {
+ const btnSelector = '.if-basic > button'
+ const containerSelector = '.if-basic > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ `content
`,
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'v-leave-from', 'v-leave-active'])
+
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'v-leave-active',
+ 'v-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'v-enter-from', 'v-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'v-enter-active',
+ 'v-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'named transition',
+ async () => {
+ const btnSelector = '.if-named > button'
+ const containerSelector = '.if-named > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'custom transition classes',
+ async () => {
+ const btnSelector = '.if-custom-classes > button'
+ const containerSelector = '.if-custom-classes > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'bye-from', 'bye-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'bye-active',
+ 'bye-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'hello-from', 'hello-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'hello-active',
+ 'hello-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition with dynamic name',
+ async () => {
+ const btnSelector = '.if-dynamic-name > button.toggle'
+ const btnChangeNameSelector = '.if-dynamic-name > button.change'
+ const containerSelector = '.if-dynamic-name > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ await click(btnChangeNameSelector)
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'changed-enter-from', 'changed-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'changed-enter-active',
+ 'changed-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition events without appear',
+ async () => {
+ const btnSelector = '.if-events-without-appear > button'
+ const containerSelector = '.if-events-without-appear > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ }),
+ ).not.contain('afterLeave')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ }),
+ ).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('withoutAppear')
+ })
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ }),
+ ).not.contain('afterEnter')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ }),
+ ).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'events with arguments',
+ async () => {
+ const btnSelector = '.if-events-with-args > button'
+ const containerSelector = '.if-events-with-args > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ await click(btnSelector)
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('withArgs')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave'])
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'before-leave',
+ 'leave',
+ ])
+
+ await timeout(200 + buffer)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withArgs')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+ expect(await html(containerSelector)).toBe('')
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('withArgs')
+ })
+
+ // enter
+ await click(btnSelector)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withArgs')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'before-enter',
+ 'enter',
+ ])
+
+ await timeout(200 + buffer)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withArgs')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'onEnterCancelled',
+ async () => {
+ const btnSelector = '.if-enter-cancelled > button'
+ const containerSelector = '.if-enter-cancelled > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+
+ // cancel (leave)
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('enterCancel')
+ })
+ expect(calls).toStrictEqual(['enterCancelled'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition on appear',
+ async () => {
+ const btnSelector = '.if-appear > button'
+ const containerSelector = '.if-appear > div'
+ const childSelector = `${containerSelector} > div`
+
+ // appear
+ expect(await classList(childSelector)).contains('test-appear-active')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition events with appear',
+ async () => {
+ const btnSelector = '.if-events-with-appear > button'
+ const containerSelector = '.if-events-with-appear > div'
+ const childSelector = `${containerSelector} > div`
+ // appear
+ expect(await classList(childSelector)).contains('test-appear-active')
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeAppear', 'onAppear'])
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeAppear', 'onAppear', 'afterAppear'])
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('withAppear')
+ })
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave'])
+
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).not.contain('afterLeave')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('withAppear')
+ })
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).not.contain('afterEnter')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ },
+ E2E_TIMEOUT,
+ )
+ test(
+ 'css: false',
+ async () => {
+ const btnSelector = '.if-css-false > button'
+ const containerSelector = '.if-css-false > div'
+ const childSelector = `${containerSelector} > div`
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ await click(btnSelector)
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('cssFalse')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+ expect(await html(containerSelector)).toBe('')
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('cssFalse')
+ })
+
+ // enter
+ await transitionStart(btnSelector, childSelector)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('cssFalse')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'no transition detected',
+ async () => {
+ const btnSelector = '.if-no-trans > button'
+ const containerSelector = '.if-no-trans > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('content
')
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['noop-leave-from', 'noop-leave-active'])
+ await nextFrame()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['noop-enter-from', 'noop-enter-active'])
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'animations',
+ async () => {
+ const btnSelector = '.if-ani > button'
+ const containerSelector = '.if-ani > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('content
')
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test-anim-leave-from', 'test-anim-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-leave-active',
+ 'test-anim-leave-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test-anim-enter-from', 'test-anim-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-enter-active',
+ 'test-anim-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'explicit transition type',
+ async () => {
+ const btnSelector = '.if-ani-explicit-type > button'
+ const containerSelector = '.if-ani-explicit-type > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('content
')
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual([
+ 'test-anim-long-leave-from',
+ 'test-anim-long-leave-active',
+ ])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-long-leave-active',
+ 'test-anim-long-leave-to',
+ ])
+
+ if (!process.env.CI) {
+ await new Promise(r => {
+ setTimeout(r, duration - buffer)
+ })
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-long-leave-active',
+ 'test-anim-long-leave-to',
+ ])
+ }
+
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual([
+ 'test-anim-long-enter-from',
+ 'test-anim-long-enter-active',
+ ])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-long-enter-active',
+ 'test-anim-long-enter-to',
+ ])
+
+ if (!process.env.CI) {
+ await new Promise(r => {
+ setTimeout(r, duration - buffer)
+ })
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-long-enter-active',
+ 'test-anim-long-enter-to',
+ ])
+ }
+
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test.todo('transition on SVG elements', async () => {}, E2E_TIMEOUT)
+
+ test(
+ 'custom transition higher-order component',
+ async () => {
+ const btnSelector = '.if-high-order > button'
+ const containerSelector = '.if-high-order > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition on child components with empty root node',
+ async () => {
+ const btnSelector = '.if-empty-root > button.toggle'
+ const btnChangeSelector = '.if-empty-root > button.change'
+ const containerSelector = '.if-empty-root > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('')
+
+ // change view -> 'two'
+ await click(btnChangeSelector)
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'two
',
+ )
+
+ // change view -> 'one'
+ await click(btnChangeSelector)
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition with v-if at component root-level',
+ async () => {
+ const btnSelector = '.if-at-component-root-level > button.toggle'
+ const btnChangeSelector = '.if-at-component-root-level > button.change'
+ const containerSelector = '.if-at-component-root-level > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('')
+
+ // change view -> 'two'
+ await click(btnChangeSelector)
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'two
',
+ )
+
+ // change view -> 'one'
+ await click(btnChangeSelector)
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'wrapping transition + fallthrough attrs',
+ async () => {
+ const btnSelector = '.if-fallthrough-attr > button'
+ const containerSelector = '.if-fallthrough-attr > div'
+
+ expect(await html(containerSelector)).toBe('content
')
+
+ await click(btnSelector)
+ // toggle again before leave finishes
+ await nextTick()
+ await click(btnSelector)
+
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition + fallthrough attrs (in-out mode)',
+ async () => {
+ const btnSelector = '.if-fallthrough-attr-in-out > button'
+ const containerSelector = '.if-fallthrough-attr-in-out > div'
+
+ expect(await html(containerSelector)).toBe('one
')
+
+ // toggle
+ await click(btnSelector)
+ await nextTick()
+ await transitionFinish(duration * 3)
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('ifInOut')
+ })
+ expect(calls).toStrictEqual([
+ 'beforeEnter',
+ 'onEnter',
+ 'afterEnter',
+ 'beforeLeave',
+ 'onLeave',
+ 'afterLeave',
+ ])
+
+ expect(await html(containerSelector)).toBe(
+ 'two
',
+ )
+
+ // clear calls
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('ifInOut')
+ })
+
+ // toggle back
+ await click(btnSelector)
+ await nextTick()
+ await transitionFinish(duration * 3)
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('ifInOut')
+ })
+ expect(calls).toStrictEqual([
+ 'beforeEnter',
+ 'onEnter',
+ 'afterEnter',
+ 'beforeLeave',
+ 'onLeave',
+ 'afterLeave',
+ ])
+
+ expect(await html(containerSelector)).toBe(
+ 'one
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
+
+ describe.todo('transition with KeepAlive', () => {})
+ describe.todo('transition with Suspense', () => {})
+ describe.todo('transition with Teleport', () => {})
+
+ describe('transition with v-show', () => {
+ test(
+ 'named transition with v-show',
+ async () => {
+ const btnSelector = '.show-named > button'
+ const containerSelector = '.show-named > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ expect(await isVisible(childSelector)).toBe(true)
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await isVisible(childSelector)).toBe(false)
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition events with v-show',
+ async () => {
+ const btnSelector = '.show-events > button'
+ const containerSelector = '.show-events > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).not.contain('afterLeave')
+ await transitionFinish()
+ expect(await isVisible(childSelector)).toBe(false)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+
+ // clear calls
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('show')
+ })
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'onLeaveCancelled (v-show only)',
+ async () => {
+ const btnSelector = '.show-leave-cancelled > button'
+ const containerSelector = '.show-leave-cancelled > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+
+ // cancel (enter)
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('showLeaveCancel')
+ })
+ expect(calls).toStrictEqual(['leaveCancelled'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await isVisible(childSelector)).toBe(true)
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition on appear with v-show',
+ async () => {
+ const btnSelector = '.show-appear > button'
+ const containerSelector = '.show-appear > div'
+ const childSelector = `${containerSelector} > div`
+
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('showAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+
+ // appear
+ expect(await classList(childSelector)).contains('test-appear-active')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('showAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await isVisible(childSelector)).toBe(false)
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition events should not call onEnter with v-show false',
+ async () => {
+ const btnSelector = '.show-appear-not-enter > button'
+ const containerSelector = '.show-appear-not-enter > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await isVisible(childSelector)).toBe(false)
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('notEnter')
+ })
+ expect(calls).toStrictEqual([])
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('notEnter')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('notEnter')
+ })
+ expect(calls).not.contain('afterEnter')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('notEnter')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ },
+ E2E_TIMEOUT,
+ )
+ })
+
+ describe('explicit durations', () => {
+ test(
+ 'single value',
+ async () => {
+ const btnSelector = '.duration-single-value > button'
+ const containerSelector = '.duration-single-value > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'enter with explicit durations',
+ async () => {
+ const btnSelector = '.duration-enter > button'
+ const containerSelector = '.duration-enter > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'leave with explicit durations',
+ async () => {
+ const btnSelector = '.duration-leave > button'
+ const containerSelector = '.duration-leave > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'separate enter and leave',
+ async () => {
+ const btnSelector = '.duration-enter-leave > button'
+ const containerSelector = '.duration-enter-leave > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish(duration * 4)
+ expect(await html(containerSelector)).toBe(
+ 'content
',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
+
+ test(
+ 'should work with keyed element',
+ async () => {
+ const btnSelector = '.keyed > button'
+ const containerSelector = '.keyed > h1'
+
+ expect(await text(containerSelector)).toContain('0')
+
+ // change key
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).classNames,
+ ).toStrictEqual(['v-leave-from', 'v-leave-active'])
+
+ await nextFrame()
+ expect(await classList(containerSelector)).toStrictEqual([
+ 'v-leave-active',
+ 'v-leave-to',
+ ])
+
+ await transitionFinish()
+ await nextFrame()
+ expect(await text(containerSelector)).toContain('1')
+
+ // change key again
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).classNames,
+ ).toStrictEqual(['v-leave-from', 'v-leave-active'])
+
+ await nextFrame()
+ expect(await classList(containerSelector)).toStrictEqual([
+ 'v-leave-active',
+ 'v-leave-to',
+ ])
+
+ await transitionFinish()
+ await nextFrame()
+ expect(await text(containerSelector)).toContain('2')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'should work with out-in mode',
+ async () => {
+ const btnSelector = '.out-in > button'
+ const containerSelector = '.out-in > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`vapor compB
`)
+
+ // compB -> compA
+ // compB leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`vapor compB
`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compB
`,
+ )
+
+ // compA enter
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ await transitionFinish()
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ // compA -> compB
+ // compA leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`vapor compA
`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ // compB enter
+ await waitForElement(childSelector, 'vapor compB', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compB
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vapor compB
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'should work with in-out mode',
+ async () => {
+ const btnSelector = '.in-out > button'
+ const containerSelector = '.in-out > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`vapor compB
`)
+
+ // compA enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `vapor compB
vapor compA
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compB
vapor compA
`,
+ )
+
+ // compB leave
+ await waitForElement(childSelector, 'vapor compB', [
+ 'fade-leave-from',
+ 'fade-leave-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compB
vapor compA
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ // tests for using vdom component in createVaporApp + vaporInteropPlugin
+ describe('interop', () => {
+ test(
+ 'render vdom component',
+ async () => {
+ const btnSelector = '.vdom > button'
+ const containerSelector = '.vdom > div'
+
+ expect(await html(containerSelector)).toBe(`vdom comp
`)
+
+ // comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`vdom comp
`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(``)
+
+ // comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`vdom comp
`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'switch between vdom/vapor component (out-in mode)',
+ async () => {
+ const btnSelector = '.vdom-vapor-out-in > button'
+ const containerSelector = '.vdom-vapor-out-in > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`vdom comp
`)
+
+ // switch to vapor comp
+ // vdom comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`vdom comp
`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+
+ // vapor comp enter
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ // switch to vdom comp
+ // vapor comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `vapor compA
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ // vdom comp enter
+ await waitForElement(childSelector, 'vdom comp', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'switch between vdom/vapor component (in-out mode)',
+ async () => {
+ const btnSelector = '.vdom-vapor-in-out > button'
+ const containerSelector = '.vdom-vapor-in-out > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`vapor compA
`)
+
+ // switch to vdom comp
+ // vdom comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `vapor compA
vdom comp
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
vdom comp
`,
+ )
+
+ // vapor comp leave
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-leave-from',
+ 'fade-leave-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
vdom comp
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+
+ // switch to vapor comp
+ // vapor comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `vdom comp
vapor compA
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
vapor compA
`,
+ )
+
+ // vdom comp leave
+ await waitForElement(childSelector, 'vdom comp', [
+ 'fade-leave-from',
+ 'fade-leave-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
vapor compA
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
+})
diff --git a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts
index 360f48085a1..e05f06e1abd 100644
--- a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts
+++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts
@@ -5,10 +5,23 @@ import {
} from '../../../packages/vue/__tests__/e2e/e2eUtils'
import connect from 'connect'
import sirv from 'sirv'
+const {
+ page,
+ click,
+ text,
+ enterValue,
+ html,
+ transitionStart,
+ waitForElement,
+ nextFrame,
+ timeout,
+} = setupPuppeteer()
-describe('vdom / vapor interop', () => {
- const { page, click, text, enterValue } = setupPuppeteer()
+const duration = process.env.CI ? 200 : 50
+const buffer = process.env.CI ? 50 : 20
+const transitionFinish = (time = duration) => timeout(time + buffer)
+describe('vdom / vapor interop', () => {
let server: any
const port = '8193'
beforeAll(() => {
@@ -22,12 +35,15 @@ describe('vdom / vapor interop', () => {
server.close()
})
+ beforeEach(async () => {
+ const baseUrl = `http://localhost:${port}/interop/`
+ await page().goto(baseUrl)
+ await page().waitForSelector('#app')
+ })
+
test(
'should work',
async () => {
- const baseUrl = `http://localhost:${port}/interop/`
- await page().goto(baseUrl)
-
expect(await text('.vapor > h2')).toContain('Vapor component in VDOM')
expect(await text('.vapor-prop')).toContain('hello')
@@ -81,4 +97,163 @@ describe('vdom / vapor interop', () => {
},
E2E_TIMEOUT,
)
+
+ describe('vdom transition', () => {
+ test(
+ 'render vapor component',
+ async () => {
+ const btnSelector = '.trans-vapor > button'
+ const containerSelector = '.trans-vapor > div'
+
+ expect(await html(containerSelector)).toBe(`vapor compA
`)
+
+ // comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `vapor compA
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(``)
+
+ // comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`vapor compA
`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'switch between vdom/vapor component (out-in mode)',
+ async () => {
+ const btnSelector = '.trans-vdom-vapor-out-in > button'
+ const containerSelector = '.trans-vdom-vapor-out-in > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`vdom comp
`)
+
+ // switch to vapor comp
+ // vdom comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `vdom comp
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+
+ // vapor comp enter
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ // switch to vdom comp
+ // vapor comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `vapor compA
`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vapor compA
`,
+ )
+
+ // vdom comp enter
+ await waitForElement(childSelector, 'vdom comp', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `vdom comp
`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
+
+ describe('vdom transition-group', () => {
+ test(
+ 'render vapor component',
+ async () => {
+ const btnSelector = '.trans-group-vapor > button'
+ const containerSelector = '.trans-group-vapor > div'
+
+ expect(await html(containerSelector)).toBe(
+ `` +
+ `` +
+ ``,
+ )
+
+ // insert
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `` +
+ `` +
+ `` +
+ `` +
+ ``,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `` +
+ `` +
+ `` +
+ `` +
+ ``,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `` +
+ `` +
+ `` +
+ `` +
+ ``,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
})
diff --git a/packages-private/vapor-e2e-test/index.html b/packages-private/vapor-e2e-test/index.html
index 7dc205e5ab0..09ea6aa607a 100644
--- a/packages-private/vapor-e2e-test/index.html
+++ b/packages-private/vapor-e2e-test/index.html
@@ -1,2 +1,11 @@
VDOM / Vapor interop
Vapor TodoMVC
+Vapor Transition
+Vapor TransitionGroup
+
+
diff --git a/packages-private/vapor-e2e-test/interop/App.vue b/packages-private/vapor-e2e-test/interop/App.vue
index 772a6989dd7..8cf42e47549 100644
--- a/packages-private/vapor-e2e-test/interop/App.vue
+++ b/packages-private/vapor-e2e-test/interop/App.vue
@@ -1,9 +1,22 @@
@@ -19,4 +32,41 @@ const passSlot = ref(true)
A test slot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages-private/vapor-e2e-test/interop/VaporComp.vue b/packages-private/vapor-e2e-test/interop/VaporComp.vue
index 88a60c782c0..f0156544919 100644
--- a/packages-private/vapor-e2e-test/interop/VaporComp.vue
+++ b/packages-private/vapor-e2e-test/interop/VaporComp.vue
@@ -27,7 +27,8 @@ const slotProp = ref('slot prop')
change slot prop
- #default:
+ #default:
+
#test:
fallback content
@@ -40,7 +41,7 @@ const slotProp = ref('slot prop')
>
Toggle default slot to vdom
-
+
slot prop: {{ foo }}
component prop: {{ msg }}
diff --git a/packages-private/vapor-e2e-test/interop/main.ts b/packages-private/vapor-e2e-test/interop/main.ts
index d5d6d7dcf8c..41155dc5cb2 100644
--- a/packages-private/vapor-e2e-test/interop/main.ts
+++ b/packages-private/vapor-e2e-test/interop/main.ts
@@ -1,4 +1,5 @@
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
+import '../transition/style.css'
createApp(App).use(vaporInteropPlugin).mount('#app')
diff --git a/packages-private/vapor-e2e-test/transition-group/App.vue b/packages-private/vapor-e2e-test/transition-group/App.vue
new file mode 100644
index 00000000000..55775743c56
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition-group/App.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
calls.push('beforeEnter')"
+ @enter="() => calls.push('onEnter')"
+ @afterEnter="() => calls.push('afterEnter')"
+ @beforeLeave="() => calls.push('beforeLeave')"
+ @leave="() => calls.push('onLeave')"
+ @afterLeave="() => calls.push('afterLeave')"
+ @beforeAppear="() => calls.push('beforeAppear')"
+ @appear="() => calls.push('onAppear')"
+ @afterAppear="() => calls.push('afterAppear')"
+ >
+ {{ item }}
+
+
+
+
+
+
+
diff --git a/packages-private/vapor-e2e-test/transition-group/components/VaporComp.vue b/packages-private/vapor-e2e-test/transition-group/components/VaporComp.vue
new file mode 100644
index 00000000000..906795d22f2
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition-group/components/VaporComp.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/packages-private/vapor-e2e-test/transition-group/components/VdomComp.vue b/packages-private/vapor-e2e-test/transition-group/components/VdomComp.vue
new file mode 100644
index 00000000000..afd7d55f2be
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition-group/components/VdomComp.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/packages-private/vapor-e2e-test/transition-group/index.html b/packages-private/vapor-e2e-test/transition-group/index.html
new file mode 100644
index 00000000000..79052a023ba
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition-group/index.html
@@ -0,0 +1,2 @@
+
+
diff --git a/packages-private/vapor-e2e-test/transition-group/main.ts b/packages-private/vapor-e2e-test/transition-group/main.ts
new file mode 100644
index 00000000000..efa06a296cc
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition-group/main.ts
@@ -0,0 +1,5 @@
+import { createVaporApp, vaporInteropPlugin } from 'vue'
+import App from './App.vue'
+import '../../../packages/vue/__tests__/e2e/style.css'
+
+createVaporApp(App).use(vaporInteropPlugin).mount('#app')
diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue
new file mode 100644
index 00000000000..4855098243b
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition/App.vue
@@ -0,0 +1,528 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
calls.withoutAppear.push('beforeEnter')"
+ @enter="() => calls.withoutAppear.push('onEnter')"
+ @after-enter="() => calls.withoutAppear.push('afterEnter')"
+ @beforeLeave="() => calls.withoutAppear.push('beforeLeave')"
+ @leave="() => calls.withoutAppear.push('onLeave')"
+ @afterLeave="() => calls.withoutAppear.push('afterLeave')"
+ >
+ content
+
+
+
+
+
+
+
{
+ calls.withArgs.push('beforeEnter')
+ el.classList.add('before-enter')
+ }
+ "
+ @enter="
+ (el, done) => {
+ calls.withArgs.push('onEnter')
+ el.classList.add('enter')
+ timeout(done, 200)
+ }
+ "
+ @after-enter="
+ el => {
+ calls.withArgs.push('afterEnter')
+ el.classList.add('after-enter')
+ }
+ "
+ @before-leave="
+ el => {
+ calls.withArgs.push('beforeLeave')
+ el.classList.add('before-leave')
+ }
+ "
+ @leave="
+ (el, done) => {
+ calls.withArgs.push('onLeave')
+ el.classList.add('leave')
+ timeout(done, 200)
+ }
+ "
+ @after-leave="
+ () => {
+ calls.withArgs.push('afterLeave')
+ }
+ "
+ >
+ content
+
+
+
+
+
+
+
{
+ calls.enterCancel.push('enterCancelled')
+ }
+ "
+ >
+ content
+
+
+
+
+
+
+
+
calls.withAppear.push('beforeEnter')"
+ @enter="() => calls.withAppear.push('onEnter')"
+ @afterEnter="() => calls.withAppear.push('afterEnter')"
+ @beforeLeave="() => calls.withAppear.push('beforeLeave')"
+ @leave="() => calls.withAppear.push('onLeave')"
+ @afterLeave="() => calls.withAppear.push('afterLeave')"
+ @beforeAppear="() => calls.withAppear.push('beforeAppear')"
+ @appear="() => calls.withAppear.push('onAppear')"
+ @afterAppear="() => calls.withAppear.push('afterAppear')"
+ >
+ content
+
+
+
+
+
+
+
calls.cssFalse.push('beforeEnter')"
+ @enter="() => calls.cssFalse.push('onEnter')"
+ @afterEnter="() => calls.cssFalse.push('afterEnter')"
+ @beforeLeave="() => calls.cssFalse.push('beforeLeave')"
+ @leave="() => calls.cssFalse.push('onLeave')"
+ @afterLeave="() => calls.cssFalse.push('afterLeave')"
+ >
+ content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ calls.ifInOut.push('beforeEnter')"
+ @enter="() => calls.ifInOut.push('onEnter')"
+ @afterEnter="() => calls.ifInOut.push('afterEnter')"
+ @beforeLeave="() => calls.ifInOut.push('beforeLeave')"
+ @leave="() => calls.ifInOut.push('onLeave')"
+ @afterLeave="() => calls.ifInOut.push('afterLeave')"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
calls.show.push('beforeEnter')"
+ @enter="() => calls.show.push('onEnter')"
+ @afterEnter="() => calls.show.push('afterEnter')"
+ @beforeLeave="() => calls.show.push('beforeLeave')"
+ @leave="() => calls.show.push('onLeave')"
+ @afterLeave="() => calls.show.push('afterLeave')"
+ >
+ content
+
+
+
+
+
+
+
calls.showLeaveCancel.push('leaveCancelled')"
+ >
+ content
+
+
+
+
+
+
+
calls.showAppear.push('beforeEnter')"
+ @enter="() => calls.showAppear.push('onEnter')"
+ @afterEnter="() => calls.showAppear.push('afterEnter')"
+ >
+ content
+
+
+
+
+
+
+
calls.notEnter.push('beforeEnter')"
+ @enter="() => calls.notEnter.push('onEnter')"
+ @afterEnter="() => calls.notEnter.push('afterEnter')"
+ >
+ content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ count }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue b/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue
new file mode 100644
index 00000000000..f6902d8cf78
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue
@@ -0,0 +1,6 @@
+
+
+ {{ msg }}
+
diff --git a/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue b/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue
new file mode 100644
index 00000000000..db90f993f12
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue
@@ -0,0 +1,6 @@
+
+
+ {{ msg }}
+
diff --git a/packages-private/vapor-e2e-test/transition/components/VaporSlot.vue b/packages-private/vapor-e2e-test/transition/components/VaporSlot.vue
new file mode 100644
index 00000000000..f5eff0100f8
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition/components/VaporSlot.vue
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/packages-private/vapor-e2e-test/transition/components/VdomComp.vue b/packages-private/vapor-e2e-test/transition/components/VdomComp.vue
new file mode 100644
index 00000000000..cb6ec7ccad1
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition/components/VdomComp.vue
@@ -0,0 +1,6 @@
+
+
+ {{ msg }}
+
diff --git a/packages-private/vapor-e2e-test/transition/index.html b/packages-private/vapor-e2e-test/transition/index.html
new file mode 100644
index 00000000000..79052a023ba
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition/index.html
@@ -0,0 +1,2 @@
+
+
diff --git a/packages-private/vapor-e2e-test/transition/main.ts b/packages-private/vapor-e2e-test/transition/main.ts
new file mode 100644
index 00000000000..e77d51d1c03
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition/main.ts
@@ -0,0 +1,6 @@
+import { createVaporApp, vaporInteropPlugin } from 'vue'
+import App from './App.vue'
+import '../../../packages/vue/__tests__/e2e/style.css'
+import './style.css'
+
+createVaporApp(App).use(vaporInteropPlugin).mount('#app')
diff --git a/packages-private/vapor-e2e-test/transition/style.css b/packages-private/vapor-e2e-test/transition/style.css
new file mode 100644
index 00000000000..e6faf6cea53
--- /dev/null
+++ b/packages-private/vapor-e2e-test/transition/style.css
@@ -0,0 +1,35 @@
+.v-enter-active,
+.v-leave-active {
+ transition: opacity 50ms ease;
+}
+
+.v-enter-from,
+.v-leave-to {
+ opacity: 0;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 50ms ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
+.test-move,
+.test-enter-active,
+.test-leave-active {
+ transition: all 50ms cubic-bezier(0.55, 0, 0.1, 1);
+}
+
+.test-enter-from,
+.test-leave-to {
+ opacity: 0;
+ transform: scaleY(0.01) translate(30px, 0);
+}
+
+.test-leave-active {
+ position: absolute;
+}
diff --git a/packages-private/vapor-e2e-test/vite.config.ts b/packages-private/vapor-e2e-test/vite.config.ts
index 1e29a4dbd13..f50fccea3ce 100644
--- a/packages-private/vapor-e2e-test/vite.config.ts
+++ b/packages-private/vapor-e2e-test/vite.config.ts
@@ -14,6 +14,11 @@ export default defineConfig({
input: {
interop: resolve(import.meta.dirname, 'interop/index.html'),
todomvc: resolve(import.meta.dirname, 'todomvc/index.html'),
+ transition: resolve(import.meta.dirname, 'transition/index.html'),
+ transitionGroup: resolve(
+ import.meta.dirname,
+ 'transition-group/index.html',
+ ),
},
},
},
diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts
index 950901e7bf9..446a917ad7c 100644
--- a/packages/compiler-dom/src/index.ts
+++ b/packages/compiler-dom/src/index.ts
@@ -76,4 +76,5 @@ export {
} from './errors'
export { resolveModifiers } from './transforms/vOn'
export { isValidHTMLNesting } from './htmlNesting'
+export { postTransformTransition } from './transforms/Transition'
export * from '@vue/compiler-core'
diff --git a/packages/compiler-dom/src/transforms/Transition.ts b/packages/compiler-dom/src/transforms/Transition.ts
index f6cf968e372..30ea083d8fc 100644
--- a/packages/compiler-dom/src/transforms/Transition.ts
+++ b/packages/compiler-dom/src/transforms/Transition.ts
@@ -1,4 +1,5 @@
import {
+ type CompilerError,
type ComponentNode,
ElementTypes,
type IfBranchNode,
@@ -15,47 +16,55 @@ export const transformTransition: NodeTransform = (node, context) => {
) {
const component = context.isBuiltInComponent(node.tag)
if (component === TRANSITION) {
- return () => {
- if (!node.children.length) {
- return
- }
+ return postTransformTransition(node, context.onError)
+ }
+ }
+}
- // warn multiple transition children
- if (hasMultipleChildren(node)) {
- context.onError(
- createDOMCompilerError(
- DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN,
- {
- start: node.children[0].loc.start,
- end: node.children[node.children.length - 1].loc.end,
- source: '',
- },
- ),
- )
- }
+export function postTransformTransition(
+ node: ComponentNode,
+ onError: (error: CompilerError) => void,
+ hasMultipleChildren: (
+ node: ComponentNode,
+ ) => boolean = defaultHasMultipleChildren,
+): () => void {
+ return () => {
+ if (!node.children.length) {
+ return
+ }
+
+ if (hasMultipleChildren(node)) {
+ onError(
+ createDOMCompilerError(DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN, {
+ start: node.children[0].loc.start,
+ end: node.children[node.children.length - 1].loc.end,
+ source: '',
+ }),
+ )
+ }
- // check if it's s single child w/ v-show
- // if yes, inject "persisted: true" to the transition props
- const child = node.children[0]
- if (child.type === NodeTypes.ELEMENT) {
- for (const p of child.props) {
- if (p.type === NodeTypes.DIRECTIVE && p.name === 'show') {
- node.props.push({
- type: NodeTypes.ATTRIBUTE,
- name: 'persisted',
- nameLoc: node.loc,
- value: undefined,
- loc: node.loc,
- })
- }
- }
+ // check if it's s single child w/ v-show
+ // if yes, inject "persisted: true" to the transition props
+ const child = node.children[0]
+ if (child.type === NodeTypes.ELEMENT) {
+ for (const p of child.props) {
+ if (p.type === NodeTypes.DIRECTIVE && p.name === 'show') {
+ node.props.push({
+ type: NodeTypes.ATTRIBUTE,
+ name: 'persisted',
+ nameLoc: node.loc,
+ value: undefined,
+ loc: node.loc,
+ })
}
}
}
}
}
-function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
+function defaultHasMultipleChildren(
+ node: ComponentNode | IfBranchNode,
+): boolean {
// #1352 filter out potential comment nodes.
const children = (node.children = node.children.filter(
c =>
@@ -66,6 +75,7 @@ function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
return (
children.length !== 1 ||
child.type === NodeTypes.FOR ||
- (child.type === NodeTypes.IF && child.branches.some(hasMultipleChildren))
+ (child.type === NodeTypes.IF &&
+ child.branches.some(defaultHasMultipleChildren))
)
}
diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
index 2fde4560ec4..fb2fff86574 100644
--- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
@@ -39,6 +39,7 @@ describe('ssr: components', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) {
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent("foo"), _mergeProps({ prop: "b" }, _attrs), null), _parent)
+ _push(\`\`)
}"
`)
@@ -49,6 +50,7 @@ describe('ssr: components', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) {
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: "b" }, _attrs), null), _parent)
+ _push(\`\`)
}"
`)
})
@@ -244,7 +246,8 @@ describe('ssr: components', () => {
_ssrRenderList(list, (i) => {
_push(\`\`)
})
- _push(\` \`)
+ _push(\`\`)
+ _push(\`\`)
} else {
_push(\`\`)
}
@@ -267,7 +270,8 @@ describe('ssr: components', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`\`)
})
- _push(\`\`)
+ _push(\`\`)
+ _push(\`\`)
} else {
_push(\`\`)
}
@@ -361,6 +365,7 @@ describe('ssr: components', () => {
_push(\`\`)
if (false) {
_push(\`\`)
+ _push(\`\`)
} else {
_push(\`\`)
}
diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts
index f1d509acfb0..d344405f3ed 100644
--- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts
@@ -396,4 +396,50 @@ describe('ssr: element', () => {
`)
})
})
+
+ describe('dynamic anchor', () => {
+ test('two consecutive components', () => {
+ expect(
+ getCompiledString(`
+
+ `),
+ ).toMatchInlineSnapshot(`
+ "\`\`)
+ _push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
+ _push(\`\`)
+ _push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
+ _push(\`
\`"
+ `)
+ })
+
+ test('multiple consecutive components', () => {
+ expect(
+ getCompiledString(`
+
+ `),
+ ).toMatchInlineSnapshot(`
+ "\`\`)
+ _push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
+ _push(\`\`)
+ _push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
+ _push(\`\`)
+ _push(_ssrRenderComponent(_component_Comp3, null, null, _parent))
+ _push(\`\`)
+ _push(_ssrRenderComponent(_component_Comp4, null, null, _parent))
+ _push(\`
\`"
+ `)
+ })
+ })
})
diff --git a/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts b/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts
index 7b3d1962c3e..712c09d0946 100644
--- a/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts
@@ -29,6 +29,7 @@ describe('ssr: attrs fallthrough', () => {
_push(\`\`)
if (true) {
_push(\`\`)
+ _push(\`\`)
} else {
_push(\`\`)
}
diff --git a/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts b/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts
index 9e70dac0bdc..0666e8949cc 100644
--- a/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts
@@ -70,6 +70,7 @@ describe('ssr: inject
+