v3: src/components sorted into feature folders, the four largest files split, and the component tests mirror the new layout — 48 components sat in one flat folder, ChatModal ran to 1301 lines, DumpCreateModal to 983, and the tests were grouped by theme, so there was no telling which component had none. Components now live in category/, chat/, comment/, dump/ (with dump/create/ for the new-dump wizard), editor/, form/, layout/ (header, page shell, floating buttons), player/, playlist/, profile/, social/ (vote, like, follow, the who-voted popover) and ui/; FeedTabBar and PresenceRow, used by the index page alone, move to pages/index/. ChatModal gives up ChatMessageItem, PendingItem, ChatEditBox and ReplyRef, the message limits and text helpers (chat/message.ts), the day and clock labels (chat/time.ts) and the draft storage (utils/chatDraft.ts, next to dumpDraft.ts). DumpCreateModal gives up StepIndicator, LocalFilePreview, TitleField and DuplicateNotice; CommentThread gives up CommentForm and CommentNode; TextEditor gives up the emoji picker popup and its skin-tone storage; MediaPlayer gives up its icons (which FilePreview imported from it), Waveform, SeekBar and the remembered volume. NewPlaylistForm, a button that opens a modal holding the form, is NewPlaylistButton now, and Thumbnail, ThumbnailPlaceholder, FilePreview and RichContentCard use named exports like everything else. The tests are split one file per component under src/test/components/<folder>/ (and src/test/pages/index/), the same 625 of them. No behaviour changes; the catalogues only move their source references.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
125
src/test/components/layout/AppHeader.test.tsx
Normal file
125
src/test/components/layout/AppHeader.test.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { BrowserRouter, Route, Routes, useLocation } from "react-router";
|
||||
import { useState } from "react";
|
||||
import { AppHeader } from "../../../components/layout/AppHeader.tsx";
|
||||
import { AuthContext } from "../../../contexts/AuthContext.ts";
|
||||
import { defaultWSValue, WSContext } from "../../../contexts/WSContext.ts";
|
||||
import type { AuthResponse } from "../../../model.ts";
|
||||
import { makeSession } from "../../fixtures.ts";
|
||||
import { currentPath, renderWithContexts } from "../../render.tsx";
|
||||
|
||||
describe("AppHeader", () => {
|
||||
test("guests get a login button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithContexts(<AppHeader />);
|
||||
await user.click(screen.getByRole("button", { name: "Log in" }));
|
||||
expect(currentPath()).toBe("/login");
|
||||
});
|
||||
|
||||
test("members get their links, chat, bell and the new-dump button", () => {
|
||||
renderWithContexts(<AppHeader disableNew />, {
|
||||
session: makeSession({ username: "ann" }),
|
||||
});
|
||||
expect(screen.getByRole("link", { name: "ann" })).toHaveAttribute(
|
||||
"href",
|
||||
"/users/ann",
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Chat" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Notifications" }))
|
||||
.toBeInTheDocument();
|
||||
const dump = screen.getByRole("button", { name: /Dump/ });
|
||||
expect(dump).toBeDisabled();
|
||||
expect(dump).toHaveAttribute("title", "Server unreachable");
|
||||
});
|
||||
|
||||
test("shows the live-updates banner when disconnected", () => {
|
||||
renderWithContexts(<AppHeader />, {
|
||||
ws: { wsStatus: "disconnected", wsErrorMessage: "Trying to reconnect…" },
|
||||
});
|
||||
expect(screen.getByRole("alert")).toHaveTextContent(
|
||||
"Live updates unavailable. Trying to reconnect…",
|
||||
);
|
||||
});
|
||||
|
||||
test("the search toggle opens and closes, giving focus back", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithContexts(<AppHeader />);
|
||||
const toggle = screen.getByRole("button", { name: "Search" });
|
||||
await user.click(toggle);
|
||||
expect(screen.getByRole("button", { name: "Cancel search" }))
|
||||
.toHaveAttribute(
|
||||
"aria-expanded",
|
||||
"true",
|
||||
);
|
||||
const [input] = screen.getAllByRole("searchbox");
|
||||
await user.type(input, "x");
|
||||
await user.keyboard("{Escape}");
|
||||
expect(screen.getByRole("button", { name: "Search" })).toHaveFocus();
|
||||
expect(screen.getAllByRole("searchbox")[0]).toHaveValue("");
|
||||
});
|
||||
|
||||
// Leaving search needs the real history stack (React Router keeps its
|
||||
// index in history.state), so these use a BrowserRouter.
|
||||
function Browser({ session }: { session?: AuthResponse }) {
|
||||
const [authResponse, setAuthResponse] = useState(session ?? null);
|
||||
function Loc() {
|
||||
const l = useLocation();
|
||||
return <output data-testid="loc">{l.pathname + l.search}</output>;
|
||||
}
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<AuthContext.Provider value={{ authResponse, setAuthResponse }}>
|
||||
<WSContext.Provider value={defaultWSValue}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<AppHeader />} />
|
||||
</Routes>
|
||||
<Loc />
|
||||
</BrowserRouter>
|
||||
</WSContext.Provider>
|
||||
</AuthContext.Provider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
const loc = () => screen.getByTestId("loc").textContent;
|
||||
afterEach(() => window.history.replaceState(null, "", "/"));
|
||||
|
||||
test("several searches, then ✕, return to where search began", async () => {
|
||||
window.history.replaceState(null, "", "/start");
|
||||
const user = userEvent.setup();
|
||||
render(<Browser />);
|
||||
await user.click(screen.getByRole("button", { name: "Search" }));
|
||||
await user.type(screen.getAllByRole("searchbox")[0], "cats{Enter}");
|
||||
expect(loc()).toBe("/search?q=cats");
|
||||
const bar = () => screen.getAllByRole("searchbox")[0];
|
||||
expect(bar()).toHaveValue("cats");
|
||||
await user.clear(bar());
|
||||
await user.type(bar(), "dogs{Enter}");
|
||||
expect(loc()).toBe("/search?q=dogs");
|
||||
await user.click(screen.getByRole("button", { name: "Cancel search" }));
|
||||
await waitFor(() => expect(loc()).toBe("/start"));
|
||||
});
|
||||
|
||||
test("Escape on the results page reverts the query instead of leaving", async () => {
|
||||
window.history.replaceState(null, "", "/search?q=cats");
|
||||
const user = userEvent.setup();
|
||||
render(<Browser />);
|
||||
const bar = screen.getAllByRole("searchbox")[0];
|
||||
expect(bar).toHaveValue("cats");
|
||||
await user.type(bar, "zzz{Escape}");
|
||||
expect(bar).toHaveValue("cats");
|
||||
expect(loc()).toBe("/search?q=cats");
|
||||
});
|
||||
|
||||
test("✕ on results that opened the session goes home", async () => {
|
||||
window.history.replaceState(null, "", "/search?q=cats");
|
||||
const user = userEvent.setup();
|
||||
render(<Browser />);
|
||||
await user.click(screen.getByRole("button", { name: "Cancel search" }));
|
||||
expect(loc()).toBe("/");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user