Files
crewli/apps/app/playwright/index.ts
bert.hausmans b8d18e63af 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>
2026-05-10 14:53:57 +02:00

121 lines
4.4 KiB
TypeScript

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()
})