cameras/src/app/recordings/play/page.test.tsx
Michael Mainguy 0acd69b9ca Play recordings in a pop-out window, like the live view
Play now opens /recordings/play?clip=… in a window of its own: the video
on black, fitted to the window, with a fading overlay naming the camera
and the time. One window is reused for whichever clip is opened next, and
it stays an ordinary link so a blocked popup still opens a tab. The page
checks the session and resolves the clip inside the recordings folder.

Also add DOM cleanup to three test files that had none: renders piled up
in the document and tests read each other's markup, which is what made
the new test pass or fail depending on its position in the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 05:38:51 -05:00

87 lines
3.4 KiB
TypeScript

import { renderToStaticMarkup } from "react-dom/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Clip } from "@/lib/recordings";
const requirePageAccess = vi.fn();
vi.mock("@/lib/access", () => ({ requirePageAccess }));
const listClips = vi.fn<() => Promise<Clip[]>>();
const allCameraRecords = vi.fn();
vi.mock("@/lib/recordings", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/recordings")>()),
listClips,
}));
vi.mock("@/lib/camera-registry", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/camera-registry")>()),
allCameraRecords,
}));
class NotFound extends Error {}
vi.mock("next/navigation", () => ({
notFound: () => {
throw new NotFound();
},
}));
const { default: PlayClipPage, generateMetadata } = await import("./page");
const ID = "22ec0000-8b90-11b5-845d-d03bf404af54";
const CLIP = `cam-${ID}-sub-rec/2026-09-19_17-06-18-101298.mp4`;
const clip: Clip = { id: CLIP, cameraId: ID, startedAt: "2026-09-19T22:06:18.000Z", bytes: 2_582_503 };
const record = { id: ID, urn: `urn:uuid:${ID}`, host: "192.168.17.129", port: 80, name: "Porch", lastSeen: "" };
const props = (id: string = CLIP) => ({
params: Promise.resolve({}),
searchParams: Promise.resolve({ clip: id }),
});
/** No ?clip= at all, which a default parameter would quietly paper over. */
const noClip = { params: Promise.resolve({}), searchParams: Promise.resolve({}) };
const markup = async (id?: string) => renderToStaticMarkup(await PlayClipPage(props(id)));
beforeEach(() => {
requirePageAccess.mockReset().mockResolvedValue(undefined);
listClips.mockReset().mockResolvedValue([clip]);
allCameraRecords.mockReset().mockResolvedValue([record]);
});
describe("/recordings/play", () => {
it("checks access before looking anything up", async () => {
requirePageAccess.mockRejectedValue(new Error("redirect /login"));
await expect(PlayClipPage(props())).rejects.toThrow("redirect /login");
await expect(generateMetadata(props())).rejects.toThrow("redirect /login");
expect(listClips).not.toHaveBeenCalled();
});
it("plays the clip through our own route, with the camera and time shown", async () => {
const html = await markup();
expect(html).toContain(`/api/recordings/clip?clip=${encodeURIComponent(CLIP)}`.replace(/&/g, "&amp;"));
expect(html).toContain("Porch");
expect(html).toContain('dateTime="2026-09-19T22:06:18.000Z"');
});
it("titles the window after the camera and the time", async () => {
const { title } = await generateMetadata(props());
expect(title).toContain("Porch");
});
it("falls back to the address for an unnamed camera, and says so for an unknown one", async () => {
allCameraRecords.mockResolvedValue([{ ...record, name: undefined }]);
expect(await markup()).toContain("192.168.17.129:80");
allCameraRecords.mockResolvedValue([]);
expect(await markup()).toContain("Unknown camera");
});
it.each([
["a clip id that isn't ours", "../../etc/passwd"],
["a clip that isn't on disk", `cam-${ID}-main-rec/2026-01-01_00-00-00-000000.mp4`],
["an empty clip id", ""],
])("is not found for %s", async (_label, id) => {
await expect(PlayClipPage(props(id))).rejects.toBeInstanceOf(NotFound);
});
it("is not found when no clip is named at all", async () => {
await expect(PlayClipPage(noClip)).rejects.toBeInstanceOf(NotFound);
await expect(generateMetadata(noClip)).rejects.toBeInstanceOf(NotFound);
});
});