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 slots?: Record 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( component: C, opts: MountOptions = {}, ): { component: C options: { props?: Record; slots?: Record; 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 } } } 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' } }) }