--- /dev/null
- const port = '8196'
+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()
++import { ports } from '../utils'
+
+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 = ports.transitionGroup
+ 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(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>` +
+ `<div class="test test-enter-from test-enter-active">e</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>` +
+ `<div class="test test-enter-active test-enter-to">e</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>` +
+ `<div class="test">e</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'leave',
+ async () => {
+ const btnSelector = '.leave > button'
+ const containerSelector = '.leave > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test test-leave-from test-leave-active">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test test-leave-from test-leave-active">c</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-leave-active test-leave-to">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test test-leave-active test-leave-to">c</div>`,
+ )
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(`<div class="test">b</div>`)
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'enter + leave',
+ async () => {
+ const btnSelector = '.enter-leave > button'
+ const containerSelector = '.enter-leave > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test test-leave-from test-leave-active">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-leave-active test-leave-to">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>`,
+ )
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'appear',
+ async () => {
+ const btnSelector = '.appear > button'
+ const containerSelector = '.appear > div'
+
+ expect(await html('.appear')).toBe(`<button>appear button</button>`)
+
+ await page().evaluate(() => {
+ return (window as any).setAppear()
+ })
+
+ // appear
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-from test-appear-active">a</div>` +
+ `<div class="test test-appear-from test-appear-active">b</div>` +
+ `<div class="test test-appear-from test-appear-active">c</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-active test-appear-to">a</div>` +
+ `<div class="test test-appear-active test-appear-to">b</div>` +
+ `<div class="test test-appear-active test-appear-to">c</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>` +
+ `<div class="test test-enter-from test-enter-active">e</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>` +
+ `<div class="test test-enter-active test-enter-to">e</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>` +
+ `<div class="test">e</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'move',
+ async () => {
+ const btnSelector = '.move > button'
+ const containerSelector = '.move > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test group-enter-from group-enter-active">d</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test group-move" style="">a</div>` +
+ `<div class="test group-leave-from group-leave-active group-move" style="">c</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test group-enter-active group-enter-to">d</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test group-move" style="">a</div>` +
+ `<div class="test group-leave-active group-move group-leave-to" style="">c</div>`,
+ )
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">d</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test" style="">a</div>`,
+ )
+ },
+ 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(
+ `<div>a</div>` + `<div>b</div>` + `<div>c</div>`,
+ )
+
+ // invalid name
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div>b</div>` + `<div>c</div>` + `<div>a</div>`)
+
+ // change name
+ expect(
+ (await transitionStart(btnChangeName, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="group-move" style="">a</div>` +
+ `<div class="group-move" style="">b</div>` +
+ `<div class="group-move" style="">c</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="" style="">a</div>` +
+ `<div class="" style="">b</div>` +
+ `<div class="" style="">c</div>`,
+ )
+ })
+
+ test('events', async () => {
+ const btnSelector = '.events > button'
+ const containerSelector = '.events > div'
+
+ expect(await html('.events')).toBe(`<button>events button</button>`)
+
+ await page().evaluate(() => {
+ return (window as any).setAppear()
+ })
+
+ // appear
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-from test-appear-active">a</div>` +
+ `<div class="test test-appear-from test-appear-active">b</div>` +
+ `<div class="test test-appear-from test-appear-active">c</div>`,
+ )
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-active test-appear-to">a</div>` +
+ `<div class="test test-appear-active test-appear-to">b</div>` +
+ `<div class="test test-appear-active test-appear-to">c</div>`,
+ )
+
+ 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(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls()
+ }),
+ ).toContain('afterAppear')
+
+ // enter + leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test test-leave-from test-leave-active">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>`,
+ )
+
+ 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(
+ `<div class="test test-leave-active test-leave-to">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>`,
+ )
+ 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(
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>`,
+ )
+
+ 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(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test-leave-from test-leave-active"><div>a</div></div>` +
+ `<div class="test-move" style=""><div>b</div></div>` +
+ `<div class="test-move" style=""><div>c</div></div>` +
+ `<div class="test-enter-from test-enter-active"><div>d</div></div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test-leave-active test-leave-to"><div>a</div></div>` +
+ `<div class="test-move" style=""><div>b</div></div>` +
+ `<div class="test-move" style=""><div>c</div></div>` +
+ `<div class="test-enter-active test-enter-to"><div>d</div></div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="" style=""><div>b</div></div>` +
+ `<div class="" style=""><div>c</div></div>` +
+ `<div class=""><div>d</div></div>`,
+ )
+ })
+})
--- /dev/null
- const port = '8195'
+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()
++import { ports } from '../utils'
+
+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 = ports.transition
+ 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(
+ `<div class="test">content</div>`,
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+ // 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(
+ '<div class="test">content</div>',
+ )
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test before-enter enter after-enter">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+ 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(
+ '<div class="test">content</div>',
+ )
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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('<div>content</div>')
+ // 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(
+ '<div class="">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'animations',
+ async () => {
+ const btnSelector = '.if-ani > button'
+ const containerSelector = '.if-ani > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('<div>content</div>')
+
+ // 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(
+ '<div class="">content</div>',
+ )
+ },
+ 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('<div>content</div>')
+
+ // 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(
+ '<div class="">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">two</div>',
+ )
+
+ // 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(
+ '<div class="test">two</div>',
+ )
+
+ // 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('<div foo="1">content</div>')
+
+ await click(btnSelector)
+ // toggle again before leave finishes
+ await nextTick()
+ await click(btnSelector)
+
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ '<div foo="1" class="">content</div>',
+ )
+ },
+ 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('<div foo="1">one</div>')
+
+ // 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(
+ '<div foo="1" class="">two</div>',
+ )
+
+ // 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(
+ '<div foo="1" class="">one</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+ 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(
+ '<div class="test" style="">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test" style="">content</div>',
+ )
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ 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(
+ '<div class="test" style="">content</div>',
+ )
+ },
+ 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(
+ '<div class="test" style="">content</div>',
+ )
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(
+ '<div class="test">content</div>',
+ )
+
+ // 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(
+ '<div class="test">content</div>',
+ )
+ },
+ 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(`<div>vapor compB</div>`)
+
+ // compB -> compA
+ // compB leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="fade-leave-from fade-leave-active">vapor compB</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compB</div>`,
+ )
+
+ // compA enter
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+
+ // compA -> compB
+ // compA leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="fade-leave-from fade-leave-active">vapor compA</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compA</div>`,
+ )
+
+ // compB enter
+ await waitForElement(childSelector, 'vapor compB', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vapor compB</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compB</div>`,
+ )
+ },
+ 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(`<div>vapor compB</div>`)
+
+ // compA enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div>vapor compB</div><div class="fade-enter-from fade-enter-active">vapor compA</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div>vapor compB</div><div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ // compB leave
+ await waitForElement(childSelector, 'vapor compB', [
+ 'fade-leave-from',
+ 'fade-leave-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compB</div><div class="">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+ },
+ 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(`<div>vdom comp</div>`)
+
+ // comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="v-leave-from v-leave-active">vdom comp</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="v-leave-active v-leave-to">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(``)
+
+ // comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="v-enter-from v-enter-active">vdom comp</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="v-enter-active v-enter-to">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div>`,
+ )
+ },
+ 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(`<div>vdom comp</div>`)
+
+ // switch to vapor comp
+ // vdom comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="fade-leave-from fade-leave-active">vdom comp</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vdom comp</div>`,
+ )
+
+ // vapor comp enter
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+
+ // switch to vdom comp
+ // vapor comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="fade-leave-from fade-leave-active">vapor compA</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compA</div>`,
+ )
+
+ // vdom comp enter
+ await waitForElement(childSelector, 'vdom comp', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div>`,
+ )
+ },
+ 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(`<div>vapor compA</div>`)
+
+ // switch to vdom comp
+ // vdom comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div>vapor compA</div><div class="fade-enter-from fade-enter-active">vdom comp</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div>vapor compA</div><div class="fade-enter-active fade-enter-to">vdom comp</div>`,
+ )
+
+ // vapor comp leave
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-leave-from',
+ 'fade-leave-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compA</div><div class="">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div>`,
+ )
+
+ // switch to vapor comp
+ // vapor comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="">vdom comp</div><div class="fade-enter-from fade-enter-active">vapor compA</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div><div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ // vdom comp leave
+ await waitForElement(childSelector, 'vdom comp', [
+ 'fade-leave-from',
+ 'fade-leave-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vdom comp</div><div class="">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
+})
} from '../../../packages/vue/__tests__/e2e/e2eUtils'
import connect from 'connect'
import sirv from 'sirv'
-const { page, click, text, enterValue, html } = setupPuppeteer()
+const {
+ page,
+ click,
+ text,
+ enterValue,
+ html,
+ transitionStart,
+ waitForElement,
+ nextFrame,
+ timeout,
+} = setupPuppeteer()
+
+const duration = process.env.CI ? 200 : 50
+const buffer = process.env.CI ? 50 : 20
+const transitionFinish = (time = duration) => timeout(time + buffer)
+
+ import { ports } from '../utils'
+ import { nextTick } from 'vue'
+
describe('vdom / vapor interop', () => {
let server: any
- const port = '8193'
+ const port = ports.vdomInterop
beforeAll(() => {
server = connect()
.use(sirv(path.resolve(import.meta.dirname, '../dist')))
E2E_TIMEOUT,
)
- describe('teleport', () => {
- const testSelector = '.teleport'
- test('render vapor component', async () => {
- const targetSelector = `${testSelector} .teleport-target`
- const containerSelector = `${testSelector} .render-vapor-comp`
- const buttonSelector = `${containerSelector} button`
+ describe('vdom transition', () => {
+ test(
+ 'render vapor component',
+ async () => {
+ const btnSelector = '.trans-vapor > button'
+ const containerSelector = '.trans-vapor > div'
- // teleport is disabled by default
- expect(await html(containerSelector)).toBe(
- `<button>toggle</button><div>vapor comp</div>`,
- )
- expect(await html(targetSelector)).toBe('')
-
- // disabled -> enabled
- await click(buttonSelector)
- await nextTick()
- expect(await html(containerSelector)).toBe(`<button>toggle</button>`)
- expect(await html(targetSelector)).toBe('<div>vapor comp</div>')
-
- // enabled -> disabled
- await click(buttonSelector)
- await nextTick()
- expect(await html(containerSelector)).toBe(
- `<button>toggle</button><div>vapor comp</div>`,
- )
- expect(await html(targetSelector)).toBe('')
+ expect(await html(containerSelector)).toBe(`<div>vapor compA</div>`)
+
+ // comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="v-leave-from v-leave-active">vapor compA</div><!---->`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="v-leave-active v-leave-to">vapor compA</div><!---->`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(`<!---->`)
+
+ // comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="v-enter-from v-enter-active">vapor compA</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="v-enter-active v-enter-to">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+ },
+ 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(`<div>vdom comp</div>`)
+
+ // switch to vapor comp
+ // vdom comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="fade-leave-from fade-leave-active">vdom comp</div><!---->`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vdom comp</div><!---->`,
+ )
+
+ // vapor comp enter
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+
+ // switch to vdom comp
+ // vapor comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="fade-leave-from fade-leave-active">vapor compA</div><!---->`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compA</div><!---->`,
+ )
+
+ // vdom comp enter
+ await waitForElement(childSelector, 'vdom comp', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div>`,
+ )
+ },
+ 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(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>`,
+ )
+
+ // insert
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>` +
+ `<div class="test-enter-from test-enter-active"><div>d</div></div>` +
+ `<div class="test-enter-from test-enter-active"><div>e</div></div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>` +
+ `<div class="test-enter-active test-enter-to"><div>d</div></div>` +
+ `<div class="test-enter-active test-enter-to"><div>e</div></div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>` +
+ `<div class=""><div>d</div></div>` +
+ `<div class=""><div>e</div></div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
++ describe('teleport', () => {
++ const testSelector = '.teleport'
++ test('render vapor component', async () => {
++ const targetSelector = `${testSelector} .teleport-target`
++ const containerSelector = `${testSelector} .render-vapor-comp`
++ const buttonSelector = `${containerSelector} button`
++
++ // teleport is disabled by default
++ expect(await html(containerSelector)).toBe(
++ `<button>toggle</button><div>vapor comp</div>`,
++ )
++ expect(await html(targetSelector)).toBe('')
++
++ // disabled -> enabled
++ await click(buttonSelector)
++ await nextTick()
++ expect(await html(containerSelector)).toBe(`<button>toggle</button>`)
++ expect(await html(targetSelector)).toBe('<div>vapor comp</div>')
++
++ // enabled -> disabled
++ await click(buttonSelector)
++ await nextTick()
++ expect(await html(containerSelector)).toBe(
++ `<button>toggle</button><div>vapor comp</div>`,
++ )
++ expect(await html(targetSelector)).toBe('')
++ })
+ })
})
})
<a href="/interop/">VDOM / Vapor interop</a>
<a href="/todomvc/">Vapor TodoMVC</a>
+<a href="/transition/">Vapor Transition</a>
+<a href="/transition-group/">Vapor TransitionGroup</a>
+ <a href="/teleport/">Vapor Teleport</a>
+
+<style>
+ a {
+ display: block;
+ margin: 10px;
+ }
+</style>
<script setup lang="ts">
-import { ref } from 'vue'
+import { ref, shallowRef } from 'vue'
- import VaporComp from './VaporComp.vue'
+ import VaporComp from './components/VaporComp.vue'
-import SimpleVaporComp from './components/SimpleVaporComp.vue'
+import VaporCompA from '../transition/components/VaporCompA.vue'
+import VdomComp from '../transition/components/VdomComp.vue'
+import VaporSlot from '../transition/components/VaporSlot.vue'
const msg = ref('hello')
const passSlot = ref(true)
+
+const toggleVapor = ref(true)
+const interopComponent = shallowRef(VdomComp)
+function toggleInteropComponent() {
+ interopComponent.value =
+ interopComponent.value === VaporCompA ? VdomComp : VaporCompA
+}
+
+const items = ref(['a', 'b', 'c'])
+const enterClick = () => items.value.push('d', 'e')
++import SimpleVaporComp from './components/SimpleVaporComp.vue'
++
+ const disabled = ref(true)
</script>
<template>
<template #test v-if="passSlot">A test slot</template>
</VaporComp>
+ <!-- transition interop -->
+ <div>
+ <div class="trans-vapor">
+ <button @click="toggleVapor = !toggleVapor">
+ toggle vapor component
+ </button>
+ <div>
+ <Transition>
+ <VaporCompA v-if="toggleVapor" />
+ </Transition>
+ </div>
+ </div>
+ <div class="trans-vdom-vapor-out-in">
+ <button @click="toggleInteropComponent">
+ switch between vdom/vapor component out-in mode
+ </button>
+ <div>
+ <Transition name="fade" mode="out-in">
+ <component :is="interopComponent"></component>
+ </Transition>
+ </div>
+ </div>
+ </div>
+ <!-- transition-group interop -->
+ <div>
+ <div class="trans-group-vapor">
+ <button @click="enterClick">insert items</button>
+ <div>
+ <transition-group name="test">
+ <VaporSlot v-for="item in items" :key="item">
+ <div>{{ item }}</div>
+ </VaporSlot>
+ </transition-group>
+ </div>
+ </div>
+ </div>
+ <!-- teleport -->
+ <div class="teleport">
+ <div class="teleport-target"></div>
+ <div class="render-vapor-comp">
+ <button @click="disabled = !disabled">toggle</button>
+ <Teleport to=".teleport-target" defer :disabled="disabled">
+ <SimpleVaporComp />
+ </Teleport>
+ </div>
+ </div>
+ <!-- teleport end-->
</template>
--- /dev/null
- teleport: 8195,
+ // make sure these ports are unique
+ export const ports = {
+ vdomInterop: 8193,
+ todomvc: 8194,
++ transition: 8195,
++ transitionGroup: 8196,
++ teleport: 8197,
+ }
input: {
interop: resolve(import.meta.dirname, 'interop/index.html'),
todomvc: resolve(import.meta.dirname, 'todomvc/index.html'),
+ teleport: resolve(import.meta.dirname, 'teleport/index.html'),
+ transition: resolve(import.meta.dirname, 'transition/index.html'),
+ transitionGroup: resolve(
+ import.meta.dirname,
+ 'transition-group/index.html',
+ ),
},
},
},
return exp.isStatic ? exp.content : null
}
+export function isInTransition(
+ context: TransformContext<ElementNode>,
+): boolean {
+ const parentNode = context.parent && context.parent.node
+ return !!(parentNode && isTransitionNode(parentNode as ElementNode))
+}
+
+export function isTransitionNode(node: ElementNode): boolean {
+ return node.type === NodeTypes.ELEMENT && isTransitionTag(node.tag)
+}
+
+export function isTransitionGroupNode(node: ElementNode): boolean {
+ return node.type === NodeTypes.ELEMENT && isTransitionGroupTag(node.tag)
+}
+
+export function isTransitionTag(tag: string): boolean {
+ tag = tag.toLowerCase()
+ return tag === 'transition' || tag === 'vaportransition'
+}
+
+export function isTransitionGroupTag(tag: string): boolean {
+ tag = tag.toLowerCase().replace(/-/g, '')
+ return tag === 'transitiongroup' || tag === 'vaportransitiongroup'
+}
+
+ export function isTeleportTag(tag: string): boolean {
+ tag = tag.toLowerCase()
+ return tag === 'teleport' || tag === 'vaporteleport'
+ }
+
export function isBuiltInComponent(tag: string): string | undefined {
- if (isTeleportTag(tag)) {
+ if (isTransitionTag(tag)) {
+ return 'VaporTransition'
+ } else if (isTransitionGroupTag(tag)) {
+ return 'VaporTransitionGroup'
++ } else if (isTeleportTag(tag)) {
+ return 'VaporTeleport'
}
}
* @internal
*/
export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { performTransitionEnter, performTransitionLeave } from './renderer'
+/**
+ * @internal
+ */
+export { ensureVaporSlotFallback } from './helpers/renderSlot'
+ /**
+ * @internal
+ */
+ export {
+ resolveTarget as resolveTeleportTarget,
+ isTeleportDisabled,
+ isTeleportDeferred,
+ } from './components/Teleport'
/**
* @internal
*/
-import { resolveDynamicComponent } from '@vue/runtime-dom'
+import { currentInstance, resolveDynamicComponent } from '@vue/runtime-dom'
- import { DynamicFragment, type VaporFragment, insert } from './block'
+ import { insert } from './block'
-import { createComponentWithFallback } from './component'
+import { createComponentWithFallback, emptyContext } from './component'
import { renderEffect } from './renderEffect'
import type { RawProps } from './componentProps'
import type { RawSlots } from './componentSlots'
insertionParent,
resetInsertionState,
} from './insertionState'
+import { DYNAMIC_COMPONENT_ANCHOR_LABEL } from '@vue/shared'
import { isHydrating, locateHydrationNode } from './dom/hydration'
+ import { DynamicFragment, type VaporFragment } from './fragment'
export function createDynamicComponent(
getter: () => any,
toReactive,
toReadonly,
} from '@vue/reactivity'
-import { getSequence, isArray, isObject, isString } from '@vue/shared'
+import {
+ FOR_ANCHOR_LABEL,
+ getSequence,
+ isArray,
+ isObject,
+ isString,
+} from '@vue/shared'
import { createComment, createTextNode } from './dom/node'
- import {
- type Block,
- VaporFragment,
- insert,
- remove as removeBlock,
- } from './block'
+ import { type Block, insert, remove as removeBlock } from './block'
import { warn } from '@vue/runtime-dom'
import { currentInstance, isVaporComponent } from './component'
import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import { applyTransitionHooks } from './components/Transition'
+import {
+ currentHydrationNode,
+ isHydrating,
+ locateHydrationNode,
+ locateVaporFragmentAnchor,
+} from './dom/hydration'
+ import { VaporFragment } from './fragment'
import {
insertionAnchor,
insertionParent,
--- /dev/null
- import { type Block, type BlockFn, DynamicFragment } from './block'
++import type { Block, BlockFn } from './block'
++import { DynamicFragment } from './fragment'
+import { renderEffect } from './renderEffect'
+
+export function createKeyedFragment(key: () => any, render: BlockFn): Block {
+ const frag = __DEV__ ? new DynamicFragment('keyed') : new DynamicFragment()
+ renderEffect(() => {
+ frag.update(render, key())
+ })
+ return frag
+}
- import { type Block, type BlockFn, DynamicFragment, insert } from './block'
+import { IF_ANCHOR_LABEL } from '@vue/shared'
+ import { type Block, type BlockFn, insert } from './block'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import {
insertionAnchor,
mountComponent,
unmountComponent,
} from './component'
- import { createComment, createTextNode } from './dom/node'
- import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
- import {
- currentHydrationNode,
- isComment,
- locateHydrationNode,
- locateVaporFragmentAnchor,
- } from './dom/hydration'
+import {
+ type TransitionHooks,
+ type TransitionProps,
+ type TransitionState,
+ performTransitionEnter,
+ performTransitionLeave,
+} from '@vue/runtime-dom'
+ import { isHydrating } from './dom/hydration'
++import { getInheritedScopeIds } from '@vue/runtime-dom'
import {
- applyTransitionHooks,
- applyTransitionLeaveHooks,
- } from './components/Transition'
+ type DynamicFragment,
+ type VaporFragment,
+ isFragment,
+ } from './fragment'
-export type Block =
- | Node
- | VaporFragment
- | DynamicFragment
- | VaporComponentInstance
- | Block[]
+export interface TransitionOptions {
+ $key?: any
+ $transition?: VaporTransitionHooks
+}
- import { isHydrating } from './dom/hydration'
- import { getInheritedScopeIds } from '@vue/runtime-dom'
+
+export interface VaporTransitionHooks extends TransitionHooks {
+ state: TransitionState
+ props: TransitionProps
+ instance: VaporComponentInstance
+ // mark transition hooks as disabled so that it skips during
+ // inserting
+ disabled?: boolean
+}
+
+export type TransitionBlock =
+ | (Node & TransitionOptions)
+ | (VaporFragment & TransitionOptions)
+ | (DynamicFragment & TransitionOptions)
+
+export type Block = TransitionBlock | VaporComponentInstance | Block[]
export type BlockFn = (...args: any[]) => Block
insert(b, parent, anchor)
}
} else {
+ if (block.anchor) {
+ insert(block.anchor, parent, anchor)
+ anchor = block.anchor
+ }
// fragment
if (block.insert) {
- // TODO handle hydration for vdom interop
- block.insert(parent, anchor)
+ block.insert(parent, anchor, (block as TransitionBlock).$transition)
} else {
- insert(block.nodes, parent, anchor, parentSuspense)
- insert(block.nodes, block.target || parent, block.targetAnchor || anchor)
++ insert(
++ block.nodes,
++ block.target || parent,
++ block.targetAnchor || anchor,
++ parentSuspense,
++ )
}
- if (block.anchor) insert(block.anchor, parent, anchor)
}
}
unregisterHMR,
warn,
} from '@vue/runtime-dom'
-import { type Block, insert, isBlock, remove } from './block'
+import {
+ type Block,
- DynamicFragment,
+ insert,
+ isBlock,
+ remove,
+ setComponentScopeId,
+ setScopeId,
+} from './block'
import {
type ShallowRef,
markRaw,
getSlot,
} from './componentSlots'
import { hmrReload, hmrRerender } from './hmr'
+import { createElement } from './dom/node'
import { isHydrating, locateHydrationNode } from './dom/hydration'
+ import { isVaporTeleport } from './components/Teleport'
import {
insertionAnchor,
insertionParent,
-import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
-import { type Block, type BlockFn, insert } from './block'
+import {
+ EMPTY_OBJ,
+ NO,
+ SLOT_ANCHOR_LABEL,
+ hasOwn,
+ isArray,
+ isFunction,
+} from '@vue/shared'
- import {
- type Block,
- type BlockFn,
- DynamicFragment,
- type VaporFragment,
- insert,
- isFragment,
- setScopeId,
- } from './block'
++import { type Block, type BlockFn, insert, setScopeId } from './block'
import { rawPropsProxyHandlers } from './componentProps'
import { currentInstance, isRef } from '@vue/runtime-dom'
import type { LooseRawProps, VaporComponentInstance } from './component'
resetInsertionState,
} from './insertionState'
import { isHydrating, locateHydrationNode } from './dom/hydration'
-import { DynamicFragment } from './fragment'
++import { DynamicFragment, type VaporFragment, isFragment } from './fragment'
export type RawSlots = Record<string, VaporSlot> & {
$?: DynamicSlotSource[]
--- /dev/null
- import {
- type Block,
- type TransitionBlock,
- type VaporTransitionHooks,
- isFragment,
- } from '../block'
+import {
+ type GenericComponentInstance,
+ type TransitionElement,
+ type TransitionHooks,
+ type TransitionHooksContext,
+ type TransitionProps,
+ TransitionPropsValidators,
+ type TransitionState,
+ baseResolveTransitionHooks,
+ checkTransitionMode,
+ currentInstance,
+ leaveCbKey,
+ resolveTransitionProps,
+ useTransitionState,
+ warn,
+} from '@vue/runtime-dom'
++import type { Block, TransitionBlock, VaporTransitionHooks } from '../block'
+import {
+ type FunctionalVaporComponent,
+ type VaporComponentInstance,
+ applyFallthroughProps,
+ isVaporComponent,
+} from '../component'
+import { extend, isArray } from '@vue/shared'
+import { renderEffect } from '../renderEffect'
++import { isFragment } from '../fragment'
+
+const decorate = (t: typeof VaporTransition) => {
+ t.displayName = 'VaporTransition'
+ t.props = TransitionPropsValidators
+ t.__vapor = true
+ return t
+}
+
+export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
+ (props, { slots, attrs }) => {
+ const children = (slots.default && slots.default()) as any as Block
+ if (!children) return
+
+ const instance = currentInstance! as VaporComponentInstance
+ const { mode } = props
+ checkTransitionMode(mode)
+
+ let resolvedProps
+ let isMounted = false
+ renderEffect(() => {
+ resolvedProps = resolveTransitionProps(props)
+ if (isMounted) {
+ // only update props for Fragment block, for later reusing
+ if (isFragment(children)) {
+ children.$transition!.props = resolvedProps
+ } else {
+ const child = findTransitionBlock(children)
+ if (child) {
+ // replace existing transition hooks
+ child.$transition!.props = resolvedProps
+ applyTransitionHooks(child, child.$transition!)
+ }
+ }
+ } else {
+ isMounted = true
+ }
+ })
+
+ // fallthrough attrs
+ let fallthroughAttrs = true
+ if (instance.hasFallthrough) {
+ renderEffect(() => {
+ // attrs are accessed in advance
+ const resolvedAttrs = extend({}, attrs)
+ const child = findTransitionBlock(children)
+ if (child) {
+ // mark single root
+ ;(child as any).$root = true
+
+ applyFallthroughProps(child, resolvedAttrs)
+ // ensure fallthrough attrs are not happened again in
+ // applyTransitionHooks
+ fallthroughAttrs = false
+ }
+ })
+ }
+
+ applyTransitionHooks(
+ children,
+ {
+ state: useTransitionState(),
+ props: resolvedProps!,
+ instance: instance,
+ } as VaporTransitionHooks,
+ fallthroughAttrs,
+ )
+
+ return children
+ },
+)
+
+const getTransitionHooksContext = (
+ key: String,
+ props: TransitionProps,
+ state: TransitionState,
+ instance: GenericComponentInstance,
+ postClone: ((hooks: TransitionHooks) => void) | undefined,
+) => {
+ const { leavingNodes } = state
+ const context: TransitionHooksContext = {
+ setLeavingNodeCache: el => {
+ leavingNodes.set(key, el)
+ },
+ unsetLeavingNodeCache: el => {
+ const leavingNode = leavingNodes.get(key)
+ if (leavingNode === el) {
+ leavingNodes.delete(key)
+ }
+ },
+ earlyRemove: () => {
+ const leavingNode = leavingNodes.get(key)
+ if (leavingNode && (leavingNode as TransitionElement)[leaveCbKey]) {
+ // force early removal (not cancelled)
+ ;(leavingNode as TransitionElement)[leaveCbKey]!()
+ }
+ },
+ cloneHooks: block => {
+ const hooks = resolveTransitionHooks(
+ block,
+ props,
+ state,
+ instance,
+ postClone,
+ )
+ if (postClone) postClone(hooks)
+ return hooks
+ },
+ }
+ return context
+}
+
+export function resolveTransitionHooks(
+ block: TransitionBlock,
+ props: TransitionProps,
+ state: TransitionState,
+ instance: GenericComponentInstance,
+ postClone?: (hooks: TransitionHooks) => void,
+): VaporTransitionHooks {
+ const context = getTransitionHooksContext(
+ String(block.$key),
+ props,
+ state,
+ instance,
+ postClone,
+ )
+ const hooks = baseResolveTransitionHooks(
+ context,
+ props,
+ state,
+ instance,
+ ) as VaporTransitionHooks
+ hooks.state = state
+ hooks.props = props
+ hooks.instance = instance as VaporComponentInstance
+ return hooks
+}
+
+export function applyTransitionHooks(
+ block: Block,
+ hooks: VaporTransitionHooks,
+ fallthroughAttrs: boolean = true,
+): VaporTransitionHooks {
+ const isFrag = isFragment(block)
+ const child = findTransitionBlock(block)
+ if (!child) {
+ // set transition hooks on fragment for reusing during it's updating
+ if (isFrag) setTransitionHooksOnFragment(block, hooks)
+ return hooks
+ }
+
+ const { props, instance, state, delayedLeave } = hooks
+ let resolvedHooks = resolveTransitionHooks(
+ child,
+ props,
+ state,
+ instance,
+ hooks => (resolvedHooks = hooks as VaporTransitionHooks),
+ )
+ resolvedHooks.delayedLeave = delayedLeave
+ setTransitionHooks(child, resolvedHooks)
+ if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks)
+
+ // fallthrough attrs
+ if (fallthroughAttrs && instance.hasFallthrough) {
+ // mark single root
+ ;(child as any).$root = true
+ renderEffect(() => applyFallthroughProps(child, instance.attrs))
+ }
+
+ return resolvedHooks
+}
+
+export function applyTransitionLeaveHooks(
+ block: Block,
+ enterHooks: VaporTransitionHooks,
+ afterLeaveCb: () => void,
+): void {
+ const leavingBlock = findTransitionBlock(block)
+ if (!leavingBlock) return undefined
+
+ const { props, state, instance } = enterHooks
+ const leavingHooks = resolveTransitionHooks(
+ leavingBlock,
+ props,
+ state,
+ instance,
+ )
+ setTransitionHooks(leavingBlock, leavingHooks)
+
+ const { mode } = props
+ if (mode === 'out-in') {
+ state.isLeaving = true
+ leavingHooks.afterLeave = () => {
+ state.isLeaving = false
+ afterLeaveCb()
+ leavingBlock.$transition = undefined
+ delete leavingHooks.afterLeave
+ }
+ } else if (mode === 'in-out') {
+ leavingHooks.delayLeave = (
+ block: TransitionElement,
+ earlyRemove,
+ delayedLeave,
+ ) => {
+ state.leavingNodes.set(String(leavingBlock.$key), leavingBlock)
+ // early removal callback
+ block[leaveCbKey] = () => {
+ earlyRemove()
+ block[leaveCbKey] = undefined
+ leavingBlock.$transition = undefined
+ delete enterHooks.delayedLeave
+ }
+ enterHooks.delayedLeave = () => {
+ delayedLeave()
+ leavingBlock.$transition = undefined
+ delete enterHooks.delayedLeave
+ }
+ }
+ }
+}
+
+const transitionBlockCache = new WeakMap<Block, TransitionBlock>()
+export function findTransitionBlock(
+ block: Block,
+ inFragment: boolean = false,
+): TransitionBlock | undefined {
+ if (transitionBlockCache.has(block)) {
+ return transitionBlockCache.get(block)
+ }
+
+ let isFrag = false
+ let child: TransitionBlock | undefined
+ if (block instanceof Node) {
+ // transition can only be applied on Element child
+ if (block instanceof Element) child = block
+ } else if (isVaporComponent(block)) {
+ child = findTransitionBlock(block.block)
+ // use component id as key
+ if (child && child.$key === undefined) child.$key = block.uid
+ } else if (isArray(block)) {
+ child = block[0] as TransitionBlock
+ let hasFound = false
+ for (const c of block) {
+ const item = findTransitionBlock(c)
+ if (item instanceof Element) {
+ if (__DEV__ && hasFound) {
+ // warn more than one non-comment child
+ warn(
+ '<transition> can only be used on a single element or component. ' +
+ 'Use <transition-group> for lists.',
+ )
+ break
+ }
+ child = item
+ hasFound = true
+ if (!__DEV__) break
+ }
+ }
+ } else if ((isFrag = isFragment(block))) {
+ if (block.insert) {
+ child = block
+ } else {
+ child = findTransitionBlock(block.nodes, true)
+ }
+ }
+
+ if (__DEV__ && !child && !inFragment && !isFrag) {
+ warn('Transition component has no valid child element')
+ }
+
+ return child
+}
+
+export function setTransitionHooksOnFragment(
+ block: Block,
+ hooks: VaporTransitionHooks,
+): void {
+ if (isFragment(block)) {
+ setTransitionHooks(block, hooks)
+ } else if (isArray(block)) {
+ for (let i = 0; i < block.length; i++) {
+ setTransitionHooksOnFragment(block[i], hooks)
+ }
+ }
+}
+
+export function setTransitionHooks(
+ block: TransitionBlock | VaporComponentInstance,
+ hooks: VaporTransitionHooks,
+): void {
+ if (isVaporComponent(block)) {
+ block = findTransitionBlock(block.block) as TransitionBlock
+ if (!block) return
+ }
+ block.$transition = hooks
+}
--- /dev/null
- DynamicFragment,
+import {
+ type ElementWithTransition,
+ type TransitionGroupProps,
+ TransitionPropsValidators,
+ baseApplyTranslation,
+ callPendingCbs,
+ currentInstance,
+ forceReflow,
+ handleMovedChildren,
+ hasCSSTransform,
+ onBeforeUpdate,
+ onUpdated,
+ resolveTransitionProps,
+ useTransitionState,
+ warn,
+} from '@vue/runtime-dom'
+import { extend, isArray } from '@vue/shared'
+import {
+ type Block,
- isFragment,
+ type TransitionBlock,
+ type VaporTransitionHooks,
+ insert,
+} from '../block'
+import {
+ resolveTransitionHooks,
+ setTransitionHooks,
+ setTransitionHooksOnFragment,
+} from './Transition'
+import {
+ type ObjectVaporComponent,
+ type VaporComponentInstance,
+ applyFallthroughProps,
+ isVaporComponent,
+} from '../component'
+import { isForBlock } from '../apiCreateFor'
+import { renderEffect } from '../renderEffect'
+import { createElement } from '../dom/node'
++import { DynamicFragment, isFragment } from '../fragment'
+
+const positionMap = new WeakMap<TransitionBlock, DOMRect>()
+const newPositionMap = new WeakMap<TransitionBlock, DOMRect>()
+
+const decorate = (t: typeof VaporTransitionGroup) => {
+ delete (t.props! as any).mode
+ t.__vapor = true
+ return t
+}
+
+export const VaporTransitionGroup: ObjectVaporComponent = decorate({
+ name: 'VaporTransitionGroup',
+
+ props: /*@__PURE__*/ extend({}, TransitionPropsValidators, {
+ tag: String,
+ moveClass: String,
+ }),
+
+ setup(props: TransitionGroupProps, { slots }) {
+ const instance = currentInstance as VaporComponentInstance
+ const state = useTransitionState()
+ const cssTransitionProps = resolveTransitionProps(props)
+
+ let prevChildren: TransitionBlock[]
+ let children: TransitionBlock[]
+ let slottedBlock: Block
+
+ onBeforeUpdate(() => {
+ prevChildren = []
+ children = getTransitionBlocks(slottedBlock)
+ if (children) {
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ if (isValidTransitionBlock(child)) {
+ prevChildren.push(child)
+ // disabled transition during enter, so the children will be
+ // inserted into the correct position immediately. this prevents
+ // `recordPosition` from getting incorrect positions in `onUpdated`
+ child.$transition!.disabled = true
+ positionMap.set(
+ child,
+ getTransitionElement(child).getBoundingClientRect(),
+ )
+ }
+ }
+ }
+ })
+
+ onUpdated(() => {
+ if (!prevChildren.length) {
+ return
+ }
+
+ const moveClass = props.moveClass || `${props.name || 'v'}-move`
+ const firstChild = getFirstConnectedChild(prevChildren)
+ if (
+ !firstChild ||
+ !hasCSSTransform(
+ firstChild as ElementWithTransition,
+ firstChild.parentNode as Node,
+ moveClass,
+ )
+ ) {
+ prevChildren = []
+ return
+ }
+
+ prevChildren.forEach(callPendingCbs)
+ prevChildren.forEach(child => {
+ child.$transition!.disabled = false
+ recordPosition(child)
+ })
+ const movedChildren = prevChildren.filter(applyTranslation)
+
+ // force reflow to put everything in position
+ forceReflow()
+
+ movedChildren.forEach(c =>
+ handleMovedChildren(
+ getTransitionElement(c) as ElementWithTransition,
+ moveClass,
+ ),
+ )
+ prevChildren = []
+ })
+
+ slottedBlock = slots.default && slots.default()
+
+ // store props and state on fragment for reusing during insert new items
+ setTransitionHooksOnFragment(slottedBlock, {
+ props: cssTransitionProps,
+ state,
+ instance,
+ } as VaporTransitionHooks)
+
+ children = getTransitionBlocks(slottedBlock)
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ if (isValidTransitionBlock(child)) {
+ if (child.$key != null) {
+ setTransitionHooks(
+ child,
+ resolveTransitionHooks(child, cssTransitionProps, state, instance!),
+ )
+ } else if (__DEV__ && child.$key == null) {
+ warn(`<transition-group> children must be keyed`)
+ }
+ }
+ }
+
+ const tag = props.tag
+ if (tag) {
+ const container = createElement(tag)
+ insert(slottedBlock, container)
+ // fallthrough attrs
+ if (instance!.hasFallthrough) {
+ ;(container as any).$root = true
+ renderEffect(() => applyFallthroughProps(container, instance!.attrs))
+ }
+ return container
+ } else {
+ const frag = __DEV__
+ ? new DynamicFragment('transitionGroup')
+ : new DynamicFragment()
+ renderEffect(() => frag.update(() => slottedBlock))
+ return frag
+ }
+ },
+})
+
+function getTransitionBlocks(block: Block) {
+ let children: TransitionBlock[] = []
+ if (block instanceof Node) {
+ children.push(block)
+ } else if (isVaporComponent(block)) {
+ children.push(...getTransitionBlocks(block.block))
+ } else if (isArray(block)) {
+ for (let i = 0; i < block.length; i++) {
+ const b = block[i]
+ const blocks = getTransitionBlocks(b)
+ if (isForBlock(b)) blocks.forEach(block => (block.$key = b.key))
+ children.push(...blocks)
+ }
+ } else if (isFragment(block)) {
+ if (block.insert) {
+ // vdom component
+ children.push(block)
+ } else {
+ children.push(...getTransitionBlocks(block.nodes))
+ }
+ }
+
+ return children
+}
+
+function isValidTransitionBlock(block: Block): boolean {
+ return !!(block instanceof Element || (isFragment(block) && block.insert))
+}
+
+function getTransitionElement(c: TransitionBlock): Element {
+ return (isFragment(c) ? (c.nodes as Element[])[0] : c) as Element
+}
+
+function recordPosition(c: TransitionBlock) {
+ newPositionMap.set(c, getTransitionElement(c).getBoundingClientRect())
+}
+
+function applyTranslation(c: TransitionBlock): TransitionBlock | undefined {
+ if (
+ baseApplyTranslation(
+ positionMap.get(c)!,
+ newPositionMap.get(c)!,
+ getTransitionElement(c) as ElementWithTransition,
+ )
+ ) {
+ return c
+ }
+}
+
+function getFirstConnectedChild(
+ children: TransitionBlock[],
+): Element | undefined {
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ const el = getTransitionElement(child)
+ if (el.isConnected) return el
+ }
+}
} from '@vue/runtime-dom'
import { renderEffect } from '../renderEffect'
import { isVaporComponent } from '../component'
- import { type Block, DynamicFragment, type TransitionBlock } from '../block'
-import type { Block } from '../block'
++import type { Block, TransitionBlock } from '../block'
import { isArray } from '@vue/shared'
+ import { DynamicFragment } from '../fragment'
export function applyVShow(target: Block, source: () => any): void {
if (isVaporComponent(target)) {
--- /dev/null
-import { type Block, type BlockFn, insert, isValidBlock, remove } from './block'
+ import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
+ import { createComment, createTextNode } from './dom/node'
-export class VaporFragment {
++import {
++ type Block,
++ type BlockFn,
++ type TransitionOptions,
++ type VaporTransitionHooks,
++ insert,
++ isValidBlock,
++ remove,
++} from './block'
++import type { TransitionHooks } from '@vue/runtime-dom'
++import {
++ currentHydrationNode,
++ isComment,
++ isHydrating,
++ locateHydrationNode,
++ locateVaporFragmentAnchor,
++} from './dom/hydration'
++import {
++ applyTransitionHooks,
++ applyTransitionLeaveHooks,
++} from './components/Transition'
+
- anchor?: Node
- insert?: (parent: ParentNode, anchor: Node | null) => void
- remove?: (parent?: ParentNode) => void
++export class VaporFragment implements TransitionOptions {
++ $key?: any
++ $transition?: VaporTransitionHooks | undefined
+ nodes: Block
++ anchor?: Node
++ insert?: (
++ parent: ParentNode,
++ anchor: Node | null,
++ transitionHooks?: TransitionHooks,
++ ) => void
++ remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
++ fallback?: BlockFn
++
+ target?: ParentNode | null
+ targetAnchor?: Node | null
- anchor: Node
+ getNodes?: () => Block
+
+ constructor(nodes: Block) {
+ this.nodes = nodes
+ }
+ }
+
+ export class DynamicFragment extends VaporFragment {
- this.anchor =
- __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
++ anchor!: Node
+ scope: EffectScope | undefined
+ current?: BlockFn
+ fallback?: BlockFn
++ /**
++ * slot only
++ * indicates forwarded slot
++ */
++ forwarded?: boolean
+
+ constructor(anchorLabel?: string) {
+ super([])
- parent && remove(this.nodes, parent)
++ if (isHydrating) {
++ locateHydrationNode(true)
++ this.hydrate(anchorLabel!)
++ } else {
++ this.anchor =
++ __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
++ }
+ }
+
+ update(render?: BlockFn, key: any = render): void {
+ if (key === this.current) {
+ return
+ }
+ this.current = key
+
+ pauseTracking()
+ const parent = this.anchor.parentNode
++ const transition = this.$transition
++ const renderBranch = () => {
++ if (render) {
++ this.scope = new EffectScope()
++ this.nodes = this.scope.run(render) || []
++ if (transition) {
++ this.$transition = applyTransitionHooks(this.nodes, transition)
++ }
++ if (parent) insert(this.nodes, parent, this.anchor)
++ } else {
++ this.scope = undefined
++ this.nodes = []
++ }
++ }
+
+ // teardown previous branch
+ if (this.scope) {
+ this.scope.stop()
- if (render) {
- this.scope = new EffectScope()
- this.nodes = this.scope.run(render) || []
- if (parent) insert(this.nodes, parent, this.anchor)
- } else {
- this.scope = undefined
- this.nodes = []
- }
++ const mode = transition && transition.mode
++ if (mode) {
++ applyTransitionLeaveHooks(this.nodes, transition, renderBranch)
++ parent && remove(this.nodes, parent)
++ if (mode === 'out-in') {
++ resetTracking()
++ return
++ }
++ } else {
++ parent && remove(this.nodes, parent)
++ }
+ }
+
++ renderBranch()
+
+ if (this.fallback && !isValidBlock(this.nodes)) {
+ parent && remove(this.nodes, parent)
+ this.nodes =
+ (this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
+ []
+ parent && insert(this.nodes, parent, this.anchor)
+ }
+
+ resetTracking()
+ }
++
++ hydrate(label: string): void {
++ // for `v-if="false"` the node will be an empty comment, use it as the anchor.
++ // otherwise, find next sibling vapor fragment anchor
++ if (isComment(currentHydrationNode!, '')) {
++ this.anchor = currentHydrationNode
++ } else {
++ const anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
++ if (anchor) {
++ this.anchor = anchor
++ } else if (__DEV__) {
++ // this should not happen
++ throw new Error(`${label} fragment anchor node was not found.`)
++ }
++ }
++ }
+ }
+
+ export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
+ return val instanceof VaporFragment
+ }
applyDynamicModel,
} from './directives/vModel'
export { withVaporDirectives } from './directives/custom'
-export { isFragment } from './fragment'
-export { VaporFragment } from './fragment'
+export { VaporTransition } from './components/Transition'
+export { VaporTransitionGroup } from './components/TransitionGroup'
++export { isFragment, VaporFragment } from './fragment'
mountComponent,
unmountComponent,
} from './component'
- import {
- type Block,
- DynamicFragment,
- VaporFragment,
- type VaporTransitionHooks,
- insert,
- isFragment,
- remove,
- } from './block'
-import { type Block, insert, remove } from './block'
-import { EMPTY_OBJ, extend, isFunction } from '@vue/shared'
++import { type Block, type VaporTransitionHooks, insert, remove } from './block'
+import {
+ EMPTY_OBJ,
+ extend,
+ isArray,
+ isFunction,
+ isReservedProp,
+} from '@vue/shared'
import { type RawProps, rawPropsProxyHandlers } from './componentProps'
import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop'
-import { VaporFragment } from './fragment'
+import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
+import {
+ currentHydrationNode,
+ isHydrating,
+ locateHydrationNode,
+ hydrateNode as vaporHydrateNode,
+} from './dom/hydration'
++import { DynamicFragment, VaporFragment, isFragment } from './fragment'
// mounting vapor components and slots in vdom
const vaporInteropImpl: Omit<