- A press that begins while a field in the row is edited only ends the editing (rowOpen latches it at pointerdown); the card layout's row press is now tested. - A related video reset in the player forgets the session's stored position. - Removing the playing video passes over known-dead items and steps the way it went. - Prev/Next off only because of dead items stay focusable and say why (HU/EN). - The docked bar's scroller leaves room for focus rings. - A focus-opened About card is not closed by its own scroll into view. - EN "Clear watch progress" for the video reset, distinct from a page's "Reset view". - The signed-out title read goes to the page yt-dlp read; the retitle writes are conditional. - Tests: the cap of three errors, the bar's own Fullscreen, the group glyph, the backfill's reads.
82 lines
3.2 KiB
TypeScript
82 lines
3.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The Browse grid's toolbar, the feed's in Plex: the watch-state pill and Sort name their group by a
|
|
// glyph (the word in the tooltip and the accessible name), and Sort ends the filters' row instead of
|
|
// taking a row of its own.
|
|
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
|
|
import { render, screen, cleanup } 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" } }),
|
|
}));
|
|
vi.mock("./plexSavedViews", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("./plexSavedViews")>()),
|
|
usePlexSavedViews: () => ({ views: [], displayView: null, isDirty: false, activeViewId: null }),
|
|
}));
|
|
|
|
import PlexBrowse from "./PlexBrowse";
|
|
import { api } from "../../lib/api";
|
|
import { PageToolbarSlot } from "../../components/ui/pageToolbarSlot";
|
|
|
|
beforeAll(() => {
|
|
class Stub {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
|
|
globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver;
|
|
});
|
|
beforeEach(() => window.history.replaceState({ _sub: { kind: "grid" }, _subOf: "v" }, ""));
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const slot = document.body.appendChild(document.createElement("div"));
|
|
|
|
async function mount() {
|
|
vi.spyOn(api, "plexLibrary").mockResolvedValue({ items: [], total: 0, offset: 0 } as never);
|
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(
|
|
<QueryClientProvider client={qc}>
|
|
<PageToolbarSlot.Provider value={slot}>
|
|
<PlexBrowse />
|
|
</PageToolbarSlot.Provider>
|
|
</QueryClientProvider>,
|
|
);
|
|
return screen.findByTestId("plex-show");
|
|
}
|
|
|
|
// The group's glyph, not the trigger's own chevron: the glyph sits in its aria-hidden wrapper.
|
|
const glyph = (el: HTMLElement) => el.querySelector('[aria-hidden="true"] > svg');
|
|
const sortTrigger = () =>
|
|
screen
|
|
.getAllByRole("button")
|
|
.find((b) => b.getAttribute("aria-label")?.startsWith("plex.filter.sort:"))!;
|
|
|
|
describe("PlexBrowse toolbar", () => {
|
|
it("the watch-state pill shows a glyph, not its group's word, and names itself", async () => {
|
|
const pill = await mount();
|
|
// The key of a value ("plex.filter.showOpt.all") holds the group's key: test the visible text.
|
|
expect(pill.textContent).toBe("plex.filter.showOpt.all");
|
|
expect(glyph(pill)).not.toBeNull();
|
|
expect(pill.getAttribute("title")).toBe("plex.filter.show");
|
|
expect(pill.getAttribute("aria-label")).toMatch(/^plex\.filter\.show: /);
|
|
});
|
|
|
|
it("Sort shows a glyph, names itself, and ends the filters' row", async () => {
|
|
const pill = await mount();
|
|
const sort = sortTrigger();
|
|
expect(sort.textContent).not.toContain("plex.filter.sort");
|
|
expect(glyph(sort)).not.toBeNull();
|
|
expect(sort.getAttribute("title")).toBe("plex.filter.sort");
|
|
expect(pill.parentElement!.contains(sort)).toBe(true);
|
|
});
|
|
});
|