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:
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user