The clip panel and the editor get a Video / GIF choice with the GIF's size, the size it is expected to come out at, and what that size means (no moving link preview over 15 MB, refused over 30 MB). A range longer than a GIF may be leaves Make unavailable with its reason. The editor makes one GIF per kept segment; Join says why it is unavailable instead of going dead. The share page shows a picture as itself with its Download, and a picture in a folder opens above the list.
537 lines
22 KiB
TypeScript
537 lines
22 KiB
TypeScript
// @vitest-environment jsdom
|
||
//
|
||
// The public watch page must render a native <audio> element for an audio-only asset
|
||
// (mp3) and a <video> for everything else. WebKit (all iOS browsers) refuses to play an audio-only
|
||
// mp3 inside a <video>, which is exactly why the branch exists — this locks it so a refactor can't
|
||
// silently drop the audio player and reintroduce the iPad-broken behavior.
|
||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||
import { render, waitFor, cleanup, fireEvent } from "@testing-library/react";
|
||
const expose = vi.hoisted(() => vi.fn());
|
||
vi.mock("../lib/browserBackgroundPlay", () => ({ exposeVideoToBackgroundPlay: expose }));
|
||
|
||
import WatchPage from "./WatchPage";
|
||
|
||
function mockMeta(meta: Record<string, unknown>) {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async () => ({ ok: true, json: async () => meta })),
|
||
);
|
||
}
|
||
|
||
afterEach(() => {
|
||
cleanup();
|
||
vi.unstubAllGlobals();
|
||
localStorage.clear();
|
||
});
|
||
|
||
describe("WatchPage share paths", () => {
|
||
it.each(["/watch/tok-p", "/file/tok-p", "/folder/tok-p", "/file/tok-p/extra"])(
|
||
"reads the token off %s",
|
||
async (path) => {
|
||
window.history.replaceState({}, "", path);
|
||
mockMeta({ needs_password: false, audio: false, file_url: "/f.mp4", title: "A clip" });
|
||
render(<WatchPage />);
|
||
await waitFor(() =>
|
||
expect(fetch).toHaveBeenCalledWith("/api/public/watch/tok-p", expect.anything()),
|
||
);
|
||
},
|
||
);
|
||
});
|
||
|
||
describe("WatchPage background play", () => {
|
||
it("hands its <video> to the background-play hook, so Brave iOS keeps it playing", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-bg");
|
||
mockMeta({ needs_password: false, audio: false, file_url: "/f.mp4", title: "A clip" });
|
||
const { container } = render(<WatchPage />);
|
||
await waitFor(() => expect(container.querySelector("video")).not.toBeNull());
|
||
expect(expose).toHaveBeenCalledWith(container.querySelector("video"));
|
||
});
|
||
});
|
||
|
||
describe("WatchPage repeat", () => {
|
||
it("repeats by default, for a video and for an audio-only asset", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-repeat-video");
|
||
mockMeta({ needs_password: false, audio: false, file_url: "/f.mp4", title: "A clip" });
|
||
const video = render(<WatchPage />);
|
||
await waitFor(() => expect(video.container.querySelector("video")).not.toBeNull());
|
||
expect(video.container.querySelector("video")!.loop).toBe(true);
|
||
cleanup();
|
||
|
||
window.history.replaceState({}, "", "/watch/tok-repeat-audio");
|
||
mockMeta({ needs_password: false, audio: true, file_url: "/f.mp3", title: "A song" });
|
||
const audio = render(<WatchPage />);
|
||
await waitFor(() => expect(audio.container.querySelector("audio")).not.toBeNull());
|
||
expect(audio.container.querySelector("audio")!.loop).toBe(true);
|
||
});
|
||
|
||
it("the toggle turns it off, and the choice is remembered for the next visit", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-repeat-toggle");
|
||
mockMeta({ needs_password: false, audio: false, file_url: "/f.mp4", title: "A clip" });
|
||
const first = render(<WatchPage />);
|
||
const toggle = await first.findByRole("button", { name: "Repeat" });
|
||
expect(toggle.getAttribute("aria-pressed")).toBe("true");
|
||
fireEvent.click(toggle);
|
||
expect(toggle.getAttribute("aria-pressed")).toBe("false");
|
||
expect(first.container.querySelector("video")!.loop).toBe(false);
|
||
cleanup();
|
||
|
||
const second = render(<WatchPage />);
|
||
await waitFor(() => expect(second.container.querySelector("video")).not.toBeNull());
|
||
expect(second.container.querySelector("video")!.loop).toBe(false);
|
||
});
|
||
});
|
||
|
||
// The Download link sat at the button's 32 beside the Repeat chip's 28 — one row, two heights.
|
||
describe("WatchPage action row", () => {
|
||
it("puts the Download on the toolbar rung, level with the Repeat chip", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-row");
|
||
mockMeta({
|
||
needs_password: false,
|
||
audio: false,
|
||
allow_download: true,
|
||
file_url: "/f.mp4",
|
||
title: "A clip",
|
||
});
|
||
const view = render(<WatchPage />);
|
||
const repeat = await view.findByRole("button", { name: "Repeat" });
|
||
const download = view.container.querySelector('a[href="/f.mp4"]')!;
|
||
expect(repeat.className).toContain("h-7");
|
||
expect(download.className).toContain("h-7");
|
||
});
|
||
|
||
// DESIGN B6: the page the stranger sees wears the app's surfaces — the scheme's ambient
|
||
// backdrop (no page fill over the body's), the title and actions on a glass panel, a glass Download.
|
||
it("sits on the app's surfaces: backdrop, a glass panel, a glass Download", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-b6");
|
||
mockMeta({
|
||
needs_password: false,
|
||
audio: false,
|
||
allow_download: true,
|
||
file_url: "/f.mp4",
|
||
title: "A clip",
|
||
});
|
||
const view = render(<WatchPage />);
|
||
const title = await view.findByRole("heading", { name: "A clip" });
|
||
expect((view.container.firstElementChild as HTMLElement).className).not.toMatch(
|
||
/(^|\s)bg-bg(\s|$)/,
|
||
);
|
||
expect(title.closest(".glass")).toBeTruthy();
|
||
const download = view.container.querySelector('a[href="/f.mp4"]')!;
|
||
expect(download.className).toContain("glass-card");
|
||
});
|
||
});
|
||
|
||
describe("WatchPage media element", () => {
|
||
it("renders a native <audio> for an audio-only asset", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-audio");
|
||
mockMeta({ needs_password: false, audio: true, file_url: "/f.mp3", title: "A song" });
|
||
const { container } = render(<WatchPage />);
|
||
await waitFor(() => expect(container.querySelector("audio")).not.toBeNull());
|
||
expect(container.querySelector("video")).toBeNull();
|
||
});
|
||
|
||
it("renders a <video> for a normal video asset", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-video");
|
||
mockMeta({ needs_password: false, audio: false, file_url: "/f.mp4", title: "A clip" });
|
||
const { container } = render(<WatchPage />);
|
||
await waitFor(() => expect(container.querySelector("video")).not.toBeNull());
|
||
expect(container.querySelector("audio")).toBeNull();
|
||
});
|
||
|
||
it("renders an info card + Download for a generic file, no player", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-file");
|
||
mockMeta({
|
||
needs_password: false,
|
||
media_kind: "file",
|
||
title: "report.pdf",
|
||
container: "pdf",
|
||
size_bytes: 2_400_000,
|
||
allow_download: true,
|
||
file_url: "/f.pdf",
|
||
});
|
||
const { container, findByText } = render(<WatchPage />);
|
||
await findByText("report.pdf");
|
||
expect(container.querySelector("video")).toBeNull();
|
||
expect(container.querySelector("audio")).toBeNull();
|
||
// Type · size chip and a Download link to the file.
|
||
await findByText(/PDF · 2\.3 MB/);
|
||
const dl = container.querySelector('a[href="/f.pdf"]');
|
||
expect(dl).not.toBeNull();
|
||
// The page's one action is the filled primary, not a muted outline.
|
||
expect(dl!.className).toContain("state-filled");
|
||
});
|
||
|
||
it("shows a shared picture as itself, no player, with its kind, size and the chat tip", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-gif");
|
||
mockMeta({
|
||
needs_password: false,
|
||
media_kind: "file",
|
||
preview: "image",
|
||
title: "Evening clip",
|
||
container: "gif",
|
||
width: 480,
|
||
height: 270,
|
||
size_bytes: 6_500_000,
|
||
allow_download: true,
|
||
file_url: "/f.gif",
|
||
});
|
||
const page = render(<WatchPage />);
|
||
const img = await page.findByRole("img", { name: "Evening clip" });
|
||
expect(img.getAttribute("src")).toBe("/f.gif");
|
||
expect(page.container.querySelector("video")).toBeNull();
|
||
expect(page.getByText(/GIF · 480×270 · 6\.2 MB/)).toBeTruthy();
|
||
expect(page.getByRole("link", { name: "Download" }).getAttribute("href")).toBe("/f.gif");
|
||
expect(page.getByText(/drag the downloaded file in/)).toBeTruthy();
|
||
});
|
||
|
||
it("a view-only picture link shows the picture, without Download or the chat tip", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-gif-view");
|
||
mockMeta({
|
||
needs_password: false,
|
||
media_kind: "file",
|
||
preview: "image",
|
||
title: "Still",
|
||
container: "gif",
|
||
allow_download: false,
|
||
file_url: "/f.gif",
|
||
});
|
||
const page = render(<WatchPage />);
|
||
await page.findByRole("img", { name: "Still" });
|
||
expect(page.queryByRole("link", { name: "Download" })).toBeNull();
|
||
expect(page.queryByText(/drag the downloaded file in/)).toBeNull();
|
||
});
|
||
|
||
it("omits the Download for a view-only file link", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-file-noview");
|
||
mockMeta({
|
||
needs_password: false,
|
||
media_kind: "file",
|
||
title: "secret.zip",
|
||
container: "zip",
|
||
size_bytes: 1000,
|
||
allow_download: false,
|
||
file_url: "/f.zip",
|
||
});
|
||
const { container, findByText } = render(<WatchPage />);
|
||
await findByText("secret.zip");
|
||
expect(container.querySelector('a[href="/f.zip"]')).toBeNull();
|
||
});
|
||
});
|
||
|
||
// Only a 404 means a link is gone. Every other failure used to render the permanent "no longer
|
||
// available" state too: a server restart, a dropped connection, and the unlock's own rate limit told a
|
||
// viewer with a valid link that it had expired or been revoked, with no way to try again.
|
||
describe("WatchPage honest failures", () => {
|
||
const res = (status: number, body: unknown = {}) => ({
|
||
ok: status < 400,
|
||
status,
|
||
json: async () => body,
|
||
});
|
||
const GONE = "This link is no longer available.";
|
||
|
||
it("a server error is a failure with a retry, not a dead link", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-5xx");
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi
|
||
.fn()
|
||
.mockResolvedValueOnce(res(503))
|
||
.mockResolvedValue(
|
||
res(200, { needs_password: false, file_url: "/f.mp4", title: "A clip" }),
|
||
),
|
||
);
|
||
const page = render(<WatchPage />);
|
||
const retry = await page.findByRole("button", { name: "Try again" });
|
||
expect(page.queryByText(GONE)).toBeNull();
|
||
expect(page.getByRole("alert").textContent).toContain("Couldn't load this link.");
|
||
|
||
fireEvent.click(retry);
|
||
await waitFor(() => expect(page.container.querySelector("video")).not.toBeNull());
|
||
});
|
||
|
||
it("a dropped connection is a failure with a retry too", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-offline");
|
||
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new TypeError("Failed to fetch")));
|
||
const page = render(<WatchPage />);
|
||
await page.findByRole("button", { name: "Try again" });
|
||
expect(page.queryByText(GONE)).toBeNull();
|
||
});
|
||
|
||
it("a 404 says the link is gone", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-404");
|
||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(res(404)));
|
||
const page = render(<WatchPage />);
|
||
await page.findByText(GONE);
|
||
expect(page.queryByRole("button", { name: "Try again" })).toBeNull();
|
||
});
|
||
|
||
it("a 404 whose video is being made again says to come back, with a retry", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-preparing");
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi
|
||
.fn()
|
||
.mockResolvedValueOnce(res(404, { detail: { code: 404, reason: "video_preparing" } }))
|
||
.mockResolvedValue(
|
||
res(200, { needs_password: false, file_url: "/f.mp4", title: "A clip" }),
|
||
),
|
||
);
|
||
const page = render(<WatchPage />);
|
||
const retry = await page.findByRole("button", { name: "Try again" });
|
||
expect(page.getByRole("alert").textContent).toContain("This video is being prepared.");
|
||
expect(page.queryByText(GONE)).toBeNull();
|
||
|
||
fireEvent.click(retry);
|
||
await waitFor(() => expect(page.container.querySelector("video")).not.toBeNull());
|
||
});
|
||
|
||
it("a Retry that is answered 'still preparing' says so and keeps the focus on Retry", async () => {
|
||
// The answer replaces the button the viewer was on: announced (an alert, inserted filled), and
|
||
// the new Retry takes the focus instead of leaving it on <body>.
|
||
window.history.replaceState({}, "", "/watch/tok-preparing");
|
||
const preparing = () => res(404, { detail: { code: 404, reason: "video_preparing" } });
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn().mockImplementation(() => Promise.resolve(preparing())),
|
||
);
|
||
const page = render(<WatchPage />);
|
||
const first = await page.findByRole("button", { name: "Try again" });
|
||
expect(document.activeElement).not.toBe(first); // the first load moves no focus
|
||
|
||
fireEvent.click(first);
|
||
await waitFor(() =>
|
||
expect(document.activeElement).toBe(page.getByRole("button", { name: "Try again" })),
|
||
);
|
||
expect(page.getByRole("alert").textContent).toContain("This video is being prepared.");
|
||
});
|
||
|
||
async function tryPassword(second: () => Promise<unknown>) {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi
|
||
.fn()
|
||
.mockResolvedValueOnce(res(200, { needs_password: true }))
|
||
.mockImplementationOnce(second),
|
||
);
|
||
const page = render(<WatchPage />);
|
||
fireEvent.change(await page.findByPlaceholderText("Enter password"), {
|
||
target: { value: "guess" },
|
||
});
|
||
fireEvent.click(page.getByRole("button", { name: "Watch" }));
|
||
return page;
|
||
}
|
||
|
||
it.each([
|
||
[{ kind: "file", video: true }, "This video is password-protected", "Watch"],
|
||
[{ kind: "file", video: false }, "This file is password-protected", "Open"],
|
||
[{ kind: "folder", video: false }, "This folder is password-protected", "Open"],
|
||
[{}, "This video is password-protected", "Watch"],
|
||
])("a password link that opens %o asks in its words", async (kind, title, button) => {
|
||
window.history.replaceState({}, "", "/file/tok-lock");
|
||
mockMeta({ needs_password: true, ...kind });
|
||
const page = render(<WatchPage />);
|
||
await page.findByText(title);
|
||
expect(page.getByRole("button", { name: button })).toBeTruthy();
|
||
});
|
||
|
||
it("a repeated Enter sends one try while it is on its way", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-enter");
|
||
const fetchMock = vi.fn().mockResolvedValueOnce(res(200, { needs_password: true }));
|
||
fetchMock.mockImplementation(() => new Promise(() => {}));
|
||
vi.stubGlobal("fetch", fetchMock);
|
||
const page = render(<WatchPage />);
|
||
const field = await page.findByPlaceholderText("Enter password");
|
||
fireEvent.change(field, { target: { value: "guess" } });
|
||
fireEvent.keyDown(field, { key: "Enter" });
|
||
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2));
|
||
fireEvent.keyDown(field, { key: "Enter" });
|
||
fireEvent.keyDown(field, { key: "Enter" });
|
||
await new Promise((r) => setTimeout(r, 20));
|
||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
it("too many password tries says so, and keeps the form", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-429");
|
||
const page = await tryPassword(async () => res(429));
|
||
await page.findByText("Too many attempts. Try again in a few minutes.");
|
||
const alert = page.getByRole("alert");
|
||
expect(alert.textContent).toBe("Too many attempts. Try again in a few minutes.");
|
||
expect(page.getByPlaceholderText("Enter password").getAttribute("aria-describedby")).toBe(
|
||
alert.id,
|
||
);
|
||
});
|
||
|
||
it("an unlock that gets no answer says so instead of doing nothing", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-unlock-offline");
|
||
const page = await tryPassword(() => Promise.reject(new TypeError("Failed to fetch")));
|
||
expect((await page.findByRole("alert")).textContent).toBe(
|
||
"Couldn't check the password. Try again.",
|
||
);
|
||
});
|
||
});
|
||
|
||
// A shared link opens where THIS browser stopped in it (the viewer has no account), and keeps the
|
||
// point as it plays — per link, so another link starts at its own place.
|
||
describe("WatchPage resume", () => {
|
||
function loaded(video: HTMLVideoElement, duration = 600) {
|
||
let t = 0;
|
||
Object.defineProperties(video, {
|
||
currentTime: { get: () => t, set: (v: number) => void (t = v), configurable: true },
|
||
duration: { get: () => duration, configurable: true },
|
||
readyState: { get: () => 1, configurable: true },
|
||
});
|
||
video.dispatchEvent(new Event("loadedmetadata"));
|
||
return video;
|
||
}
|
||
|
||
it("keeps where it stopped, and opens there on the next visit", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-resume");
|
||
mockMeta({ needs_password: false, audio: false, file_url: "/f.mp4", title: "A clip" });
|
||
const first = render(<WatchPage />);
|
||
await waitFor(() => expect(first.container.querySelector("video")).not.toBeNull());
|
||
const video = loaded(first.container.querySelector("video")!);
|
||
expect(video.currentTime).toBe(0);
|
||
video.dispatchEvent(new Event("playing"));
|
||
video.currentTime = 250;
|
||
video.dispatchEvent(new Event("pause"));
|
||
cleanup();
|
||
|
||
const second = render(<WatchPage />);
|
||
await waitFor(() => expect(second.container.querySelector("video")).not.toBeNull());
|
||
expect(loaded(second.container.querySelector("video")!).currentTime).toBe(250);
|
||
cleanup();
|
||
|
||
window.history.replaceState({}, "", "/watch/tok-other");
|
||
const other = render(<WatchPage />);
|
||
await waitFor(() => expect(other.container.querySelector("video")).not.toBeNull());
|
||
expect(loaded(other.container.querySelector("video")!).currentTime).toBe(0);
|
||
});
|
||
});
|
||
|
||
// A folder link opens the folder's list: a video or a song plays in the same player a file link has,
|
||
// above the list; a document only downloads; Download follows the link's own setting.
|
||
describe("WatchPage folder", () => {
|
||
const file = (id: number, extra: Record<string, unknown> = {}) => ({
|
||
id,
|
||
title: `Clip ${id}`,
|
||
audio: false,
|
||
media_kind: "video",
|
||
size_bytes: 1000,
|
||
duration_s: 71,
|
||
file_url: `/api/public/watch/tok-folder/files/${id}/file`,
|
||
poster_url: null,
|
||
...extra,
|
||
});
|
||
const folder = (extra: Record<string, unknown> = {}) => ({
|
||
needs_password: false,
|
||
kind: "folder",
|
||
name: "Friends",
|
||
allow_download: true,
|
||
files: [file(1), file(2), file(3, { title: "Plan.pdf", media_kind: "file", duration_s: null })],
|
||
...extra,
|
||
});
|
||
const scrolled = vi.fn();
|
||
Element.prototype.scrollIntoView = scrolled;
|
||
|
||
it("lists the folder's files and plays the one picked above the list", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-folder");
|
||
mockMeta(folder());
|
||
const page = render(<WatchPage />);
|
||
expect((await page.findByRole("heading", { level: 1 })).textContent).toBe("Friends");
|
||
expect(page.getByText(/^3 files/)).toBeTruthy();
|
||
expect(page.container.querySelector("video")).toBeNull();
|
||
|
||
fireEvent.click(page.getByRole("button", { name: "Play: Clip 2" }));
|
||
const video = page.container.querySelector("video")!;
|
||
expect(video.getAttribute("src")).toBe("/api/public/watch/tok-folder/files/2/file");
|
||
expect(video.autoplay).toBe(true);
|
||
expect(page.getByRole("button", { name: "Play: Clip 2" }).getAttribute("aria-current")).toBe(
|
||
"true",
|
||
);
|
||
// The file's title sits under the folder's, one level down.
|
||
expect(page.getByRole("heading", { level: 2 }).textContent).toBe("Clip 2");
|
||
expect(scrolled).toHaveBeenCalled();
|
||
|
||
fireEvent.click(page.getByRole("button", { name: "Play: Clip 1" }));
|
||
expect(page.container.querySelector("video")!.getAttribute("src")).toBe(
|
||
"/api/public/watch/tok-folder/files/1/file",
|
||
);
|
||
});
|
||
|
||
it("opens a picture above the list, shown rather than played", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-folder");
|
||
mockMeta(
|
||
folder({
|
||
files: [
|
||
file(1),
|
||
file(4, {
|
||
title: "Wave.gif",
|
||
media_kind: "file",
|
||
preview: "image",
|
||
container: "gif",
|
||
duration_s: null,
|
||
}),
|
||
],
|
||
}),
|
||
);
|
||
const page = render(<WatchPage />);
|
||
fireEvent.click(await page.findByRole("button", { name: "Open: Wave.gif" }));
|
||
const img = await page.findByRole("img", { name: "Wave.gif" });
|
||
expect(img.getAttribute("src")).toBe("/api/public/watch/tok-folder/files/4/file");
|
||
expect(page.container.querySelector("video")).toBeNull();
|
||
expect(page.getByRole("heading", { level: 2 }).textContent).toBe("Wave.gif");
|
||
});
|
||
|
||
it("offers a document for download only, and Download only where the link allows it", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-folder");
|
||
mockMeta(folder());
|
||
const page = render(<WatchPage />);
|
||
await page.findByText("Plan.pdf");
|
||
expect(page.queryByRole("button", { name: "Play: Plan.pdf" })).toBeNull();
|
||
const pdf = page.getByRole("link", { name: "Download: Plan.pdf" });
|
||
expect(pdf.getAttribute("href")).toBe("/api/public/watch/tok-folder/files/3/file");
|
||
cleanup();
|
||
|
||
mockMeta(folder({ allow_download: false }));
|
||
const streamOnly = render(<WatchPage />);
|
||
await streamOnly.findByText("Plan.pdf");
|
||
expect(streamOnly.queryAllByRole("link", { name: /^Download/ })).toEqual([]);
|
||
});
|
||
|
||
it("each file keeps its own place in a folder", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-folder-resume");
|
||
mockMeta(folder());
|
||
const page = render(<WatchPage />);
|
||
fireEvent.click(await page.findByRole("button", { name: "Play: Clip 1" }));
|
||
const video = page.container.querySelector("video")!;
|
||
let t = 0;
|
||
Object.defineProperties(video, {
|
||
currentTime: { get: () => t, set: (v: number) => void (t = v), configurable: true },
|
||
duration: { get: () => 600, configurable: true },
|
||
readyState: { get: () => 1, configurable: true },
|
||
});
|
||
video.dispatchEvent(new Event("loadedmetadata"));
|
||
video.dispatchEvent(new Event("playing"));
|
||
video.currentTime = 250;
|
||
video.dispatchEvent(new Event("pause"));
|
||
const points = JSON.stringify(localStorage);
|
||
expect(points).toContain("tok-folder-resume/1");
|
||
expect(points).not.toContain('"tok-folder-resume"');
|
||
});
|
||
|
||
it("says when the folder is empty", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-folder");
|
||
mockMeta(folder({ files: [] }));
|
||
const page = render(<WatchPage />);
|
||
expect(await page.findByText("This folder is empty.")).toBeTruthy();
|
||
expect(page.getByText("0 files")).toBeTruthy();
|
||
});
|
||
|
||
it("a password folder link asks for the folder's password", async () => {
|
||
window.history.replaceState({}, "", "/watch/tok-folder-locked");
|
||
mockMeta({ needs_password: true, kind: "folder" });
|
||
const page = render(<WatchPage />);
|
||
expect(await page.findByText("This folder is password-protected")).toBeTruthy();
|
||
});
|
||
});
|