The 43 react-refresh/only-export-components warnings the gate let through meant an edit to a provider (or AnchoredPopover, Toaster, plexDetailUi, …) could not be hot-swapped: Fast Refresh reloads a module that also exports hooks or helpers. - Each provider's contexts, the types its hooks return and the hooks themselves live in a sibling context module (navigationContext.ts, channelsContext.ts, …); the provider file keeps the component. loadAccountFilters moved next to the feed filters' defaults (feedFiltersInit.ts), following navigationInit/playlistsInit. - Helpers that sat in component files have their own modules: welcomeMode, downloadTitle, plexPlayable, channelSync, ui/anchoredPopoverHooks, ui/pageToolbarSlot, ui/pagerContent, ui/toastLevels, ui/overlayOrder, ui/tableAlign, lib/plexDetail; the cells of the channel columns and the registry's page wrappers are components files of their own (channelCells, modulePages). - usePersistedTab, an alias of useAccountPersistedState, is gone; its callers use the original. npm run lint is now eslint --max-warnings 0, so a warning fails the gate like an error does.
66 lines
2.7 KiB
TypeScript
66 lines
2.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// Contract of the header Discover ⟷ Browse toggle. The floating header wrapper is pointer-events-none,
|
|
// so the toggle MUST re-enable pointer events on its root or it is click-dead (the bug this test guards
|
|
// against). Also: it only renders on the two title-browse views, marks the active one, and drives
|
|
// PlexBrowse's sub-view through the bridge. i18n is key-passthrough; the bridge is mocked.
|
|
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
|
import { render, screen, fireEvent, 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 }) };
|
|
});
|
|
|
|
const controls = { kind: "discover" as string, open: vi.fn() };
|
|
vi.mock("./plexViewContext", () => ({
|
|
usePlexViewControls: () => controls,
|
|
}));
|
|
|
|
import PlexViewToggle from "./PlexViewToggle";
|
|
|
|
beforeEach(() => {
|
|
controls.kind = "discover";
|
|
controls.open.mockReset();
|
|
});
|
|
afterEach(cleanup);
|
|
|
|
describe("PlexViewToggle", () => {
|
|
it("re-enables pointer events on its root (the header wrapper is pointer-events-none)", () => {
|
|
const { container } = render(<PlexViewToggle />);
|
|
// Without this class the whole toggle is click-dead inside the pointer-events-none header.
|
|
expect(container.querySelector(".pointer-events-auto")).not.toBeNull();
|
|
});
|
|
|
|
it("renders the two options with the current view pressed", () => {
|
|
render(<PlexViewToggle />);
|
|
const btns = [
|
|
...screen.getByRole("group", { name: "plex.view.label" }).querySelectorAll("button"),
|
|
];
|
|
expect(btns.map((b) => b.textContent)).toEqual(["plex.discover.tab", "plex.view.browse"]);
|
|
const pressed = btns
|
|
.filter((b) => b.getAttribute("aria-pressed") === "true")
|
|
.map((b) => b.textContent);
|
|
expect(pressed).toEqual(["plex.discover.tab"]);
|
|
});
|
|
|
|
it("opens the grid view when Browse is picked", () => {
|
|
render(<PlexViewToggle />);
|
|
fireEvent.click(screen.getByRole("button", { name: "plex.view.browse" }));
|
|
expect(controls.open).toHaveBeenCalledWith({ kind: "grid" });
|
|
});
|
|
|
|
it("opens the discover view when Discover is picked from the grid", () => {
|
|
controls.kind = "grid";
|
|
render(<PlexViewToggle />);
|
|
fireEvent.click(screen.getByRole("button", { name: "plex.discover.tab" }));
|
|
expect(controls.open).toHaveBeenCalledWith({ kind: "discover" });
|
|
});
|
|
|
|
it("renders nothing on a deeper view (no Discover/Browse choice there)", () => {
|
|
controls.kind = "playlists";
|
|
const { container } = render(<PlexViewToggle />);
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
});
|