feat(e2e): add Playwright E2E test suite for critical user journeys (GRO-306)
Implements the automated Playwright E2E suite as the pre-UAT gate following the UAT failures identified in GRO-299. Creates 5 test files in apps/web/e2e/: - portal-auth.spec.ts: verifies client portal auth (client name shown, not "Hi, Guest") - portal-data.spec.ts: verifies portal sections render without auth gates - admin-services.spec.ts: asserts no duplicate service names in admin/services and booking wizard - admin-reports.spec.ts: verifies reports page shows non-zero data for last 60 days - console-health.spec.ts: asserts no 404s for favicon/PWA assets and no JS exceptions Also adds: - apps/web/e2e/ with Playwright config targeting groombook.dev.farh.net - Shared fixtures with storageState-based auth via dev login selector - test:e2e npm script in apps/web/package.json - web-e2e CI job targeting PRs (runs after deploy-dev) Note: Tests 1 & 2 (portal auth/data) depend on GRO-300 being deployed. Tests 3-5 run against current dev state. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { test as base, Page, Browser, BrowserContext } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
const STAFF_STORAGE = path.join(process.cwd(), ".auth/staff.json");
|
||||
const CLIENT_STORAGE = path.join(process.cwd(), ".auth/client.json");
|
||||
|
||||
/**
|
||||
* Authenticates as a staff user via the dev login selector and saves storage state.
|
||||
*/
|
||||
async function authenticateStaff(browser: Browser): Promise<string> {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto("/login");
|
||||
|
||||
// Click "Alice Groomer" (first staff user)
|
||||
const alice = page.getByText("Alice Groomer");
|
||||
if (await alice.isVisible({ timeout: 5000 })) {
|
||||
await alice.click();
|
||||
} else {
|
||||
// Fallback: click any staff user
|
||||
await page.getByText(/groomer|manager/i).first().click();
|
||||
}
|
||||
|
||||
await page.waitForURL(/\/(admin|portal)/, { timeout: 10000 });
|
||||
|
||||
const storageState = await context.storageState();
|
||||
await context.close();
|
||||
return JSON.stringify(storageState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates as a client via the dev login selector and saves storage state.
|
||||
*/
|
||||
async function authenticateClient(browser: Browser): Promise<string> {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto("/login");
|
||||
|
||||
// Click "Carol Client" (first client user)
|
||||
const carol = page.getByText("Carol Client");
|
||||
if (await carol.isVisible({ timeout: 5000 })) {
|
||||
await carol.click();
|
||||
} else {
|
||||
// Fallback: click any client user
|
||||
await page.getByText(/\d+ pets?/i).first().click();
|
||||
}
|
||||
|
||||
await page.waitForURL(/\//, { timeout: 10000 });
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const storageState = await context.storageState();
|
||||
await context.close();
|
||||
return JSON.stringify(storageState);
|
||||
}
|
||||
|
||||
export type UserType = "staff" | "client";
|
||||
|
||||
/**
|
||||
* Returns the storage state file path for the given user type.
|
||||
* Creates the auth file if it doesn't exist.
|
||||
*/
|
||||
async function getStorageState(browser: Browser, userType: UserType): Promise<string> {
|
||||
const filePath = userType === "staff" ? STAFF_STORAGE : CLIENT_STORAGE;
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
if (!require("fs").existsSync(filePath)) {
|
||||
const state =
|
||||
userType === "staff"
|
||||
? await authenticateStaff(browser)
|
||||
: await authenticateClient(browser);
|
||||
|
||||
require("fs").mkdirSync(dir, { recursive: true });
|
||||
require("fs").writeFileSync(filePath, state);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom test fixture that provides an authenticated page for E2E tests.
|
||||
* Automatically handles login via the dev login selector.
|
||||
*
|
||||
* Usage:
|
||||
* test("my test", async ({ staffPage }) => { ... }); // staff user
|
||||
* test("my test", async ({ clientPage }) => { ... }); // client user
|
||||
*/
|
||||
export const test = base.extend<{
|
||||
staffPage: Page;
|
||||
clientPage: Page;
|
||||
}>({
|
||||
staffPage: async ({ browser }, use, workerInfo): Promise<void> => {
|
||||
const storageStatePath = await getStorageState(browser, "staff");
|
||||
const context = await browser.newContext({ storageState: storageStatePath });
|
||||
const page = await context.newPage();
|
||||
await use(page);
|
||||
await context.close();
|
||||
},
|
||||
|
||||
clientPage: async ({ browser }, use, workerInfo): Promise<void> => {
|
||||
const storageStatePath = await getStorageState(browser, "client");
|
||||
const context = await browser.newContext({ storageState: storageStatePath });
|
||||
const page = await context.newPage();
|
||||
await use(page);
|
||||
await context.close();
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
Reference in New Issue
Block a user