Updates 1
CI / ci (push) Canceled after 0s

The session built:

Appointments page: a single list, newest first.
Block Time: edit and delete.
Customer CSV export: with barber notes excluded.
Guest-link regression test: a test that an appointment id alone is rejected, plus fixes to the stale docs.

It built the scheduler, job runner with live progress, backup and restore, and the health alerts. Its own report says a real delete-and-restore on throwaway Docker passed, but I haven’t repeated that. It also says real SMTP and large dumps were not exercised. The manager image needs rebuilding for the backups to work, because it now installs 7zip.
This commit is contained in:
Stefan Lazic committed 2026-10-17 22:29:00 +01:00
1 parent 9f981173f4
commit 17eee5b932
95 files changed
+7955 -150

No files matched your search

+26 -20
View File
@@ -66,25 +66,33 @@ test.describe("customer booking journeys", () => {
await expect(findAppointmentCard(page, SEEDED.services.skinfade, SEEDED.barberSkinfadeBeard.name).first()).toBeVisible();
});
test("3. customer views upcoming appointments", async ({ page }) => {
test("3. customer views all appointments in one list", async ({ page }) => {
await signIn(page, SEEDED.registeredCustomer.email);
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);
// One list, no tabs.
await expect(page.getByRole("tab")).toHaveCount(0);
// Seeded: an upcoming CONFIRMED Haircut & Style with Marcus Reed — reschedulable and cancellable.
// The seed also has a completed Haircut & Style with Marcus Reed, so pick the card by status too.
const upcomingSeeded = findAppointmentCard(page, SEEDED.services.haircut, SEEDED.owner.name).filter({ hasText: "Confirmed" });
await expect(upcomingSeeded).toBeVisible();
await expect(upcomingSeeded.getByText("Confirmed")).toBeVisible();
await expect(upcomingSeeded.getByRole("link", { name: "Reschedule" })).toBeVisible();
await expect(upcomingSeeded.getByRole("button", { name: "Cancel", exact: true })).toBeVisible();
await expect(upcomingSeeded.getByRole("button", { name: "Book Again" })).toHaveCount(0);
// 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);
// Seeded: a COMPLETED Haircut & Style with Leo Vance — same list, Book Again instead.
const pastSeeded = findAppointmentCard(page, SEEDED.services.haircut, SEEDED.barberHaircutOnly.name).filter({ hasText: "Completed" });
await expect(pastSeeded).toBeVisible();
await expect(pastSeeded.getByText("Completed")).toBeVisible();
await expect(pastSeeded.getByRole("button", { name: "Book Again" })).toBeVisible();
await expect(pastSeeded.getByRole("link", { name: "Reschedule" })).toHaveCount(0);
// 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();
// Newest first: the upcoming appointment is above the completed one.
const upcomingBox = await upcomingSeeded.boundingBox();
const pastBox = await pastSeeded.boundingBox();
expect(upcomingBox!.y).toBeLessThan(pastBox!.y);
});
test("4. customer reschedules an appointment", async ({ page }) => {
@@ -120,11 +128,11 @@ test.describe("customer booking journeys", () => {
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);
const upcomingCard = findAppointmentCard(page, SEEDED.services.beardTrim, SEEDED.barberSkinfadeBeard.name).first();
await expect(upcomingCard).toBeVisible();
await upcomingCard.getByRole("button", { name: "Cancel", exact: true }).click();
// Scoped to the currently-open <dialog> — the Upcoming tab can have several
// Scoped to the currently-open <dialog> — the list 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();
@@ -134,12 +142,13 @@ test.describe("customer booking journeys", () => {
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);
// Cancellation never deletes — the same card stays in the list, now with a Cancelled pill
// and Book Again instead of Reschedule/Cancel.
const cancelledCard = findAppointmentCard(page, SEEDED.services.beardTrim, SEEDED.barberSkinfadeBeard.name).first();
await expect(cancelledCard).toBeVisible();
await expect(cancelledCard.getByText("Cancelled")).toBeVisible();
await expect(cancelledCard.getByRole("button", { name: "Book Again" })).toBeVisible();
await expect(cancelledCard.getByRole("link", { name: "Reschedule" })).toHaveCount(0);
});
test("6. customer books again from history", async ({ page }) => {
@@ -147,8 +156,7 @@ test.describe("customer booking journeys", () => {
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);
const completedCard = findAppointmentCard(page, SEEDED.services.haircut, SEEDED.barberHaircutOnly.name).filter({ hasText: "Completed" });
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.
@@ -161,8 +169,6 @@ test.describe("customer booking journeys", () => {
// 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();
});