import { afterEach, describe, expect, test, vi } from "vitest"; import { screen, waitFor, within } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { Settings } from "../../pages/Settings.tsx"; import { DEFAULT_SETTINGS } from "../../../shared/settings.ts"; import { calls, fail, route } from "../fetch.ts"; import { makeSession, makeToken } from "../fixtures.ts"; import { renderApp } from "../render.tsx"; import { AUTH_STORAGE_KEY } from "../../utils/auth/storage.ts"; const instance = vi.hoisted(() => ({ emailEnabled: false })); vi.mock("../../config/instance.ts", () => ({ DEFAULT_STYLE: "smooth", get EMAIL_ENABLED() { return instance.emailEnabled; }, })); const annSession = makeSession({ id: "ann", username: "ann", email: "ann@x.test", }); function open(session = annSession) { route("GET /api/me/grants", () => [], { fallback: true }); const user = userEvent.setup(); renderApp(, { route: "/settings", path: "/settings", session }); return user; } const row = (label: string) => screen.getByText(label).closest(".profile-appearance-row") as HTMLElement; describe("Settings", () => { afterEach(() => { instance.emailEnabled = false; document.documentElement.removeAttribute("data-style"); document.documentElement.removeAttribute("data-color-scheme"); }); test("theme, colour scheme, default tab and sounds; no digests without SMTP", async () => { const user = open(); expect(await screen.findByRole("heading", { name: "Settings" })) .toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Brutalist" })); expect(document.documentElement.getAttribute("data-style")).toBe( "brutalist", ); await user.click(screen.getByRole("button", { name: "Dark" })); expect(document.documentElement.getAttribute("data-color-scheme")).toBe( "dark", ); await user.click( within(row("Default tab")).getByRole("button", { name: "Followed" }), ); expect(localStorage.getItem("default-feed-tab")).toBe("follow"); await user.click( within(row("Sounds")).getByRole("button", { name: "Off" }), ); expect(localStorage.getItem("sounds")).toBe("off"); expect(within(row("Weekly digest")).getByRole("button", { name: "On" })) .toBeDisabled(); expect(screen.getByText(/doesn't send email/)).toBeInTheDocument(); }); test("the digest switches save to the account", async () => { instance.emailEnabled = true; route("GET /api/me/settings", () => ({ ...DEFAULT_SETTINGS, digestMonthly: false, })); route( "PATCH /api/me/settings", (req) => ({ ...DEFAULT_SETTINGS, ...(req.body as object) }), ); const user = open(); const weeklyOff = await waitFor(() => { const button = within(row("Weekly digest")).getByRole("button", { name: "Off", }); expect(button).toBeEnabled(); return button; }); expect( within(row("Monthly recap")).getByRole("button", { name: "Off" }), ).toHaveClass("active"); expect(screen.queryByText(/doesn't send email/)).toBeNull(); await user.click(weeklyOff); await waitFor(() => expect(calls("PATCH /api/me/settings")[0]?.body).toEqual({ digestWeekly: false, }) ); expect(weeklyOff).toHaveClass("active"); }); test("the username is shown, and can't be changed here", async () => { open(); await screen.findByText("Username"); expect(row("Username")).toHaveTextContent(annSession.user.username); expect(screen.queryByRole("textbox", { name: "Username" })).toBeNull(); expect(screen.queryByRole("button", { name: "Rename" })).toBeNull(); }); test("change password keeps you signed in with the fresh tokens", async () => { const freshToken = makeToken({ sub: "ann" }); route( "PUT /api/me/password", () => ({ token: freshToken, mediaToken: "fresh-media" }), ); const user = open(); await user.click( await screen.findByRole("button", { name: "Change password…" }), ); const dialog = screen.getByRole("dialog", { name: "Change password" }); await user.type( within(dialog).getByLabelText("Current password"), "oldpassword", ); await user.type( within(dialog).getByLabelText("New password"), "newpassword", ); await user.type( within(dialog).getByLabelText("Confirm new password"), "different", ); expect(await within(dialog).findByText("Passwords do not match")) .toBeInTheDocument(); await user.clear(within(dialog).getByLabelText("Confirm new password")); await user.type( within(dialog).getByLabelText("Confirm new password"), "newpassword", ); await user.click( within(dialog).getByRole("button", { name: "Change password" }), ); expect(await within(dialog).findByText("Password changed successfully.")) .toBeInTheDocument(); expect(JSON.parse(localStorage.getItem(AUTH_STORAGE_KEY)!).token).toBe( freshToken, ); expect(calls("PUT /api/me/password")[0].body).toEqual({ currentPassword: "oldpassword", newPassword: "newpassword", }); }); test("a wrong current password is reported", async () => { route( "PUT /api/me/password", () => fail("Current password is incorrect", 400), ); const user = open(); await user.click( await screen.findByRole("button", { name: "Change password…" }), ); const dialog = screen.getByRole("dialog", { name: "Change password" }); await user.type(within(dialog).getByLabelText("Current password"), "wrong"); await user.type( within(dialog).getByLabelText("New password"), "newpassword", ); await user.type( within(dialog).getByLabelText("Confirm new password"), "newpassword{Enter}", ); expect(await within(dialog).findByRole("alert")).toHaveTextContent( "Current password is incorrect", ); }); test("lists the instances the account signs in to, and disconnects one", async () => { const now = new Date().toISOString(); let grants = [{ clientId: "https://music.example", host: "music.example", name: "Music", grantedAt: now, lastUsedAt: now, }]; route("GET /api/me/grants", () => grants); route("DELETE /api/me/grants/:host", () => { grants = []; return null; }); const user = open(); expect(await screen.findByText("Music")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Disconnect" })); expect( await screen.findByText(/You haven't signed in to another gerbeur/), ).toBeInTheDocument(); expect(calls("DELETE /api/me/grants/:host")[0].path).toBe( "/api/me/grants/music.example", ); }); test("an account from another instance changes its password there", async () => { open(makeSession({ id: "al", username: "alice@home.example" })); const link = await screen.findByRole("link", { name: "Change it on home.example", }); expect(link).toHaveAttribute( "href", `${location.protocol}//home.example/settings`, ); expect(screen.queryByText("Other instances")).not.toBeInTheDocument(); expect(calls("GET /api/me/grants")).toHaveLength(0); }); });