Each section of the user settings page (Profile, Account, Notifications, API Access, Data & Privacy) is now a real, linkable path (/settings/profile, /settings/account, etc.) instead of local toggle state, so sections can be bookmarked, shared, opened in a new tab, and reached via the browser back/forward buttons. - Route now matches an optional segment (settings/:section?), keeping the single route so the shared loader/action and form submissions are unchanged. An unknown section redirects back to /settings. - The shared settings nav components render proper <Link>s when given a buildHref/backHref, and keep their button/onClick behaviour for the league settings page (which has unsaved-changes guards). - The settings page derives the active section and mobile grid/section view from the URL param instead of useState. - Notifications "Account settings" link and the Discord OAuth callback now point at /settings/account. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jt8Hhsio4bHGMaDZy7ftBs
56 lines
2.1 KiB
TypeScript
56 lines
2.1 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
|
|
vi.mock("~/lib/auth.server", () => ({
|
|
auth: { api: { getSession: vi.fn() } },
|
|
}));
|
|
vi.mock("~/lib/cloudinary.server", () => ({ deleteCloudinaryImageByUrl: vi.fn() }));
|
|
vi.mock("~/lib/email.server", () => ({
|
|
sendEmail: vi.fn(),
|
|
wrapInEmailTemplate: vi.fn(),
|
|
emailParagraph: vi.fn(),
|
|
escapeHtml: vi.fn(),
|
|
}));
|
|
vi.mock("~/models/user", () => ({
|
|
findUserById: vi.fn(),
|
|
findUserByUsername: vi.fn(),
|
|
isUserInActiveDraft: vi.fn(),
|
|
updateUser: vi.fn(),
|
|
anonymizeUserAccount: vi.fn(),
|
|
USERNAME_RE: /^[a-zA-Z0-9_-]{3,30}$/,
|
|
}));
|
|
vi.mock("~/models/account", () => ({ findLinkedAccountsByUserId: vi.fn() }));
|
|
vi.mock("~/models/team", () => ({ findTeamsByOwnerId: vi.fn(), removeTeamOwner: vi.fn() }));
|
|
vi.mock("~/models/commissioner", () => ({ removeAllCommissionersByUserId: vi.fn() }));
|
|
|
|
import { loader } from "../settings";
|
|
import { auth } from "~/lib/auth.server";
|
|
|
|
type LoaderArgs = Parameters<typeof loader>[0];
|
|
|
|
function loaderArgs(section?: string): LoaderArgs {
|
|
const path = section ? `/settings/${section}` : "/settings";
|
|
return {
|
|
params: { section },
|
|
request: new Request(`http://localhost${path}`),
|
|
context: {},
|
|
} as unknown as LoaderArgs;
|
|
}
|
|
|
|
describe("/settings loader section handling", () => {
|
|
it("redirects an unknown section back to /settings", async () => {
|
|
const result = (await loader(loaderArgs("bogus"))) as Response;
|
|
expect(result).toBeInstanceOf(Response);
|
|
expect(result.status).toBe(302);
|
|
expect(result.headers.get("Location")).toBe("/settings");
|
|
// The bad section short-circuits before we ever touch the session.
|
|
expect(auth.api.getSession).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not redirect a valid section before auth runs", async () => {
|
|
vi.mocked(auth.api.getSession).mockResolvedValue(null as never);
|
|
const result = (await loader(loaderArgs("account"))) as Response;
|
|
// Falls through to the auth check, which redirects to login (not /settings).
|
|
expect(result.headers.get("Location")).toBe("/login?redirectTo=/settings");
|
|
expect(auth.api.getSession).toHaveBeenCalled();
|
|
});
|
|
});
|