Files
siftlode/frontend/src/lib/plexBackdrop.dom.test.tsx
T
peter c3d88d9b20 refactor: component files export only components; lint fails on a warning
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.
2026-09-17 02:59:09 +02:00

104 lines
3.6 KiB
TypeScript

// @vitest-environment jsdom
//
// E5 S8: the crossfading art backdrop hook. Covers the pieces that don't need real layout — that a
// pool paints #root + isolates it (so the z-index:-1 overlay can paint above #root's own background),
// creates exactly one overlay layer when it may animate, and HARD-OFFS the rotation (no overlay, first
// image only) under prefers-reduced-motion, a single image, or when disabled. matchMedia is stubbed
// per-test; the hook reads it at effect time.
import { describe, it, expect, afterEach, vi } from "vitest";
import { render, cleanup } from "@testing-library/react";
import { useCrossfadeBackdrop } from "./plexDetail";
function mockReducedMotion(reduce: boolean) {
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
matches: query.includes("prefers-reduced-motion") ? reduce : false,
media: query,
addEventListener: () => {},
removeEventListener: () => {},
addListener: () => {},
removeListener: () => {},
dispatchEvent: () => false,
})) as unknown as typeof window.matchMedia;
}
function Harness({
arts,
enabled = true,
animate = true,
}: {
arts: (string | null | undefined)[];
enabled?: boolean;
animate?: boolean;
}) {
useCrossfadeBackdrop(arts, enabled, animate);
return null;
}
function root() {
return document.getElementById("root")!;
}
// The hook paints #root, so it must exist in the document (main.tsx renders into it in the real app).
function withRoot() {
const el = document.createElement("div");
el.id = "root";
document.body.appendChild(el);
return el;
}
afterEach(() => {
cleanup();
document.getElementById("root")?.remove();
});
const overlays = () =>
Array.from(root().children).filter((c) => (c as HTMLElement).style.zIndex === "-1");
describe("useCrossfadeBackdrop", () => {
it("paints + isolates #root and creates one overlay when it can animate", () => {
withRoot();
mockReducedMotion(false);
render(<Harness arts={["/a.jpg", "/b.jpg", "/c.jpg"]} />);
expect(root().style.backgroundImage).toContain('url("/a.jpg")');
expect(root().style.isolation).toBe("isolate"); // needed for the z-index:-1 overlay to show
expect(overlays().length).toBe(1);
});
it("hard-offs the rotation under prefers-reduced-motion (static, no overlay)", () => {
withRoot();
mockReducedMotion(true);
render(<Harness arts={["/a.jpg", "/b.jpg"]} />);
expect(root().style.backgroundImage).toContain('url("/a.jpg")'); // first image still painted
expect(overlays().length).toBe(0); // but no crossfade layer
});
it("stays static for a single image (no overlay)", () => {
withRoot();
mockReducedMotion(false);
render(<Harness arts={["/only.jpg", null, undefined]} />);
expect(root().style.backgroundImage).toContain('url("/only.jpg")');
expect(overlays().length).toBe(0);
});
it("does not animate when animate=false (crossfade pref off)", () => {
withRoot();
mockReducedMotion(false);
render(<Harness arts={["/a.jpg", "/b.jpg"]} animate={false} />);
expect(root().style.backgroundImage).toContain('url("/a.jpg")');
expect(overlays().length).toBe(0);
});
it("clears #root when disabled and removes the overlay on unmount", () => {
withRoot();
mockReducedMotion(false);
const { rerender, unmount } = render(<Harness arts={["/a.jpg", "/b.jpg"]} enabled={false} />);
expect(root().style.backgroundImage).toBe("");
expect(overlays().length).toBe(0);
rerender(<Harness arts={["/a.jpg", "/b.jpg"]} enabled={true} />);
expect(overlays().length).toBe(1);
unmount();
expect(overlays().length).toBe(0); // cleaned up
expect(root().style.isolation).toBe("");
});
});