feat: flip routing — customer portal at /, admin at /admin #57
@@ -46,7 +46,7 @@ test("complete booking flow", async ({ page }) => {
|
||||
|
||||
// ── Step 1: Select a service ──────────────────────────────────────────────
|
||||
|
||||
await page.goto("/book");
|
||||
await page.goto("/admin/book");
|
||||
await expect(page.getByText("Book an Appointment")).toBeVisible();
|
||||
await expect(page.getByText("Choose a service")).toBeVisible();
|
||||
|
||||
@@ -99,7 +99,7 @@ test("booking form validation — required fields", async ({ page }) => {
|
||||
route.fulfill({ json: [MOCK_SLOT] })
|
||||
);
|
||||
|
||||
await page.goto("/book");
|
||||
await page.goto("/admin/book");
|
||||
await page.getByText("Full Groom").click();
|
||||
await page.getByRole("button", { name: /\d{1,2}:\d{2}/ }).first().click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
@@ -115,6 +115,6 @@ test("no services available — shows message", async ({ page }) => {
|
||||
route.fulfill({ json: [] })
|
||||
);
|
||||
|
||||
await page.goto("/book");
|
||||
await page.goto("/admin/book");
|
||||
await expect(page.getByText("No services available")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -40,18 +40,18 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
|
||||
test("clients page shows client list", async ({ page }) => {
|
||||
await page.goto("/clients");
|
||||
await page.goto("/admin/clients");
|
||||
await expect(page.getByText("Alice Johnson")).toBeVisible();
|
||||
await expect(page.getByText("Bob Williams")).toBeVisible();
|
||||
});
|
||||
|
||||
test("clients page shows search input", async ({ page }) => {
|
||||
await page.goto("/clients");
|
||||
await page.goto("/admin/clients");
|
||||
await expect(page.getByPlaceholder(/search/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("clicking a client shows their details", async ({ page }) => {
|
||||
await page.goto("/clients");
|
||||
await page.goto("/admin/clients");
|
||||
await expect(page.getByText("Alice Johnson")).toBeVisible();
|
||||
await page.getByText("Alice Johnson").click();
|
||||
// Email appears in both the list row and the detail panel once selected
|
||||
|
||||
@@ -40,45 +40,51 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
});
|
||||
|
||||
test("appointments page loads", async ({ page }) => {
|
||||
test("customer portal loads at root", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("navigation").getByText("Paws & Reflect")).toBeVisible();
|
||||
await expect(page.locator("nav")).toBeVisible();
|
||||
});
|
||||
|
||||
test("admin appointments page loads", async ({ page }) => {
|
||||
await page.goto("/admin");
|
||||
await expect(page.getByText("Groom Book")).toBeVisible();
|
||||
// Calendar/appointments view renders
|
||||
await expect(page.locator("nav")).toBeVisible();
|
||||
});
|
||||
|
||||
test("clients page loads", async ({ page }) => {
|
||||
await page.goto("/clients");
|
||||
test("admin clients page loads", async ({ page }) => {
|
||||
await page.goto("/admin/clients");
|
||||
await expect(page.getByText("Groom Book")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Clients" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("services page loads", async ({ page }) => {
|
||||
await page.goto("/services");
|
||||
test("admin services page loads", async ({ page }) => {
|
||||
await page.goto("/admin/services");
|
||||
await expect(page.getByText("Groom Book")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Services" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("staff page loads", async ({ page }) => {
|
||||
await page.goto("/staff");
|
||||
test("admin staff page loads", async ({ page }) => {
|
||||
await page.goto("/admin/staff");
|
||||
await expect(page.getByText("Groom Book")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Staff" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("invoices page loads", async ({ page }) => {
|
||||
await page.goto("/invoices");
|
||||
test("admin invoices page loads", async ({ page }) => {
|
||||
await page.goto("/admin/invoices");
|
||||
await expect(page.getByText("Groom Book")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Invoices" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("reports page loads", async ({ page }) => {
|
||||
await page.goto("/reports");
|
||||
test("admin reports page loads", async ({ page }) => {
|
||||
await page.goto("/admin/reports");
|
||||
await expect(page.getByText("Groom Book")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Reports" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("booking portal loads", async ({ page }) => {
|
||||
await page.goto("/book");
|
||||
test("admin booking portal loads", async ({ page }) => {
|
||||
await page.goto("/admin/book");
|
||||
await expect(page.getByText("Book an Appointment")).toBeVisible();
|
||||
await expect(page.getByText("Choose a service")).toBeVisible();
|
||||
});
|
||||
|
||||
+19
-13
@@ -10,14 +10,14 @@ import { GroupBookingPage } from "./pages/GroupBooking.js";
|
||||
import { CustomerPortal } from "./portal/CustomerPortal.js";
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ to: "/", label: "Appointments" },
|
||||
{ to: "/clients", label: "Clients" },
|
||||
{ to: "/services", label: "Services" },
|
||||
{ to: "/staff", label: "Staff" },
|
||||
{ to: "/invoices", label: "Invoices" },
|
||||
{ to: "/group-bookings", label: "Group Bookings" },
|
||||
{ to: "/reports", label: "Reports" },
|
||||
{ to: "/portal", label: "Customer Portal" },
|
||||
{ to: "/admin", label: "Appointments" },
|
||||
{ to: "/admin/clients", label: "Clients" },
|
||||
{ to: "/admin/services", label: "Services" },
|
||||
{ to: "/admin/staff", label: "Staff" },
|
||||
{ to: "/admin/invoices", label: "Invoices" },
|
||||
{ to: "/admin/group-bookings", label: "Group Bookings" },
|
||||
{ to: "/admin/reports", label: "Reports" },
|
||||
{ to: "/", label: "Customer Portal" },
|
||||
];
|
||||
|
||||
function AdminLayout() {
|
||||
@@ -36,7 +36,7 @@ function AdminLayout() {
|
||||
>
|
||||
<strong style={{ marginRight: "1rem", fontSize: 16 }}>Groom Book</strong>
|
||||
<Link
|
||||
to="/book"
|
||||
to="/admin/book"
|
||||
style={{
|
||||
padding: "0.35rem 0.75rem",
|
||||
borderRadius: 4,
|
||||
@@ -52,7 +52,9 @@ function AdminLayout() {
|
||||
</Link>
|
||||
{NAV_LINKS.map(({ to, label }) => {
|
||||
const active =
|
||||
to === "/" ? location.pathname === "/" : location.pathname.startsWith(to);
|
||||
to === "/admin"
|
||||
? location.pathname === "/admin"
|
||||
: location.pathname.startsWith(to);
|
||||
return (
|
||||
<Link
|
||||
key={to}
|
||||
@@ -91,9 +93,13 @@ function AdminLayout() {
|
||||
export function App() {
|
||||
const location = useLocation();
|
||||
|
||||
if (location.pathname.startsWith("/portal")) {
|
||||
return <CustomerPortal />;
|
||||
if (location.pathname.startsWith("/admin")) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/admin/*" element={<AdminLayout />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
return <AdminLayout />;
|
||||
return <CustomerPortal />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, within, act } from "@testing-library/react";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { App } from "../App.js";
|
||||
|
||||
@@ -11,30 +11,28 @@ beforeEach(() => {
|
||||
} as unknown as Response);
|
||||
});
|
||||
|
||||
async function renderApp(route = "/") {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={[route]}>
|
||||
<App />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
function renderApp(route = "/admin") {
|
||||
render(
|
||||
<MemoryRouter initialEntries={[route]}>
|
||||
<App />
|
||||
</MemoryRouter>
|
||||
);
|
||||
return screen.getByRole("navigation");
|
||||
}
|
||||
|
||||
describe("App navigation", () => {
|
||||
it("renders the Groom Book brand", async () => {
|
||||
const nav = await renderApp();
|
||||
it("renders the Groom Book brand", () => {
|
||||
const nav = renderApp();
|
||||
expect(within(nav).getByText("Groom Book")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the Book CTA button", async () => {
|
||||
const nav = await renderApp();
|
||||
it("renders the Book CTA button", () => {
|
||||
const nav = renderApp();
|
||||
expect(within(nav).getByText("Book")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all primary nav links", async () => {
|
||||
const nav = await renderApp();
|
||||
it("renders all primary nav links", () => {
|
||||
const nav = renderApp();
|
||||
const expectedLinks = [
|
||||
"Appointments",
|
||||
"Clients",
|
||||
@@ -49,10 +47,20 @@ describe("App navigation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("highlights the active route link", async () => {
|
||||
const nav = await renderApp("/clients");
|
||||
it("highlights the active route link", () => {
|
||||
const nav = renderApp("/admin/clients");
|
||||
const clientsLink = within(nav).getByText("Clients");
|
||||
// Active links use fontWeight 600
|
||||
expect(clientsLink).toHaveStyle({ fontWeight: "600" });
|
||||
});
|
||||
|
||||
it("renders customer portal at root", () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/"]}>
|
||||
<App />
|
||||
</MemoryRouter>
|
||||
);
|
||||
// Customer portal should render at root - no admin nav present
|
||||
expect(screen.queryByText("Groom Book")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user