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/e2e/tests/admin-services.spec.ts
T
groombook-engineer[bot] 391c5b70d9 fix(e2e): resolve remaining 2 E2E test failures
- console-health: add 502/Failed to load resource filter to admin page test (portal page already had it)
- admin-services: mock /api/book/services endpoint used by booking wizard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 15:48:21 +00:00

104 lines
3.7 KiB
TypeScript

import { test, expect } from "./fixtures.js";
/**
* E2E tests for services deduplication.
* Verifies that service names are unique in both admin services list
* and in the booking service picker.
*/
const MOCK_SERVICES = [
{ id: "svc-1", name: "Full Groom", description: "Bath and haircut", basePriceCents: 7500, durationMinutes: 90, isActive: true },
{ id: "svc-2", name: "Bath Only", description: "Just the bath", basePriceCents: 3500, durationMinutes: 45, isActive: true },
{ id: "svc-3", name: "Nail Trim", description: "Just nails", basePriceCents: 1500, durationMinutes: 15, isActive: true },
];
test.describe("Services Deduplication", () => {
test.beforeEach(async ({ page }) => {
// Mock services endpoint FIRST before navigation
// Also mock /api/book/services used by the booking wizard
await page.route("**/api/services**", (route) =>
route.fulfill({ json: MOCK_SERVICES })
);
await page.route("**/api/book/services**", (route) =>
route.fulfill({ json: MOCK_SERVICES })
);
// Login as staff
await page.goto("/login");
await page.getByText("Alice Groomer").click();
await expect(page).toHaveURL("/admin");
});
test("admin services page shows no duplicate service names", async ({ page }) => {
await page.goto("/admin/services");
// Wait for services to load
await page.waitForTimeout(1_000);
// Collect all service names from the table
const serviceNameCells = page.locator("table tbody tr td:first-child");
const count = await serviceNameCells.count();
const names: string[] = [];
for (let i = 0; i < count; i++) {
const text = await serviceNameCells.nth(i).textContent();
if (text) names.push(text.trim());
}
// Check for duplicates
const duplicates = names.filter((name, index) => names.indexOf(name) !== index);
// Assert no duplicate names
expect(duplicates, `Found duplicate service names: ${duplicates.join(", ")}`).toHaveLength(0);
// Verify all names are unique using Set comparison
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBe(names.length);
});
test("admin services page renders all services", async ({ page }) => {
await page.goto("/admin/services");
// Wait for table to render
await expect(page.locator("table")).toBeVisible({ timeout: 10_000 });
// Should show the heading
await expect(page.locator("h1")).toContainText("Services");
// Should show all unique services
const rowCount = await page.locator("table tbody tr").count();
expect(rowCount).toBeGreaterThan(0);
});
test("booking service picker shows no duplicates", async ({ page }) => {
await page.goto("/admin/book");
// Wait for services to load in the booking wizard
await page.waitForTimeout(1_000);
// Collect service names from the picker
const serviceCards = page.locator("text=/Full Groom|Bath Only|Nail Trim/");
const serviceNames: string[] = [];
// Get all text content that looks like service names
const allText = await page.locator("body").textContent();
if (allText) {
const matches = allText.match(/(?:Full Groom|Bath Only|Nail Trim)/g);
if (matches) {
serviceNames.push(...matches);
}
}
// Check for duplicates in the booking picker
const duplicates = serviceNames.filter((name, index) => serviceNames.indexOf(name) !== index);
expect(duplicates, `Found duplicate service names in booking picker: ${duplicates.join(", ")}`).toHaveLength(0);
});
test("booking wizard step 1 shows services", async ({ page }) => {
await page.goto("/admin/book");
// Should show at least one service
await expect(page.getByText("Full Groom")).toBeVisible({ timeout: 10_000 });
});
});