Every locale formatter takes the language as an argument. Under the React
Compiler a formatter that read the i18n singleton was memoized on its other
arguments, so a card kept "2 hr 4 min" after a switch to Hungarian; plexLang()
had the same flaw and left Plex queries in the old language. Components now
take the language from useTranslation, and the signature makes forgetting it
a type error.
- ratings, file sizes, stream stats: the language's decimal mark ("8,3")
- view and subscriber counts compact in the language ("1,2 E")
- seconds as the language's unit ("6 mp") in the sliders and the stats
- a person's birth and death dates as dates, not ISO strings
- A-Z lists by the shown name in the language's alphabet (compareText),
the tag catalog re-sorted by its translated names
- lint local/locale-format (was date-format) also refuses Intl.NumberFormat
outside lib/format.ts, localeCompare, and a component reading the language
off the i18n singleton
71 lines
2.5 KiB
TypeScript
71 lines
2.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The conversation list as a keyboard list (E11 S5): `j`/`k` walk the conversations and Enter opens
|
|
// the one the row's name button opens — not the pop-out button beside it. The live query, the key
|
|
// state and the thread are stubbed: the list itself is what is under test.
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { act, cleanup, render, screen } from "@testing-library/react";
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return {
|
|
...actual,
|
|
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
|
|
};
|
|
});
|
|
|
|
const conversation = (id: number, name: string) => ({
|
|
partner: { id, name, avatar_url: null },
|
|
last_message: { kind: "system", body: `hello from ${name}`, created_at: "2026-09-13T00:00:00Z" },
|
|
unread: 0,
|
|
});
|
|
const items = [conversation(0, "Siftlode"), conversation(7, "Anna"), conversation(9, "Bence")];
|
|
|
|
vi.mock("../lib/useLiveQuery", () => ({
|
|
useLiveQuery: () => ({ data: { items }, isError: false, isLoading: false, refetch: () => {} }),
|
|
}));
|
|
vi.mock("../lib/messaging", () => ({
|
|
SYSTEM_ID: 0,
|
|
POLL_MS: 20000,
|
|
partnerPub: async () => "",
|
|
useKeyState: () => "ready",
|
|
}));
|
|
const openChat = vi.fn();
|
|
vi.mock("../lib/chatDock", () => ({ openChat: (...a: unknown[]) => openChat(...a) }));
|
|
vi.mock("./ChatThread", () => ({
|
|
default: ({ partnerId }: { partnerId: number }) => <div data-testid="thread">{partnerId}</div>,
|
|
}));
|
|
|
|
import Messages from "./Messages";
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
history.replaceState(null, "");
|
|
});
|
|
|
|
function press(key: string) {
|
|
act(() => {
|
|
(document.activeElement ?? document.body).dispatchEvent(
|
|
new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }),
|
|
);
|
|
});
|
|
}
|
|
|
|
describe("Messages · the conversation list is a keyboard list", () => {
|
|
it("walks the conversations with j/k and opens the focused one on Enter", () => {
|
|
render(<Messages meId={1} />);
|
|
const focusedRow = () => document.activeElement?.getAttribute("data-nav-index");
|
|
press("j");
|
|
press("j");
|
|
press("j");
|
|
expect(focusedRow()).toBe("2");
|
|
expect(document.activeElement?.textContent).toContain("Bence");
|
|
press("k");
|
|
expect(focusedRow()).toBe("1");
|
|
expect(document.activeElement?.textContent).toContain("Anna");
|
|
press("Enter");
|
|
expect(screen.getByTestId("thread").textContent).toBe("7");
|
|
expect(openChat).not.toHaveBeenCalled();
|
|
});
|
|
});
|