This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
app/apps/web/e2e/tests/admin-services.spec.ts
T
Flea Flicker fa9aa5cff1 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>
2026-03-31 21:43:06 +00:00

94 lines
2.9 KiB
TypeScript

import { test, expect } from "./fixtures.js";
/**
* E2E test: Services Deduplication (GRO-306)
*
* Verifies there are no duplicate service names in:
* 1. The admin services table (/admin/services)
* 2. The booking wizard service picker (/admin/book)
*
* This test runs against current dev state (no GRO-300 dependency).
*/
test.describe("Admin Services Deduplication", () => {
test("admin services table has no duplicate names", async ({
staffPage,
}) => {
await staffPage.goto("/admin/services");
await staffPage.waitForLoadState("networkidle");
// Wait for the table to load
await expect(staffPage.locator("table")).toBeVisible({ timeout: 10000 });
// Collect all service name cells from the Name column (first column)
const nameCells = staffPage.locator("table tbody tr td:first-child");
const count = await nameCells.count();
expect(count).toBeGreaterThan(0);
const names: string[] = [];
for (let i = 0; i < count; i++) {
const text = (await nameCells.nth(i).textContent())?.trim() ?? "";
names.push(text);
}
// Check for duplicates
const seen = new Set<string>();
const duplicates: string[] = [];
for (const name of names) {
if (name === "—") continue; // skip empty/placeholder
if (seen.has(name)) {
duplicates.push(name);
}
seen.add(name);
}
expect(duplicates).toHaveLength(0);
});
test("booking wizard service picker has no duplicate names", async ({
staffPage,
}) => {
await staffPage.goto("/admin/book");
await staffPage.waitForLoadState("networkidle");
// Wait for services to load
await expect(
staffPage.getByText("Choose a service")
).toBeVisible({ timeout: 10000 });
// Wait a bit for the services to render
await staffPage.waitForTimeout(1000);
// Collect all service names from the service cards
// Each service card shows the name in a div with fontWeight 600
const serviceNames = await staffPage
.locator("text=/^[^-].*$/") // rough: get text nodes that aren't empty
.all();
// More precise: get service name elements from the service cards
// The service cards have div > div:first-child with the name
const cards = staffPage.locator('[role="button"]');
const cardCount = await cards.count();
expect(cardCount).toBeGreaterThan(0);
const names: string[] = [];
for (let i = 0; i < cardCount; i++) {
const card = cards.nth(i);
// The name is in the first child div with fontWeight 600
const nameEl = card.locator("div").first();
const text = (await nameEl.textContent())?.trim() ?? "";
if (text) names.push(text);
}
// Check for duplicates
const seen = new Set<string>();
const duplicates: string[] = [];
for (const name of names) {
if (seen.has(name)) {
duplicates.push(name);
}
seen.add(name);
}
expect(duplicates).toHaveLength(0);
});
});