import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
import path from 'node:path'
import { createApp, ref } from 'vue'
describe('e2e: TransitionGroup', () => {
const { page, html, nextFrame, timeout } = setupPuppeteer()
const baseUrl = `file://${path.resolve(__dirname, './transition.html')}`
const duration = process.env.CI ? 200 : 50
const buffer = process.env.CI ? 20 : 5
const htmlWhenTransitionStart = () =>
page().evaluate(() => {
;(document.querySelector('#toggleBtn') as any)!.click()
return Promise.resolve().then(() => {
return document.querySelector('#container')!.innerHTML
})
})
const transitionFinish = (time = duration) => timeout(time + buffer)
beforeEach(async () => {
await page().goto(baseUrl)
await page().waitForSelector('#app')
})
test(
'enter',
async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
template: `
button
`,
setup: () => {
const items = ref(['a', 'b', 'c'])
const click = () => items.value.push('d', 'e')
return { click, items }
},
}).mount('#app')
})
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
expect(await htmlWhenTransitionStart()).toBe(
`a
` +
`b
` +
`c
` +
`d
` +
`e
`,
)
await nextFrame()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
` +
`d
` +
`e
`,
)
await transitionFinish()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
` +
`d
` +
`e
`,
)
},
E2E_TIMEOUT,
)
test(
'leave',
async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
template: `
button
`,
setup: () => {
const items = ref(['a', 'b', 'c'])
const click = () => (items.value = ['b'])
return { click, items }
},
}).mount('#app')
})
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
expect(await htmlWhenTransitionStart()).toBe(
`a
` +
`b
` +
`c
`,
)
await nextFrame()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
await transitionFinish()
expect(await html('#container')).toBe(`b
`)
},
E2E_TIMEOUT,
)
test(
'enter + leave',
async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
template: `
button
`,
setup: () => {
const items = ref(['a', 'b', 'c'])
const click = () => (items.value = ['b', 'c', 'd'])
return { click, items }
},
}).mount('#app')
})
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
expect(await htmlWhenTransitionStart()).toBe(
`a
` +
`b
` +
`c
` +
`d
`,
)
await nextFrame()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
` +
`d
`,
)
await transitionFinish()
expect(await html('#container')).toBe(
`b
` +
`c
` +
`d
`,
)
},
E2E_TIMEOUT,
)
test(
'appear',
async () => {
const appearHtml = await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
template: `
button
`,
setup: () => {
const items = ref(['a', 'b', 'c'])
const click = () => items.value.push('d', 'e')
return { click, items }
},
}).mount('#app')
return Promise.resolve().then(() => {
return document.querySelector('#container')!.innerHTML
})
})
// appear
expect(appearHtml).toBe(
`a
` +
`b
` +
`c
`,
)
await nextFrame()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
await transitionFinish()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
// enter
expect(await htmlWhenTransitionStart()).toBe(
`a
` +
`b
` +
`c
` +
`d
` +
`e
`,
)
await nextFrame()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
` +
`d
` +
`e
`,
)
await transitionFinish()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
` +
`d
` +
`e
`,
)
},
E2E_TIMEOUT,
)
test(
'move',
async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
template: `
button
`,
setup: () => {
const items = ref(['a', 'b', 'c'])
const click = () => (items.value = ['d', 'b', 'a'])
return { click, items }
},
}).mount('#app')
})
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
expect(await htmlWhenTransitionStart()).toBe(
`d
` +
`b
` +
`a
` +
`c
`,
)
await nextFrame()
expect(await html('#container')).toBe(
`d
` +
`b
` +
`a
` +
`c
`,
)
await transitionFinish(duration * 2)
expect(await html('#container')).toBe(
`d
` +
`b
` +
`a
`,
)
},
E2E_TIMEOUT,
)
test(
'dynamic name',
async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
template: `
button
button
`,
setup: () => {
const items = ref(['a', 'b', 'c'])
const name = ref('invalid')
const click = () => (items.value = ['b', 'c', 'a'])
const changeName = () => {
name.value = 'group'
items.value = ['a', 'b', 'c']
}
return { click, items, name, changeName }
},
}).mount('#app')
})
expect(await html('#container')).toBe(
`a
` + `b
` + `c
`,
)
// invalid name
expect(await htmlWhenTransitionStart()).toBe(
`b
` + `c
` + `a
`,
)
// change name
const moveHtml = await page().evaluate(() => {
;(document.querySelector('#changeNameBtn') as any).click()
return Promise.resolve().then(() => {
return document.querySelector('#container')!.innerHTML
})
})
expect(moveHtml).toBe(
`a
` +
`b
` +
`c
`,
)
// not sure why but we just have to wait really long for this to
// pass consistently :/
await transitionFinish(duration * 4 + buffer)
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
},
E2E_TIMEOUT,
)
test(
'events',
async () => {
const onLeaveSpy = vi.fn()
const onEnterSpy = vi.fn()
const onAppearSpy = vi.fn()
const beforeLeaveSpy = vi.fn()
const beforeEnterSpy = vi.fn()
const beforeAppearSpy = vi.fn()
const afterLeaveSpy = vi.fn()
const afterEnterSpy = vi.fn()
const afterAppearSpy = vi.fn()
await page().exposeFunction('onLeaveSpy', onLeaveSpy)
await page().exposeFunction('onEnterSpy', onEnterSpy)
await page().exposeFunction('onAppearSpy', onAppearSpy)
await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy)
await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
await page().exposeFunction('beforeAppearSpy', beforeAppearSpy)
await page().exposeFunction('afterLeaveSpy', afterLeaveSpy)
await page().exposeFunction('afterEnterSpy', afterEnterSpy)
await page().exposeFunction('afterAppearSpy', afterAppearSpy)
const appearHtml = await page().evaluate(() => {
const {
beforeAppearSpy,
onAppearSpy,
afterAppearSpy,
beforeEnterSpy,
onEnterSpy,
afterEnterSpy,
beforeLeaveSpy,
onLeaveSpy,
afterLeaveSpy,
} = window as any
const { createApp, ref } = (window as any).Vue
createApp({
template: `
button
`,
setup: () => {
const items = ref(['a', 'b', 'c'])
const click = () => (items.value = ['b', 'c', 'd'])
return {
click,
items,
beforeAppearSpy,
onAppearSpy,
afterAppearSpy,
beforeEnterSpy,
onEnterSpy,
afterEnterSpy,
beforeLeaveSpy,
onLeaveSpy,
afterLeaveSpy,
}
},
}).mount('#app')
return Promise.resolve().then(() => {
return document.querySelector('#container')!.innerHTML
})
})
expect(beforeAppearSpy).toBeCalled()
expect(onAppearSpy).toBeCalled()
expect(afterAppearSpy).not.toBeCalled()
expect(appearHtml).toBe(
`a
` +
`b
` +
`c
`,
)
await nextFrame()
expect(afterAppearSpy).not.toBeCalled()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
await transitionFinish()
expect(afterAppearSpy).toBeCalled()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
`,
)
// enter + leave
expect(await htmlWhenTransitionStart()).toBe(
`a
` +
`b
` +
`c
` +
`d
`,
)
expect(beforeLeaveSpy).toBeCalled()
expect(onLeaveSpy).toBeCalled()
expect(afterLeaveSpy).not.toBeCalled()
expect(beforeEnterSpy).toBeCalled()
expect(onEnterSpy).toBeCalled()
expect(afterEnterSpy).not.toBeCalled()
await nextFrame()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
` +
`d
`,
)
expect(afterLeaveSpy).not.toBeCalled()
expect(afterEnterSpy).not.toBeCalled()
await transitionFinish()
expect(await html('#container')).toBe(
`b
` +
`c
` +
`d
`,
)
expect(afterLeaveSpy).toBeCalled()
expect(afterEnterSpy).toBeCalled()
},
E2E_TIMEOUT,
)
test('warn unkeyed children', () => {
createApp({
template: `
{{item}}
`,
setup: () => {
const items = ref(['a', 'b', 'c'])
return { items }
},
}).mount(document.createElement('div'))
expect(` children must be keyed`).toHaveBeenWarned()
})
test('not warn unkeyed text children w/ whitespace preserve', () => {
const app = createApp({
template: `
1
2
`,
})
app.config.compilerOptions.whitespace = 'preserve'
app.mount(document.createElement('div'))
expect(` children must be keyed`).not.toHaveBeenWarned()
})
// #5168, #7898, #9067
test(
'avoid set transition hooks for comment node',
async () => {
await page().evaluate(duration => {
const { createApp, ref, h, createCommentVNode } = (window as any).Vue
const show = ref(false)
createApp({
template: `
button
`,
components: {
Child: {
setup() {
return () =>
show.value
? h('div', { class: 'test' }, 'child')
: createCommentVNode('v-if', true)
},
},
},
setup: () => {
const items = ref([])
const click = () => {
items.value = ['a', 'b', 'c']
setTimeout(() => {
show.value = true
}, duration)
}
return { click, items }
},
}).mount('#app')
}, duration)
expect(await html('#container')).toBe(``)
expect(await htmlWhenTransitionStart()).toBe(
`a
` +
`b
` +
`c
` +
``,
)
await transitionFinish(duration)
await nextFrame()
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
` +
`child
`,
)
await transitionFinish(duration)
expect(await html('#container')).toBe(
`a
` +
`b
` +
`c
` +
`child
`,
)
},
E2E_TIMEOUT,
)
// #4621, #4622, #5153
test(
'avoid set transition hooks for text node',
async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
const app = createApp({
template: `
button
`,
setup: () => {
const show = ref(false)
const click = () => {
show.value = true
}
return { show, click }
},
})
app.config.compilerOptions.whitespace = 'preserve'
app.mount('#app')
})
expect(await html('#container')).toBe(`foo
` + ` `)
expect(await htmlWhenTransitionStart()).toBe(
`foo
` +
` ` +
`bar
`,
)
await nextFrame()
expect(await html('#container')).toBe(
`foo
` +
` ` +
`bar
`,
)
await transitionFinish(duration)
expect(await html('#container')).toBe(
`foo
` + ` ` + `bar
`,
)
},
E2E_TIMEOUT,
)
// #6105
test(
'with scale',
async () => {
await page().evaluate(() => {
const { createApp, ref, onMounted } = (window as any).Vue
createApp({
template: `
`,
setup: () => {
const items = ref(['a', 'b', 'c'])
const click = () => {
items.value.reverse()
}
onMounted(() => {
const styleNode = document.createElement('style')
styleNode.innerHTML = `.v-move {
transition: transform 0.5s ease;
}`
document.body.appendChild(styleNode)
})
return { items, click }
},
}).mount('#app')
})
const original_top = await page().$eval('ul li:nth-child(1)', node => {
return node.getBoundingClientRect().top
})
const new_top = await page().evaluate(() => {
const el = document.querySelector('ul li:nth-child(1)')
const p = new Promise(resolve => {
el!.addEventListener('transitionstart', () => {
const new_top = el!.getBoundingClientRect().top
resolve(new_top)
})
})
;(document.querySelector('#toggleBtn') as any)!.click()
return p
})
expect(original_top).toBeLessThan(new_top as number)
},
E2E_TIMEOUT,
)
test(
'not leaking after children unmounted',
async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref, nextTick } = (window as any).Vue
const show = ref(true)
createApp({
components: {
Child: {
setup: () => {
// Big arrays kick GC earlier
const test = ref([...Array(3000)].map((_, i) => ({ i })))
// @ts-expect-error - Custom property and same lib as runtime is used
window.__REF__ = new WeakRef(test)
return { test }
},
template: `
{{ test.length }}
`,
},
},
template: `
`,
setup() {
return { show }
},
}).mount('#app')
show.value = false
await nextTick()
})
const isCollected = async () =>
// @ts-expect-error - Custom property
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
},
E2E_TIMEOUT,
)
})