fix(player): a held key on faded bands stays one blind press; a play asked for mid-restart is the new session's
The repeats of a Space/Enter that landed on faded bands pressed the control once the bands were up; they are swallowed with it. A play or pause while a session restart is out now sets that session's intent instead of playing the outgoing stream. Tests: Skip intro outside the bands keeps its keys, every restore takeover (wheel, finger, scrollbar, key) is covered, and the chip handoff test tells the chip before from the row's first control.
This commit is contained in:
@@ -313,6 +313,54 @@ describe("keyboard focus in the bands", () => {
|
||||
expect(video().paused).toBe(false);
|
||||
});
|
||||
|
||||
it.each([" ", "Enter"])(
|
||||
"a held %j on a control the bands faded from is one blind press, repeats and all",
|
||||
async (key) => {
|
||||
mount({ details: { A: item("A") } });
|
||||
await playingAt("/media/A", 100);
|
||||
vi.useFakeTimers();
|
||||
video()._paused = true;
|
||||
act(() => void video().play());
|
||||
const mute = screen.getByLabelText("plex.player.mute");
|
||||
fireEvent.keyDown(document.body, { key: "Tab" });
|
||||
act(() => mute.focus());
|
||||
fireEvent.keyDown(mute, { key: "ArrowUp" });
|
||||
act(() => vi.advanceTimersByTime(10_500));
|
||||
expect(bandHidden()).toBe(true);
|
||||
expect(fireEvent.keyDown(mute, { key })).toBe(false);
|
||||
const toggles = pause.mock.calls.length + play.mock.calls.length;
|
||||
// the bands are up by now, and the OS repeats the held key: still not the control's
|
||||
expect(fireEvent.keyDown(mute, { key, repeat: true })).toBe(false);
|
||||
expect(fireEvent.keyDown(mute, { key, repeat: true })).toBe(false);
|
||||
expect(pause.mock.calls.length + play.mock.calls.length).toBe(toggles);
|
||||
// a new press on the shown bands is the control's own again
|
||||
fireEvent.keyUp(mute, { key });
|
||||
expect(fireEvent.keyDown(mute, { key })).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("a Space or Enter on Skip intro, outside the faded bands, stays Skip intro's", async () => {
|
||||
const intro = {
|
||||
...item("A"),
|
||||
markers: [{ type: "intro", start_s: 50, end_s: 200 }],
|
||||
} as unknown as PlexItemDetail;
|
||||
mount({ details: { A: intro } });
|
||||
await playingAt("/media/A", 100);
|
||||
vi.useFakeTimers();
|
||||
video()._paused = true;
|
||||
act(() => void video().play());
|
||||
const skip = screen.getByRole("button", { name: /plex\.player\.skipIntro/ });
|
||||
fireEvent.keyDown(document.body, { key: "Tab" });
|
||||
act(() => skip.focus());
|
||||
act(() => vi.advanceTimersByTime(10_500));
|
||||
expect(bandHidden()).toBe(true);
|
||||
const pauses = pause.mock.calls.length;
|
||||
// not prevented: the button's own activation runs, and Space does not pause the film instead
|
||||
expect(fireEvent.keyDown(skip, { key: " " })).toBe(true);
|
||||
expect(fireEvent.keyDown(skip, { key: "Enter" })).toBe(true);
|
||||
expect(pause.mock.calls.length).toBe(pauses);
|
||||
});
|
||||
|
||||
it("a tap leaving a tapped button for the picture shows the hidden bands", async () => {
|
||||
mount({ details: { A: item("A") } });
|
||||
await playingAt("/media/A", 100);
|
||||
@@ -512,6 +560,55 @@ describe("a Restart of the paused film whose session starts past 0", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("a play asked for while a restart is out", () => {
|
||||
it("is the new session's: the outgoing stream never plays", async () => {
|
||||
const { session } = mount({ details: { A: item("A") } });
|
||||
await playingAt("/media/A", 100);
|
||||
act(() => void video().pause());
|
||||
let answer: (s: Awaited<ReturnType<typeof api.plexSession>>) => void = () => {};
|
||||
session.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
answer = resolve;
|
||||
}),
|
||||
);
|
||||
const asked = session.mock.calls.length;
|
||||
// paused, and a seek outside the loaded region: a restart that lands paused
|
||||
fireEvent.keyDown(document.body, { key: "ArrowRight" });
|
||||
await waitFor(() => expect(session.mock.calls.length).toBe(asked + 1));
|
||||
const plays = play.mock.calls.length;
|
||||
fireEvent.keyDown(document.body, { key: " " });
|
||||
expect(play.mock.calls.length).toBe(plays); // the old stream stays silent
|
||||
expect(video().paused).toBe(true);
|
||||
await act(async () => answer({ ...direct("A"), start_s: 110 } as never));
|
||||
act(() => void video().dispatchEvent(new Event("loadedmetadata")));
|
||||
expect(play.mock.calls.length).toBe(plays + 1); // and the new one plays, as asked
|
||||
});
|
||||
|
||||
it("a pause asked for while a playing restart is out lands paused", async () => {
|
||||
const { session } = mount({ details: { A: item("A") } });
|
||||
await playingAt("/media/A", 100);
|
||||
act(() => void video().play());
|
||||
let answer: (s: Awaited<ReturnType<typeof api.plexSession>>) => void = () => {};
|
||||
session.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
answer = resolve;
|
||||
}),
|
||||
);
|
||||
const asked = session.mock.calls.length;
|
||||
fireEvent.keyDown(document.body, { key: "ArrowRight" });
|
||||
await waitFor(() => expect(session.mock.calls.length).toBe(asked + 1));
|
||||
fireEvent.keyDown(document.body, { key: " " });
|
||||
expect(video().paused).toBe(true);
|
||||
const plays = play.mock.calls.length;
|
||||
await act(async () => answer({ ...direct("A"), start_s: 110 } as never));
|
||||
act(() => void video().dispatchEvent(new Event("loadedmetadata")));
|
||||
act(() => void video().dispatchEvent(new Event("canplay")));
|
||||
expect(play.mock.calls.length).toBe(plays);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the C5 row", () => {
|
||||
it("has no Stop, Restart or ±10 s buttons, and keeps the rare tools behind the ⋯", async () => {
|
||||
mount({ details: { A: item("A") } });
|
||||
|
||||
Reference in New Issue
Block a user