Files
siftlode/frontend/src/components/Messages.keyboard.dom.test.tsx
T
peter 8731508645 feat(keys): the conversation list walks with j/k; the cheat sheet names keys in the reader's language
E11 S5 — the HU/EN sweep and the keyboard-only pass over every module.

- The keyboard cheat sheet printed "Space" and "Menu" on a Hungarian sheet: a keycap that carries a
  WORD (not a legend every layout prints alike, like Esc, Ctrl or Enter) is now named by the caller in
  the reader's language (`formatChord(chord, words)`, `keys.cap.*`: Szóköz / Menü).
- The Plex player's ⌫/Esc line said "Stop & back to feed", but the player goes back to wherever it
  was opened from (Home, a Plex page): "Stop & go back" / "Leállítás és vissza".
- Messages: the conversation list is a keyboard list (NavGrid + navItemAttrs, the name button is the
  open element; the pop-out button keeps its own Enter). D1 covers every list whose rows open one
  thing; this one was missed in S4. Notification cards have no single open action and stay out, like
  the download queue.

Sweep, measured: every static `t("…")` key used in the source exists in both languages (the only
misses are template prefixes); the HU values equal to EN are proper nouns and loanwords (Admin, Plex,
Trakt, Shorts). Palette in Hungarian: "téma" makes "Téma: Midnight" the active row ahead of the channel
"Topik - mindig témában", live region "7 találat".

Keyboard-only pass in Chrome, Hungarian UI — 1024×768: every `g` chord lands on its module, `j` rings
the first item (solid 2px accent) on Home, Feed, Channels, Playlists, Plex and Downloads, `s` focuses
the search on Feed, Channels, Playlists and Plex, no page scrolls sideways. 3440×1440: `j` → → ↓
walks Home, Feed (8 columns: 0 → 1 → 9), Channels, Playlists, Plex and Downloads, no sideways scroll.
The help sheet lists every line translated; inside the Plex player "Szóköz / K" and "⌫ / Esc
Leállítás és vissza". Messages: a real `j` rings the row, Enter opens the thread.
2026-09-13 07:00:51 +02:00

68 lines
2.4 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: () => ({ 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();
});
});