fix(plex): make the header Discover/Browse toggle clickable
The floating header wrapper is pointer-events-none (so clicks fall through its empty area to the content below); every interactive child must re-enable pointer events. PlexViewToggle rendered its SegmentedToggle without the pointer-events-auto wrapper the sibling PlexScopeToggle has, so the whole Discover/Browse switch was click-dead — the buttons rendered and were correctly wired, but no real mouse click ever reached them. Add `className="pointer-events-auto shrink-0"` to the toggle and a regression test that asserts the root re-enables pointer events (plus the render/onChange/hide-on-deeper-view contract). Verified live on localdev: real clicks now switch Discover⟷Browse (?v=browse) in both directions; a header-wide hit-test audit shows no other dead controls. Gate: tsc/lint/knip/prettier clean, 418 vitest.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// @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("./PlexViewBridge", () => ({
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -11,9 +11,13 @@ export default function PlexViewToggle() {
|
||||
// Only the two title-browse views host the Discover/Browse choice; deeper views (show/info/person/…)
|
||||
// and the playlists/collections browse surfaces hide it.
|
||||
if (!ctl || (ctl.kind !== "discover" && ctl.kind !== "grid")) return null;
|
||||
// The floating header wrapper is pointer-events-none (so clicks fall through its empty area to the
|
||||
// content below); every interactive child must re-enable pointer events or it is click-dead — the
|
||||
// sibling PlexScopeToggle does the same.
|
||||
return (
|
||||
<SegmentedToggle
|
||||
size="md"
|
||||
className="pointer-events-auto shrink-0"
|
||||
ariaLabel={t("plex.view.label")}
|
||||
options={[
|
||||
{ value: "discover", label: t("plex.discover.tab") },
|
||||
|
||||
Reference in New Issue
Block a user