This commit is contained in:
Stefan Lazic committed 2026-08-03 21:15:24 +01:00
1 parent ec5583f972
commit 546613e677
189 files changed
+9873 -655

No files matched your search

+38 -17
View File
@@ -1,11 +1,12 @@
import { expect, test } from "@playwright/test";
import { Client } from "pg";
import { createPool } from "mariadb";
import { buildMysqlPoolConfig } from "../src/lib/db/mysql-pool-config";
import { SEEDED, completeBookingFlow, signIn, uniqueEmail } from "./helpers";
/**
* The 12 owner-administration journeys required by the Phase 6 brief. Same
* principles as staff-journeys.spec.ts: real forms against the real dev
* server and seeded Postgres, `test.describe.serial` because several
* server and seeded MySQL (migrated from Postgres in Phase 7), `test.describe.serial` because several
* journeys deliberately chain or share seeded fixtures (Marcus Reed is
* both the seeded owner *and* a working barber — see prisma/seed.ts), and
* every mutation a test makes to shared configuration (business details,
@@ -19,16 +20,15 @@ import { SEEDED, completeBookingFlow, signIn, uniqueEmail } from "./helpers";
test.describe.serial("owner administration journeys", () => {
test.afterAll(async () => {
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
const pool = createPool(buildMysqlPoolConfig(process.env.DATABASE_URL));
// Journey 7 creates a category/service with no deterministic id to
// upsert against on a re-run — clean up by name instead.
await client.query(`DELETE FROM "Service" WHERE name LIKE 'E2E Test Service%'`);
await client.query(`DELETE FROM "ServiceCategory" WHERE name LIKE 'E2E Test Category%'`);
await pool.query("DELETE FROM `Service` WHERE name LIKE 'E2E Test Service%'");
await pool.query("DELETE FROM `ServiceCategory` WHERE name LIKE 'E2E Test Category%'");
// Journey 5 adds a real staff member (User+StaffProfile+StaffInvitation) with no
// deterministic id either — deleting the User cascades to the rest.
await client.query(`DELETE FROM "User" WHERE email LIKE 'e2e-new-barber-%@e2e-test.local'`);
await client.end();
await pool.query("DELETE FROM `User` WHERE email LIKE 'e2e-new-barber-%@e2e-test.local'");
await pool.end();
});
test("1. owner sees a real, data-backed dashboard with configuration warnings", async ({ page }) => {
@@ -99,16 +99,15 @@ test.describe.serial("owner administration journeys", () => {
// are upserted by a deterministic id, so on a database that already had
// them from an earlier seed run their date is whatever it was back
// then, not necessarily still "N days from today" — read the real date
// straight from Postgres instead of predicting it from wall-clock time.
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
const { rows } = await client.query<{ id: string; start_at: Date; first_name: string; last_name: string }>(
`SELECT a.id, a."startAt" as start_at, c."firstName" as first_name, c."lastName" as last_name
FROM "Appointment" a JOIN "CustomerProfile" c ON c.id = a."customerId"
WHERE a.id LIKE '%-appt-upcoming-confirmed' OR a.id LIKE '%-appt-leo-future'
ORDER BY a."startAt" ASC`,
// straight from the database instead of predicting it from wall-clock time.
const pool = createPool(buildMysqlPoolConfig(process.env.DATABASE_URL));
const rows = await pool.query<{ id: string; start_at: Date; first_name: string; last_name: string }[]>(
"SELECT a.id, a.startAt as start_at, c.firstName as first_name, c.lastName as last_name" +
" FROM Appointment a JOIN CustomerProfile c ON c.id = a.customerId" +
" WHERE a.id LIKE '%-appt-upcoming-confirmed' OR a.id LIKE '%-appt-leo-future'" +
" ORDER BY a.startAt ASC",
);
await client.end();
await pool.end();
if (rows.length === 0) throw new Error("Seeded opening-hours conflict fixtures not found — check prisma/seed.ts");
// Prefer a fixture that doesn't fall on Sunday (already closed by default) so toggling it
@@ -328,4 +327,26 @@ test.describe.serial("owner administration journeys", () => {
await expect(page.getByRole("heading", { name: "Calendar" })).toBeVisible();
await expect(page.getByRole("link", { name: "Current", exact: true })).toBeVisible();
});
test("13. owner reports show real, data-backed appointment/service/staff/customer/loyalty/notification figures", async ({ page }) => {
await signIn(page, SEEDED.owner.email, "password123");
await page.goto("/owner");
await page.getByRole("link", { name: "View Reports" }).click();
await expect(page).toHaveURL(/\/owner\/reports/);
await expect(page.getByRole("heading", { name: "Reports", exact: true })).toBeVisible();
// The section labels are <h2>s — role-scoped so the owner sidebar's
// own "Services" link (same text) doesn't collide (strict-mode violation).
for (const heading of ["Appointments", "Services", "Staff", "Customers", "Loyalty", "Notifications"]) {
await expect(page.getByRole("heading", { name: heading, exact: true, level: 2 })).toBeVisible();
}
// Seeded fixtures guarantee at least one completed Haircut & Style —
// real aggregated data, not an empty-state placeholder.
await expect(page.getByText("Haircut & Style")).toBeVisible();
// The 7/30/90-day presets are real links, not decoration.
await page.getByRole("link", { name: "Last 7 days" }).click();
await expect(page).toHaveURL(/from=.*to=/);
await expect(page.getByRole("heading", { name: "Reports" })).toBeVisible();
});
});