fix(plex): the backdrop crossfade toggle wins over reduced motion once set
prefers-reduced-motion hard-disabled the rotation whatever the toggle said, and Windows reports it whenever its animation effects are off — so the owner never saw the crossfade. Untouched, the setting now follows the system (and the toggle shows that state); a pick made on the toggle is stored and wins on every device.
This commit is contained in:
@@ -2,12 +2,17 @@
|
||||
//
|
||||
// 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.
|
||||
// creates exactly one overlay layer when it may animate, and stays static (no overlay, first image
|
||||
// only) for a single image, the crossfade off, or when disabled. Then the crossfade setting itself:
|
||||
// untouched it follows prefers-reduced-motion, and the reader's own pick wins over it (the owner, whose
|
||||
// Windows animation effects are off, never saw the crossfade — 2026-10-03). matchMedia is stubbed per test.
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { render, cleanup } from "@testing-library/react";
|
||||
import { useCrossfadeBackdrop } from "./plexDetail";
|
||||
import { render, cleanup, renderHook, act } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createElement, type ReactNode } from "react";
|
||||
import { api } from "./api";
|
||||
import { qk } from "./queryKeys";
|
||||
import { useCrossfadeBackdrop, useDetailPrefs } from "./plexDetail";
|
||||
|
||||
function mockReducedMotion(reduce: boolean) {
|
||||
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
|
||||
@@ -64,12 +69,11 @@ describe("useCrossfadeBackdrop", () => {
|
||||
expect(overlays().length).toBe(1);
|
||||
});
|
||||
|
||||
it("hard-offs the rotation under prefers-reduced-motion (static, no overlay)", () => {
|
||||
it("animates under prefers-reduced-motion once the reader's setting says so", () => {
|
||||
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
|
||||
expect(overlays().length).toBe(1);
|
||||
});
|
||||
|
||||
it("stays static for a single image (no overlay)", () => {
|
||||
@@ -101,3 +105,44 @@ describe("useCrossfadeBackdrop", () => {
|
||||
expect(root().style.isolation).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the crossfade setting", () => {
|
||||
const prefsWith = (preferences: Record<string, unknown>) => {
|
||||
const qc = new QueryClient();
|
||||
qc.setQueryData(qk.me(), { preferences });
|
||||
return ({ children }: { children: ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: qc }, children);
|
||||
};
|
||||
|
||||
it.each([
|
||||
[false, true],
|
||||
[true, false],
|
||||
])("untouched, follows the system (reduced motion %s → crossfade %s)", (reduce, on) => {
|
||||
mockReducedMotion(reduce);
|
||||
const { result } = renderHook(() => useDetailPrefs(), { wrapper: prefsWith({}) });
|
||||
expect(result.current.crossfade).toBe(on);
|
||||
});
|
||||
|
||||
it("keeps the reader's own pick over the system", () => {
|
||||
mockReducedMotion(true);
|
||||
const on = renderHook(() => useDetailPrefs(), {
|
||||
wrapper: prefsWith({ plexInfoArtCrossfade: true }),
|
||||
});
|
||||
expect(on.result.current.crossfade).toBe(true);
|
||||
mockReducedMotion(false);
|
||||
const off = renderHook(() => useDetailPrefs(), {
|
||||
wrapper: prefsWith({ plexInfoArtCrossfade: false }),
|
||||
});
|
||||
expect(off.result.current.crossfade).toBe(false);
|
||||
});
|
||||
|
||||
it("turns on from the state in force, and stores that as the reader's pick", () => {
|
||||
mockReducedMotion(true);
|
||||
const save = vi.spyOn(api, "savePrefs").mockResolvedValue({} as never);
|
||||
const { result } = renderHook(() => useDetailPrefs(), { wrapper: prefsWith({}) });
|
||||
act(() => result.current.toggleCrossfade());
|
||||
expect(result.current.crossfade).toBe(true);
|
||||
expect(save).toHaveBeenCalledWith({ plexInfoArtCrossfade: true });
|
||||
save.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user