Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86f254e939 | |||
| 8d005942df | |||
| b49978710b |
+10
-1
@@ -86,7 +86,7 @@ export const { signIn, signOut, useSession, changePassword } = authClient;
|
||||
| # | Scenario | Steps | Pass Criteria | Fail Criteria |
|
||||
|---|----------|-------|---------------|---------------|
|
||||
| TC-WEB-SSO-1 | Sign-in page shows SSO button | Navigate to app root URL | Sign-in page displayed with "Sign in with SSO" button visible | No SSO button, 403 before page loads |
|
||||
| TC-WEB-SSO-2 | Click SSO redirects to Authentik | Click "Sign in with SSO" button | Browser redirected to Authentik login at auth.farh.net | No redirect, error shown, button does nothing |
|
||||
| TC-WEB-SSO-2 | Click SSO redirects to Authentik (GRO-2572) | **Fresh session only (no pre-existing auth cookie).** Click "Sign in with SSO" button | Browser navigates to Authentik login at auth.farh.net within ~1 s — address bar changes to auth.farh.net URL | No redirect, error shown, button stays disabled, user remains on /login. Regression: prior to GRO-2572 fix the client never followed the `data.url` returned by Better Auth. Run from a clean incognito context to avoid a stale cookie masking the defect. |
|
||||
| TC-WEB-SSO-3 | Valid OIDC credentials authenticate | At Authentik, enter valid credentials and authenticate | Redirected back to app with active session | Redirect loop, 403, session not established |
|
||||
| TC-WEB-SSO-4 | Post-login dashboard accessible | After SSO flow completes, dashboard loads | Dashboard displays correctly with user identity shown | Blank page, 403, session not active |
|
||||
| TC-WEB-SSO-5 | User identity displayed correctly | After SSO login, check header/nav | User name/email/initials shown in nav, role reflected in UI | No user indicator, wrong user shown |
|
||||
@@ -320,6 +320,15 @@ the seeded UAT customer (`uat-customer@groombook.dev`), not just unit-rendered.
|
||||
| TC-WEB-5.16.2 | PWA install prompt | Load app on supported browser | Install prompt appears when criteria met |
|
||||
| TC-WEB-5.16.3 | Touch interactions | Use touch gestures on mobile | All interactions work with touch input |
|
||||
|
||||
#### 5.16a Portal Tab Rows — Mobile Overflow (GRO-730 / GRO-1026)
|
||||
|
||||
| # | Scenario | Steps | Expected |
|
||||
|---|----------|-------|----------|
|
||||
| TC-WEB-5.16.4 | My Pets tab row — horizontal scroll, no visible scrollbar | Sign in as customer → My Pets. Set viewport to 390px. If 3+ pets are seeded, the pet-selector row overflows. | Pet selector row scrolls horizontally; native scrollbar is **not** visible (`scrollbar-width: none` / `scrollbar-hide` applied). |
|
||||
| TC-WEB-5.16.5 | My Pets section tab row — no visible scrollbar | On the same My Pets view, observe the tabs row (Basic Info / Medical / Grooming / History). | Tabs row scrolls horizontally when needed; native scrollbar is not visible. |
|
||||
| TC-WEB-5.16.6 | Billing/Payments tab row — no wrap, no visible scrollbar | Sign in as customer → Billing/Payments at 390px. | Tab row (Invoices / Payment Methods / Packages) does **not** wrap to a second line; scrolls horizontally if needed; native scrollbar not visible. |
|
||||
| TC-WEB-5.16.7 | Desktop — no visual regression | Open My Pets and Billing/Payments at ≥1024px. | No layout change; tab rows display identically to before the fix. |
|
||||
|
||||
### 5.17 Error & Empty States
|
||||
|
||||
| # | Scenario | Steps | Expected |
|
||||
|
||||
@@ -45,6 +45,12 @@ function LoginPage() {
|
||||
if (result?.error) {
|
||||
setError(result.error.message ?? "Sign-in failed");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
// Better Auth returns the IdP authorize URL in data.url with redirect:true rather than
|
||||
// issuing an HTTP 30x — the client must follow it (GRO-2572).
|
||||
if (result?.data?.url) {
|
||||
window.location.href = result.data.url;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, within, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { App } from "../App";
|
||||
|
||||
@@ -232,6 +233,7 @@ describe("Dev login selector", () => {
|
||||
});
|
||||
|
||||
it("does not redirect when a dev user is already selected", async () => {
|
||||
|
||||
localStorage.setItem("dev-user", JSON.stringify({ type: "staff", id: "s1", name: "Sarah" }));
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
@@ -269,3 +271,61 @@ describe("Dev login selector", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GRO-2572 — SSO button follows redirect URL", () => {
|
||||
it("navigates to data.url when signIn.social returns a redirect", async () => {
|
||||
// Mock signIn.social to return the redirect payload Better Auth sends
|
||||
vi.mock("../lib/auth-client.js", () => ({
|
||||
useSession: () => ({ data: null, isPending: false }),
|
||||
signIn: {
|
||||
social: vi.fn().mockResolvedValue({
|
||||
data: { redirect: true, url: "https://auth.farh.net/application/o/authorize/?test=1" },
|
||||
error: null,
|
||||
}),
|
||||
},
|
||||
signOut: vi.fn(),
|
||||
changePassword: vi.fn(),
|
||||
}));
|
||||
|
||||
const assignMock = vi.fn();
|
||||
Object.defineProperty(window, "location", {
|
||||
value: { ...window.location, href: "", origin: "https://uat.groombook.dev" },
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(window.location, "href", {
|
||||
set: assignMock,
|
||||
get: () => "",
|
||||
});
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url === "/api/dev/config") {
|
||||
return Promise.resolve({ ok: true, json: async () => ({ authDisabled: false }) } as Response);
|
||||
}
|
||||
if (url === "/api/auth/get-session") {
|
||||
return Promise.resolve({ ok: true, json: async () => null } as unknown as Response);
|
||||
}
|
||||
if (url === "/api/setup/status") {
|
||||
return Promise.resolve({ ok: true, json: async () => ({ needsSetup: false }) } as Response);
|
||||
}
|
||||
if (url === "/api/auth/providers") {
|
||||
return Promise.resolve({ ok: true, json: async () => ({ providers: ["authentik"] }) } as Response);
|
||||
}
|
||||
return Promise.resolve({ ok: true, json: async () => [] } as Response);
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/login"]}>
|
||||
<App />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const ssoButton = await screen.findByRole("button", { name: /sign in with sso/i });
|
||||
await userEvent.click(ssoButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(assignMock).toHaveBeenCalledWith(
|
||||
"https://auth.farh.net/application/o/authorize/?test=1"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +78,15 @@ input:focus, select:focus, textarea:focus {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* ─── Scrollbar hide utility ─── */
|
||||
.scrollbar-hide {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ─── Scrollbar polish ─── */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@@ -130,7 +130,7 @@ function BillingPaymentsInner({ sessionId, readOnly }: BillingPaymentsProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
|
||||
{([
|
||||
{ id: "invoices" as const, label: "Invoices", icon: DollarSign },
|
||||
{ id: "payment" as const, label: "Payment Methods", icon: CreditCard },
|
||||
|
||||
@@ -145,7 +145,7 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Pet Selector */}
|
||||
<div className="flex gap-3 overflow-x-auto pb-1">
|
||||
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{pets.map(p => (
|
||||
<button
|
||||
key={p.id}
|
||||
@@ -191,7 +191,7 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 bg-white rounded-xl border border-stone-200 p-1 overflow-x-auto">
|
||||
<div className="flex gap-1 bg-white rounded-xl border border-stone-200 p-1 overflow-x-auto scrollbar-hide">
|
||||
{([
|
||||
{ id: "info", label: "Basic Info", icon: PawPrint },
|
||||
{ id: "medical", label: "Medical", icon: Heart },
|
||||
|
||||
Reference in New Issue
Block a user