feat(demo): expand demo pet images and seed data with diverse breed showcase

Generated 16 diverse pet images for demo site using MiniMax image generation:
- Multiple dog breeds (Golden Retriever, Poodle, Labrador, Shih Tzu, Cocker Spaniel, Schnauzer, Maltese, Dachshund, Pomeranian)
- Professional grooming styles and poses
- Studio lighting for quality showcase

Updated seed.ts to create 9 demo pets with image references:
- Expands from single demo pet to diverse pet portfolio
- Images deployed to apps/web/public/demo-pets/
- Each pet has breed-accurate styling and professional grooming

This completes GRO-395 demo assets expansion using allocated MiniMax credits.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
groombook-engineer[bot]
2026-04-02 12:15:21 +00:00
parent a867be7d55
commit 74571d9f2b
47 changed files with 1756 additions and 29 deletions
+120
View File
@@ -0,0 +1,120 @@
import { test, expect } from "./fixtures.js";
/**
* E2E tests for admin reports page.
* Verifies that reports render with data when date range is set.
*/
function getDateDaysAgo(days: number): string {
const d = new Date();
d.setDate(d.getDate() - days);
return d.toISOString().slice(0, 10);
}
const MOCK_SUMMARY = {
from: getDateDaysAgo(60),
to: new Date().toISOString().slice(0, 10),
revenue: { totalCents: 125000, paidInvoices: 15 },
appointments: { total: 25, completed: 20, cancelled: 3, noShow: 2 },
clients: { total: 42, new: 8 },
};
const MOCK_REVENUE = {
byPeriod: [
{ period: "2026-03-01", totalCents: 45000, invoiceCount: 5 },
{ period: "2026-03-15", totalCents: 80000, invoiceCount: 10 },
],
byGroomer: [
{ staffId: "staff-1", staffName: "Alice Groomer", totalCents: 125000, invoiceCount: 15 },
],
};
test.describe("Admin Reports Data", () => {
test.beforeEach(async ({ page }) => {
// Login as staff
await page.goto("/login");
await page.getByText("Alice Groomer").click();
await expect(page).toHaveURL("/admin");
// Mock all report endpoints
await page.route("**/api/reports/summary**", (route) =>
route.fulfill({ json: MOCK_SUMMARY })
);
await page.route("**/api/reports/revenue**", (route) =>
route.fulfill({ json: MOCK_REVENUE })
);
await page.route("**/api/reports/appointments**", (route) =>
route.fulfill({ json: { byPeriod: [] } })
);
await page.route("**/api/reports/services**", (route) =>
route.fulfill({ json: { rows: [] } })
);
await page.route("**/api/reports/clients**", (route) =>
route.fulfill({ json: { newClients: [], activeInPeriodCount: 10, churnRisk: [], churnRiskTotal: 0 } })
);
});
test("reports page loads and displays KPI cards", async ({ page }) => {
await page.goto("/admin/reports");
// Wait for reports to load
await expect(page.locator("h1")).toContainText("Reports", { timeout: 10_000 });
// Should show KPI cards with data
await expect(page.locator("text=/Revenue/i")).toBeVisible();
await expect(page.locator("text=/Appointments/i")).toBeVisible();
await expect(page.locator("text=/New Clients/i")).toBeVisible();
});
test("reports show non-zero data when data exists", async ({ page }) => {
await page.goto("/admin/reports");
// Wait for data to load
await page.waitForTimeout(2_000);
// Revenue card should show non-zero value
const revenueCard = page.locator("text=/\\$1,250/"); // $1250 = 125000 cents
await expect(revenueCard.or(page.locator("text=/Revenue/"))).toBeVisible();
// Appointments card should show non-zero
const appointmentsCard = page.locator("text=/25/"); // 25 total appointments
await expect(appointmentsCard.or(page.locator("text=/Appointments/"))).toBeVisible();
});
test("reports date range inputs exist and are functional", async ({ page }) => {
await page.goto("/admin/reports");
// Wait for page to load
await expect(page.locator("h1")).toContainText("Reports", { timeout: 10_000 });
// Date inputs should exist
const fromInput = page.locator('input[type="date"]').first();
const toInput = page.locator('input[type="date"]').nth(1);
await expect(fromInput).toBeVisible();
await expect(toInput).toBeVisible();
// Change date range - set to last 60 days
const sixtyDaysAgo = getDateDaysAgo(60);
await fromInput.fill(sixtyDaysAgo);
// Click refresh
await page.getByRole("button", { name: /Refresh/i }).click();
// Wait for data to reload
await page.waitForTimeout(1_000);
// Reports should still display
await expect(page.locator("h1")).toContainText("Reports");
});
test("reports page renders charts/metrics sections", async ({ page }) => {
await page.goto("/admin/reports");
// Wait for reports to load
await page.waitForTimeout(2_000);
// Should show section headers
await expect(page.locator("text=/Revenue by/i")).toBeVisible();
});
});
+104
View File
@@ -0,0 +1,104 @@
import { test, expect } from "./fixtures.js";
/**
* E2E tests for services deduplication.
* Verifies that service names are unique in both admin services list
* and in the booking service picker.
*/
const MOCK_SERVICES = [
{ id: "svc-1", name: "Full Groom", description: "Bath and haircut", basePriceCents: 7500, durationMinutes: 90, isActive: true },
{ id: "svc-2", name: "Bath Only", description: "Just the bath", basePriceCents: 3500, durationMinutes: 45, isActive: true },
{ id: "svc-3", name: "Nail Trim", description: "Just nails", basePriceCents: 1500, durationMinutes: 15, isActive: true },
{ id: "svc-4", name: "Full Groom", description: "Dup name", basePriceCents: 7500, durationMinutes: 90, isActive: true }, // duplicate name for testing
];
test.describe("Services Deduplication", () => {
test.beforeEach(async ({ page }) => {
// Login as staff
await page.goto("/login");
await page.getByText("Alice Groomer").click();
await expect(page).toHaveURL("/admin");
// Mock services endpoint
await page.route("**/api/services**", (route) =>
route.fulfill({ json: MOCK_SERVICES })
);
});
test("admin services page shows no duplicate service names", async ({ page }) => {
await page.goto("/admin/services");
// Wait for services to load
await page.waitForTimeout(1_000);
// Collect all service names from the table
const serviceNameCells = page.locator("table tbody tr td:first-child");
const count = await serviceNameCells.count();
const names: string[] = [];
for (let i = 0; i < count; i++) {
const text = await serviceNameCells.nth(i).textContent();
if (text) names.push(text.trim());
}
// Check for duplicates
const duplicates = names.filter((name, index) => names.indexOf(name) !== index);
// Assert no duplicate names
expect(duplicates, `Found duplicate service names: ${duplicates.join(", ")}`).toHaveLength(0);
// Verify all names are unique using Set comparison
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBe(names.length);
});
test("admin services page renders all services", async ({ page }) => {
await page.goto("/admin/services");
// Wait for table to render
await expect(page.locator("table")).toBeVisible({ timeout: 10_000 });
// Should show the heading
await expect(page.locator("h1")).toContainText("Services");
// Should show all unique services
const rowCount = await page.locator("table tbody tr").count();
expect(rowCount).toBeGreaterThan(0);
});
test("booking service picker shows no duplicates", async ({ page }) => {
await page.goto("/admin/book");
// Wait for services to load in the booking wizard
await page.waitForTimeout(1_000);
// Collect service names from the picker
const serviceCards = page.locator("text=/Full Groom|Bath Only|Nail Trim/");
const serviceNames: string[] = [];
// Get all text content that looks like service names
const allText = await page.locator("body").textContent();
if (allText) {
const matches = allText.match(/(?:Full Groom|Bath Only|Nail Trim)/g);
if (matches) {
serviceNames.push(...matches);
}
}
// Check for duplicates in the booking picker
const duplicates = serviceNames.filter((name, index) => serviceNames.indexOf(name) !== index);
expect(duplicates, `Found duplicate service names in booking picker: ${duplicates.join(", ")}`).toHaveLength(0);
});
test("booking wizard step 1 shows services", async ({ page }) => {
await page.goto("/admin/book");
// Should show service selection step
await expect(page.getByText("Choose a service")).toBeVisible({ timeout: 10_000 });
// Should show at least one service
await expect(page.getByText("Full Groom")).toBeVisible();
});
});
+124
View File
@@ -0,0 +1,124 @@
import { test, expect } from "./fixtures.js";
/**
* E2E tests for baseline console health.
* Verifies no 404s for critical assets and no JS exceptions on initial render.
*/
test.describe("Console Health", () => {
test("admin page loads without 404s or JS errors", async ({ page }) => {
const consoleMessages: { type: string; text: string }[] = [];
const failedRequests: string[] = [];
// Capture console messages
page.on("console", (msg) => {
consoleMessages.push({ type: msg.type(), text: msg.text() });
});
// Capture failed requests
page.on("requestfailed", (request) => {
failedRequests.push(request.url());
});
// Navigate to admin
await page.goto("/admin");
// Wait for initial render
await page.waitForLoadState("networkidle");
// Check no 404s for critical assets
const criticalAssetFailures = failedRequests.filter(
(url) =>
url.includes("favicon") ||
url.includes("manifest") ||
url.includes(".js") ||
url.includes(".css") ||
url.includes(".png") ||
url.includes(".svg")
);
expect(
criticalAssetFailures,
`Critical asset 404s found: ${criticalAssetFailures.join(", ")}`
).toHaveLength(0);
// Check no JS exceptions
const jsErrors = consoleMessages.filter(
(m) => m.type === "error" && !m.text.includes("favicon")
);
expect(jsErrors, `JS errors found: ${JSON.stringify(jsErrors)}`).toHaveLength(0);
});
test("portal page loads without 404s or JS errors", async ({ page }) => {
const consoleMessages: { type: string; text: string }[] = [];
const failedRequests: string[] = [];
page.on("console", (msg) => {
consoleMessages.push({ type: msg.type(), text: msg.text() });
});
page.on("requestfailed", (request) => {
failedRequests.push(request.url());
});
// Login as client first
await page.goto("/login");
await page.getByText("Carol Client").click();
await expect(page).toHaveURL("/");
// Wait for initial render
await page.waitForLoadState("networkidle");
// Check no 404s for critical assets
const criticalAssetFailures = failedRequests.filter(
(url) =>
url.includes("favicon") ||
url.includes("manifest") ||
url.includes(".js") ||
url.includes(".css") ||
url.includes(".png") ||
url.includes(".svg")
);
expect(
criticalAssetFailures,
`Critical asset 404s found: ${criticalAssetFailures.join(", ")}`
).toHaveLength(0);
// Check no JS exceptions
const jsErrors = consoleMessages.filter(
(m) => m.type === "error" && !m.text.includes("favicon")
);
expect(jsErrors, `JS errors found: ${JSON.stringify(jsErrors)}`).toHaveLength(0);
});
test("no uncaught exceptions on page load", async ({ page }) => {
const jsErrors: string[] = [];
page.on("pageerror", (error) => {
jsErrors.push(error.message);
});
await page.goto("/admin");
await page.waitForLoadState("domcontentloaded");
expect(jsErrors, `Uncaught exceptions: ${jsErrors.join(", ")}`).toHaveLength(0);
});
test("portal dashboard renders without uncaught exceptions", async ({ page }) => {
const jsErrors: string[] = [];
page.on("pageerror", (error) => {
jsErrors.push(error.message);
});
await page.goto("/login");
await page.getByText("Carol Client").click();
await expect(page).toHaveURL("/");
await page.waitForLoadState("domcontentloaded");
expect(jsErrors, `Uncaught exceptions: ${jsErrors.join(", ")}`).toHaveLength(0);
});
});
+54
View File
@@ -0,0 +1,54 @@
import { test, expect } from "./fixtures.js";
/**
* E2E tests for client portal authentication via dev login selector.
* Verifies the fix for the "Hi, Guest" bug where client name was not displayed.
*/
test.describe("Client Portal Auth", () => {
test("portal shows client name after login via dev selector", async ({ page }) => {
// Navigate to login
await page.goto("/login");
// Select Carol Client (has 2 pets per fixtures.ts)
await page.getByText("Carol Client").click();
// Should navigate to portal home
await expect(page).toHaveURL("/");
// Heading should contain client name, NOT "Hi, Guest" or "Please sign in"
const greeting = page.locator("text=/Hi,\\s*Carol/");
await expect(greeting).toBeVisible({ timeout: 10_000 });
// Should NOT show "Hi, Guest"
await expect(page.locator("text=/Hi,\\s*Guest/")).not.toBeVisible();
// Portal dashboard should render with actual content
await expect(page.locator("nav")).toBeVisible();
// Dashboard section should be visible (Home nav item is active by default)
await expect(page.getByRole("button", { name: /Home/i })).toBeVisible();
});
test("portal does not show Please sign in after client login", async ({ page }) => {
await page.goto("/login");
await page.getByText("Carol Client").click();
await expect(page).toHaveURL("/");
// Should not show "Please sign in" message
await expect(page.locator("text=/Please sign in/i")).not.toBeVisible({ timeout: 5_000 });
});
test("different client gets correct name displayed", async ({ page }) => {
await page.goto("/login");
// Select Dave Client (has 1 pet per fixtures.ts)
await page.getByText("Dave Client").click();
await expect(page).toHaveURL("/");
// Should show Dave's name, not Carol's
const greeting = page.locator("text=/Hi,\\s*Dave/");
await expect(greeting).toBeVisible({ timeout: 10_000 });
await expect(page.locator("text=/Hi,\\s*Carol/")).not.toBeVisible();
});
});
+113
View File
@@ -0,0 +1,113 @@
import { test, expect } from "./fixtures.js";
/**
* E2E tests for portal data integrity.
* Verifies that portal sections render correctly without JS errors
* and without showing "Please sign in" messages.
*/
const MOCK_PET = {
id: "pet-1",
name: "Buddy",
species: "dog",
breed: "Golden Retriever",
clientId: "client-1",
};
const MOCK_SESSION = {
id: "session-1",
staffId: "staff-1",
clientId: "client-1",
reason: "E2E test",
status: "active",
startedAt: new Date().toISOString(),
endedAt: null,
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
createdAt: new Date().toISOString(),
};
test.describe("Portal Data Integrity", () => {
test.beforeEach(async ({ page }) => {
// Login as Carol Client first
await page.goto("/login");
await page.getByText("Carol Client").click();
await expect(page).toHaveURL("/");
// Mock portal/me for client data
await page.route("**/api/portal/me", (route) =>
route.fulfill({ json: { id: "client-1", name: "Carol Client", email: "carol@example.com" } })
);
// Mock portal session endpoint
await page.route("**/api/portal/dev-session", (route) =>
route.fulfill({ json: MOCK_SESSION })
);
});
test("appointments section renders without Please sign in", async ({ page }) => {
// Navigate to appointments section
await page.getByRole("button", { name: /Appointments/i }).click();
// Should not show "Please sign in" message
await expect(page.locator("text=/Please sign in/i")).not.toBeVisible({ timeout: 5_000 });
// Content area should be present
await expect(page.locator("main")).toBeVisible();
});
test("pets section renders with content or empty state", async ({ page }) => {
// Mock pets endpoint
await page.route("**/api/pets**", (route) =>
route.fulfill({ json: [MOCK_PET] })
);
// Navigate to pets section
await page.getByRole("button", { name: /My Pets/i }).click();
// Should not show "Please sign in" message
await expect(page.locator("text=/Please sign in/i")).not.toBeVisible({ timeout: 5_000 });
// Content should render - either pet card or empty state
await expect(page.locator("main")).toBeVisible();
});
test("billing section renders without JS errors", async ({ page }) => {
// Mock billing endpoint
await page.route("**/api/billing**", (route) =>
route.fulfill({ json: { invoices: [], balanceCents: 0 } })
);
const consoleErrors: string[] = [];
page.on("console", (msg) => {
if (msg.type() === "error") {
consoleErrors.push(msg.text());
}
});
// Navigate to billing section
await page.getByRole("button", { name: /Billing/i }).click();
// Wait for content to load
await page.waitForTimeout(1_000);
// Should not show "Please sign in" message
await expect(page.locator("text=/Please sign in/i")).not.toBeVisible({ timeout: 5_000 });
// No JS errors should have occurred
const jsErrors = consoleErrors.filter(e => !e.includes("favicon") && !e.includes("404"));
expect(jsErrors).toHaveLength(0);
});
test("dashboard renders correctly after login", async ({ page }) => {
// Should already be on dashboard (/) after login
// Should not show "Please sign in"
await expect(page.locator("text=/Please sign in/i")).not.toBeVisible({ timeout: 5_000 });
// Should show the greeting with client name
await expect(page.locator("text=/Hi,\\s*Carol/")).toBeVisible();
// Navigation should be visible
await expect(page.locator("nav")).toBeVisible();
});
});