fix(GRO-2373): add Sign out button to in-portal chrome sidebar (#77)
CI / Test (push) Successful in 35s
CI / Lint & Typecheck (push) Successful in 48s
CI / Build & Push Docker Image (push) Successful in 13s

This commit was merged in pull request #77.
This commit is contained in:
2026-06-11 18:24:29 +00:00
parent 7a8b59ab87
commit ddc4e3e052
3 changed files with 89 additions and 1 deletions
+71
View File
@@ -1057,4 +1057,75 @@ describe("OOBE portal-creation flow (GRO-2359)", () => {
Object.defineProperty(window, "location", { value: originalLocation, configurable: true });
});
it("reaches the shared signOut() handler from the in-portal chrome sidebar (GRO-2373)", async () => {
// Pre-GRO-2373, the customer portal chrome (Home, Appointments, My Pets,
// Report Cards, Billing, Messages, Settings) had no visible sign-out
// control — only the OOBE and the no-access card exposed one. This
// leaves users signed-in with no escape hatch. The fix lands a
// "Sign out" button in the sidebar footer that wires to the same
// canonical `signOut()` already used by OOBE / no-access / AdminLayout.
signOutSpy.mockClear();
const originalLocation = window.location;
Object.defineProperty(window, "location", {
value: { href: "" },
writable: true,
configurable: true,
});
global.fetch = vi.fn((input: RequestInfo, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "/api/branding") return Promise.resolve(brandingResponse);
if (url === "/api/auth/get-session") {
return Promise.resolve({
ok: true,
json: async () => ({ user: { email: "uat-customer@groombook.dev", role: "customer" } }),
} as Response);
}
if (url === "/api/portal/session-from-auth" && init?.method === "POST") {
return Promise.resolve({
ok: true,
status: 201,
json: async () => ({ sessionId: "chrome-sess-1", clientId: "client-1", clientName: "Jane Doe" }),
} as Response);
}
return Promise.resolve({ ok: true, json: async () => ({}) } as Response);
}) as unknown as typeof fetch;
const { CustomerPortal } = await import("../portal/CustomerPortal.js");
render(
<MemoryRouter initialEntries={["/"]}>
<CustomerPortal />
</MemoryRouter>
);
// Land on the chrome (proof: customer greeting is rendered, no
// no-access card, no OOBE).
await waitFor(() => {
expect(screen.getByText(/Hi, Jane/)).toBeInTheDocument();
});
expect(screen.queryByText(/Portal access not configured/i)).not.toBeInTheDocument();
expect(screen.queryByText(/set up your portal/i)).not.toBeInTheDocument();
// The new chrome sign-out is scoped by data-testid so it doesn't
// collide with other surfaces that may also render "Sign out" labels
// (e.g. the impersonation banner uses "End Session").
const signOutButton = screen.getByTestId("portal-chrome-signout");
expect(signOutButton).toHaveTextContent(/Sign out/i);
fireEvent.click(signOutButton);
// Same canonical handler as OOBE / no-access / AdminLayout — never
// a raw fetch("/api/auth/sign-out") and never a navigate() without
// signOut() (the OOBE/no-access surface uses window.location.href
// for a hard reload so cached state is reset).
await waitFor(() => {
expect(signOutSpy).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
expect(window.location.href).toBe("/login");
});
Object.defineProperty(window, "location", { value: originalLocation, configurable: true });
});
});