|
|
|
@@ -39,7 +39,7 @@ import KeyboardHelp from "../../components/ui/KeyboardHelp";
|
|
|
|
|
import { closeKeyboardHelp, toggleKeyboardHelp } from "../../lib/keymap";
|
|
|
|
|
import { ConfirmProvider } from "../../components/ui/ConfirmProvider";
|
|
|
|
|
import { useConfirm } from "../../components/ui/confirmContext";
|
|
|
|
|
import { api, type Me, type Video } from "../../lib/api";
|
|
|
|
|
import { api, type Me, type Video, type VideoStatus } from "../../lib/api";
|
|
|
|
|
import { clearAll, notify } from "../../lib/notifications";
|
|
|
|
|
import { qk } from "../../lib/queryKeys";
|
|
|
|
|
import { writeVideoState } from "../video/videoWrites";
|
|
|
|
@@ -139,7 +139,13 @@ describe("PlayerModal — where each item starts", () => {
|
|
|
|
|
const B = vid("B", 0);
|
|
|
|
|
clock.current = 830;
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A, B]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A, B]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
await waitFor(() => expect(lastStart()).toEqual({ videoId: "A", start: 748 }));
|
|
|
|
|
fireEvent.click(screen.getAllByRole("button", { name: "player.next" })[0]!);
|
|
|
|
@@ -175,7 +181,7 @@ describe("PlayerModal — removing the playing video from its playlist", () => {
|
|
|
|
|
queue={[A!, B!, C!]}
|
|
|
|
|
startIndex={1}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
onRemoveFromList={async () => true}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
@@ -194,7 +200,7 @@ describe("PlayerModal — removing the playing video from its playlist", () => {
|
|
|
|
|
queue={[A!]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
onState={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
onRemoveFromList={async () => true}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
@@ -211,7 +217,7 @@ describe("PlayerModal — removing the playing video from its playlist", () => {
|
|
|
|
|
queue={[A!, B!]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
onState={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
onRemoveFromList={async () => false}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
@@ -236,7 +242,7 @@ describe("PlayerModal — removing the playing video from its playlist", () => {
|
|
|
|
|
queue={[A!, B!, C!, D!]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
onRemoveFromList={() => new Promise<boolean>((r) => (answer = r))}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
@@ -270,7 +276,7 @@ describe("PlayerModal — removing the playing video from its playlist", () => {
|
|
|
|
|
queue={[A!, B!]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
onRemoveFromList={(v) =>
|
|
|
|
|
confirm({ title: "t", message: v.title, confirmLabel: "remove", danger: true })
|
|
|
|
|
}
|
|
|
|
@@ -299,7 +305,13 @@ describe("PlayerModal — toasts raised while it is open", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
act(() => void notify({ level: "error", message: "Couldn't remove the video." }));
|
|
|
|
|
expect(screen.getByTestId("player-modal").textContent).toContain("Couldn't remove the video.");
|
|
|
|
@@ -311,7 +323,13 @@ describe("PlayerModal — toasts raised while it is open", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const stage = document.querySelector(".player-stage")!;
|
|
|
|
|
Object.defineProperty(document, "fullscreenElement", { configurable: true, get: () => stage });
|
|
|
|
@@ -334,7 +352,13 @@ describe("PlayerModal — the dialog contract", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
return renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={onClose} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -478,7 +502,13 @@ describe("PlayerModal — the description", () => {
|
|
|
|
|
} as Awaited<ReturnType<typeof api.videoDetail>>);
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
fireEvent.mouseEnter(screen.getByRole("heading", { name: "Video A" }));
|
|
|
|
|
fireEvent.pointerEnter(screen.getByRole("heading", { name: "Video A" }));
|
|
|
|
@@ -499,7 +529,13 @@ describe("PlayerModal — no side strips", () => {
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
const B = vid("B", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A, B]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A, B]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
expect(screen.getAllByRole("button", { name: "player.next" })).toHaveLength(1);
|
|
|
|
|
expect(screen.getAllByRole("button", { name: "player.previous" })).toHaveLength(1);
|
|
|
|
@@ -516,7 +552,13 @@ describe("PlayerModal — no side strips", () => {
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
const B = vid("B", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A, B]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A, B]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const named = [
|
|
|
|
|
"player.previous",
|
|
|
|
@@ -537,7 +579,13 @@ describe("PlayerModal — no side strips", () => {
|
|
|
|
|
const onClose = vi.fn();
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={onClose} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const close = screen.getByTestId("player-close");
|
|
|
|
|
expect(close.getAttribute("aria-label")).toBe("player.close");
|
|
|
|
@@ -556,7 +604,13 @@ describe("PlayerModal — the click layer over the video", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
return screen.getAllByTestId("yt-click-layer");
|
|
|
|
|
};
|
|
|
|
@@ -631,7 +685,13 @@ describe("PlayerModal — fits the window's height", () => {
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
const B = vid("B", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A, B]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A, B]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
// stage → its wrapper (with the clip panel) → the card → the row
|
|
|
|
|
const row =
|
|
|
|
@@ -678,7 +738,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
const B = vid("B", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A, B]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A, B]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const stage = document.querySelector<HTMLElement>(".player-stage")!;
|
|
|
|
|
const card = stage.parentElement!.parentElement!; // the stage sits in a wrapper (+ clip panel)
|
|
|
|
@@ -708,7 +774,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
const B = vid("B", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A, B]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A, B]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const row = () =>
|
|
|
|
|
document.querySelector<HTMLElement>(".player-stage")!.parentElement!.parentElement!
|
|
|
|
@@ -745,7 +817,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
await waitFor(() => expect(lastStart()?.videoId).toBe("A"));
|
|
|
|
|
// Shut by the user: closing the clip leaves it shut, and its strip has no scissors.
|
|
|
|
@@ -766,7 +844,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
await waitFor(() => expect(lastStart()?.videoId).toBe("A"));
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "clip.open" }));
|
|
|
|
@@ -801,7 +885,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
} as Awaited<ReturnType<typeof api.videoDetail>>);
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const text = await screen.findByText("A long description");
|
|
|
|
|
const panel = screen.getByTestId("player-info");
|
|
|
|
@@ -841,7 +931,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
});
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
act(() => playerEvents.onReady?.());
|
|
|
|
|
// Closed until asked: nothing is fetched for a player nobody opened the list in.
|
|
|
|
@@ -860,7 +956,7 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
// A related video played in place is the item it now is: tracked like a queue item, while the
|
|
|
|
|
// queue stays where it was and Back returns to the opened one.
|
|
|
|
|
const playRel2 = async (
|
|
|
|
|
onState: (id: string, status: string, v?: Video) => void,
|
|
|
|
|
onState: Parameters<typeof PlayerModal>[0]["onState"],
|
|
|
|
|
queue?: Video[],
|
|
|
|
|
opts: { onRemoveFromList?: (v: Video) => Promise<boolean>; before?: () => void } = {},
|
|
|
|
|
) => {
|
|
|
|
@@ -890,7 +986,7 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
it("wide: a related video played in place shows its own title and controls", async () => {
|
|
|
|
|
await playRel2(() => {});
|
|
|
|
|
await playRel2(() => Promise.resolve());
|
|
|
|
|
expect(screen.getByRole("heading", { name: "Video rel2" })).toBeTruthy();
|
|
|
|
|
expect(screen.getAllByTestId("add-rel2").length).toBeGreaterThan(0);
|
|
|
|
|
expect(screen.getAllByTestId("download-rel2").length).toBeGreaterThan(0);
|
|
|
|
@@ -903,7 +999,7 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
position_seconds: id === "rel2" ? 500 : 77,
|
|
|
|
|
}));
|
|
|
|
|
clock.current = 77;
|
|
|
|
|
await playRel2(() => {});
|
|
|
|
|
await playRel2(() => Promise.resolve());
|
|
|
|
|
clock.current = 500;
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "player.back" }));
|
|
|
|
|
// Where leaving it for the related one checkpointed it, not the queue's snapshot (40).
|
|
|
|
@@ -934,7 +1030,7 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
position_seconds: id === "rel2" ? 500 : 77,
|
|
|
|
|
}));
|
|
|
|
|
clock.current = 77;
|
|
|
|
|
await playRel2(() => {});
|
|
|
|
|
await playRel2(() => Promise.resolve());
|
|
|
|
|
clock.current = 500;
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "player.back" }));
|
|
|
|
|
await waitFor(() => expect(lastStart()?.videoId).toBe("A"));
|
|
|
|
@@ -952,7 +1048,7 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
// Its row stays in the list while it plays; a new player for the same video is never built (one
|
|
|
|
|
// per video), so Restart there is a seek.
|
|
|
|
|
it("wide: Restart on the related video already playing seeks it to 0", async () => {
|
|
|
|
|
await playRel2(() => {});
|
|
|
|
|
await playRel2(() => Promise.resolve());
|
|
|
|
|
const before = started.length;
|
|
|
|
|
const list = screen.getByTestId("player-related");
|
|
|
|
|
const row = within(list)
|
|
|
|
@@ -977,7 +1073,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
});
|
|
|
|
|
const A = vid("A", 40);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
act(() => playerEvents.onReady?.());
|
|
|
|
|
fireEvent.click(screen.getByRole("tab", { name: "player.related.title" }));
|
|
|
|
@@ -1029,7 +1131,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
|
|
|
render(
|
|
|
|
|
<QueryClientProvider client={qc}>
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>
|
|
|
|
|
</QueryClientProvider>,
|
|
|
|
|
);
|
|
|
|
|
await settle();
|
|
|
|
@@ -1053,8 +1161,83 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
expect(screen.getAllByTitle("player.markWatched").length).toBeGreaterThan(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// The toggle's own writes come back as session marks too, stamped when the server answers: each
|
|
|
|
|
// side is weighed at its answer, so neither a later mark nor a late answer of its own is misread.
|
|
|
|
|
const hostedToggle = () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
wideWindow(true);
|
|
|
|
|
const host: { qc?: QueryClient } = {};
|
|
|
|
|
// As the hosts do: the write goes out, its failure is the host's to say, the write is returned.
|
|
|
|
|
const onState = (id: string, status: VideoStatus) => {
|
|
|
|
|
const write = writeVideoState(host.qc!, id, status);
|
|
|
|
|
write.catch(() => {});
|
|
|
|
|
return write;
|
|
|
|
|
};
|
|
|
|
|
host.qc = renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={onState} />,
|
|
|
|
|
).qc;
|
|
|
|
|
return host.qc;
|
|
|
|
|
};
|
|
|
|
|
const answersInTurn = () => {
|
|
|
|
|
const answers: Array<() => void> = [];
|
|
|
|
|
vi.spyOn(api, "setState").mockImplementation(
|
|
|
|
|
() => new Promise((resolve) => answers.push(() => resolve({ ok: true } as never))),
|
|
|
|
|
);
|
|
|
|
|
return answers;
|
|
|
|
|
};
|
|
|
|
|
const shows = (title: string) => expect(screen.getAllByTitle(title).length).toBeGreaterThan(0);
|
|
|
|
|
const tick = () => new Promise((r) => setTimeout(r, 5));
|
|
|
|
|
|
|
|
|
|
it("takes a mark written elsewhere after its own click (a notice's Undo)", async () => {
|
|
|
|
|
vi.spyOn(api, "setState").mockResolvedValue({ ok: true } as never);
|
|
|
|
|
const qc = hostedToggle();
|
|
|
|
|
fireEvent.click(screen.getAllByTitle("player.markWatched")[0]!);
|
|
|
|
|
await settle();
|
|
|
|
|
shows("player.watchedUnmark");
|
|
|
|
|
await act(async () => void (await tick()));
|
|
|
|
|
await act(async () => void (await writeVideoState(qc, "A", "new")));
|
|
|
|
|
await waitFor(() => shows("player.markWatched"));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps its last click while the answer to the one before it is late", async () => {
|
|
|
|
|
const answers = answersInTurn();
|
|
|
|
|
hostedToggle();
|
|
|
|
|
fireEvent.click(screen.getAllByTitle("player.markWatched")[0]!);
|
|
|
|
|
fireEvent.click(screen.getAllByTitle("player.watchedUnmark")[0]!);
|
|
|
|
|
await act(async () => void answers[0]!());
|
|
|
|
|
await settle();
|
|
|
|
|
shows("player.markWatched");
|
|
|
|
|
await act(async () => void (await tick()));
|
|
|
|
|
await act(async () => void answers[1]!());
|
|
|
|
|
await settle();
|
|
|
|
|
shows("player.markWatched");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps its last click when the answers come back the other way round", async () => {
|
|
|
|
|
const answers = answersInTurn();
|
|
|
|
|
hostedToggle();
|
|
|
|
|
fireEvent.click(screen.getAllByTitle("player.markWatched")[0]!);
|
|
|
|
|
fireEvent.click(screen.getAllByTitle("player.watchedUnmark")[0]!);
|
|
|
|
|
await act(async () => void answers[1]!());
|
|
|
|
|
await settle();
|
|
|
|
|
await act(async () => void (await tick()));
|
|
|
|
|
await act(async () => void answers[0]!());
|
|
|
|
|
await settle();
|
|
|
|
|
shows("player.markWatched");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("drops its mark when the server refused it", async () => {
|
|
|
|
|
vi.spyOn(api, "setState").mockRejectedValue(new Error("offline"));
|
|
|
|
|
hostedToggle();
|
|
|
|
|
fireEvent.click(screen.getAllByTitle("player.markWatched")[0]!);
|
|
|
|
|
await settle();
|
|
|
|
|
shows("player.markWatched");
|
|
|
|
|
expect(screen.queryAllByTitle("player.watchedUnmark")).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("wide: a queue step leaves the related video for the queue's next item", async () => {
|
|
|
|
|
await playRel2(() => {}, [vid("A", 40), vid("B", 0)]);
|
|
|
|
|
await playRel2(() => Promise.resolve(), [vid("A", 40), vid("B", 0)]);
|
|
|
|
|
next();
|
|
|
|
|
await waitFor(() => expect(lastStart()?.videoId).toBe("B"));
|
|
|
|
|
expect(screen.getByRole("heading", { name: "Video B" })).toBeTruthy();
|
|
|
|
@@ -1063,7 +1246,7 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
|
|
|
|
|
// Removing from the list removes the queue's item — not the related video on screen.
|
|
|
|
|
it("wide: offers no removal from the list while a related video plays", async () => {
|
|
|
|
|
await playRel2(() => {}, undefined, { onRemoveFromList: async () => true });
|
|
|
|
|
await playRel2(() => Promise.resolve(), undefined, { onRemoveFromList: async () => true });
|
|
|
|
|
expect(screen.queryByRole("button", { name: /playlists\.removeItemOf/ })).toBeNull();
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "player.back" }));
|
|
|
|
|
await waitFor(() => expect(lastStart()?.videoId).toBe("A"));
|
|
|
|
@@ -1076,14 +1259,26 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
wideWindow(true);
|
|
|
|
|
const wideView = renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
expect(screen.queryByRole("tab", { name: "player.related.title" })).toBeNull();
|
|
|
|
|
expect(screen.getByTestId("player-description")).toBeTruthy();
|
|
|
|
|
wideView.unmount();
|
|
|
|
|
wideWindow(false);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
expect(screen.queryByRole("button", { name: "player.related.title" })).toBeNull();
|
|
|
|
|
});
|
|
|
|
@@ -1103,7 +1298,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
});
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
act(() => playerEvents.onReady?.());
|
|
|
|
|
fireEvent.click(screen.getByRole("tab", { name: "player.related.title" }));
|
|
|
|
@@ -1139,7 +1340,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
});
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
act(() => playerEvents.onReady?.());
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "player.related.title" }));
|
|
|
|
@@ -1163,7 +1370,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
const onClose = vi.fn();
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={onClose} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
act(() => playerEvents.onReady?.());
|
|
|
|
|
expect(screen.queryByRole("tab", { name: "player.related.title" })).toBeNull();
|
|
|
|
@@ -1188,7 +1401,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
});
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
act(() => playerEvents.onReady?.());
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "player.related.title" }));
|
|
|
|
@@ -1247,7 +1466,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
qc.setQueryData(qk.me(), me);
|
|
|
|
|
const ui = () => (
|
|
|
|
|
<QueryClientProvider client={qc}>
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>
|
|
|
|
|
</QueryClientProvider>
|
|
|
|
|
);
|
|
|
|
|
wideWindow(false);
|
|
|
|
@@ -1273,7 +1498,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const row = screen.getByRole("tablist").className.split(" ");
|
|
|
|
|
expect(row).toEqual(expect.arrayContaining(["flex", "flex-wrap", "gap-1"]));
|
|
|
|
@@ -1284,7 +1515,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const stage = document.querySelector<HTMLElement>(".player-stage")!;
|
|
|
|
|
const card = stage.parentElement!.parentElement!; // the stage sits in a wrapper (+ clip panel)
|
|
|
|
@@ -1310,7 +1547,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
>);
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const panel = screen.getByTestId("player-description");
|
|
|
|
|
expect(await within(panel).findByText("player.descriptionFailed")).toBeTruthy();
|
|
|
|
@@ -1326,7 +1569,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockRejectedValue(new Error("boom"));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "player.description" }));
|
|
|
|
|
expect(await screen.findByText("player.descriptionFailed")).toBeTruthy();
|
|
|
|
@@ -1338,7 +1587,13 @@ describe("PlayerModal — the side panel on a wide window", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
const card = document.querySelector<HTMLElement>(".player-stage")!.parentElement!;
|
|
|
|
|
expect(card.className).not.toMatch(/(^|\s)flex(\s|$)/);
|
|
|
|
@@ -1365,7 +1620,13 @@ describe("PlayerModal — the docked bar's fullscreen", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
fireEvent.click(screen.getByLabelText("player.fullscreen"));
|
|
|
|
|
return screen.getAllByLabelText("player.fullscreen").length;
|
|
|
|
@@ -1399,7 +1660,13 @@ describe("PlayerModal — clip mode", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={onClose} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -1449,7 +1716,13 @@ describe("PlayerModal — clip mode", () => {
|
|
|
|
|
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
|
|
|
|
const S = { ...vid("S", 0), is_short: true } as Video;
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={S} queue={[S]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={S}
|
|
|
|
|
queue={[S]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
await waitFor(() => expect(lastStart()?.videoId).toBe("S"));
|
|
|
|
|
key("c");
|
|
|
|
@@ -1524,7 +1797,13 @@ describe("PlayerModal — clip mode", () => {
|
|
|
|
|
const A = vid("A", 0);
|
|
|
|
|
const B = vid("B", 0);
|
|
|
|
|
renderModal(
|
|
|
|
|
<PlayerModal video={A} queue={[A, B]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
|
|
|
|
<PlayerModal
|
|
|
|
|
video={A}
|
|
|
|
|
queue={[A, B]}
|
|
|
|
|
startIndex={0}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
onState={() => Promise.resolve()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
await waitFor(() => expect(lastStart()?.videoId).toBe("A"));
|
|
|
|
|
// Auto-advance on: an ending video steps on to B.
|
|
|
|
|