3388895912
* Add dev/demo login selector for quick user switching When AUTH_DISABLED=true, the app now shows a login selector page that lists staff members and clients from the database. Selecting a user sets a localStorage-based session and sends X-Dev-User-Id header on all API requests. A persistent bottom bar shows the active persona with a "Switch user" link. - API: /api/dev/config (public) and /api/dev/users (auth-disabled only) - API: auth middleware reads X-Dev-User-Id header when auth is disabled - Frontend: DevLoginSelector page, DevSessionIndicator bar - Frontend: fetch interceptor injects X-Dev-User-Id on /api/* calls - Tests: 7 passing (5 nav + 2 dev login) Closes #60 Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(e2e): seed dev user in localStorage to prevent login redirect E2E tests were failing because the dev login selector redirects to /login when AUTH_DISABLED=true and no dev user is in localStorage. Added a shared Playwright fixture that pre-seeds localStorage with a default dev user before each test. Also rebased onto latest main to resolve merge conflict in App.test.tsx. Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(e2e): mock /api/dev/config to bypass auth redirect in tests The fixture now also mocks /api/dev/config to return authDisabled: false, preventing the app from entering the redirect flow during E2E tests. Previously only seeded localStorage, but the async config fetch from the real Docker API was still triggering the redirect check. Co-Authored-By: Paperclip <noreply@paperclip.ing> --------- Co-authored-by: Groom Book CTO <cto@groombook.app> Co-authored-by: Paperclip <noreply@paperclip.ing>
121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
import { test, expect } from "./fixtures.js";
|
|
|
|
/**
|
|
* Booking portal happy-path E2E test.
|
|
*
|
|
* All API calls are mocked so this runs without a live backend.
|
|
* The test walks through all 4 steps of the booking wizard and
|
|
* verifies the confirmation screen is shown.
|
|
*/
|
|
|
|
const MOCK_SERVICE = {
|
|
id: "svc-1",
|
|
name: "Full Groom",
|
|
description: "Bath, dry, haircut, nail trim",
|
|
basePriceCents: 7500,
|
|
durationMinutes: 90,
|
|
isActive: true,
|
|
};
|
|
|
|
const MOCK_SLOT = "2026-03-20T09:00:00.000Z";
|
|
|
|
const MOCK_BOOKING_RESULT = {
|
|
appointment: {
|
|
id: "appt-1",
|
|
startTime: MOCK_SLOT,
|
|
endTime: "2026-03-20T10:30:00.000Z",
|
|
},
|
|
client: { id: "client-1", name: "Jane Smith", email: "jane@example.com" },
|
|
pet: { id: "pet-1", name: "Buddy" },
|
|
};
|
|
|
|
test("complete booking flow", async ({ page }) => {
|
|
// ── Mock API routes ──────────────────────────────────────────────────────
|
|
|
|
await page.route("/api/book/services", (route) =>
|
|
route.fulfill({ json: [MOCK_SERVICE] })
|
|
);
|
|
|
|
await page.route("/api/book/availability**", (route) =>
|
|
route.fulfill({ json: [MOCK_SLOT] })
|
|
);
|
|
|
|
await page.route("/api/book/appointments", (route) =>
|
|
route.fulfill({ status: 200, json: MOCK_BOOKING_RESULT })
|
|
);
|
|
|
|
// ── Step 1: Select a service ──────────────────────────────────────────────
|
|
|
|
await page.goto("/admin/book");
|
|
await expect(page.getByText("Book an Appointment")).toBeVisible();
|
|
await expect(page.getByText("Choose a service")).toBeVisible();
|
|
|
|
// Wait for services to load and click the service card
|
|
await expect(page.getByText("Full Groom")).toBeVisible();
|
|
await page.getByText("Full Groom").click();
|
|
|
|
// ── Step 2: Pick date and time ────────────────────────────────────────────
|
|
|
|
await expect(page.getByText("Choose a date and time")).toBeVisible();
|
|
|
|
// Wait for the slot to appear and select it
|
|
await expect(page.getByRole("button", { name: /\d{1,2}:\d{2}/ })).toBeVisible();
|
|
await page.getByRole("button", { name: /\d{1,2}:\d{2}/ }).first().click();
|
|
|
|
await page.getByRole("button", { name: "Continue" }).click();
|
|
|
|
// ── Step 3: Enter contact info ────────────────────────────────────────────
|
|
|
|
await expect(page.getByText("Your information")).toBeVisible();
|
|
|
|
await page.getByPlaceholder("Jane Smith").fill("Jane Smith");
|
|
await page.getByPlaceholder("jane@example.com").fill("jane@example.com");
|
|
await page.getByPlaceholder("Buddy").fill("Buddy");
|
|
await page.locator("select").selectOption("dog");
|
|
|
|
await page.getByRole("button", { name: "Review booking" }).click();
|
|
|
|
// ── Step 4: Confirm booking ───────────────────────────────────────────────
|
|
|
|
await expect(page.getByText("Confirm your booking")).toBeVisible();
|
|
await expect(page.getByText("Full Groom")).toBeVisible();
|
|
await expect(page.getByText("Jane Smith")).toBeVisible();
|
|
await expect(page.getByText("Buddy")).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: "Confirm booking" }).click();
|
|
|
|
// ── Step 5: Success screen ────────────────────────────────────────────────
|
|
|
|
await expect(page.getByText("Booking confirmed!")).toBeVisible();
|
|
await expect(page.getByText("jane@example.com")).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Book another appointment" })).toBeVisible();
|
|
});
|
|
|
|
test("booking form validation — required fields", async ({ page }) => {
|
|
await page.route("/api/book/services", (route) =>
|
|
route.fulfill({ json: [MOCK_SERVICE] })
|
|
);
|
|
await page.route("/api/book/availability**", (route) =>
|
|
route.fulfill({ json: [MOCK_SLOT] })
|
|
);
|
|
|
|
await page.goto("/admin/book");
|
|
await page.getByText("Full Groom").click();
|
|
await page.getByRole("button", { name: /\d{1,2}:\d{2}/ }).first().click();
|
|
await page.getByRole("button", { name: "Continue" }).click();
|
|
|
|
// Submit without filling required fields
|
|
await page.getByRole("button", { name: "Review booking" }).click();
|
|
|
|
await expect(page.getByText("Please fill in all required fields.")).toBeVisible();
|
|
});
|
|
|
|
test("no services available — shows message", async ({ page }) => {
|
|
await page.route("/api/book/services", (route) =>
|
|
route.fulfill({ json: [] })
|
|
);
|
|
|
|
await page.goto("/admin/book");
|
|
await expect(page.getByText("No services available")).toBeVisible();
|
|
});
|