path: ~/.cache/puppeteer
key: chromium-${{ hashFiles('pnpm-lock.yaml') }}
+ - name: Setup cache for Playwright browsers
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ hashFiles('pnpm-lock.yaml') }}
+
- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- run: pnpm install
- run: node node_modules/puppeteer/install.mjs
+ - run: pnpm exec playwright install chromium
- name: Run e2e tests
run: pnpm run test-e2e
"format-check": "prettier --check --cache .",
"test": "vitest",
"test-unit": "vitest --project unit*",
- "test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e",
+ "test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e --project e2e-browser",
"test-dts": "run-s build-dts test-dts-only",
"test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json",
"test-coverage": "vitest run --project unit* --coverage",
"@types/node": "^24.13.1",
"@types/semver": "^7.7.1",
"@types/serve-handler": "^6.1.4",
+ "@vitest/browser-playwright": "4.1.8",
"@vitest/coverage-v8": "^4.1.8",
"@vitest/eslint-plugin": "^1.6.19",
"@vue/consolidate": "1.0.0",
"marked": "13.0.3",
"npm-run-all2": "^9.0.1",
"picocolors": "^1.1.1",
+ "playwright": "^1.61.0",
"prettier": "^3.8.4",
"pretty-bytes": "^7.1.0",
"pug": "^3.0.4",
-import type { ElementHandle } from 'puppeteer'
-import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
-import path from 'node:path'
-import { Transition, createApp, h, nextTick, ref } from 'vue'
+import type { ElementHandle } from './e2eBrowserUtils'
+import { E2E_TIMEOUT, setupBrowserE2E } from './e2eBrowserUtils'
describe('e2e: Transition', () => {
- const { page, html, classList, style, isVisible, timeout, nextFrame, click } =
- setupPuppeteer()
- const baseUrl = `file://${path.resolve(__dirname, './transition.html')}`
-
- const duration = process.env.CI ? 200 : 50
- const buffer = process.env.CI ? 50 : 20
+ const {
+ page,
+ reset,
+ html,
+ classList,
+ style,
+ isVisible,
+ timeout,
+ nextFrame,
+ click,
+ } = setupBrowserE2E()
+
+ const duration = 50
+ const buffer = 20
+
+ const nextTick = () => (window as any).Vue.nextTick()
const transitionFinish = (time = duration) => timeout(time + buffer)
})
beforeEach(async () => {
- await page().goto(baseUrl)
+ await reset()
await page().waitForSelector('#app')
})
'test-anim-long-leave-to',
])
- if (!process.env.CI) {
- await new Promise(r => {
- setTimeout(r, duration - buffer)
- })
- expect(await classList('#container div')).toStrictEqual([
- 'test-anim-long-leave-active',
- 'test-anim-long-leave-to',
- ])
- }
+ await new Promise(r => {
+ setTimeout(r, duration - buffer)
+ })
+ expect(await classList('#container div')).toStrictEqual([
+ 'test-anim-long-leave-active',
+ 'test-anim-long-leave-to',
+ ])
await transitionFinish(duration * 2)
expect(await html('#container')).toBe('<!--v-if-->')
'test-anim-long-enter-to',
])
- if (!process.env.CI) {
- await new Promise(r => {
- setTimeout(r, duration - buffer)
- })
- expect(await classList('#container div')).toStrictEqual([
- 'test-anim-long-enter-active',
- 'test-anim-long-enter-to',
- ])
- }
+ await new Promise(r => {
+ setTimeout(r, duration - buffer)
+ })
+ expect(await classList('#container div')).toStrictEqual([
+ 'test-anim-long-enter-active',
+ 'test-anim-long-enter-to',
+ ])
await transitionFinish(duration * 2)
expect(await html('#container')).toBe('<div class="">content</div>')
await click('#toggleBtn')
await nextFrame()
expect(await html('#container')).toBe('<div class="">Loading...</div>')
+ // The warning is from the initial `view = null` branch, where the
+ // dynamic component renders as an empty Suspense default slot.
+ expect('<Suspense> slots expect a single root node.').toHaveBeenWarned()
await page().evaluate(() => {
// @ts-expect-error
expect(await html('#container')).toBe('<div class="test">one</div>')
// trigger twice
- classWhenTransitionStart()
+ await classWhenTransitionStart()
classWhenTransitionStart()
await nextFrame()
expect(await html('#container')).toBe(
)
// trigger twice
- classWhenTransitionStart()
+ await classWhenTransitionStart()
await nextFrame()
expect(await html('#container')).toBe(
'<div>Top</div><div class="test test-leave-active test-leave-to">one</div><div>Bottom</div>',
test(
'warn invalid durations',
async () => {
+ const { createApp } = (window as any).Vue
createApp({
template: `
<div id="container">
})
test('warn when used on multiple elements', async () => {
+ const { Transition, createApp, h } = (window as any).Vue
createApp({
render() {
return h(Transition, null, {
})
test('warn when invalid transition mode', () => {
+ const { createApp } = (window as any).Vue
createApp({
template: `
<div id="container">
test(`HOC w/ merged hooks`, async () => {
const innerSpy = vi.fn()
const outerSpy = vi.fn()
+ const { Transition, createApp, h, nextTick, ref } = (window as any).Vue
const MyTransition = {
render(this: any) {
return h(
Transition,
{
- onLeave(el, end) {
+ onLeave(el: Element, end: () => void) {
innerSpy()
end()
},
-import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
-import path from 'node:path'
-import { createApp, ref } from 'vue'
+import { E2E_TIMEOUT, setupBrowserE2E } from './e2eBrowserUtils'
describe('e2e: TransitionGroup', () => {
- const { page, html, nextFrame, timeout } = setupPuppeteer()
- const baseUrl = `file://${path.resolve(__dirname, './transition.html')}`
+ const { page, reset, html, nextFrame, timeout } = setupBrowserE2E()
- const duration = process.env.CI ? 200 : 50
- const buffer = process.env.CI ? 20 : 5
+ const duration = 50
+ const buffer = 20
const htmlWhenTransitionStart = () =>
page().evaluate(() => {
const transitionFinish = (time = duration) => timeout(time + buffer)
beforeEach(async () => {
- await page().goto(baseUrl)
+ await reset()
await page().waitForSelector('#app')
})
)
test('warn unkeyed children', () => {
+ const { createApp, ref } = (window as any).Vue
createApp({
template: `
<transition-group name="test">
})
test('not warn unkeyed text children w/ whitespace preserve', () => {
+ const { createApp } = (window as any).Vue
const app = createApp({
template: `
<transition-group name="test">
template: `
<div id="container">
<transition-group name="test">
- <div class="test">foo</div>
- <div class="test" v-if="show">bar</div>
+ <div key="1" class="test">foo</div>
+ <div key="2" class="test" v-if="show">bar</div>
</transition-group>
</div>
<button id="toggleBtn" @click="click">button</button>
--- /dev/null
+import { cdp } from 'vitest/browser'
+
+export const E2E_TIMEOUT: number = 30 * 1000
+
+const maxTries = 30
+const vueGlobalBuildUrl = new URL('../../dist/vue.global.js', import.meta.url)
+ .href
+const transitionStyle = `
+ .test {
+ -webkit-transition: opacity 50ms ease;
+ transition: opacity 50ms ease;
+ }
+ .group-move {
+ -webkit-transition: -webkit-transform 50ms ease;
+ transition: transform 50ms ease;
+ }
+ .v-appear,
+ .v-enter,
+ .v-leave-active,
+ .test-appear,
+ .test-enter,
+ .test-leave-active,
+ .test-reflow-enter,
+ .test-reflow-leave-to,
+ .hello,
+ .bye.active,
+ .changed-enter {
+ opacity: 0;
+ }
+ .test-reflow-leave-active,
+ .test-reflow-enter-active {
+ -webkit-transition: opacity 50ms ease;
+ transition: opacity 50ms ease;
+ }
+ .test-reflow-leave-from {
+ opacity: 0.9;
+ }
+ .test-anim-enter-active {
+ animation: test-enter 50ms;
+ -webkit-animation: test-enter 50ms;
+ }
+ .test-anim-leave-active {
+ animation: test-leave 50ms;
+ -webkit-animation: test-leave 50ms;
+ }
+ .test-anim-long-enter-active {
+ animation: test-enter 100ms;
+ -webkit-animation: test-enter 100ms;
+ }
+ .test-anim-long-leave-active {
+ animation: test-leave 100ms;
+ -webkit-animation: test-leave 100ms;
+ }
+ @keyframes test-enter {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+ @-webkit-keyframes test-enter {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+ @keyframes test-leave {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+ }
+ @-webkit-keyframes test-leave {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+ }
+`
+
+export const timeout = (n: number): Promise<void> =>
+ new Promise(resolve => setTimeout(resolve, n))
+
+export async function expectByPolling(
+ poll: () => Promise<any>,
+ expected: string,
+): Promise<void> {
+ for (let tries = 0; tries < maxTries; tries++) {
+ const actual = (await poll()) || ''
+ if (actual.indexOf(expected) > -1 || tries === maxTries - 1) {
+ expect(actual).toMatch(expected)
+ break
+ } else {
+ await timeout(50)
+ }
+ }
+}
+
+export interface ElementHandle<T extends Element = Element> {
+ evaluate<R>(fn: (node: T) => R | Promise<R>): Promise<R>
+}
+
+interface BrowserPage {
+ goto(url?: string): Promise<void>
+ waitForSelector(selector: string): Promise<Element>
+ evaluate<R>(fn: () => R | Promise<R>): Promise<R>
+ evaluate<Arg, R>(fn: (arg: Arg) => R | Promise<R>, arg: Arg): Promise<R>
+ exposeFunction(name: string, fn: (...args: any[]) => any): Promise<void>
+ $eval<R>(selector: string, fn: (node: Element) => R | Promise<R>): Promise<R>
+ $$eval<R>(
+ selector: string,
+ fn: (nodes: Element[]) => R | Promise<R>,
+ ): Promise<R>
+ createCDPSession(): Promise<{
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>
+ }>
+ on(event: 'pageerror', handler: (...args: any[]) => void): void
+ off(event: 'pageerror', handler: (...args: any[]) => void): void
+}
+
+interface BrowserUtils {
+ page: () => BrowserPage
+ reset(): Promise<void>
+ click(selector: string): Promise<void>
+ count(selector: string): Promise<number>
+ text(selector: string): Promise<string | null>
+ value(selector: string): Promise<string>
+ html(selector: string): Promise<string>
+ classList(selector: string): Promise<string[]>
+ style(selector: string, property: keyof CSSStyleDeclaration): Promise<any>
+ children(selector: string): Promise<any[]>
+ isVisible(selector: string): Promise<boolean>
+ isChecked(selector: string): Promise<boolean>
+ isFocused(selector: string): Promise<boolean>
+ setValue(selector: string, value: string): Promise<void>
+ typeValue(selector: string, value: string): Promise<void>
+ enterValue(selector: string, value: string): Promise<void>
+ clearValue(selector: string): Promise<void>
+ timeout(time: number): Promise<void>
+ nextFrame(): Promise<void>
+}
+
+type PageErrorHandler = {
+ error: EventListener
+ rejection: EventListener
+}
+
+function installVueGlobalBuild() {
+ return new Promise<void>((resolve, reject) => {
+ const script = document.createElement('script')
+ script.async = false
+ script.src = vueGlobalBuildUrl
+ script.onload = () => {
+ script.remove()
+ if ((window as any).Vue) {
+ resolve()
+ } else {
+ reject(new Error('Failed to expose Vue from vue.global.js.'))
+ }
+ }
+ script.onerror = () => {
+ script.remove()
+ reject(new Error(`Failed to load ${vueGlobalBuildUrl}.`))
+ }
+ document.head.appendChild(script)
+ })
+}
+
+function installTransitionStyle() {
+ const style = document.createElement('style')
+ style.dataset.vueTransitionE2e = ''
+ style.textContent = transitionStyle
+ document.head.appendChild(style)
+}
+
+const vueGlobalBuildReady = installVueGlobalBuild()
+installTransitionStyle()
+
+export function setupBrowserE2E(): BrowserUtils {
+ const pageErrorHandlers = new Map<
+ (...args: any[]) => void,
+ PageErrorHandler
+ >()
+ const initialHeadNodes = new Set<Node>(Array.from(document.head.childNodes))
+
+ function resetPageErrorHandlers() {
+ pageErrorHandlers.forEach(({ error, rejection }) => {
+ window.removeEventListener('error', error)
+ window.removeEventListener('unhandledrejection', rejection)
+ })
+ pageErrorHandlers.clear()
+ }
+
+ function resetHead() {
+ Array.from(document.head.childNodes).forEach(node => {
+ if (
+ !initialHeadNodes.has(node) &&
+ !(
+ node instanceof HTMLStyleElement &&
+ node.dataset.vueTransitionE2e != null
+ )
+ ) {
+ node.remove()
+ }
+ })
+ }
+
+ async function resetPage() {
+ // Browser mode runs in Vitest's iframe instead of loading transition.html.
+ // Keep these specs on the same global build that `test-e2e` prepares.
+ resetPageErrorHandlers()
+ await vueGlobalBuildReady
+ resetHead()
+ localStorage.clear()
+ sessionStorage.clear()
+ document.body.innerHTML = '<div id="app"></div>'
+ }
+
+ function getElement<T extends Element = Element>(selector: string): T {
+ const el = document.querySelector<T>(selector)
+ if (!el) {
+ throw new Error(`Unable to find element: ${selector}`)
+ }
+ return el
+ }
+
+ function createElementHandle<T extends Element>(node: T): ElementHandle<T> {
+ return {
+ async evaluate<R>(fn: (node: T) => R | Promise<R>) {
+ return (await fn(node)) as Awaited<R>
+ },
+ }
+ }
+
+ function toExposedArg(arg: unknown) {
+ return arg instanceof Element ? createElementHandle(arg) : arg
+ }
+
+ const browserPage: BrowserPage = {
+ async goto() {
+ await resetPage()
+ },
+
+ async waitForSelector(selector) {
+ const existing = document.querySelector(selector)
+ if (existing) {
+ return existing
+ }
+
+ return await new Promise<Element>((resolve, reject) => {
+ const observer = new MutationObserver(() => {
+ const el = document.querySelector(selector)
+ if (el) {
+ cleanup()
+ resolve(el)
+ }
+ })
+ const timer = setTimeout(() => {
+ cleanup()
+ reject(new Error(`Timed out waiting for selector: ${selector}`))
+ }, 1000)
+ const cleanup = () => {
+ clearTimeout(timer)
+ observer.disconnect()
+ }
+
+ observer.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ })
+ })
+ },
+
+ async evaluate(fn: (...args: any[]) => any, arg?: unknown) {
+ const result = await fn(arg)
+ // Match the async boundary Puppeteer's page.evaluate used to provide.
+ await Promise.resolve() // Vue patch job queued by the evaluated callback.
+ await Promise.resolve() // Suspense async setup / branch resolution.
+ await Promise.resolve() // DOM transition start queued after resolution.
+ return result
+ },
+
+ async exposeFunction(name, fn) {
+ ;(window as any)[name] = (...args: unknown[]) =>
+ fn(...args.map(toExposedArg))
+ },
+
+ async $eval(selector, fn) {
+ return (await fn(getElement(selector))) as Awaited<ReturnType<typeof fn>>
+ },
+
+ async $$eval(selector, fn) {
+ return (await fn(
+ Array.from(document.querySelectorAll(selector)),
+ )) as Awaited<ReturnType<typeof fn>>
+ },
+
+ async createCDPSession() {
+ const session = cdp() as {
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>
+ }
+ return {
+ send: (method, params) => session.send(method, params),
+ }
+ },
+
+ on(event, handler) {
+ if (event !== 'pageerror') {
+ return
+ }
+ const error = ((e: ErrorEvent) => handler(e.error || e.message)) as
+ | EventListener
+ | any
+ const rejection = ((e: PromiseRejectionEvent) => handler(e.reason)) as
+ | EventListener
+ | any
+ pageErrorHandlers.set(handler, { error, rejection })
+ window.addEventListener('error', error)
+ window.addEventListener('unhandledrejection', rejection)
+ },
+
+ off(event, handler) {
+ if (event !== 'pageerror') {
+ return
+ }
+ const listeners = pageErrorHandlers.get(handler)
+ if (listeners) {
+ window.removeEventListener('error', listeners.error)
+ window.removeEventListener('unhandledrejection', listeners.rejection)
+ pageErrorHandlers.delete(handler)
+ }
+ },
+ }
+
+ async function click(selector: string) {
+ getElement<HTMLElement>(selector).click()
+ }
+
+ async function reset() {
+ await resetPage()
+ }
+
+ async function count(selector: string) {
+ return document.querySelectorAll(selector).length
+ }
+
+ async function text(selector: string) {
+ return getElement(selector).textContent
+ }
+
+ async function value(selector: string) {
+ return getElement<HTMLInputElement>(selector).value
+ }
+
+ async function html(selector: string) {
+ return getElement(selector).innerHTML
+ }
+
+ async function classList(selector: string) {
+ return Array.from(getElement(selector).classList)
+ }
+
+ async function children(selector: string) {
+ return Array.from(getElement(selector).children)
+ }
+
+ async function style(selector: string, property: keyof CSSStyleDeclaration) {
+ return window.getComputedStyle(getElement(selector))[property]
+ }
+
+ async function isVisible(selector: string) {
+ return window.getComputedStyle(getElement(selector)).display !== 'none'
+ }
+
+ async function isChecked(selector: string) {
+ return getElement<HTMLInputElement>(selector).checked
+ }
+
+ async function isFocused(selector: string) {
+ return getElement(selector) === document.activeElement
+ }
+
+ async function setValue(selector: string, value: string) {
+ const el = getElement<HTMLInputElement>(selector)
+ el.value = value
+ el.dispatchEvent(new Event('input'))
+ }
+
+ async function typeValue(selector: string, value: string) {
+ const el = getElement<HTMLInputElement>(selector)
+ el.value = value
+ el.dispatchEvent(new Event('input'))
+ }
+
+ async function enterValue(selector: string, value: string) {
+ await typeValue(selector, value)
+ getElement<HTMLInputElement>(selector).dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'Enter' }),
+ )
+ }
+
+ async function clearValue(selector: string) {
+ getElement<HTMLInputElement>(selector).value = ''
+ }
+
+ async function nextFrame() {
+ return new Promise<void>(resolve => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => resolve())
+ })
+ })
+ }
+
+ return {
+ page: () => browserPage,
+ reset,
+ click,
+ count,
+ text,
+ value,
+ html,
+ classList,
+ style,
+ children,
+ isVisible,
+ isChecked,
+ isFocused,
+ setValue,
+ typeValue,
+ enterValue,
+ clearValue,
+ timeout,
+ nextFrame,
+ }
+}
+++ /dev/null
-<script src="../../dist/vue.global.js"></script>
-
-<div id="app"></div>
-<style>
- .test {
- -webkit-transition: opacity 50ms ease;
- transition: opacity 50ms ease;
- }
- .group-move {
- -webkit-transition: -webkit-transform 50ms ease;
- transition: transform 50ms ease;
- }
- .v-appear,
- .v-enter,
- .v-leave-active,
- .test-appear,
- .test-enter,
- .test-leave-active,
- .test-reflow-enter,
- .test-reflow-leave-to,
- .hello,
- .bye.active,
- .changed-enter {
- opacity: 0;
- }
- .test-reflow-leave-active,
- .test-reflow-enter-active {
- -webkit-transition: opacity 50ms ease;
- transition: opacity 50ms ease;
- }
- .test-reflow-leave-from {
- opacity: 0.9;
- }
- .test-anim-enter-active {
- animation: test-enter 50ms;
- -webkit-animation: test-enter 50ms;
- }
- .test-anim-leave-active {
- animation: test-leave 50ms;
- -webkit-animation: test-leave 50ms;
- }
- .test-anim-long-enter-active {
- animation: test-enter 100ms;
- -webkit-animation: test-enter 100ms;
- }
- .test-anim-long-leave-active {
- animation: test-leave 100ms;
- -webkit-animation: test-leave 100ms;
- }
- @keyframes test-enter {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
- @-webkit-keyframes test-enter {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
- @keyframes test-leave {
- from {
- opacity: 1;
- }
- to {
- opacity: 0;
- }
- }
- @-webkit-keyframes test-leave {
- from {
- opacity: 1;
- }
- to {
- opacity: 0;
- }
- }
-</style>
'@types/serve-handler':
specifier: ^6.1.4
version: 6.1.4
+ '@vitest/browser-playwright':
+ specifier: 4.1.8
+ version: 4.1.8(playwright@1.61.0)(vite@8.0.16)(vitest@4.1.8)
'@vitest/coverage-v8':
specifier: ^4.1.8
- version: 4.1.8(vitest@4.1.8)
+ version: 4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8)
'@vitest/eslint-plugin':
specifier: ^1.6.19
version: 1.6.19(@typescript-eslint/eslint-plugin@8.61.0)(eslint@10.4.1)(typescript@5.6.3)(vitest@4.1.8)
picocolors:
specifier: ^1.1.1
version: 1.1.1
+ playwright:
+ specifier: ^1.61.0
+ version: 1.61.0
prettier:
specifier: ^3.8.4
version: 3.8.4
version: 8.0.16(@types/node@24.13.1)(esbuild@0.28.0)(sass@1.100.0)(yaml@2.9.0)
vitest:
specifier: ^4.1.8
- version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16)
+ version: 4.1.8(@types/node@24.13.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16)
packages-private/dts-built-test:
dependencies:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
+ '@blazediff/core@1.9.1':
+ resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==}
+
'@bramus/specificity@2.4.2':
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
hasBin: true
resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==}
engines: {node: '>= 10.0.0'}
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
'@puppeteer/browsers@3.0.4':
resolution: {integrity: sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA==}
engines: {node: '>=22.12.0'}
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
vue: ^3.2.25
+ '@vitest/browser-playwright@4.1.8':
+ resolution: {integrity: sha512-SR7FqgegaexEg73xvf3ArtygXegagMdXnL0EZMpxrWvvhQxvicD/E8p0ib0J91riPRtQUViyh67Xjw3NqvyhVg==}
+ peerDependencies:
+ playwright: '*'
+ vitest: 4.1.8
+
+ '@vitest/browser@4.1.8':
+ resolution: {integrity: sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==}
+ peerDependencies:
+ vitest: 4.1.8
+
'@vitest/coverage-v8@4.1.8':
resolution: {integrity: sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==}
peerDependencies:
flatted@3.3.1:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
monaco-editor@0.55.1:
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
engines: {node: '>=0.10'}
hasBin: true
+ playwright-core@1.61.0:
+ resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.61.0:
+ resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ pngjs@7.0.0:
+ resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
+ engines: {node: '>=14.19.0'}
+
postcss-modules-extract-imports@3.1.0:
resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==}
engines: {node: ^10 || ^12 || >= 14}
resolution: {integrity: sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ==}
hasBin: true
+ sirv@3.0.2:
+ resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
+ engines: {node: '>=18'}
+
slice-ansi@7.1.0:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'}
token-stream@1.0.0:
resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==}
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
tough-cookie@6.0.1:
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
engines: {node: '>=16'}
'@bcoe/v8-coverage@1.0.2': {}
+ '@blazediff/core@1.9.1': {}
+
'@bramus/specificity@2.4.2':
dependencies:
css-tree: 3.2.1
'@parcel/watcher-win32-x64': 2.4.1
optional: true
+ '@polka/url@1.0.0-next.29': {}
+
'@puppeteer/browsers@3.0.4':
dependencies:
modern-tar: 0.7.6
vite: 8.0.16(@types/node@24.13.1)(esbuild@0.28.0)(sass@1.100.0)(yaml@2.9.0)
vue: link:packages/vue
- '@vitest/coverage-v8@4.1.8(vitest@4.1.8)':
+ '@vitest/browser-playwright@4.1.8(playwright@1.61.0)(vite@8.0.16)(vitest@4.1.8)':
+ dependencies:
+ '@vitest/browser': 4.1.8(vite@8.0.16)(vitest@4.1.8)
+ '@vitest/mocker': 4.1.8(vite@8.0.16)
+ playwright: 1.61.0
+ tinyrainbow: 3.1.0
+ vitest: 4.1.8(@types/node@24.13.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16)
+ transitivePeerDependencies:
+ - bufferutil
+ - msw
+ - utf-8-validate
+ - vite
+
+ '@vitest/browser@4.1.8(vite@8.0.16)(vitest@4.1.8)':
+ dependencies:
+ '@blazediff/core': 1.9.1
+ '@vitest/mocker': 4.1.8(vite@8.0.16)
+ '@vitest/utils': 4.1.8
+ magic-string: 0.30.21
+ pngjs: 7.0.0
+ sirv: 3.0.2
+ tinyrainbow: 3.1.0
+ vitest: 4.1.8(@types/node@24.13.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16)
+ ws: 8.21.0
+ transitivePeerDependencies:
+ - bufferutil
+ - msw
+ - utf-8-validate
+ - vite
+
+ '@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.8
obug: 2.1.1
std-env: 4.0.0
tinyrainbow: 3.1.0
- vitest: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16)
+ vitest: 4.1.8(@types/node@24.13.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16)
+ optionalDependencies:
+ '@vitest/browser': 4.1.8(vite@8.0.16)(vitest@4.1.8)
'@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.61.0)(eslint@10.4.1)(typescript@5.6.3)(vitest@4.1.8)':
dependencies:
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0)(eslint@10.4.1)(typescript@5.6.3)
typescript: 5.6.3
- vitest: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16)
+ vitest: 4.1.8(@types/node@24.13.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16)
transitivePeerDependencies:
- supports-color
flatted@3.3.1: {}
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
dompurify: 3.2.7
marked: 14.0.0
+ mrmime@2.0.1: {}
+
ms@2.0.0: {}
ms@2.1.3: {}
pidtree@0.6.0: {}
+ playwright-core@1.61.0: {}
+
+ playwright@1.61.0:
+ dependencies:
+ playwright-core: 1.61.0
+ optionalDependencies:
+ fsevents: 2.3.2
+
+ pngjs@7.0.0: {}
+
postcss-modules-extract-imports@3.1.0(postcss@8.5.15):
dependencies:
postcss: 8.5.15
simple-git-hooks@2.13.1: {}
+ sirv@3.0.2:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
slice-ansi@7.1.0:
dependencies:
ansi-styles: 6.2.3
token-stream@1.0.0: {}
+ totalist@3.0.1: {}
+
tough-cookie@6.0.1:
dependencies:
tldts: 7.0.16
sass: 1.100.0
yaml: 2.9.0
- vitest@4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16):
+ vitest@4.1.8(@types/node@24.13.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.13.1
- '@vitest/coverage-v8': 4.1.8(vitest@4.1.8)
+ '@vitest/browser-playwright': 4.1.8(playwright@1.61.0)(vite@8.0.16)(vitest@4.1.8)
+ '@vitest/coverage-v8': 4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8)
jsdom: 29.1.1
transitivePeerDependencies:
- msw
import { configDefaults, defineConfig } from 'vitest/config'
+import { playwright } from '@vitest/browser-playwright'
import { entries } from './scripts/aliases.js'
export default defineConfig({
environment: 'jsdom',
isolate: true,
include: ['packages/vue/__tests__/e2e/*.spec.ts'],
+ exclude: [
+ 'packages/vue/__tests__/e2e/Transition.spec.ts',
+ 'packages/vue/__tests__/e2e/TransitionGroup.spec.ts',
+ ],
+ },
+ },
+ {
+ extends: true,
+ define: {
+ __BROWSER__: true,
+ },
+ test: {
+ name: 'e2e-browser',
+ include: [
+ 'packages/vue/__tests__/e2e/Transition.spec.ts',
+ 'packages/vue/__tests__/e2e/TransitionGroup.spec.ts',
+ ],
+ browser: {
+ enabled: true,
+ provider: playwright(),
+ headless: true,
+ instances: [{ browser: 'chromium' }],
+ },
},
},
],