Files
siftlode/frontend/src/components/plexCards.dom.test.tsx
T
peter 5b5e76b73d E5 S3: unified Plex card — scrim-reveal hover + rest polish
The poster card (grid + person page) gets the Seerr-style hover: on hover a
bottom scrim slides up with the title, a "year · runtime · ★rating" meta line,
and a 3-line synopsis — the info you'd otherwise click through for — plus a
less-flat play ring (radial-gradient + ring shadow) and a top-right quick-action
cluster (Info + watched-toggle, revealed on hover / always on touch). Rest-state
badges (season / type tag) yield to the scrim on hover; the playable-cost tint
stays. (§1 A + C from the enrichment study.)

Backend (the data the scrim needs, mirroring S1's `art`): the grid
(browse.unified_library) now emits a lang-coalesced, truncated `summary` (new
query.summary_col) + the Plex `rating`; movie_card/show_card do the same (EN) for
the detail/related/person cards. PlexCard.rating type added (summary already
existed for episodes).

Gate: tsc x3 / eslint / prettier / knip / vitest 326 green; ruff green; backend
lane full suite green. Self-E2E on the live dev server (rebuilt on this branch,
S2-enriched): the grid serves rating+summary (Shawshank 9.3, Godfather 9.2, real
synopses) and the card renders the play ring + scrim (The Expanse: ring, synopsis,
★8.1) — verified via the DOM (the pane wouldn't composite for a screenshot).
2026-08-20 20:30:30 +02:00

103 lines
3.5 KiB
TypeScript

// @vitest-environment jsdom
//
// Plex card a11y: locks the accessible names on the icon-only card controls (the E4 a11y sweep). These
// buttons are icon-only (WatchedIcon / Info / ListPlus, no text), so their name comes solely from the
// aria-label — a dropped label silently reintroduces an unnamed button for screen-reader users. i18n is
// key-passthrough, so the asserted names are the i18n keys.
import { describe, it, expect, afterEach, vi } from "vitest";
import { render, screen, cleanup } 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 }) };
});
import { PlexPosterCard, SeasonCard, EpisodeCard } from "./plexCards";
import type { PlexCard, PlexSeasonDetail } from "../lib/api";
const movie = (status?: string): PlexCard => ({
id: "1",
type: "movie",
title: "M",
thumb: "",
status,
});
const episode = (status?: string): PlexCard => ({
id: "e1",
type: "episode",
title: "E1",
thumb: "",
status,
});
const season = (status = "new"): PlexSeasonDetail => ({
id: "s1",
season_number: 1,
title: "S1",
thumb: "",
episode_count: 3,
status,
episodes: [],
});
afterEach(cleanup);
describe("PlexPosterCard a11y", () => {
it("names the watched-toggle (mark-watched when unwatched) and the info button", () => {
render(
<PlexPosterCard card={movie()} onOpen={vi.fn()} onInfo={vi.fn()} onToggleWatched={vi.fn()} />,
);
expect(screen.getByRole("button", { name: "plex.markWatched" })).toBeTruthy();
expect(screen.getByRole("button", { name: "plex.info.title" })).toBeTruthy();
});
it("names the watched-toggle as mark-unwatched when the movie is watched", () => {
render(
<PlexPosterCard
card={movie("watched")}
onOpen={vi.fn()}
onInfo={vi.fn()}
onToggleWatched={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: "plex.markUnwatched" })).toBeTruthy();
});
it("reveals the synopsis + rating on the hover scrim when the card carries them", () => {
render(
<PlexPosterCard
card={{ ...movie(), year: 2023, rating: 7.5, summary: "A quiet synopsis." }}
onOpen={vi.fn()}
onInfo={vi.fn()}
onToggleWatched={vi.fn()}
/>,
);
expect(screen.getByText("A quiet synopsis.")).toBeTruthy();
expect(screen.getByText("7.5")).toBeTruthy(); // rating.toFixed(1)
});
it("omits the scrim when the card has no synopsis or rating", () => {
render(
<PlexPosterCard card={movie()} onOpen={vi.fn()} onInfo={vi.fn()} onToggleWatched={vi.fn()} />,
);
expect(screen.queryByText(/synopsis/i)).toBeNull();
});
});
describe("SeasonCard a11y", () => {
it("names the watched-toggle and the add-season button", () => {
render(<SeasonCard se={season()} onOpen={vi.fn()} onToggleWatched={vi.fn()} onAdd={vi.fn()} />);
expect(screen.getByRole("button", { name: "plex.series.markSeasonWatched" })).toBeTruthy();
expect(screen.getByRole("button", { name: "plex.playlist.addSeason" })).toBeTruthy();
});
});
describe("EpisodeCard a11y", () => {
it("names the watched-toggle and the add-episode button", () => {
render(
<EpisodeCard ep={episode()} onPlay={vi.fn()} onToggleWatched={vi.fn()} onAdd={vi.fn()} />,
);
expect(screen.getByRole("button", { name: "plex.markWatched" })).toBeTruthy();
expect(screen.getByRole("button", { name: "plex.playlist.addEpisode" })).toBeTruthy();
});
});