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>
87 lines
3.4 KiB
TypeScript
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, "&"));
|
|
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);
|
|
});
|
|
});
|