frigate/web/e2e/helpers/ws-mocker.ts
Josh Hawkins c750372586
Add frontend tests (#22783)
* basic e2e frontend test framework

* improve mock data generation and add test cases

* more cases

* add e2e tests to PR template

* don't generate mock data in PR CI

* satisfy codeql check

* fix flaky system page tab tests by guarding against crashes from incomplete mock stats

* reduce local test runs to 4 workers to match CI
2026-04-06 16:33:28 -06:00

126 lines
3.6 KiB
TypeScript

/**
* WebSocket mock using Playwright's native page.routeWebSocket().
*
* Intercepts the app's WebSocket connection and simulates the Frigate
* WS protocol: onConnect handshake, camera_activity expansion, and
* topic-based state updates.
*/
import type { Page, WebSocketRoute } from "@playwright/test";
import { cameraActivityPayload } from "../fixtures/mock-data/camera-activity";
export class WsMocker {
private mockWs: WebSocketRoute | null = null;
private cameras: string[];
constructor(cameras: string[] = ["front_door", "backyard", "garage"]) {
this.cameras = cameras;
}
async install(page: Page) {
await page.routeWebSocket("**/ws", (ws) => {
this.mockWs = ws;
ws.onMessage((msg) => {
this.handleClientMessage(msg.toString());
});
});
}
private handleClientMessage(raw: string) {
let data: { topic: string; payload?: unknown; message?: string };
try {
data = JSON.parse(raw);
} catch {
return;
}
if (data.topic === "onConnect") {
// Send initial camera_activity state
this.sendCameraActivity();
// Send initial stats
this.send(
"stats",
JSON.stringify({
cameras: Object.fromEntries(
this.cameras.map((c) => [
c,
{
camera_fps: 5,
detection_fps: 5,
process_fps: 5,
skipped_fps: 0,
detection_enabled: 1,
connection_quality: "excellent",
},
]),
),
service: {
last_updated: Date.now() / 1000,
uptime: 86400,
version: "0.15.0-test",
latest_version: "0.15.0",
storage: {},
},
detectors: {},
cpu_usages: {},
gpu_usages: {},
camera_fps: 15,
process_fps: 15,
skipped_fps: 0,
detection_fps: 15,
}),
);
}
// Echo back state commands (e.g., modelState, jobState, etc.)
if (data.topic === "modelState") {
this.send("model_state", JSON.stringify({}));
}
if (data.topic === "embeddingsReindexProgress") {
this.send("embeddings_reindex_progress", JSON.stringify(null));
}
if (data.topic === "birdseyeLayout") {
this.send("birdseye_layout", JSON.stringify(null));
}
if (data.topic === "jobState") {
this.send("job_state", JSON.stringify({}));
}
if (data.topic === "audioTranscriptionState") {
this.send("audio_transcription_state", JSON.stringify("idle"));
}
// Camera toggle commands: echo back the new state
const toggleMatch = data.topic?.match(
/^(.+)\/(detect|recordings|snapshots|audio|enabled|notifications|ptz_autotracker|review_alerts|review_detections|object_descriptions|review_descriptions|audio_transcription)\/set$/,
);
if (toggleMatch) {
const [, camera, feature] = toggleMatch;
this.send(`${camera}/${feature}/state`, data.payload);
}
}
/** Send a raw WS message to the app */
send(topic: string, payload: unknown) {
if (!this.mockWs) return;
this.mockWs.send(JSON.stringify({ topic, payload }));
}
/** Send camera_activity with default or custom state */
sendCameraActivity(overrides?: Parameters<typeof cameraActivityPayload>[1]) {
const payload = cameraActivityPayload(this.cameras, overrides);
this.send("camera_activity", payload);
}
/** Send a review update */
sendReview(review: unknown) {
this.send("reviews", JSON.stringify(review));
}
/** Send an event update */
sendEvent(event: unknown) {
this.send("events", JSON.stringify(event));
}
}