import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { RestoreAccountCard } from "./restore-account-card"; // The screen is an offer, and the offer is the only thing standing between a diver // and a logbook that gets erased on a date they can read here. So what is worth // pinning is that the date survives to the screen in every shape the API can send it, // that the click actually spends the token it was handed, and that a refusal is shown // rather than swallowed - a failed restore leaves the account exactly as deleted as it // was, and the diver has to be told which failure they hit. const { router, restoreAccount, auth } = vi.hoisted(() => ({ router: { push: vi.fn() }, restoreAccount: vi.fn(), auth: { restore: null as { restoreToken: string; email: string; purgeAfter: string | null; } | null, }, })); vi.mock("next/navigation", () => ({ useRouter: () => router })); vi.mock("@/contexts/AuthContext", () => ({ useAuth: () => ({ restore: auth.restore, restoreAccount }), })); const OFFER = { restoreToken: "res-1", email: "diver@example.com", // Midday UTC on purpose: the date renders in the reader's own timezone, and a // midnight one would land on a different day either side of the world. purgeAfter: "2026-09-04T12:00:00Z", }; beforeEach(() => { vi.clearAllMocks(); auth.restore = OFFER; restoreAccount.mockResolvedValue(undefined); }); const pageText = () => document.body.textContent ?? ""; const restoreButton = () => screen.getByRole("button", { name: /restore my account/i }); describe("the restore offer", () => { it("names the account and the day it stops being recoverable", () => { render(); expect(pageText()).toContain("diver@example.com"); expect(pageText()).toContain("September"); expect(pageText()).toContain("2026"); expect(pageText()).toContain("Your dives, dive drafts, dive sites"); }); // The API leaves `purge_after` out for a row flagged with no clock to count from. // The offer still stands - it just cannot name a day, and must not render one. it("still offers the account back with no date to name", () => { auth.restore = { ...OFFER, purgeAfter: null }; render(); expect(pageText()).not.toContain("Invalid Date"); expect(pageText()).toContain("Your dives, dive drafts, dive sites"); expect(restoreButton()).toBeVisible(); }); it("spends the token it was handed and signs the diver back in", async () => { const user = userEvent.setup(); render(); await user.click(restoreButton()); expect(restoreAccount).toHaveBeenCalledWith("res-1"); await waitFor(() => expect(router.push).toHaveBeenCalledWith("/home")); }); // The one refusal a diver most needs stated plainly: the purge ran while the offer // was on screen. A generic "please try again" would send them looking for another // way in that cannot exist. it("shows the API's own explanation when the account is already gone", async () => { restoreAccount.mockRejectedValue({ response: { data: { detail: "This account has already been permanently deleted and cannot be restored.", }, }, }); const user = userEvent.setup(); render(); await user.click(restoreButton()); expect(await screen.findByRole("alert")).toHaveTextContent( /already been permanently deleted/i, ); expect(router.push).not.toHaveBeenCalled(); }); // Nothing to offer means nothing to render: the page above sends the visitor back // to sign in again, and a card drawing an empty offer in the meantime would ask // them to click a button holding no token. it("renders nothing without an offer", () => { auth.restore = null; const { container } = render(); expect(container).toBeEmptyDOMElement(); }); });