mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-02 09:02:15 +03:00
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
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Camera activity WebSocket payload factory.
|
||||
*
|
||||
* The camera_activity topic payload is double-serialized:
|
||||
* the WS message contains { topic: "camera_activity", payload: JSON.stringify(activityMap) }
|
||||
*/
|
||||
|
||||
export interface CameraActivityState {
|
||||
config: {
|
||||
enabled: boolean;
|
||||
detect: boolean;
|
||||
record: boolean;
|
||||
snapshots: boolean;
|
||||
audio: boolean;
|
||||
audio_transcription: boolean;
|
||||
notifications: boolean;
|
||||
notifications_suspended: number;
|
||||
autotracking: boolean;
|
||||
alerts: boolean;
|
||||
detections: boolean;
|
||||
object_descriptions: boolean;
|
||||
review_descriptions: boolean;
|
||||
};
|
||||
motion: boolean;
|
||||
objects: Array<{
|
||||
label: string;
|
||||
score: number;
|
||||
box: [number, number, number, number];
|
||||
area: number;
|
||||
ratio: number;
|
||||
region: [number, number, number, number];
|
||||
current_zones: string[];
|
||||
id: string;
|
||||
}>;
|
||||
audio_detections: Array<{
|
||||
label: string;
|
||||
score: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
function defaultCameraActivity(): CameraActivityState {
|
||||
return {
|
||||
config: {
|
||||
enabled: true,
|
||||
detect: true,
|
||||
record: true,
|
||||
snapshots: true,
|
||||
audio: false,
|
||||
audio_transcription: false,
|
||||
notifications: false,
|
||||
notifications_suspended: 0,
|
||||
autotracking: false,
|
||||
alerts: true,
|
||||
detections: true,
|
||||
object_descriptions: false,
|
||||
review_descriptions: false,
|
||||
},
|
||||
motion: false,
|
||||
objects: [],
|
||||
audio_detections: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function cameraActivityPayload(
|
||||
cameras: string[],
|
||||
overrides?: Partial<Record<string, Partial<CameraActivityState>>>,
|
||||
): string {
|
||||
const activity: Record<string, CameraActivityState> = {};
|
||||
for (const name of cameras) {
|
||||
activity[name] = {
|
||||
...defaultCameraActivity(),
|
||||
...overrides?.[name],
|
||||
} as CameraActivityState;
|
||||
}
|
||||
// Double-serialize: the WS payload is a JSON string
|
||||
return JSON.stringify(activity);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[{"id": "case-001", "name": "Package Theft Investigation", "description": "Review of suspicious activity near the front porch", "created_at": 1775407931.3863528, "updated_at": 1775483531.3863528}]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* FrigateConfig factory for E2E tests.
|
||||
*
|
||||
* Uses a real config snapshot generated from the Python backend's FrigateConfig
|
||||
* model. This guarantees all fields are present and match what the app expects.
|
||||
* Tests override specific fields via DeepPartial.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const configSnapshot = JSON.parse(
|
||||
readFileSync(resolve(__dirname, "config-snapshot.json"), "utf-8"),
|
||||
);
|
||||
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||
};
|
||||
|
||||
function deepMerge<T extends Record<string, unknown>>(
|
||||
base: T,
|
||||
overrides?: DeepPartial<T>,
|
||||
): T {
|
||||
if (!overrides) return base;
|
||||
const result = { ...base };
|
||||
for (const key of Object.keys(overrides) as (keyof T)[]) {
|
||||
const val = overrides[key];
|
||||
if (
|
||||
val !== undefined &&
|
||||
typeof val === "object" &&
|
||||
val !== null &&
|
||||
!Array.isArray(val) &&
|
||||
typeof base[key] === "object" &&
|
||||
base[key] !== null &&
|
||||
!Array.isArray(base[key])
|
||||
) {
|
||||
result[key] = deepMerge(
|
||||
base[key] as Record<string, unknown>,
|
||||
val as DeepPartial<Record<string, unknown>>,
|
||||
) as T[keyof T];
|
||||
} else if (val !== undefined) {
|
||||
result[key] = val as T[keyof T];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// The base config is a real snapshot from the Python backend.
|
||||
// Apply test-specific overrides: friendly names, camera groups, version.
|
||||
export const BASE_CONFIG = {
|
||||
...configSnapshot,
|
||||
version: "0.15.0-test",
|
||||
cameras: {
|
||||
...configSnapshot.cameras,
|
||||
front_door: {
|
||||
...configSnapshot.cameras.front_door,
|
||||
friendly_name: "Front Door",
|
||||
},
|
||||
backyard: {
|
||||
...configSnapshot.cameras.backyard,
|
||||
friendly_name: "Backyard",
|
||||
},
|
||||
garage: {
|
||||
...configSnapshot.cameras.garage,
|
||||
friendly_name: "Garage",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function configFactory(
|
||||
overrides?: DeepPartial<typeof BASE_CONFIG>,
|
||||
): typeof BASE_CONFIG {
|
||||
return deepMerge(BASE_CONFIG, overrides);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[{"id": "event-person-001", "label": "person", "sub_label": null, "camera": "front_door", "start_time": 1775487131.3863528, "end_time": 1775487161.3863528, "false_positive": false, "zones": ["front_yard"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "abc123", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.92, "score": 0.92, "region": [0.1, 0.1, 0.5, 0.8], "box": [0.2, 0.15, 0.45, 0.75], "area": 0.18, "ratio": 0.6, "type": "object", "description": "A person walking toward the front door", "average_estimated_speed": 1.2, "velocity_angle": 45.0, "path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]]}}, {"id": "event-car-001", "label": "car", "sub_label": null, "camera": "backyard", "start_time": 1775483531.3863528, "end_time": 1775483576.3863528, "false_positive": false, "zones": ["driveway"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "def456", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.87, "score": 0.87, "region": [0.3, 0.2, 0.9, 0.7], "box": [0.35, 0.25, 0.85, 0.65], "area": 0.2, "ratio": 1.25, "type": "object", "description": "A car parked in the driveway", "average_estimated_speed": 0.0, "velocity_angle": 0.0, "path_data": []}}, {"id": "event-person-002", "label": "person", "sub_label": null, "camera": "garage", "start_time": 1775479931.3863528, "end_time": 1775479951.3863528, "false_positive": false, "zones": [], "thumbnail": null, "has_clip": false, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "ghi789", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.78, "score": 0.78, "region": [0.0, 0.0, 0.6, 0.9], "box": [0.1, 0.05, 0.5, 0.85], "area": 0.32, "ratio": 0.5, "type": "object", "description": null, "average_estimated_speed": 0.5, "velocity_angle": 90.0, "path_data": [[[0.1, 0.4], 0.0]]}}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1775490731.3863528, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1775483531.3863528, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1775492531.3863528, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}]
|
||||
@@ -0,0 +1,426 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate E2E mock data from backend Pydantic and Peewee models.
|
||||
|
||||
Run from the repo root:
|
||||
PYTHONPATH=/workspace/frigate python3 web/e2e/fixtures/mock-data/generate-mock-data.py
|
||||
|
||||
Strategy:
|
||||
- FrigateConfig: instantiate the Pydantic config model, then model_dump()
|
||||
- API responses: instantiate Pydantic response models (ReviewSegmentResponse,
|
||||
EventResponse, ExportModel, ExportCaseModel) to validate all required fields
|
||||
- If the backend adds a required field, this script fails at instantiation time
|
||||
- The Peewee model field list is checked to detect new columns that would
|
||||
appear in .dicts() API responses but aren't in our mock data
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
OUTPUT_DIR = Path(__file__).parent
|
||||
NOW = time.time()
|
||||
HOUR = 3600
|
||||
|
||||
CAMERAS = ["front_door", "backyard", "garage"]
|
||||
|
||||
|
||||
def check_pydantic_fields(pydantic_class, mock_keys, model_name):
|
||||
"""Verify mock data covers all fields declared in the Pydantic response model.
|
||||
|
||||
The Pydantic response model is what the frontend actually receives.
|
||||
Peewee models may have extra legacy columns that are filtered out by
|
||||
FastAPI's response_model validation.
|
||||
"""
|
||||
required_fields = set()
|
||||
for name, field_info in pydantic_class.model_fields.items():
|
||||
required_fields.add(name)
|
||||
|
||||
missing = required_fields - mock_keys
|
||||
if missing:
|
||||
print(
|
||||
f" ERROR: {model_name} response model has fields not in mock data: {missing}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f" Add these fields to the mock data in this script.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
extra = mock_keys - required_fields
|
||||
if extra:
|
||||
print(
|
||||
f" NOTE: {model_name} mock data has extra fields (not in response model): {extra}",
|
||||
)
|
||||
|
||||
|
||||
def generate_config():
|
||||
"""Generate FrigateConfig from the Python backend model."""
|
||||
from frigate.config import FrigateConfig
|
||||
|
||||
config = FrigateConfig.model_validate_json(
|
||||
json.dumps(
|
||||
{
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
cam: {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": f"rtsp://10.0.0.{i+1}:554/video",
|
||||
"roles": ["detect"],
|
||||
}
|
||||
]
|
||||
},
|
||||
"detect": {"height": 720, "width": 1280, "fps": 5},
|
||||
}
|
||||
for i, cam in enumerate(CAMERAS)
|
||||
},
|
||||
"camera_groups": {
|
||||
"default": {
|
||||
"cameras": CAMERAS,
|
||||
"icon": "generic",
|
||||
"order": 0,
|
||||
},
|
||||
"outdoor": {
|
||||
"cameras": ["front_door", "backyard"],
|
||||
"icon": "generic",
|
||||
"order": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
snapshot = config.model_dump()
|
||||
|
||||
# Runtime-computed fields not in the Pydantic dump
|
||||
all_attrs = set()
|
||||
for attrs in snapshot.get("model", {}).get("attributes_map", {}).values():
|
||||
all_attrs.update(attrs)
|
||||
snapshot["model"]["all_attributes"] = sorted(all_attrs)
|
||||
snapshot["model"]["colormap"] = {}
|
||||
|
||||
return snapshot
|
||||
|
||||
|
||||
def generate_reviews():
|
||||
"""Generate ReviewSegmentResponse[] validated against Pydantic + Peewee."""
|
||||
from frigate.api.defs.response.review_response import ReviewSegmentResponse
|
||||
|
||||
reviews = [
|
||||
ReviewSegmentResponse(
|
||||
id="review-alert-001",
|
||||
camera="front_door",
|
||||
severity="alert",
|
||||
start_time=datetime.fromtimestamp(NOW - 2 * HOUR),
|
||||
end_time=datetime.fromtimestamp(NOW - 2 * HOUR + 30),
|
||||
has_been_reviewed=False,
|
||||
thumb_path="/clips/front_door/review-alert-001-thumb.jpg",
|
||||
data=json.dumps(
|
||||
{
|
||||
"audio": [],
|
||||
"detections": ["person-abc123"],
|
||||
"objects": ["person"],
|
||||
"sub_labels": [],
|
||||
"significant_motion_areas": [],
|
||||
"zones": ["front_yard"],
|
||||
}
|
||||
),
|
||||
),
|
||||
ReviewSegmentResponse(
|
||||
id="review-alert-002",
|
||||
camera="backyard",
|
||||
severity="alert",
|
||||
start_time=datetime.fromtimestamp(NOW - 3 * HOUR),
|
||||
end_time=datetime.fromtimestamp(NOW - 3 * HOUR + 45),
|
||||
has_been_reviewed=True,
|
||||
thumb_path="/clips/backyard/review-alert-002-thumb.jpg",
|
||||
data=json.dumps(
|
||||
{
|
||||
"audio": [],
|
||||
"detections": ["car-def456"],
|
||||
"objects": ["car"],
|
||||
"sub_labels": [],
|
||||
"significant_motion_areas": [],
|
||||
"zones": ["driveway"],
|
||||
}
|
||||
),
|
||||
),
|
||||
ReviewSegmentResponse(
|
||||
id="review-detect-001",
|
||||
camera="garage",
|
||||
severity="detection",
|
||||
start_time=datetime.fromtimestamp(NOW - 4 * HOUR),
|
||||
end_time=datetime.fromtimestamp(NOW - 4 * HOUR + 20),
|
||||
has_been_reviewed=False,
|
||||
thumb_path="/clips/garage/review-detect-001-thumb.jpg",
|
||||
data=json.dumps(
|
||||
{
|
||||
"audio": [],
|
||||
"detections": ["person-ghi789"],
|
||||
"objects": ["person"],
|
||||
"sub_labels": [],
|
||||
"significant_motion_areas": [],
|
||||
"zones": [],
|
||||
}
|
||||
),
|
||||
),
|
||||
ReviewSegmentResponse(
|
||||
id="review-detect-002",
|
||||
camera="front_door",
|
||||
severity="detection",
|
||||
start_time=datetime.fromtimestamp(NOW - 5 * HOUR),
|
||||
end_time=datetime.fromtimestamp(NOW - 5 * HOUR + 15),
|
||||
has_been_reviewed=False,
|
||||
thumb_path="/clips/front_door/review-detect-002-thumb.jpg",
|
||||
data=json.dumps(
|
||||
{
|
||||
"audio": [],
|
||||
"detections": ["car-jkl012"],
|
||||
"objects": ["car"],
|
||||
"sub_labels": [],
|
||||
"significant_motion_areas": [],
|
||||
"zones": ["front_yard"],
|
||||
}
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
result = [r.model_dump(mode="json") for r in reviews]
|
||||
|
||||
# Verify mock data covers all Pydantic response model fields
|
||||
check_pydantic_fields(
|
||||
ReviewSegmentResponse, set(result[0].keys()), "ReviewSegment"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_events():
|
||||
"""Generate EventResponse[] validated against Pydantic + Peewee."""
|
||||
from frigate.api.defs.response.event_response import EventResponse
|
||||
|
||||
events = [
|
||||
EventResponse(
|
||||
id="event-person-001",
|
||||
label="person",
|
||||
sub_label=None,
|
||||
camera="front_door",
|
||||
start_time=NOW - 2 * HOUR,
|
||||
end_time=NOW - 2 * HOUR + 30,
|
||||
false_positive=False,
|
||||
zones=["front_yard"],
|
||||
thumbnail=None,
|
||||
has_clip=True,
|
||||
has_snapshot=True,
|
||||
retain_indefinitely=False,
|
||||
plus_id=None,
|
||||
model_hash="abc123",
|
||||
detector_type="cpu",
|
||||
model_type="ssd",
|
||||
data={
|
||||
"top_score": 0.92,
|
||||
"score": 0.92,
|
||||
"region": [0.1, 0.1, 0.5, 0.8],
|
||||
"box": [0.2, 0.15, 0.45, 0.75],
|
||||
"area": 0.18,
|
||||
"ratio": 0.6,
|
||||
"type": "object",
|
||||
"description": "A person walking toward the front door",
|
||||
"average_estimated_speed": 1.2,
|
||||
"velocity_angle": 45.0,
|
||||
"path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]],
|
||||
},
|
||||
),
|
||||
EventResponse(
|
||||
id="event-car-001",
|
||||
label="car",
|
||||
sub_label=None,
|
||||
camera="backyard",
|
||||
start_time=NOW - 3 * HOUR,
|
||||
end_time=NOW - 3 * HOUR + 45,
|
||||
false_positive=False,
|
||||
zones=["driveway"],
|
||||
thumbnail=None,
|
||||
has_clip=True,
|
||||
has_snapshot=True,
|
||||
retain_indefinitely=False,
|
||||
plus_id=None,
|
||||
model_hash="def456",
|
||||
detector_type="cpu",
|
||||
model_type="ssd",
|
||||
data={
|
||||
"top_score": 0.87,
|
||||
"score": 0.87,
|
||||
"region": [0.3, 0.2, 0.9, 0.7],
|
||||
"box": [0.35, 0.25, 0.85, 0.65],
|
||||
"area": 0.2,
|
||||
"ratio": 1.25,
|
||||
"type": "object",
|
||||
"description": "A car parked in the driveway",
|
||||
"average_estimated_speed": 0.0,
|
||||
"velocity_angle": 0.0,
|
||||
"path_data": [],
|
||||
},
|
||||
),
|
||||
EventResponse(
|
||||
id="event-person-002",
|
||||
label="person",
|
||||
sub_label=None,
|
||||
camera="garage",
|
||||
start_time=NOW - 4 * HOUR,
|
||||
end_time=NOW - 4 * HOUR + 20,
|
||||
false_positive=False,
|
||||
zones=[],
|
||||
thumbnail=None,
|
||||
has_clip=False,
|
||||
has_snapshot=True,
|
||||
retain_indefinitely=False,
|
||||
plus_id=None,
|
||||
model_hash="ghi789",
|
||||
detector_type="cpu",
|
||||
model_type="ssd",
|
||||
data={
|
||||
"top_score": 0.78,
|
||||
"score": 0.78,
|
||||
"region": [0.0, 0.0, 0.6, 0.9],
|
||||
"box": [0.1, 0.05, 0.5, 0.85],
|
||||
"area": 0.32,
|
||||
"ratio": 0.5,
|
||||
"type": "object",
|
||||
"description": None,
|
||||
"average_estimated_speed": 0.5,
|
||||
"velocity_angle": 90.0,
|
||||
"path_data": [[[0.1, 0.4], 0.0]],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
result = [e.model_dump(mode="json") for e in events]
|
||||
|
||||
check_pydantic_fields(EventResponse, set(result[0].keys()), "Event")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_exports():
|
||||
"""Generate ExportModel[] validated against Pydantic + Peewee."""
|
||||
from frigate.api.defs.response.export_response import ExportModel
|
||||
|
||||
exports = [
|
||||
ExportModel(
|
||||
id="export-001",
|
||||
camera="front_door",
|
||||
name="Front Door - Person Alert",
|
||||
date=NOW - 1 * HOUR,
|
||||
video_path="/exports/export-001.mp4",
|
||||
thumb_path="/exports/export-001-thumb.jpg",
|
||||
in_progress=False,
|
||||
export_case_id=None,
|
||||
),
|
||||
ExportModel(
|
||||
id="export-002",
|
||||
camera="backyard",
|
||||
name="Backyard - Car Detection",
|
||||
date=NOW - 3 * HOUR,
|
||||
video_path="/exports/export-002.mp4",
|
||||
thumb_path="/exports/export-002-thumb.jpg",
|
||||
in_progress=False,
|
||||
export_case_id="case-001",
|
||||
),
|
||||
ExportModel(
|
||||
id="export-003",
|
||||
camera="garage",
|
||||
name="Garage - In Progress",
|
||||
date=NOW - 0.5 * HOUR,
|
||||
video_path="/exports/export-003.mp4",
|
||||
thumb_path="/exports/export-003-thumb.jpg",
|
||||
in_progress=True,
|
||||
export_case_id=None,
|
||||
),
|
||||
]
|
||||
|
||||
result = [e.model_dump(mode="json") for e in exports]
|
||||
|
||||
check_pydantic_fields(ExportModel, set(result[0].keys()), "Export")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_cases():
|
||||
"""Generate ExportCaseModel[] validated against Pydantic + Peewee."""
|
||||
from frigate.api.defs.response.export_case_response import ExportCaseModel
|
||||
|
||||
cases = [
|
||||
ExportCaseModel(
|
||||
id="case-001",
|
||||
name="Package Theft Investigation",
|
||||
description="Review of suspicious activity near the front porch",
|
||||
created_at=NOW - 24 * HOUR,
|
||||
updated_at=NOW - 3 * HOUR,
|
||||
),
|
||||
]
|
||||
|
||||
result = [c.model_dump(mode="json") for c in cases]
|
||||
|
||||
check_pydantic_fields(ExportCaseModel, set(result[0].keys()), "ExportCase")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_review_summary():
|
||||
"""Generate ReviewSummary for the calendar filter."""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
|
||||
return {
|
||||
today: {
|
||||
"day": today,
|
||||
"reviewed_alert": 1,
|
||||
"reviewed_detection": 0,
|
||||
"total_alert": 2,
|
||||
"total_detection": 2,
|
||||
},
|
||||
yesterday: {
|
||||
"day": yesterday,
|
||||
"reviewed_alert": 3,
|
||||
"reviewed_detection": 2,
|
||||
"total_alert": 3,
|
||||
"total_detection": 4,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def write_json(filename, data):
|
||||
path = OUTPUT_DIR / filename
|
||||
path.write_text(json.dumps(data, default=str))
|
||||
print(f" {path.name} ({path.stat().st_size} bytes)")
|
||||
|
||||
|
||||
def main():
|
||||
print("Generating E2E mock data from backend models...")
|
||||
print(" Validating against Pydantic response models + Peewee DB columns")
|
||||
print()
|
||||
|
||||
write_json("config-snapshot.json", generate_config())
|
||||
write_json("reviews.json", generate_reviews())
|
||||
write_json("events.json", generate_events())
|
||||
write_json("exports.json", generate_exports())
|
||||
write_json("cases.json", generate_cases())
|
||||
write_json("review-summary.json", generate_review_summary())
|
||||
|
||||
print()
|
||||
print("All mock data validated against backend schemas.")
|
||||
print("If this script fails, update the mock data to match the new schema.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* User profile factories for E2E tests.
|
||||
*/
|
||||
|
||||
export interface UserProfile {
|
||||
username: string;
|
||||
role: string;
|
||||
allowed_cameras: string[] | null;
|
||||
}
|
||||
|
||||
export function adminProfile(overrides?: Partial<UserProfile>): UserProfile {
|
||||
return {
|
||||
username: "admin",
|
||||
role: "admin",
|
||||
allowed_cameras: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function viewerProfile(overrides?: Partial<UserProfile>): UserProfile {
|
||||
return {
|
||||
username: "viewer",
|
||||
role: "viewer",
|
||||
allowed_cameras: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function restrictedProfile(
|
||||
cameras: string[],
|
||||
overrides?: Partial<UserProfile>,
|
||||
): UserProfile {
|
||||
return {
|
||||
username: "restricted",
|
||||
role: "viewer",
|
||||
allowed_cameras: cameras,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"2026-04-06": {"day": "2026-04-06", "reviewed_alert": 1, "reviewed_detection": 0, "total_alert": 2, "total_detection": 2}, "2026-04-05": {"day": "2026-04-05", "reviewed_alert": 3, "reviewed_detection": 2, "total_alert": 3, "total_detection": 4}}
|
||||
@@ -0,0 +1 @@
|
||||
[{"id": "review-alert-001", "camera": "front_door", "start_time": "2026-04-06T09:52:11.386353", "end_time": "2026-04-06T09:52:41.386353", "has_been_reviewed": false, "severity": "alert", "thumb_path": "/clips/front_door/review-alert-001-thumb.jpg", "data": {"audio": [], "detections": ["person-abc123"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}, {"id": "review-alert-002", "camera": "backyard", "start_time": "2026-04-06T08:52:11.386353", "end_time": "2026-04-06T08:52:56.386353", "has_been_reviewed": true, "severity": "alert", "thumb_path": "/clips/backyard/review-alert-002-thumb.jpg", "data": {"audio": [], "detections": ["car-def456"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["driveway"]}}, {"id": "review-detect-001", "camera": "garage", "start_time": "2026-04-06T07:52:11.386353", "end_time": "2026-04-06T07:52:31.386353", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/garage/review-detect-001-thumb.jpg", "data": {"audio": [], "detections": ["person-ghi789"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": []}}, {"id": "review-detect-002", "camera": "front_door", "start_time": "2026-04-06T06:52:11.386353", "end_time": "2026-04-06T06:52:26.386353", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/front_door/review-detect-002-thumb.jpg", "data": {"audio": [], "detections": ["car-jkl012"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}]
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* FrigateStats factory for E2E tests.
|
||||
*/
|
||||
|
||||
import type { DeepPartial } from "./config";
|
||||
|
||||
function cameraStats(_name: string) {
|
||||
return {
|
||||
audio_dBFPS: 0,
|
||||
audio_rms: 0,
|
||||
camera_fps: 5.0,
|
||||
capture_pid: 100,
|
||||
detection_enabled: 1,
|
||||
detection_fps: 5.0,
|
||||
ffmpeg_pid: 101,
|
||||
pid: 102,
|
||||
process_fps: 5.0,
|
||||
skipped_fps: 0,
|
||||
connection_quality: "excellent" as const,
|
||||
expected_fps: 5,
|
||||
reconnects_last_hour: 0,
|
||||
stalls_last_hour: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export const BASE_STATS = {
|
||||
cameras: {
|
||||
front_door: cameraStats("front_door"),
|
||||
backyard: cameraStats("backyard"),
|
||||
garage: cameraStats("garage"),
|
||||
},
|
||||
cpu_usages: {
|
||||
"1": { cmdline: "frigate.app", cpu: "5.0", cpu_average: "4.5", mem: "2.1" },
|
||||
},
|
||||
detectors: {
|
||||
cpu: {
|
||||
detection_start: 0,
|
||||
inference_speed: 75.5,
|
||||
pid: 200,
|
||||
},
|
||||
},
|
||||
gpu_usages: {},
|
||||
npu_usages: {},
|
||||
processes: {},
|
||||
service: {
|
||||
last_updated: Date.now() / 1000,
|
||||
storage: {
|
||||
"/media/frigate/recordings": {
|
||||
free: 50000000000,
|
||||
total: 100000000000,
|
||||
used: 50000000000,
|
||||
mount_type: "ext4",
|
||||
},
|
||||
"/tmp/cache": {
|
||||
free: 500000000,
|
||||
total: 1000000000,
|
||||
used: 500000000,
|
||||
mount_type: "tmpfs",
|
||||
},
|
||||
},
|
||||
uptime: 86400,
|
||||
latest_version: "0.15.0",
|
||||
version: "0.15.0-test",
|
||||
},
|
||||
camera_fps: 15.0,
|
||||
process_fps: 15.0,
|
||||
skipped_fps: 0,
|
||||
detection_fps: 15.0,
|
||||
};
|
||||
|
||||
export function statsFactory(
|
||||
overrides?: DeepPartial<typeof BASE_STATS>,
|
||||
): typeof BASE_STATS {
|
||||
if (!overrides) return BASE_STATS;
|
||||
return { ...BASE_STATS, ...overrides } as typeof BASE_STATS;
|
||||
}
|
||||
Reference in New Issue
Block a user