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>
124 lines
5.1 KiB
TypeScript
124 lines
5.1 KiB
TypeScript
import type { Locator } from '@playwright/test'
|
|
import type { Component } from 'vue'
|
|
|
|
import type { HooksConfig } from '../../../playwright'
|
|
|
|
/**
|
|
* mountWithProviders — Playwright Component Testing analogue of
|
|
* tests/utils/mountWithVuexy.ts.
|
|
*
|
|
* Why two helpers?
|
|
* ----------------
|
|
* Playwright CT's `mount()` API is structurally different from
|
|
* @vue/test-utils' `mount()`: provider plugins (Vuetify, Pinia,
|
|
* Router, VueQuery) must be registered in `playwright/index.ts`'s
|
|
* `beforeMount` hook rather than passed at call time. This helper
|
|
* is a thin ergonomic wrapper that:
|
|
*
|
|
* - Forwards `hooksConfig` to the beforeMount hook (typed via
|
|
* HooksConfig from playwright/index.ts)
|
|
* - Returns the standard Playwright Locator from CT's mount, so
|
|
* downstream test code uses normal Playwright assertions
|
|
* (component.click(), expect(component).toBeVisible(), etc.)
|
|
* - Serves as a single, discoverable surface for "how do I mount
|
|
* a component in this codebase" questions
|
|
*
|
|
* Lifecycle note (TEMPORARY VUETIFY):
|
|
* -----------------------------------
|
|
* The Vuetify plugin line in `playwright/index.ts`'s `beforeMount`
|
|
* hook is INTENTIONALLY temporary state. F3 (PrimeVue foundation,
|
|
* RFC-WS-FRONTEND-PRIMEVUE §6) replaces it with PrimeVue. We do NOT
|
|
* abstract behind a "pluggable UI framework" indirection because:
|
|
*
|
|
* 1. We are NOT retaining Vuetify; the abstraction would itself
|
|
* need to be removed in F3.
|
|
* 2. The swap is mechanical (~2-hour) and atomic; abstraction adds
|
|
* cognitive cost without paying back.
|
|
* 3. Reviewers seeing "Vuetify in test infra in a PrimeVue migration
|
|
* sprint" should read this JSDoc and dev-docs/ARCH-TESTING.md §6
|
|
* for context.
|
|
*
|
|
* Equivalence to mountWithVuexy.ts:
|
|
* ---------------------------------
|
|
* | Capability | Vitest (mountWithVuexy) | Playwright CT (this) |
|
|
* |----------------------------------|-------------------------------|----------------------|
|
|
* | Vuetify w/ tokens | createVuetify({components,…}) | beforeMount hook |
|
|
* | Pinia (actions execute) | createTestingPinia | createPinia |
|
|
* | TanStack Query (fresh client) | per-call new QueryClient | per-test in hook |
|
|
* | Memory-history router | per-call createRouter | per-test in hook |
|
|
* | initialPath / initialQuery | options.initialPath | hooksConfig.… |
|
|
* | Initial Pinia state | options.initialState | hooksConfig.piniaInitialState |
|
|
* | Notification mock | createNotificationMock + plug | (see assertNotification below) |
|
|
*
|
|
* Notification assertions:
|
|
* ------------------------
|
|
* Playwright runs in a separate Node process from the browser and
|
|
* cannot use `vi.fn()` spies on store actions. Instead, tests assert
|
|
* on the rendered UI (e.g. `await expect(page.getByRole('alert'))
|
|
* .toContainText('Saved')`) or read pinia state via page.evaluate.
|
|
* This is a deliberate divergence from the Vitest pattern — UI
|
|
* assertions are stronger than spy assertions for a real-browser
|
|
* runner.
|
|
*
|
|
* Type signature constraint:
|
|
* --------------------------
|
|
* Playwright CT's MountResult uses internal types. We accept the
|
|
* native MountOptions shape and wrap; tests should import the
|
|
* Playwright-CT `expect`/`test` and call `mountWithProviders` to
|
|
* get the locator back.
|
|
*/
|
|
export interface MountOptions {
|
|
props?: Record<string, unknown>
|
|
slots?: Record<string, unknown>
|
|
hooksConfig?: HooksConfig
|
|
}
|
|
|
|
// Re-export so callers have one import surface.
|
|
export type { HooksConfig } from '../../../playwright'
|
|
|
|
// The actual mount call must be done by the test using Playwright CT's
|
|
// `mount` fixture (`test('…', async ({ mount }) => …)`). We expose a
|
|
// helper that builds the options object correctly. This avoids the
|
|
// "two ways to mount" footgun: there's the Playwright fixture, and
|
|
// there's our wrapper that produces its arguments.
|
|
export function buildMountArgs<C extends Component>(
|
|
component: C,
|
|
opts: MountOptions = {},
|
|
): {
|
|
component: C
|
|
options: { props?: Record<string, unknown>; slots?: Record<string, unknown>; hooksConfig?: HooksConfig }
|
|
} {
|
|
return {
|
|
component,
|
|
options: {
|
|
props: opts.props,
|
|
slots: opts.slots,
|
|
hooksConfig: opts.hooksConfig,
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience: assert on the notification store via the browser's
|
|
* window.__pinia exposed in the beforeMount hook. Returns the current
|
|
* notification state as serialised JSON.
|
|
*/
|
|
export async function readNotificationState(
|
|
componentLocator: Locator,
|
|
): Promise<{ visible: boolean; message: string; type: string }> {
|
|
const page = componentLocator.page()
|
|
|
|
return page.evaluate(() => {
|
|
interface Win { __pinia?: { state: { value: Record<string, unknown> } } }
|
|
const w = window as unknown as Win
|
|
|
|
const state = w.__pinia?.state?.value?.notification as
|
|
| { visible: boolean; message: string; type: string }
|
|
| undefined
|
|
|
|
return state
|
|
? { visible: state.visible, message: state.message, type: state.type }
|
|
: { visible: false, message: '', type: 'info' }
|
|
})
|
|
}
|