From: edison Date: Wed, 17 Jun 2026 02:30:27 +0000 (+0800) Subject: test(e2e): stabilize transition tests with vitest browser mode (#14970) X-Git-Tag: v3.6.0-beta.16~1^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;p=thirdparty%2Fvuejs%2Fcore.git test(e2e): stabilize transition tests with vitest browser mode (#14970) --- diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86b4da3a26..94c42848be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,6 +62,12 @@ jobs: 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 @@ -73,6 +79,7 @@ jobs: - 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 diff --git a/package.json b/package.json index f7d0843b2b..8b735d3d97 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "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", @@ -74,6 +74,7 @@ "@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", @@ -93,6 +94,7 @@ "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", diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index fc331e8565..17519e8322 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1,15 +1,23 @@ -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) @@ -22,7 +30,7 @@ describe('e2e: Transition', () => { }) beforeEach(async () => { - await page().goto(baseUrl) + await reset() await page().waitForSelector('#app') }) @@ -970,15 +978,13 @@ describe('e2e: Transition', () => { '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('') @@ -994,15 +1000,13 @@ describe('e2e: Transition', () => { '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('
content
') @@ -2322,6 +2326,9 @@ describe('e2e: Transition', () => { await click('#toggleBtn') await nextFrame() expect(await html('#container')).toBe('
Loading...
') + // The warning is from the initial `view = null` branch, where the + // dynamic component renders as an empty Suspense default slot. + expect(' slots expect a single root node.').toHaveBeenWarned() await page().evaluate(() => { // @ts-expect-error @@ -2536,7 +2543,7 @@ describe('e2e: Transition', () => { expect(await html('#container')).toBe('
one
') // trigger twice - classWhenTransitionStart() + await classWhenTransitionStart() classWhenTransitionStart() await nextFrame() expect(await html('#container')).toBe( @@ -2606,7 +2613,7 @@ describe('e2e: Transition', () => { ) // trigger twice - classWhenTransitionStart() + await classWhenTransitionStart() await nextFrame() expect(await html('#container')).toBe( '
Top
one
Bottom
', @@ -3415,6 +3422,7 @@ describe('e2e: Transition', () => { test( 'warn invalid durations', async () => { + const { createApp } = (window as any).Vue createApp({ template: `
@@ -3500,6 +3508,7 @@ describe('e2e: Transition', () => { }) test('warn when used on multiple elements', async () => { + const { Transition, createApp, h } = (window as any).Vue createApp({ render() { return h(Transition, null, { @@ -3513,6 +3522,7 @@ describe('e2e: Transition', () => { }) test('warn when invalid transition mode', () => { + const { createApp } = (window as any).Vue createApp({ template: `
@@ -3529,13 +3539,14 @@ describe('e2e: Transition', () => { 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() }, diff --git a/packages/vue/__tests__/e2e/TransitionGroup.spec.ts b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts index 77b713ada2..2f4eaa60ae 100644 --- a/packages/vue/__tests__/e2e/TransitionGroup.spec.ts +++ b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts @@ -1,13 +1,10 @@ -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(() => { @@ -20,7 +17,7 @@ describe('e2e: TransitionGroup', () => { const transitionFinish = (time = duration) => timeout(time + buffer) beforeEach(async () => { - await page().goto(baseUrl) + await reset() await page().waitForSelector('#app') }) @@ -678,6 +675,7 @@ describe('e2e: TransitionGroup', () => { ) test('warn unkeyed children', () => { + const { createApp, ref } = (window as any).Vue createApp({ template: ` @@ -694,6 +692,7 @@ describe('e2e: TransitionGroup', () => { }) test('not warn unkeyed text children w/ whitespace preserve', () => { + const { createApp } = (window as any).Vue const app = createApp({ template: ` @@ -788,8 +787,8 @@ describe('e2e: TransitionGroup', () => { template: `
-
foo
-
bar
+
foo
+
bar
diff --git a/packages/vue/__tests__/e2e/e2eBrowserUtils.ts b/packages/vue/__tests__/e2e/e2eBrowserUtils.ts new file mode 100644 index 0000000000..c1d3cef5c9 --- /dev/null +++ b/packages/vue/__tests__/e2e/e2eBrowserUtils.ts @@ -0,0 +1,442 @@ +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 => + new Promise(resolve => setTimeout(resolve, n)) + +export async function expectByPolling( + poll: () => Promise, + expected: string, +): Promise { + 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 { + evaluate(fn: (node: T) => R | Promise): Promise +} + +interface BrowserPage { + goto(url?: string): Promise + waitForSelector(selector: string): Promise + evaluate(fn: () => R | Promise): Promise + evaluate(fn: (arg: Arg) => R | Promise, arg: Arg): Promise + exposeFunction(name: string, fn: (...args: any[]) => any): Promise + $eval(selector: string, fn: (node: Element) => R | Promise): Promise + $$eval( + selector: string, + fn: (nodes: Element[]) => R | Promise, + ): Promise + createCDPSession(): Promise<{ + send(method: string, params?: Record): Promise + }> + on(event: 'pageerror', handler: (...args: any[]) => void): void + off(event: 'pageerror', handler: (...args: any[]) => void): void +} + +interface BrowserUtils { + page: () => BrowserPage + reset(): Promise + click(selector: string): Promise + count(selector: string): Promise + text(selector: string): Promise + value(selector: string): Promise + html(selector: string): Promise + classList(selector: string): Promise + style(selector: string, property: keyof CSSStyleDeclaration): Promise + children(selector: string): Promise + isVisible(selector: string): Promise + isChecked(selector: string): Promise + isFocused(selector: string): Promise + setValue(selector: string, value: string): Promise + typeValue(selector: string, value: string): Promise + enterValue(selector: string, value: string): Promise + clearValue(selector: string): Promise + timeout(time: number): Promise + nextFrame(): Promise +} + +type PageErrorHandler = { + error: EventListener + rejection: EventListener +} + +function installVueGlobalBuild() { + return new Promise((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(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 = '
' + } + + function getElement(selector: string): T { + const el = document.querySelector(selector) + if (!el) { + throw new Error(`Unable to find element: ${selector}`) + } + return el + } + + function createElementHandle(node: T): ElementHandle { + return { + async evaluate(fn: (node: T) => R | Promise) { + return (await fn(node)) as Awaited + }, + } + } + + 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((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> + }, + + async $$eval(selector, fn) { + return (await fn( + Array.from(document.querySelectorAll(selector)), + )) as Awaited> + }, + + async createCDPSession() { + const session = cdp() as { + send(method: string, params?: Record): Promise + } + 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(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(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(selector).checked + } + + async function isFocused(selector: string) { + return getElement(selector) === document.activeElement + } + + async function setValue(selector: string, value: string) { + const el = getElement(selector) + el.value = value + el.dispatchEvent(new Event('input')) + } + + async function typeValue(selector: string, value: string) { + const el = getElement(selector) + el.value = value + el.dispatchEvent(new Event('input')) + } + + async function enterValue(selector: string, value: string) { + await typeValue(selector, value) + getElement(selector).dispatchEvent( + new KeyboardEvent('keydown', { key: 'Enter' }), + ) + } + + async function clearValue(selector: string) { + getElement(selector).value = '' + } + + async function nextFrame() { + return new Promise(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, + } +} diff --git a/packages/vue/__tests__/e2e/transition.html b/packages/vue/__tests__/e2e/transition.html deleted file mode 100644 index ab404d67dc..0000000000 --- a/packages/vue/__tests__/e2e/transition.html +++ /dev/null @@ -1,82 +0,0 @@ - - -
- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f614adedf..050c904d90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,9 +72,12 @@ importers: '@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) @@ -129,6 +132,9 @@ importers: 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 @@ -185,7 +191,7 @@ importers: 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: @@ -507,6 +513,9 @@ packages: 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 @@ -898,6 +907,9 @@ packages: 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'} @@ -1570,6 +1582,17 @@ packages: 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: @@ -2148,6 +2171,11 @@ packages: 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} @@ -2620,6 +2648,10 @@ packages: 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==} @@ -2746,6 +2778,20 @@ packages: 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} @@ -3010,6 +3056,10 @@ packages: 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'} @@ -3146,6 +3196,10 @@ packages: 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'} @@ -3481,6 +3535,8 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@blazediff/core@1.9.1': {} + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 @@ -3753,6 +3809,8 @@ snapshots: '@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 @@ -4259,7 +4317,37 @@ snapshots: 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 @@ -4271,7 +4359,9 @@ snapshots: 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: @@ -4281,7 +4371,7 @@ snapshots: 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 @@ -4861,6 +4951,9 @@ snapshots: flatted@3.3.1: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5278,6 +5371,8 @@ snapshots: dompurify: 3.2.7 marked: 14.0.0 + mrmime@2.0.1: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -5382,6 +5477,16 @@ snapshots: 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 @@ -5757,6 +5862,12 @@ snapshots: 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 @@ -5879,6 +5990,8 @@ snapshots: token-stream@1.0.0: {} + totalist@3.0.1: {} + tough-cookie@6.0.1: dependencies: tldts: 7.0.16 @@ -5984,7 +6097,7 @@ snapshots: 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) @@ -6008,7 +6121,8 @@ snapshots: 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 diff --git a/vitest.config.ts b/vitest.config.ts index 1ce6ce82b6..2fbc92f920 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,4 +1,5 @@ import { configDefaults, defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser-playwright' import { entries } from './scripts/aliases.js' export default defineConfig({ @@ -85,6 +86,29 @@ 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' }], + }, }, }, ],