diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..05d9b352 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +apps/app/tests/playwright-ct/**/__screenshots__/**/*.png filter=lfs diff=lfs merge=lfs -text +apps/app/tests/playwright-e2e/**/__screenshots__/**/*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 0f4834fe..6a973024 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,14 @@ storage/framework/views/* .phpunit.result.cache coverage/ +# Playwright runtime artifacts (test-results, blob-report, html-report, +# .cache build dir, playwright traces). __screenshots__/ is committed +# (via Git LFS, see .gitattributes). +apps/app/test-results/ +apps/app/playwright-report/ +apps/app/blob-report/ +apps/app/playwright/.cache/ + # Misc *.pem .cache/ diff --git a/apps/app/package.json b/apps/app/package.json index dcef67f4..fa6d6dbe 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -14,7 +14,11 @@ "msw:init": "msw init public/ --save", "postinstall": "npm run build:icons && npm run msw:init", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:component": "playwright test --config=playwright-ct.config.ts", + "test:e2e": "playwright test --config=playwright.config.ts", + "test:visual": "playwright test --config=playwright-ct.config.ts --grep @visual", + "test:visual:update": "playwright test --config=playwright-ct.config.ts --grep @visual --update-snapshots" }, "dependencies": { "@casl/ability": "6.7.3", @@ -67,6 +71,7 @@ "@antfu/eslint-config-ts": "0.43.1", "@antfu/eslint-config-vue": "0.43.1", "@antfu/utils": "0.7.10", + "@axe-core/playwright": "^4.11.3", "@fullcalendar/core": "6.1.19", "@fullcalendar/daygrid": "6.1.19", "@fullcalendar/interaction": "6.1.19", @@ -82,6 +87,8 @@ "@iconify/vue": "4.1.2", "@intlify/unplugin-vue-i18n": "11.0.1", "@pinia/testing": "^1.0.3", + "@playwright/experimental-ct-vue": "^1.59.1", + "@playwright/test": "^1.59.1", "@stylistic/eslint-plugin-js": "0.0.4", "@stylistic/eslint-plugin-ts": "0.0.4", "@stylistic/stylelint-config": "1.0.1", @@ -101,6 +108,7 @@ "@typescript-eslint/parser": "7.18.0", "@vitejs/plugin-vue": "6.0.1", "@vitejs/plugin-vue-jsx": "5.1.1", + "@vue/compiler-dom": "^3.5.34", "@vue/test-utils": "^2.4.9", "axe-core": "^4.11.4", "baseline-browser-mapping": "^2.10.16", diff --git a/apps/app/playwright-ct.config.ts b/apps/app/playwright-ct.config.ts new file mode 100644 index 00000000..2be125e4 --- /dev/null +++ b/apps/app/playwright-ct.config.ts @@ -0,0 +1,65 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig, devices } from '@playwright/experimental-ct-vue' +import vue from '@vitejs/plugin-vue' + +// Component-test config — mounts individual components in a real +// Chromium via Playwright Component Testing. Used by: +// pnpm test:component (all CT tests, baselines verified) +// pnpm test:visual (subset tagged @visual) +// pnpm test:visual:update (regenerate visual baselines) +// +// E2E (real backend + real frontend) lives in playwright.config.ts. + +const sharedAliases = { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@core': fileURLToPath(new URL('./src/@core', import.meta.url)), + '@layouts': fileURLToPath(new URL('./src/@layouts', import.meta.url)), + '@images': fileURLToPath(new URL('./src/assets/images/', import.meta.url)), + '@styles': fileURLToPath(new URL('./src/assets/styles/', import.meta.url)), +} + +export default defineConfig({ + testDir: './tests/playwright-ct', + testMatch: /.*\.spec\.ts$/, + snapshotDir: './tests/playwright-ct/__screenshots__', + snapshotPathTemplate: '{snapshotDir}/{testFilePath}/{arg}{ext}', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? 'github' : 'list', + + use: { + trace: 'off', + viewport: { width: 1440, height: 900 }, + ctPort: 3100, + ctViteConfig: { + plugins: [vue()], + resolve: { alias: sharedAliases }, + + // Vuetify ships ESM that Vite needs to inline-process; matches + // the convention from vitest.config.ts's component project. + ssr: { noExternal: ['vuetify'] }, + }, + }, + + // Linux-Chromium-only baselines per RFC §A.5 — Firefox/WebKit and + // mobile/responsive viewports deferred to a later sprint. Running + // baselines on macOS (dev) vs Linux (CI) will produce a 1-2px + // anti-alias diff; pixel tolerance below absorbs that, but the + // canonical baseline IS the dev machine for now (no CI yet — see + // BACKLOG TEST-INFRA-002). + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.001, + threshold: 0.2, + }, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/apps/app/playwright.config.ts b/apps/app/playwright.config.ts new file mode 100644 index 00000000..8b6dfb26 --- /dev/null +++ b/apps/app/playwright.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from '@playwright/test' + +// E2E config — drives a real Vite dev server + a real Laravel test +// server. Used by `pnpm test:e2e`. Component tests live in +// playwright-ct.config.ts (different runner). + +export default defineConfig({ + testDir: './tests/playwright-e2e', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: 0, + workers: 1, + reporter: process.env.CI ? 'github' : 'list', + + use: { + baseURL: process.env.E2E_FRONTEND_URL ?? 'http://localhost:5173', + trace: 'off', + video: 'off', + screenshot: 'off', + viewport: { width: 1440, height: 900 }, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + // Auto-start the SPA dev server. Laravel's test server is started + // by the per-test fixture in tests/playwright-e2e/fixtures/laravel.ts + // because its lifecycle requires per-run seed control. + webServer: { + command: 'pnpm dev', + url: process.env.E2E_FRONTEND_URL ?? 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + stdout: 'ignore', + stderr: 'pipe', + }, +}) diff --git a/apps/app/playwright/index.html b/apps/app/playwright/index.html new file mode 100644 index 00000000..fd8b764d --- /dev/null +++ b/apps/app/playwright/index.html @@ -0,0 +1,12 @@ + + + + + + Playwright CT — Crewli + + +
+ + + diff --git a/apps/app/playwright/index.ts b/apps/app/playwright/index.ts new file mode 100644 index 00000000..3a85b0f1 --- /dev/null +++ b/apps/app/playwright/index.ts @@ -0,0 +1,120 @@ +import { beforeMount } from '@playwright/experimental-ct-vue/hooks' +import { createPinia, setActivePinia } from 'pinia' +import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query' +import { type RouteRecordRaw, createMemoryHistory, createRouter } from 'vue-router' +import { type ThemeDefinition, createVuetify } from 'vuetify' + +// vuetify/components namespace import: required to register the full +// component set on a freshly-created Vuetify instance per test, mirroring +// tests/utils/mountWithVuexy.ts. Test infra only. +import * as components from 'vuetify/components' // eslint-disable-line no-restricted-imports +import * as directives from 'vuetify/directives' + +// Plain-CSS token sheet — JSDOM evaluates :root custom properties from +// this import so getComputedStyle(el).getPropertyValue('--tt-status-…') +// resolves during component tests. Path resolved by the alias map in +// playwright-ct.config.ts. +import '@/styles/tokens/_timetable.css' + +// ============================================================================= +// HOOKS CONFIG (per-test, opt-in) +// ============================================================================= +// +// Tests pass `hooksConfig` to mount() to override defaults. Shape: +// { +// initialRoute?: string, +// initialQuery?: Record, +// routes?: RouteRecordRaw[], +// piniaInitialState?: Record>, +// // injected by mountWithProviders.ts wrapper: +// notificationMockKey?: string, +// } +// +// Defaults below render every component with the full Vuexy/Vuetify +// stack. F3 (PrimeVue foundation) replaces the Vuetify plugin line +// here with PrimeVue and updates the sanity test — that is a ~2-hour +// swap, not a rewrite. Vuetify is INTENTIONAL TEMPORARY STATE in this +// file; do not abstract behind a "UI framework provider" indirection +// because the abstraction would itself need to be removed in F3. +// See dev-docs/ARCH-TESTING.md §6 for the migration timeline. +// ============================================================================= + +export interface HooksConfig { + initialRoute?: string + initialQuery?: Record + routes?: RouteRecordRaw[] + piniaInitialState?: Record> +} + +const defaultTheme: ThemeDefinition = { + dark: false, + colors: { + primary: '#1f7ad1', + error: '#d63d4b', + success: '#2fa66a', + warning: '#e0992c', + info: '#1f7ad1', + }, +} + +beforeMount(async ({ app, hooksConfig }) => { + // ---- Vuetify (TEMPORARY: replaced by PrimeVue in F3) ----------------- + const vuetify = createVuetify({ + components, + directives, + theme: { defaultTheme: 'crewliLight', themes: { crewliLight: defaultTheme } }, + }) + + app.use(vuetify) + + // ---- Pinia ---------------------------------------------------------- + // Fresh instance per test. We do NOT use @pinia/testing's + // createTestingPinia here because it depends on Vitest's `vi.fn`, + // which doesn't exist in Playwright's Node runtime. Tests that need + // to assert on store actions should snapshot store state via + // page.evaluate() against window.__pinia (exposed below). + const pinia = createPinia() + + app.use(pinia) + setActivePinia(pinia) + + if (hooksConfig?.piniaInitialState) { + // Hydrate store state directly. Stores are created lazily on first + // use(); pre-hydration via pinia.state.value is safe. + pinia.state.value = { + ...pinia.state.value, + ...hooksConfig.piniaInitialState, + } + } + + // Expose pinia on window for cross-frame state assertions. + ;(globalThis as { __pinia?: typeof pinia }).__pinia = pinia + + // ---- TanStack Vue Query --------------------------------------------- + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false, refetchOnWindowFocus: false }, + mutations: { retry: false }, + }, + }) + + app.use(VueQueryPlugin, { queryClient }) + + // ---- Router (memory history; no auth guards) ------------------------ + const routes: RouteRecordRaw[] = hooksConfig?.routes ?? [ + { path: '/', component: { template: '
' } }, + { path: '/:pathMatch(.*)*', component: { template: '
' } }, + ] + + const router = createRouter({ history: createMemoryHistory(), routes }) + + app.use(router) + + if (hooksConfig?.initialRoute) { + await router.push({ + path: hooksConfig.initialRoute, + query: hooksConfig.initialQuery, + }) + } + await router.isReady() +}) diff --git a/apps/app/pnpm-lock.yaml b/apps/app/pnpm-lock.yaml index 1761002e..63e3066f 100644 --- a/apps/app/pnpm-lock.yaml +++ b/apps/app/pnpm-lock.yaml @@ -157,6 +157,9 @@ importers: '@antfu/utils': specifier: 0.7.10 version: 0.7.10 + '@axe-core/playwright': + specifier: ^4.11.3 + version: 4.11.3(playwright-core@1.59.1) '@fullcalendar/core': specifier: 6.1.19 version: 6.1.19 @@ -198,10 +201,16 @@ importers: version: 4.1.2(vue@3.5.22(typescript@5.9.3)) '@intlify/unplugin-vue-i18n': specifier: 11.0.1 - version: 11.0.1(@vue/compiler-dom@3.5.22)(eslint@8.57.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + version: 11.0.1(@vue/compiler-dom@3.5.34)(eslint@8.57.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) '@pinia/testing': specifier: ^1.0.3 version: 1.0.3(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))) + '@playwright/experimental-ct-vue': + specifier: ^1.59.1 + version: 1.59.1(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1) + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 '@stylistic/eslint-plugin-js': specifier: 0.0.4 version: 0.0.4 @@ -216,7 +225,7 @@ importers: version: 2.1.3(stylelint@16.8.0(typescript@5.9.3)) '@testing-library/vue': specifier: ^8.1.0 - version: 8.1.0(@vue/compiler-dom@3.5.22)(@vue/compiler-sfc@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + version: 8.1.0(@vue/compiler-dom@3.5.34)(@vue/compiler-sfc@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) '@tiptap/extension-character-count': specifier: ^2.27.1 version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) @@ -259,9 +268,12 @@ importers: '@vitejs/plugin-vue-jsx': specifier: 5.1.1 version: 5.1.1(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + '@vue/compiler-dom': + specifier: ^3.5.34 + version: 3.5.34 '@vue/test-utils': specifier: ^2.4.9 - version: 2.4.9(@vue/compiler-dom@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + version: 2.4.9(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) axe-core: specifier: ^4.11.4 version: 4.11.4 @@ -387,7 +399,7 @@ importers: version: 0.18.6(@vueuse/core@10.11.1(vue@3.5.22(typescript@5.9.3)))(rollup@4.52.5) unplugin-vue-components: specifier: 0.27.5 - version: 0.27.5(@babel/parser@7.28.5)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3)) + version: 0.27.5(@babel/parser@7.29.3)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3)) unplugin-vue-router: specifier: 0.8.8 version: 0.8.8(rollup@4.52.5)(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) @@ -467,6 +479,11 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@axe-core/playwright@4.11.3': + resolution: {integrity: sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -554,6 +571,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-proposal-decorators@7.28.0': resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} engines: {node: '>=6.9.0'} @@ -611,6 +633,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@boundaries/elements@2.0.1': resolution: {integrity: sha512-sAWO3D8PFP6pBXdxxW93SQi/KQqqhE2AAHo3AgWfdtJXwO6bfK6/wUN81XnOZk0qRC6vHzUEKhjwVD9dtDWvxg==} engines: {node: '>=18.18'} @@ -1157,6 +1183,20 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/experimental-ct-core@1.59.1': + resolution: {integrity: sha512-U7+jNROBJxfwjM/G7011+UNEyLiI5zIT1HWAn1k89WZIWl5RUWaCGWlYkYdAZwBSVfGstjF9AgkzmS0RsF8Ulw==} + engines: {node: '>=18'} + + '@playwright/experimental-ct-vue@1.59.1': + resolution: {integrity: sha512-RygXcwXQwRHzcdaQAXpKiHEl8XDPepZKmfHDNPCSnCN1g9ylaAvtNF6s7DgpsxlHqLlwgc2DmMr1n3D/OOVKyQ==} + engines: {node: '>=18'} + hasBin: true + + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -2013,6 +2053,13 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vue: ^3.0.0 + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + '@vitejs/plugin-vue@6.0.1': resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2086,9 +2133,15 @@ packages: '@vue/compiler-core@3.5.22': resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + '@vue/compiler-core@3.5.34': + resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + '@vue/compiler-dom@3.5.22': resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} + '@vue/compiler-dom@3.5.34': + resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + '@vue/compiler-sfc@3.5.22': resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==} @@ -2143,6 +2196,9 @@ packages: '@vue/shared@3.5.22': resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + '@vue/shared@3.5.34': + resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + '@vue/test-utils@2.4.9': resolution: {integrity: sha512-YwgowiO1mPleZqpgAGfxvWu/A5A8nkLrbyH2SqiQRkyzCIaDzzo27/2uS/F1g7fRLvl8BUY0+Sr1eC+6+IHfrw==} peerDependencies: @@ -3219,6 +3275,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + 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} @@ -4207,6 +4268,16 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -5212,6 +5283,46 @@ packages: peerDependencies: vue: '>=3.2.13' + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@7.1.12: resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5703,6 +5814,11 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@axe-core/playwright@4.11.3(playwright-core@1.59.1)': + dependencies: + axe-core: 4.11.4 + playwright-core: 1.59.1 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5826,6 +5942,10 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -5896,6 +6016,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@boundaries/elements@2.0.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)': dependencies: eslint-import-resolver-node: 0.3.9 @@ -6254,12 +6379,12 @@ snapshots: '@intlify/shared@11.1.12': {} - '@intlify/unplugin-vue-i18n@11.0.1(@vue/compiler-dom@3.5.22)(eslint@8.57.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))': + '@intlify/unplugin-vue-i18n@11.0.1(@vue/compiler-dom@3.5.34)(eslint@8.57.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) '@intlify/bundle-utils': 11.0.1(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3))) '@intlify/shared': 11.1.12 - '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.22)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.34)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) '@rollup/pluginutils': 5.3.0(rollup@4.52.5) '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) @@ -6278,12 +6403,12 @@ snapshots: - supports-color - typescript - '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.22)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))': + '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.34)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))': dependencies: '@babel/parser': 7.28.5 optionalDependencies: '@intlify/shared': 11.1.12 - '@vue/compiler-dom': 3.5.22 + '@vue/compiler-dom': 3.5.34 vue: 3.5.22(typescript@5.9.3) vue-i18n: 11.1.12(vue@3.5.22(typescript@5.9.3)) @@ -6385,6 +6510,47 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/experimental-ct-core@1.59.1(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)': + dependencies: + playwright: 1.59.1 + playwright-core: 1.59.1 + vite: 6.4.2(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + '@playwright/experimental-ct-vue@1.59.1(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)': + dependencies: + '@playwright/experimental-ct-core': 1.59.1(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) + '@vitejs/plugin-vue': 5.2.4(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - vite + - vue + - yaml + + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + '@polka/url@1.0.0-next.29': {} '@popperjs/core@2.11.8': {} @@ -6614,11 +6780,11 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/vue@8.1.0(@vue/compiler-dom@3.5.22)(@vue/compiler-sfc@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))': + '@testing-library/vue@8.1.0(@vue/compiler-dom@3.5.34)(@vue/compiler-sfc@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))': dependencies: '@babel/runtime': 7.29.2 '@testing-library/dom': 9.3.4 - '@vue/test-utils': 2.4.9(@vue/compiler-dom@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + '@vue/test-utils': 2.4.9(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) vue: 3.5.22(typescript@5.9.3) optionalDependencies: '@vue/compiler-sfc': 3.5.22 @@ -7257,6 +7423,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-vue@5.2.4(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + dependencies: + vite: 7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) + vue: 3.5.22(typescript@5.9.3) + '@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 @@ -7365,11 +7536,24 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/shared': 3.5.34 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.22': dependencies: '@vue/compiler-core': 3.5.22 '@vue/shared': 3.5.22 + '@vue/compiler-dom@3.5.34': + dependencies: + '@vue/compiler-core': 3.5.34 + '@vue/shared': 3.5.34 + '@vue/compiler-sfc@3.5.22': dependencies: '@babel/parser': 7.28.5 @@ -7436,7 +7620,7 @@ snapshots: '@vue/language-core@3.1.2(typescript@5.9.3)': dependencies: '@volar/language-core': 2.4.23 - '@vue/compiler-dom': 3.5.22 + '@vue/compiler-dom': 3.5.34 '@vue/shared': 3.5.22 alien-signals: 3.0.3 muggle-string: 0.4.1 @@ -7469,9 +7653,11 @@ snapshots: '@vue/shared@3.5.22': {} - '@vue/test-utils@2.4.9(@vue/compiler-dom@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))': + '@vue/shared@3.5.34': {} + + '@vue/test-utils@2.4.9(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))': dependencies: - '@vue/compiler-dom': 3.5.22 + '@vue/compiler-dom': 3.5.34 js-beautify: 1.15.4 vue: 3.5.22(typescript@5.9.3) vue-component-type-helpers: 3.2.7 @@ -8853,6 +9039,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -9901,6 +10090,14 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} pngjs@5.0.0: {} @@ -10933,7 +11130,7 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.3 - unplugin-vue-components@0.27.5(@babel/parser@7.28.5)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3)): + unplugin-vue-components@0.27.5(@babel/parser@7.29.3)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.3.0(rollup@4.52.5) @@ -10947,7 +11144,7 @@ snapshots: unplugin: 1.16.1 vue: 3.5.22(typescript@5.9.3) optionalDependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.3 transitivePeerDependencies: - rollup - supports-color @@ -11099,7 +11296,7 @@ snapshots: '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5) '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5) - '@vue/compiler-dom': 3.5.22 + '@vue/compiler-dom': 3.5.34 kolorist: 1.8.0 magic-string: 0.30.21 vite: 7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) @@ -11128,6 +11325,21 @@ snapshots: svgo: 3.3.2 vue: 3.5.22(typescript@5.9.3) + vite@6.4.2(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.11 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.9.2 + fsevents: 2.3.3 + sass: 1.76.0 + tsx: 4.20.6 + yaml: 2.8.1 + vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.11 diff --git a/apps/app/tests/playwright-ct/smoke.spec.ts b/apps/app/tests/playwright-ct/smoke.spec.ts new file mode 100644 index 00000000..10ecff83 --- /dev/null +++ b/apps/app/tests/playwright-ct/smoke.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from '@playwright/experimental-ct-vue' + +// B1 smoke: proves Playwright Component Testing is wired and Chromium +// boots. Replaces no functionality; deleted once a real component +// test (B2 sanity) supersedes it. +test('chromium boots in CT runner', async ({ page }) => { + expect(page).toBeTruthy() +}) diff --git a/lefthook.yml b/lefthook.yml index a8a0fc33..ff45bb65 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -29,3 +29,10 @@ pre-push: # behaviour. (Pushing with zero new commits would be skipped # under lefthook but is a no-op for the sync-staleness warning # anyway, so behaviour stays effectively 1:1.) + git-lfs: + # `git lfs install --skip-repo` only sets up global clean/smudge + # filters — it does NOT install the per-repo pre-push hook + # (which would conflict with lefthook). We delegate the LFS + # upload step here so screenshot baselines tracked via LFS get + # pushed alongside their commits. + run: git lfs pre-push {1} {2}