]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
chore: Merge branch 'edison/feat/vaporTeleport' into edison/testVapor
authordaiwei <daiwei521@126.com>
Mon, 23 Jun 2025 07:22:11 +0000 (15:22 +0800)
committerdaiwei <daiwei521@126.com>
Mon, 23 Jun 2025 07:24:35 +0000 (15:24 +0800)
24 files changed:
1  2 
packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts
packages-private/vapor-e2e-test/__tests__/transition.spec.ts
packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts
packages-private/vapor-e2e-test/index.html
packages-private/vapor-e2e-test/interop/App.vue
packages-private/vapor-e2e-test/interop/components/VaporComp.vue
packages-private/vapor-e2e-test/utils.ts
packages-private/vapor-e2e-test/vite.config.ts
packages/compiler-vapor/src/generators/component.ts
packages/compiler-vapor/src/utils.ts
packages/runtime-core/src/index.ts
packages/runtime-vapor/src/apiCreateDynamicComponent.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/apiCreateFragment.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/components/Transition.ts
packages/runtime-vapor/src/components/TransitionGroup.ts
packages/runtime-vapor/src/directives/vShow.ts
packages/runtime-vapor/src/fragment.ts
packages/runtime-vapor/src/index.ts
packages/runtime-vapor/src/vdomInterop.ts

index ba050f0f26380bdd6be9ff7d8df617b0034af5be,0000000000000000000000000000000000000000..13e1f1df3f14098ddd044bbd35d74595a0e7a6f2
mode 100644,000000..100644
--- /dev/null
@@@ -1,406 -1,0 +1,407 @@@
-   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>`,
 +    )
 +  })
 +})
index 0bfc30598cca9cf7c74e187e15b7d9eeebbcb564,0000000000000000000000000000000000000000..ccb0475f34e9b60944b2cc9fe0f06c1cb10c3cf0
mode 100644,000000..100644
--- /dev/null
@@@ -1,1660 -1,0 +1,1661 @@@
-   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,
 +    )
 +  })
 +})
index e05f06e1abd7b999664428deafca228b479a5d03,734c9fde19038548939350a5e0cbe527cfbc4102..e4959121cad7714f2c8be3014fdeba775c972ba9
@@@ -5,25 -5,13 +5,28 @@@ import 
  } 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('')
++      })
+     })
    })
  })
index 09ea6aa607a4283ce54ff83a4fd91428153002ff,bb1234e8e10d79f1613e1067a8cfaec9528df651..85a18d79ea353993be1c2233e3b624c8e895da8d
@@@ -1,11 -1,3 +1,12 @@@
  <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>
index 8cf42e475498f62f71155efdc4dc147117f23fa4,dcdd5f99aceafdc6f5cf9d24715f0ed2f3cad6d8..7bfdd6abf0fd5a934abb3f6c3c1e68541f91fd90
@@@ -1,22 -1,11 +1,25 @@@
  <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>
index 0000000000000000000000000000000000000000,a42064b705089b40d9ea212f92f39dee377745a2..88461f8dba5ea09b0abe651053e8c23ad4e101c7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,6 +1,8 @@@
 -  teleport: 8195,
+ // make sure these ports are unique
+ export const ports = {
+   vdomInterop: 8193,
+   todomvc: 8194,
++  transition: 8195,
++  transitionGroup: 8196,
++  teleport: 8197,
+ }
index f50fccea3cee448746785d46ced5fafcaa12925d,a2816f4b6db4bdaf2831b42401a9d92a1bd330ff..2cfb660dbf53b3fcb601fb8043b0909265103151
@@@ -14,11 -14,7 +14,12 @@@ export default defineConfig(
        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',
 +        ),
        },
      },
    },
index d2c7eca3bb1501bd2a1048fccd5e54655de18251,9b99ef869cf3d1b97e448a846675410e30ef751c..b509b9ba04b1767c989b39c45864b00683fc70d0
@@@ -90,35 -89,13 +90,42 @@@ export function getLiteralExpressionVal
    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'
    }
  }
index 4fe35c8b498c515816839621901b910181d8596b,1ac5cb9bac0739b126837a7808dd4909ab4fd7f3..a4063a06668e268af3befe0fe48cc38793e469c4
@@@ -566,14 -557,14 +566,22 @@@ export { startMeasure, endMeasure } fro
   * @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
   */
index 7942b76569e24092eb1cf37c00d520141e085b77,409d73d7df00212de15295e8af64aee5d9b4c600..e0341a54e90604c3d923e434b2d0fe978573aa07
@@@ -1,6 -1,6 +1,6 @@@
 -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'
@@@ -9,8 -9,8 +9,9 @@@ import 
    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,
index 19ee7182716f6b091cb20dc57b7df9a221f93a5f,a0f780406c9b05b5f7e0a29dcf4b7737af6df718..059f2176ad3a5607e9b4d6637300b746c8de9b12
@@@ -11,32 -11,16 +11,28 @@@ import 
    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,
index 50179b89ef95857f617f660c10033ef34fd21c19,0000000000000000000000000000000000000000..d4bb8763681979bc4155d1c563b730b8b8740c97
mode 100644,000000..100644
--- /dev/null
@@@ -1,10 -1,0 +1,11 @@@
- 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
 +}
index ba13df83d70375d3fc0e272ef3cca5f9e988ee27,37f6077b0f5517755f61400026bf82f64d4c2052..f7c70f758216536319aac5caf1e93600d9cd887f
@@@ -1,5 -1,4 +1,5 @@@
- 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,
index 6a8dc3310dff0eabd2492a25d339912b2cd741f5,f1791904ce85cc0a654761a0646f4a8d3ee2be29..fdf25cc58361430ab6909b0ac2847e2d2e05b8e7
@@@ -5,48 -5,19 +5,41 @@@ import 
    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
  
@@@ -221,16 -64,14 +101,21 @@@ export function insert
        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)
    }
  }
  
index 6d63920a9f0ac2a7dcf46ec260dc858b10ed1e67,38054e710cc0d1d5106cebb7b7e9b3973ceec41d..681586a5b5180bfe2cb85939a737bf6403e7bb0f
@@@ -25,15 -25,7 +25,14 @@@ import 
    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,
@@@ -66,8 -58,8 +65,9 @@@ import 
    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,
index b3a931deee89ae4fe3ba3df7bc39ea3eaa909dfb,2f8c3dd3ce34c493d029e93a5fc4e78a1825c693..d3ef5b01470d3283ccfffa0067c3ded1cdfd1a01
@@@ -1,20 -1,5 +1,12 @@@
 -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'
@@@ -25,6 -10,7 +17,7 @@@ import 
    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[]
index 5422c39ba90f6d7c67f5aca760078a763d234e9d,0000000000000000000000000000000000000000..560b277cbb91aea2ed368bca0bbd84dd288766ac
mode 100644,000000..100644
--- /dev/null
@@@ -1,326 -1,0 +1,322 @@@
- 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
 +}
index 2eff0e91dad913ed87d5668dccc63af067d42bd8,0000000000000000000000000000000000000000..e3b4fb4fb0acaf950d5b1a01fc049bec0eae3421
mode 100644,000000..100644
--- /dev/null
@@@ -1,227 -1,0 +1,226 @@@
-   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
 +  }
 +}
index 5cd9c66f2945523b21a1b661bc24eda3af8f81fa,b0fc22c14c869923111322373e08692fe57b7c6d..406b372c20c255d34112899bc99f8793b0aac347
@@@ -6,8 -6,9 +6,9 @@@ import 
  } 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)) {
index 0000000000000000000000000000000000000000,3e4fcb221c377b6cfb0ca14bef1bbebe9da7372b..258e848b0434ecf91c2484233e7dc42206ff0ed6
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,69 +1,140 @@@
 -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
+ }
index c801a84c694dc1cef7632c4fb29904349775d916,051944443addea692db772ecdb3c7eb850425e17..ef2b6188b775f4788eeeab773ee141e564d23cea
@@@ -44,5 -43,5 +45,6 @@@ export 
    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'
index dc6efb40afcd3928b54b9bbb31cf5cd9d14a4016,d5bbc71466b7047bfda470d4126a9278e60fd5e3..8bf3ebd1d9df4dc883e0db342f3b93df9cc346b5
@@@ -37,34 -28,14 +37,27 @@@ import 
    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<