diff --git a/frontend/src/components/PlexPlayer.controls.dom.test.tsx b/frontend/src/components/PlexPlayer.controls.dom.test.tsx index 2916bc87..ec26ecb5 100644 --- a/frontend/src/components/PlexPlayer.controls.dom.test.tsx +++ b/frontend/src/components/PlexPlayer.controls.dom.test.tsx @@ -224,6 +224,37 @@ describe("a tap on the picture (C3)", () => { }); }); +// E28 review E6: the volume slider counted as a text field, so its arrows never woke the bands — the +// bar faded from under a keyboard user adjusting it — and nothing held the bands while a key-focused +// control sat in them. +describe("keyboard focus in the bands", () => { + it("an arrow on the volume slider wakes the bands", async () => { + mount({ details: { A: item("A") } }); + await playingAt("/media/A", 100); + pressPicture("touch"); // hide + expect(bandHidden()).toBe(true); + fireEvent.keyDown(screen.getByLabelText("plex.player.volume"), { key: "ArrowUp" }); + expect(bandHidden()).toBe(false); + }); + + it("the bands stay while a key-focused control is in them, and fade 3 s after it leaves", async () => { + mount({ details: { A: item("A") } }); + await playingAt("/media/A", 100); + vi.useFakeTimers(); + video()._paused = true; + act(() => void video().play()); // playing: the timer may hide the bands + const vol = screen.getByLabelText("plex.player.volume"); + fireEvent.keyDown(document.body, { key: "Tab" }); // the keyboard is in use: focus shows + act(() => vol.focus()); + fireEvent.keyDown(vol, { key: "ArrowUp" }); + act(() => vi.advanceTimersByTime(5000)); + expect(bandHidden()).toBe(false); + act(() => vol.blur()); + act(() => vi.advanceTimersByTime(3000)); + expect(bandHidden()).toBe(true); + }); +}); + describe("a double-tap on a side third skips 10 s (C5)", () => { it("skips from where it plays, again for each further tap, and takes back the first tap's toggle", async () => { const { session } = mount({ details: { A: item("A") } }); @@ -255,6 +286,22 @@ describe("a double-tap on a side third skips 10 s (C5)", () => { expect(session.mock.calls.length).toBe(calls); }); + // E28 review E5: the tap that brings the bands back must not hand the SECOND tap to them — a double-tap + // at the bottom would land on the seek bar (a seek to wherever the finger was) instead of the skip. + it("a tap that reveals the bands keeps them out of the finger's way for the double-tap window", async () => { + mount({ details: { A: item("A") } }); + await playingAt("/media/A", 1500); + const band = () => + screen.getByLabelText("plex.player.playPause").closest("[class*='bottom-0']")!.className; + pressPicture("touch", 450); // hide + vi.useFakeTimers(); + pressPicture("touch", 450); // reveal + expect(bandHidden()).toBe(false); + expect(band()).toContain("pointer-events-none"); + act(() => vi.advanceTimersByTime(300)); + expect(band()).not.toContain("pointer-events-none"); + }); + it("the left third skips back", async () => { const { session } = mount({ details: { A: item("A") } }); await playingAt("/media/A", 1500); @@ -340,6 +387,27 @@ describe("the C5 row", () => { await waitFor(() => expect(document.activeElement).toBe(more)); }); + // E28 review E4: an episode's Next comes from its detail, which is unknown while the next episode + // loads — the button unmounted from under the focus that had just pressed it (focus on
). + it("Next keeps the focus while the next item's detail loads", async () => { + let arrive: (d: PlexItemDetail) => void = () => {}; + mount({ details: { A: item("A", 0, "B") } }); + vi.mocked(api.plexItem).mockImplementation((id: string) => + id === "A" + ? Promise.resolve(item("A", 0, "B")) + : new Promise