import { test as base, expect, type Page } from "@playwright/test" import { ensureTestUser, type TestUserCredentials } from "../api/helpers/test-user" const API_BASE = "/api" /** * Shared E2E test fixtures. * * Worker-scoped: * - `testUser` – persistent test user (created/reused once per worker) * * Test-scoped: * - `authedPage` – Page with logged-in user (token injection via sessionStorage) * - `accessToken` – raw JWT access token */ type E2eWorkerFixtures = { testUser: TestUserCredentials } type E2eFixtures = { authedPage: Page accessToken: string } export const test = base.extend({ /** * Override page fixture: BrowserSync keeps a WebSocket open permanently, * preventing "load" and "networkidle" from resolving. We default all * navigation methods to "domcontentloaded". */ page: async ({ page }, use) => { const origGoto = page.goto.bind(page) const origReload = page.reload.bind(page) const origGoBack = page.goBack.bind(page) const origGoForward = page.goForward.bind(page) page.goto = ((url: string, opts?: any) => origGoto(url, { waitUntil: "domcontentloaded", ...opts })) as typeof page.goto page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload page.goBack = ((opts?: any) => origGoBack({ waitUntil: "domcontentloaded", ...opts })) as typeof page.goBack page.goForward = ((opts?: any) => origGoForward({ waitUntil: "domcontentloaded", ...opts })) as typeof page.goForward await use(page) }, // Worker-scoped: create/reuse test user once per worker testUser: [ async ({ playwright }, use) => { const baseURL = process.env.CODING_URL || "https://localhost:3000" const user = await ensureTestUser(baseURL) await use(user) }, { scope: "worker" }, ], accessToken: async ({ testUser }, use) => { await use(testUser.accessToken) }, // Test-scoped: Page with logged-in user via sessionStorage token injection authedPage: async ({ page, testUser, baseURL }, use) => { // Navigate to home so domain is set for sessionStorage await page.goto("/", { waitUntil: "domcontentloaded" }) await page.waitForLoadState("domcontentloaded") // Inject auth token into sessionStorage (adapt key names to your app) await page.evaluate( ({ token, user }) => { sessionStorage.setItem("auth_token", token) sessionStorage.setItem( "auth_user", JSON.stringify({ email: user.email, firstName: user.firstName, lastName: user.lastName, }) ) }, { token: testUser.accessToken, user: testUser, } ) // Reload so the app reads the token from sessionStorage await page.reload({ waitUntil: "domcontentloaded" }) await page.waitForLoadState("domcontentloaded") await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 }) await use(page) }, }) // ── Helpers ────────────────────────────────────────────────────────────────── /** * Wait for the SPA to be ready (appContainer rendered). * Returns the detected language prefix from the URL. */ export async function waitForSpaReady(page: Page): Promise { await page.waitForLoadState("domcontentloaded") await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 }) const url = page.url() const match = url.match(/\/([a-z]{2})(\/|$)/) return match?.[1] || "de" } /** * Navigate to a route, prepending the current language prefix. */ export async function navigateToRoute(page: Page, routePath: string): Promise { const url = page.url() const match = url.match(/\/([a-z]{2})(\/|$)/) const lang = match?.[1] || "de" const fullPath = routePath === "/" ? `/${lang}` : `/${lang}${routePath}` await page.goto(fullPath) await page.waitForLoadState("domcontentloaded") await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 }) } /** * Click an SPA link and verify no full page reload occurred. */ export async function clickSpaLink(page: Page, linkSelector: string): Promise { await page.evaluate(() => { ;(window as any).__spa_navigation_marker = true }) await page.locator(linkSelector).first().click() await page.waitForLoadState("domcontentloaded") const markerExists = await page.evaluate(() => { return (window as any).__spa_navigation_marker === true }) if (!markerExists) { throw new Error(`SPA navigation failed: full page reload detected when clicking "${linkSelector}"`) } } export { expect, API_BASE, type Page }