test(fe): cover the palette's content, AltGr, the banner's name, share colours, playlist headers

- AppCommandPalette had no test: modules gated by the account, hidden channels left out, the
  playlist row selecting before it navigates, and the surprise riding setPage's `then`.
- keymap: an Alt/AltGr-modified key (Ctrl+Alt on Windows, a constant on the HU layout) fires no
  bare-key or Mod row.
- Banner: the dismiss button falls back to "Close" rather than going unnamed.
- ShareDialog: a protected link's lit lock and Revoke's danger colour stay on the icon.
- Both playlist headers are rendered now: the YouTube one needed the toolbar slot it portals into.
  Meta line sums known runtimes and treats an ABSENT duration as nothing (null never exercised the
  guard: `3000 + null` is 3000); a one-row list offers no view controls.
This commit is contained in:
2026-09-19 17:23:55 +02:00
parent cedcbb1434
commit dc902d2c0e
6 changed files with 374 additions and 1 deletions
@@ -0,0 +1,73 @@
// @vitest-environment jsdom
//
// The Plex playlist header's meta line — the count and the playlist's total runtime, mirroring the
// YouTube playlist header — was never rendered by a test, so a regression in the sum shipped green
// (E9 review). The runtime sums the WHOLE list, an unknown duration counts as nothing, and a list
// nothing sums for says no runtime at all.
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
import { render, cleanup, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
vi.mock("react-i18next", async (importOriginal) => ({
...(await importOriginal<typeof import("react-i18next")>()),
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
}));
import PlexPlaylistView from "./PlexPlaylistView";
import { api, type PlexCard } from "../lib/api";
beforeAll(() => {
class Stub {
observe() {}
unobserve() {}
disconnect() {}
takeRecords() {
return [];
}
}
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver;
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
// An unknown runtime is an ABSENT field: `5400 + null` is 5400 in JS, so null would prove nothing
// about the sum's guard; `undefined` turns an unguarded sum into NaN.
const movie = (id: string, duration_seconds?: number) =>
({
id,
type: "movie",
title: `Movie ${id}`,
year: 2020,
thumb: "",
duration_seconds,
}) as unknown as PlexCard;
async function meta(items: PlexCard[]) {
vi.spyOn(api, "plexPlaylist").mockResolvedValue({ id: 5, title: "Films", items } as never);
render(
<QueryClientProvider
client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}
>
<PlexPlaylistView playlistId={5} onBack={() => {}} onPlay={() => {}} />
</QueryClientProvider>,
);
await screen.findByText(`Movie ${items[0]!.id}`);
return screen.getByText(
(_, el) => /^plex\.browse\.items/.test(el?.textContent ?? "") && el?.childElementCount === 0,
).textContent;
}
describe("PlexPlaylistView · the header's meta line", () => {
it("sums the known runtimes and counts an unknown one as nothing", async () => {
expect(await meta([movie("1", 5400), movie("2"), movie("3", 1800)])).toBe(
"plex.browse.items · 2h 0m",
);
});
it("drops the runtime when nothing sums", async () => {
expect(await meta([movie("1")])).toBe("plex.browse.items");
});
});