import { beforeEach, describe, expect, it } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"; import Dashboard from "../routes/Dashboard.svelte"; import { clearMocks, jsonResponse, mockData, mockEndpoint, setupAuthenticatedUser, setupFetchMock, setupUnauthenticatedUser, } from "./mocks.ts"; import { unsafeAsISODate } from "../lib/types/branded"; import { navigate, routes } from "../lib/router.svelte"; const STORAGE_KEY = "tranquil_pds_session"; describe("Dashboard", () => { beforeEach(() => { clearMocks(); setupFetchMock(); navigate(routes.dashboard); }); describe("authentication guard", () => { it("redirects to login when not authenticated", async () => { setupUnauthenticatedUser(); render(Dashboard); await waitFor(() => { expect(globalThis.location.pathname).toBe("/app/login"); }); }); it("shows loading state while checking auth", () => { const { container } = render(Dashboard); expect(container.querySelector(".skeleton-header")).toBeInTheDocument(); expect(container.querySelectorAll(".skeleton-nav-item").length) .toBeGreaterThan(0); }); }); describe("authenticated view", () => { beforeEach(() => { setupAuthenticatedUser(); }); it("displays user account info and sidebar structure", async () => { render(Dashboard); await waitFor(() => { expect(screen.getAllByText(/@testuser\.test\.tranquil\.dev/).length) .toBeGreaterThan(0); expect(screen.getByText(/did:web:test\.tranquil\.dev:u:testuser/)) .toBeInTheDocument(); expect(screen.queryByText("Verified")).not.toBeInTheDocument(); }); }); it("displays unverified badge when email not confirmed", async () => { setupAuthenticatedUser({ emailConfirmed: false }); render(Dashboard); await waitFor(() => { expect(screen.getByText("Unverified")).toBeInTheDocument(); expect(screen.getByText("Unverified")).toHaveClass("badge", "warning"); }); }); it("displays sidebar navigation items", async () => { render(Dashboard); await waitFor(() => { const navItems = [ /general/i, /security/i, /sessions/i, /app passwords/i, ]; for (const name of navItems) { expect(screen.getByRole("button", { name })).toBeInTheDocument(); } }); }); it("keeps the overview as the dashboard landing page", async () => { render(Dashboard); expect(await screen.findByRole("heading", { name: "Overview" })).toBeInTheDocument(); expect(screen.getByText("Your account, in one place.")).toBeInTheDocument(); expect(globalThis.location.pathname).toBe("/app/dashboard"); expect(screen.getByRole("link", { name: "Export your data" })).toHaveAttribute("href", "/app/data"); }); it("closes mobile navigation after choosing a section and moves focus to its heading", async () => { render(Dashboard); const menu = await screen.findByRole("button", { name: "Menu" }); await fireEvent.click(menu); expect(menu).toHaveAttribute("aria-expanded", "true"); await fireEvent.click(screen.getByRole("button", { name: "General" })); await waitFor(() => { expect(menu).toHaveAttribute("aria-expanded", "false"); expect(screen.getByRole("heading", { name: "General" })).toHaveFocus(); }); }); it("shows migrated account context without local export or repository actions", async () => { setupAuthenticatedUser({ accountKind: "migrated", migratedToPds: "https://new.example", migratedAt: unsafeAsISODate("2026-09-01T00:00:00Z") }); render(Dashboard); expect(await screen.findByText("Account Migrated")).toBeInTheDocument(); expect(screen.queryByRole("link", { name: "Export your data" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Repository Explorer" })).not.toBeInTheDocument(); expect(screen.getByRole("link", { name: "DID Document" })).toBeInTheDocument(); }); it("renders settings without crashing", async () => { navigate(routes.settings); render(Dashboard); await waitFor(() => { expect(screen.getByRole("button", { name: /change email/i })) .toBeInTheDocument(); }); }); it("displays invite codes nav when invites are required and user is admin", async () => { setupAuthenticatedUser({ isAdmin: true }); mockEndpoint( "com.atproto.server.describeServer", () => jsonResponse(mockData.describeServer({ inviteCodeRequired: true })), ); render(Dashboard); await waitFor(() => { expect(screen.getByRole("button", { name: /invite codes/i })) .toBeInTheDocument(); }); }); }); describe("logout functionality", () => { beforeEach(() => { setupAuthenticatedUser(); localStorage.setItem(STORAGE_KEY, JSON.stringify(mockData.session())); mockEndpoint("/oauth/revoke", () => jsonResponse({})); }); it("calls oauth revoke and navigates to login on logout", async () => { let revokeCalled = false; mockEndpoint("/oauth/revoke", () => { revokeCalled = true; return jsonResponse({}); }); render(Dashboard); await waitFor(() => { expect(screen.getByRole("button", { name: /@testuser/i })) .toBeInTheDocument(); }); await fireEvent.click(screen.getByRole("button", { name: /@testuser/i })); await waitFor(() => { expect(screen.getByRole("button", { name: /sign out/i })) .toBeInTheDocument(); }); await fireEvent.click(screen.getByRole("button", { name: /sign out/i })); await waitFor(() => { expect(revokeCalled).toBe(true); expect(globalThis.location.pathname).toBe("/app/login"); }); }); it("clears session from localStorage after logout", async () => { const storedSession = localStorage.getItem(STORAGE_KEY); expect(storedSession).not.toBeNull(); render(Dashboard); await waitFor(() => { expect(screen.getByRole("button", { name: /@testuser/i })) .toBeInTheDocument(); }); await fireEvent.click(screen.getByRole("button", { name: /@testuser/i })); await waitFor(() => { expect(screen.getByRole("button", { name: /sign out/i })) .toBeInTheDocument(); }); await fireEvent.click(screen.getByRole("button", { name: /sign out/i })); await waitFor(() => { expect(localStorage.getItem(STORAGE_KEY)).toBeNull(); }); }); }); });