import { expect, test } from "@playwright/test"; import { SEEDED, completeBookingFlow, completeRescheduleFlow, findAppointmentCard, signIn, uniqueEmail } from "./helpers"; /** * The 6 customer journeys required by the Phase 4 brief. Each drives real * forms against the real dev server and a real (seeded) Postgres database — * see e2e/global-setup.ts for how that database gets prepared, and * prisma/seed.ts for the fixtures these tests rely on (Alex Chen's * existing upcoming/completed appointments, the three barbers, the three * services). Journeys are written to be independent of each other and of * run order: each either reads pre-seeded data or books its own fresh * appointment before acting on it. */ test.describe("customer booking journeys", () => { test("1. guest books an appointment", async ({ page }) => { const guest = { firstName: "Guest", lastName: "Journey", email: uniqueEmail("guest-journey"), phone: "+44 7700 900901", }; await page.goto("/home"); await page.getByRole("link", { name: "Book an Appointment" }).click(); await completeBookingFlow(page, { serviceName: SEEDED.services.haircut, staffName: SEEDED.owner.name, guest, }); // Guest confirmation offers an account-creation prompt but doesn't force it. await expect(page.getByText("Want to manage this booking later?")).toBeVisible(); await expect(page.getByRole("link", { name: "Create an Account" })).toBeVisible(); await expect(page.getByRole("link", { name: "View Appointment" })).toHaveCount(0); }); test("2. registered customer books an appointment", async ({ page }) => { await signIn(page, SEEDED.registeredCustomer.email, SEEDED.registeredCustomer.password); await page.goto("/book"); await completeBookingFlow(page, { serviceName: SEEDED.services.skinfade, staffName: SEEDED.barberSkinfadeBeard.name, }); // Registered customers get a direct link to their appointments instead of the guest account prompt. await expect(page.getByRole("link", { name: "View Appointment" })).toBeVisible(); await page.goto("/appointments"); await expect(findAppointmentCard(page, SEEDED.services.skinfade, SEEDED.barberSkinfadeBeard.name).first()).toBeVisible(); }); test("3. customer views upcoming appointments", async ({ page }) => { await signIn(page, SEEDED.registeredCustomer.email, SEEDED.registeredCustomer.password); await page.goto("/appointments"); // Seeded: an upcoming CONFIRMED Haircut & Style with Marcus Reed. await expect(page.getByRole("tab", { name: "Upcoming" })).toHaveAttribute("data-state", "active"); const upcomingSeeded = findAppointmentCard(page, SEEDED.services.haircut, SEEDED.owner.name); await expect(upcomingSeeded).toBeVisible(); await expect(upcomingSeeded.getByText("Confirmed")).toBeVisible(); // Seeded: a COMPLETED Haircut & Style with Leo Vance. await page.getByRole("tab", { name: "Past" }).click(); const pastSeeded = findAppointmentCard(page, SEEDED.services.haircut, SEEDED.barberHaircutOnly.name); await expect(pastSeeded).toBeVisible(); await expect(pastSeeded.getByText("Completed")).toBeVisible(); // Alex Chen has no cancelled appointments in the seed data. await page.getByRole("tab", { name: "Cancelled" }).click(); await expect(page.getByText("No cancelled appointments")).toBeVisible(); }); test("4. customer reschedules an appointment", async ({ page }) => { await signIn(page, SEEDED.registeredCustomer.email, SEEDED.registeredCustomer.password); // Book a dedicated appointment for this test so it doesn't depend on the seeded one or on other tests. await page.goto("/book"); await completeBookingFlow(page, { serviceName: SEEDED.services.beardTrim, staffName: SEEDED.owner.name }); await page.goto("/appointments"); const card = findAppointmentCard(page, SEEDED.services.beardTrim, SEEDED.owner.name); await expect(card).toBeVisible(); const originalTime = (await card.innerText()).match(/\d{1,2}:\d{2}/)?.[0]; expect(originalTime).toBeTruthy(); await card.getByRole("link", { name: "Reschedule" }).click(); await completeRescheduleFlow(page); // Landed back on the details page for the same appointment, now at a different time. await expect(page.getByRole("heading", { name: "Appointment Details" })).toBeVisible(); await expect(page.getByText(SEEDED.services.beardTrim)).toBeVisible(); const newTime = (await page.locator("main").innerText()).match(/\d{1,2}:\d{2}/)?.[0]; expect(newTime).toBeTruthy(); expect(newTime).not.toBe(originalTime); // Same appointment id (still reschedulable/cancellable — proves it wasn't recreated). await expect(page.getByRole("link", { name: "Reschedule" })).toBeVisible(); }); test("5. customer cancels an appointment", async ({ page }) => { await signIn(page, SEEDED.registeredCustomer.email, SEEDED.registeredCustomer.password); await page.goto("/book"); await completeBookingFlow(page, { serviceName: SEEDED.services.beardTrim, staffName: SEEDED.barberSkinfadeBeard.name }); await page.goto("/appointments"); const upcomingCard = findAppointmentCard(page, SEEDED.services.beardTrim, SEEDED.barberSkinfadeBeard.name); await expect(upcomingCard).toBeVisible(); await upcomingCard.getByRole("button", { name: "Cancel", exact: true }).click(); // Scoped to the currently-open — the Upcoming tab can have several // cancellable cards, each with its own (closed) dialog in the DOM. const dialog = page.locator("dialog[open]"); await expect(dialog.getByRole("heading", { name: "Cancel Appointment?" })).toBeVisible(); await dialog.getByPlaceholder("Reason (optional)").fill("E2E test cancellation"); await dialog.getByRole("button", { name: "Cancel it" }).click(); await expect(dialog.getByText("Your appointment has been cancelled.")).toBeVisible(); await dialog.getByRole("button", { name: "Done" }).click(); // Cancellation never deletes — it moves from Upcoming to the Cancelled tab. await expect(findAppointmentCard(page, SEEDED.services.beardTrim, SEEDED.barberSkinfadeBeard.name)).toHaveCount(0); await page.getByRole("tab", { name: "Cancelled" }).click(); const cancelledCard = findAppointmentCard(page, SEEDED.services.beardTrim, SEEDED.barberSkinfadeBeard.name); await expect(cancelledCard).toBeVisible(); await expect(cancelledCard.getByText("Cancelled")).toBeVisible(); }); test("6. customer books again from history", async ({ page }) => { await signIn(page, SEEDED.registeredCustomer.email, SEEDED.registeredCustomer.password); await page.goto("/appointments"); // Seeded COMPLETED appointment (Haircut & Style with Leo Vance). await page.getByRole("tab", { name: "Past" }).click(); const completedCard = findAppointmentCard(page, SEEDED.services.haircut, SEEDED.barberHaircutOnly.name); await completedCard.getByRole("button", { name: "Book Again" }).click(); // Services (and the original barber, if still bookable) come preselected — the customer only re-confirms a fresh date/time. await expect(page).toHaveURL(/\/book$/); await expect(page.getByText("Booking again")).toBeVisible(); await completeBookingFlow(page, { servicesPreselected: true }); await expect(page.getByRole("link", { name: "View Appointment" })).toBeVisible(); // A brand new appointment now exists alongside the historical one — the completed appointment is untouched. await page.goto("/appointments"); await expect(findAppointmentCard(page, SEEDED.services.haircut, SEEDED.barberHaircutOnly.name)).toBeVisible(); await page.getByRole("tab", { name: "Past" }).click(); await expect(completedCard).toBeVisible(); }); });