// @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. // 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, fireEvent, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), 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( {}} onPlay={() => {}} /> , ); await screen.findByText(`Movie ${items[0]!.id}`); return screen.getByText( (_, el) => /^plex\.browse\.items/.test(el?.textContent ?? "") && el?.childElementCount === 0, ).textContent; } /** Pick `label` from the menu `open` opens. The surface is visibility:hidden in jsdom (floating-ui * never positions it), so the row is matched on its text. */ async function pickFromMenu(open: () => void, label: string) { open(); const row = await waitFor(() => { const found = screen .getAllByRole("menuitem", { hidden: true }) .find((b) => b.textContent === label); expect(found).toBeTruthy(); return found!; }); fireEvent.click(row); } 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 · 2 hr 0 min", ); }); it("drops the runtime when nothing sums", async () => { expect(await meta([movie("1")])).toBe("plex.browse.items"); }); }); // Enter on an unchanged (or emptied) title wrote the same name back to Plex, and an empty // one armed the focus hand-back while leaving the field open. Now both close with no write, as the // collection rename does. describe("PlexPlaylistView · rename", () => { it("closes without a write when the title is unchanged or empty", async () => { const write = vi.spyOn(api, "plexRenamePlaylist").mockResolvedValue({} as never); await meta([movie("1", 5400)]); for (const value of [null, " "]) { await pickFromMenu( () => fireEvent.click(screen.getByRole("button", { name: "plex.playlist.menu" })), "plex.playlist.rename", ); const field = await screen.findByRole("textbox", { name: "plex.playlist.rename" }); if (value !== null) fireEvent.change(field, { target: { value } }); fireEvent.keyDown(field, { key: "Enter" }); await screen.findByRole("heading", { name: "Films" }); } expect(write).not.toHaveBeenCalled(); }); });