chore(test-infra): install Playwright + axe-core; configure CT and e2e runners; enable Git LFS for screenshots
B1 of TEST-INFRA-001 (RFC-WS-FRONTEND-PRIMEVUE Amendment A-1).
- Add @playwright/test, @playwright/experimental-ct-vue,
@axe-core/playwright as dev deps in apps/app
- Add @vue/compiler-dom (transitively required by ct-vue's Vite build
pipeline; not auto-resolved on Vite 7)
- Install Chromium via `playwright install chromium` (host cache only,
not committed)
- Configure Git LFS clean/smudge filters globally; track
apps/app/tests/playwright-{ct,e2e}/__screenshots__/**/*.png
- Integrate `git lfs pre-push` into lefthook.yml since LFS's per-repo
hook would conflict with the existing sync-staleness hook
- Add playwright/index.html + playwright/index.ts hook file with the
full provider stack (Vuetify [TEMPORARY: replaced in F3 by PrimeVue],
Pinia, TanStack Vue Query, memory-history Router with no auth
guards)
- Add playwright.config.ts (e2e, Chromium-only, baseURL :5173, auto-
starts `pnpm dev` via webServer)
- Add playwright-ct.config.ts (component testing, Linux-Chromium-only
baselines, maxDiffPixelRatio 0.001, snapshot path template,
ssr.noExternal: ['vuetify'] mirroring vitest.config.ts)
- Add scripts: test:component, test:e2e, test:visual,
test:visual:update
- Add smoke test proving Chromium boots in the CT runner
- Update .gitignore for Playwright runtime artifacts (test-results/,
playwright-report/, blob-report/, playwright/.cache/)
Vitest's existing 402 tests still pass unchanged.
DoD-17 / DoD-19 CI integration deferred to TEST-INFRA-002 per Amendment
A-1 scope cut (no CI exists in this repo today).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
apps/app/playwright/index.html
Normal file
12
apps/app/playwright/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Playwright CT — Crewli</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
120
apps/app/playwright/index.ts
Normal file
120
apps/app/playwright/index.ts
Normal file
@@ -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<string, string>,
|
||||
// routes?: RouteRecordRaw[],
|
||||
// piniaInitialState?: Record<string, Record<string, unknown>>,
|
||||
// // 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<string, string>
|
||||
routes?: RouteRecordRaw[]
|
||||
piniaInitialState?: Record<string, Record<string, unknown>>
|
||||
}
|
||||
|
||||
const defaultTheme: ThemeDefinition = {
|
||||
dark: false,
|
||||
colors: {
|
||||
primary: '#1f7ad1',
|
||||
error: '#d63d4b',
|
||||
success: '#2fa66a',
|
||||
warning: '#e0992c',
|
||||
info: '#1f7ad1',
|
||||
},
|
||||
}
|
||||
|
||||
beforeMount<HooksConfig>(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: '<div data-test="ct-route-root" />' } },
|
||||
{ path: '/:pathMatch(.*)*', component: { template: '<div data-test="ct-route-catchall" />' } },
|
||||
]
|
||||
|
||||
const router = createRouter({ history: createMemoryHistory(), routes })
|
||||
|
||||
app.use(router)
|
||||
|
||||
if (hooksConfig?.initialRoute) {
|
||||
await router.push({
|
||||
path: hooksConfig.initialRoute,
|
||||
query: hooksConfig.initialQuery,
|
||||
})
|
||||
}
|
||||
await router.isReady()
|
||||
})
|
||||
Reference in New Issue
Block a user