Files
crewli/apps/app/playwright/index.ts
bert.hausmans 82af11754a test(infra): mountWithProviders helper + Vuetify CT sanity test
B2 of TEST-INFRA-001 (RFC-WS-FRONTEND-PRIMEVUE Amendment A-1).

- Add tests/playwright-ct/utils/mountWithProviders.ts: ergonomic
  wrapper around Playwright CT's mount() exposing buildMountArgs()
  and readNotificationState(). Documents the Vue Test Utils ↔
  Playwright CT API divergence (provider plugins must be wired in
  beforeMount, not at call time) and the Vuetify-temp lifecycle
  (replaced by PrimeVue in F3).
- Add tests/playwright-ct/components/SanityButtonHarness.vue: a
  v-btn harness with a click counter; lives in a .vue file so Vite
  bundles its CSS-side-effect imports for the browser context
  (Playwright CT runs the test orchestrator in Node and components
  in a Vite-bundled browser, unlike Vitest's single jsdom graph).
- Add tests/playwright-ct/components/sanity-vuetify.spec.ts: two
  tests proving (a) v-btn renders and propagates clicks, (b) the
  --v-theme-primary CSS variable resolves to a parseable RGB triplet.
- Update playwright/index.ts: import 'vuetify/styles' so the v-btn
  renders with its actual visual appearance (not unstyled). Required
  for B3's visual baselines.

3 component tests pass. 402 Vitest tests still pass unchanged.
Lint + typecheck clean on new files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:56:48 +02:00

126 lines
4.6 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'
// Vuetify base styles — required for v-btn / v-chip / v-card etc. to
// render with their actual visual appearance (not a default browser
// look). Without this, Playwright visual baselines would capture
// unstyled components. Removed in F3 alongside the Vuetify plugin.
import 'vuetify/styles'
// Plain-CSS token sheet — 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()
})