Phase 7
This commit is contained in:
189 files changed
+9873
-655
No files matched your search
+38
-17
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user