- "Pop out" on each dashboard card opens /cameras/[id]/live in a named popup sized to the main stream (a plain link if popups are blocked). - The live page shows only the frame, never stretched past its own size, with a fading name/time/refresh-rate overlay and error handling. - The card pauses its own polling while its pop-out is open; the two coordinate over a BroadcastChannel. - Share frame rendering (FrameImage) between card and pop-out. - Ignore a stray build cache folder; export vrek log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { announcePopout, openPopout, popoutUrl, popoutWindowName, watchPopout } from "./popout";
|
|
|
|
const ID = "11111111-2222-3333-4444-555555555555";
|
|
const OTHER = "99999999-2222-3333-4444-555555555555";
|
|
// BroadcastChannel delivers asynchronously; give it a turn of the event loop.
|
|
const settle = () => new Promise((r) => setTimeout(r, 20));
|
|
|
|
const cleanups: (() => void)[] = [];
|
|
afterEach(() => {
|
|
while (cleanups.length) cleanups.pop()!();
|
|
});
|
|
|
|
describe("popout URLs", () => {
|
|
it("builds the live page URL and a per-camera window name", () => {
|
|
expect(popoutUrl(ID, 500)).toBe(`/cameras/${ID}/live?refresh=500`);
|
|
expect(popoutWindowName(ID)).toBe(`camera-${ID}`);
|
|
});
|
|
});
|
|
|
|
describe("openPopout", () => {
|
|
it("opens a popup sized to the stream's aspect ratio and focuses it", () => {
|
|
vi.spyOn(window.screen, "availWidth", "get").mockReturnValue(2000);
|
|
const focus = vi.fn();
|
|
const open = vi.spyOn(window, "open").mockReturnValue({ focus } as unknown as Window);
|
|
expect(openPopout(ID, 1000, 4096 / 1860)).toBe(true);
|
|
expect(open).toHaveBeenCalledWith(`/cameras/${ID}/live?refresh=1000`, `camera-${ID}`, "popup,width=1280,height=581");
|
|
expect(focus).toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses 16:9 and at most 80% of a small screen", () => {
|
|
vi.spyOn(window.screen, "availWidth", "get").mockReturnValue(1000);
|
|
const open = vi.spyOn(window, "open").mockReturnValue({ focus: vi.fn() } as unknown as Window);
|
|
openPopout(ID, 1000);
|
|
expect(open.mock.calls[0][2]).toBe("popup,width=800,height=450");
|
|
});
|
|
|
|
it("uses the default width when the screen size is unknown", () => {
|
|
vi.spyOn(window.screen, "availWidth", "get").mockReturnValue(0);
|
|
const open = vi.spyOn(window, "open").mockReturnValue({ focus: vi.fn() } as unknown as Window);
|
|
openPopout(ID, 1000);
|
|
expect(open.mock.calls[0][2]).toBe("popup,width=1280,height=720");
|
|
});
|
|
|
|
it("reports a blocked popup", () => {
|
|
vi.spyOn(window, "open").mockReturnValue(null);
|
|
expect(openPopout(ID, 1000)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("pop-out and card coordination", () => {
|
|
it("tells a watching card when its camera's pop-out opens and closes", async () => {
|
|
const onChange = vi.fn();
|
|
cleanups.push(watchPopout(ID, onChange));
|
|
const close = announcePopout(ID);
|
|
await settle();
|
|
expect(onChange).toHaveBeenLastCalledWith(true);
|
|
close();
|
|
await settle();
|
|
expect(onChange).toHaveBeenLastCalledWith(false);
|
|
});
|
|
|
|
it("answers a card that starts watching after the pop-out opened", async () => {
|
|
cleanups.push(announcePopout(ID));
|
|
await settle();
|
|
const onChange = vi.fn();
|
|
cleanups.push(watchPopout(ID, onChange));
|
|
await settle();
|
|
expect(onChange).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it("ignores other cameras' pop-outs", async () => {
|
|
const onChange = vi.fn();
|
|
cleanups.push(watchPopout(ID, onChange));
|
|
cleanups.push(announcePopout(OTHER));
|
|
await settle();
|
|
expect(onChange).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("announces closing when the window goes away", async () => {
|
|
const onChange = vi.fn();
|
|
cleanups.push(watchPopout(ID, onChange));
|
|
cleanups.push(announcePopout(ID));
|
|
await settle();
|
|
window.dispatchEvent(new Event("pagehide"));
|
|
await settle();
|
|
expect(onChange).toHaveBeenLastCalledWith(false);
|
|
});
|
|
|
|
it("does nothing where BroadcastChannel is unavailable", () => {
|
|
vi.stubGlobal("BroadcastChannel", undefined);
|
|
const onChange = vi.fn();
|
|
expect(() => {
|
|
watchPopout(ID, onChange)();
|
|
announcePopout(ID)();
|
|
}).not.toThrow();
|
|
expect(onChange).not.toHaveBeenCalled();
|
|
});
|
|
});
|