fix(plex): the player's stage follows the tablet toolbar, and its bands stay out of a double-tap
- The stage is sized in dvh (80dvh × 1.25 windowed, 100dvh fullscreen); the viewport-units contract no longer exempts the player. - A tap that reveals the bands keeps them pointer-events-none for the double-tap window, so the second tap of a skip at the bottom is not a seek. - An arrow on the volume slider wakes the bands, and a keyboard focus in a band holds them (`holdOnFocus`, `:focus-visible` only, so a clicked button does not pin the bar). - Next stays mounted (disabled) while the next episode's detail loads, keeping the focus.
This commit is contained in:
@@ -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 <body>).
|
||||
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<PlexItemDetail>((r) => (arrive = r)),
|
||||
);
|
||||
await playingAt("/media/A", 100);
|
||||
const next = screen.getByLabelText("plex.player.next");
|
||||
act(() => next.focus());
|
||||
fireEvent.click(next);
|
||||
await waitFor(() => expect(api.plexItem).toHaveBeenCalledWith("B", expect.anything()));
|
||||
expect(document.activeElement).toBe(next);
|
||||
expect(next.getAttribute("aria-disabled")).toBe("true");
|
||||
await act(async () => arrive(item("B", 0, "C")));
|
||||
expect(document.activeElement).toBe(screen.getByLabelText("plex.player.next"));
|
||||
});
|
||||
|
||||
it("Next shows only when there is a next item", async () => {
|
||||
mount({ details: { A: item("A") } });
|
||||
await playingAt("/media/A", 100);
|
||||
|
||||
Reference in New Issue
Block a user