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:
Josh Hawkins
2026-04-06 16:33:28 -06:00
committed by GitHub
parent ed3bebc967
commit c750372586
34 changed files with 2790 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
/**
* Auth and cross-cutting tests -- HIGH tier.
*
* Tests protected route access for admin/viewer roles,
* access denied page rendering, viewer nav restrictions,
* and all routes smoke test.
*/
import { test, expect } from "../fixtures/frigate-test";
import { viewerProfile } from "../fixtures/mock-data/profile";
test.describe("Auth - Admin Access @high", () => {
test("admin can access /system and sees system tabs", async ({
frigateApp,
}) => {
await frigateApp.goto("/system");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await frigateApp.page.waitForTimeout(3000);
// System page should have named tab buttons
await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
timeout: 5_000,
});
});
test("admin can access /config and Monaco editor loads", async ({
frigateApp,
}) => {
await frigateApp.goto("/config");
await frigateApp.page.waitForTimeout(5000);
const editor = frigateApp.page.locator(
".monaco-editor, [data-keybinding-context]",
);
await expect(editor.first()).toBeVisible({ timeout: 10_000 });
});
test("admin can access /logs and sees service tabs", async ({
frigateApp,
}) => {
await frigateApp.goto("/logs");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
});
test("admin sees Classification nav on desktop", async ({ frigateApp }) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
await expect(
frigateApp.page.locator('a[href="/classification"]'),
).toBeVisible();
});
});
test.describe("Auth - Viewer Restrictions @high", () => {
test("viewer sees Access Denied on /system", async ({ frigateApp, page }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/system");
await page.waitForTimeout(2000);
// Should show "Access Denied" text
await expect(page.getByText("Access Denied")).toBeVisible({
timeout: 5_000,
});
});
test("viewer sees Access Denied on /config", async ({ frigateApp, page }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/config");
await page.waitForTimeout(2000);
await expect(page.getByText("Access Denied")).toBeVisible({
timeout: 5_000,
});
});
test("viewer sees Access Denied on /logs", async ({ frigateApp, page }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/logs");
await page.waitForTimeout(2000);
await expect(page.getByText("Access Denied")).toBeVisible({
timeout: 5_000,
});
});
test("viewer can access Live page and sees cameras", async ({
frigateApp,
page,
}) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/");
await page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(page.locator("[data-camera='front_door']")).toBeVisible({
timeout: 10_000,
});
});
test("viewer can access Review page and sees severity tabs", async ({
frigateApp,
page,
}) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/review");
await page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(page.getByLabel("Alerts")).toBeVisible({ timeout: 5_000 });
});
test("viewer can access all main user routes without crash", async ({
frigateApp,
page,
}) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
const routes = ["/", "/review", "/explore", "/export", "/settings"];
for (const route of routes) {
await page.goto(route);
await page.waitForSelector("#pageRoot", { timeout: 10_000 });
}
});
});
test.describe("Auth - All Routes Smoke @high", () => {
test("all user routes render without crash", async ({ frigateApp }) => {
const routes = ["/", "/review", "/explore", "/export", "/settings"];
for (const route of routes) {
await frigateApp.goto(route);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
timeout: 10_000,
});
}
});
test("admin routes render with specific content", async ({ frigateApp }) => {
// System page should have tab controls
await frigateApp.goto("/system");
await frigateApp.page.waitForTimeout(3000);
await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
timeout: 5_000,
});
// Logs page should have service tabs
await frigateApp.goto("/logs");
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
});
});
+34
View File
@@ -0,0 +1,34 @@
/**
* Chat page tests -- MEDIUM tier.
*
* Tests chat interface rendering, input area, and example prompt buttons.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Chat Page @medium", () => {
test("chat page renders without crash", async ({ frigateApp }) => {
await frigateApp.goto("/chat");
await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("body")).toBeVisible();
});
test("chat page has interactive input or buttons", async ({ frigateApp }) => {
await frigateApp.goto("/chat");
await frigateApp.page.waitForTimeout(2000);
const interactive = frigateApp.page.locator("input, textarea, button");
const count = await interactive.count();
expect(count).toBeGreaterThan(0);
});
test("chat input accepts text", async ({ frigateApp }) => {
await frigateApp.goto("/chat");
await frigateApp.page.waitForTimeout(2000);
const input = frigateApp.page.locator("input, textarea").first();
if (await input.isVisible().catch(() => false)) {
await input.fill("What cameras detected a person today?");
const value = await input.inputValue();
expect(value.length).toBeGreaterThan(0);
}
});
});
+33
View File
@@ -0,0 +1,33 @@
/**
* Classification page tests -- MEDIUM tier.
*
* Tests model selection view rendering and interactive elements.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Classification @medium", () => {
test("classification page renders without crash", async ({ frigateApp }) => {
await frigateApp.goto("/classification");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
test("classification page shows content and controls", async ({
frigateApp,
}) => {
await frigateApp.goto("/classification");
await frigateApp.page.waitForTimeout(2000);
const text = await frigateApp.page.textContent("#pageRoot");
expect(text?.length).toBeGreaterThan(0);
});
test("classification page has interactive elements", async ({
frigateApp,
}) => {
await frigateApp.goto("/classification");
await frigateApp.page.waitForTimeout(2000);
const buttons = frigateApp.page.locator("#pageRoot button");
const count = await buttons.count();
expect(count).toBeGreaterThanOrEqual(0);
});
});
+44
View File
@@ -0,0 +1,44 @@
/**
* Config Editor page tests -- MEDIUM tier.
*
* Tests Monaco editor loading, YAML content rendering,
* save button presence, and copy button interaction.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Config Editor @medium", () => {
test("config editor loads Monaco editor with content", async ({
frigateApp,
}) => {
await frigateApp.goto("/config");
await frigateApp.page.waitForTimeout(5000);
// Monaco editor should render with a specific class
const editor = frigateApp.page.locator(
".monaco-editor, [data-keybinding-context]",
);
await expect(editor.first()).toBeVisible({ timeout: 10_000 });
});
test("config editor has action buttons", async ({ frigateApp }) => {
await frigateApp.goto("/config");
await frigateApp.page.waitForTimeout(5000);
const buttons = frigateApp.page.locator("button");
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
});
test("config editor button clicks do not crash", async ({ frigateApp }) => {
await frigateApp.goto("/config");
await frigateApp.page.waitForTimeout(5000);
// Find buttons with SVG icons (copy, save, etc.)
const iconButtons = frigateApp.page.locator("button:has(svg)");
const count = await iconButtons.count();
if (count > 0) {
// Click the first icon button (likely copy)
await iconButtons.first().click();
await frigateApp.page.waitForTimeout(500);
}
await expect(frigateApp.page.locator("body")).toBeVisible();
});
});
+97
View File
@@ -0,0 +1,97 @@
/**
* Explore page tests -- HIGH tier.
*
* Tests search input with text entry and clearing, camera filter popover
* opening with camera names, and content rendering with mock events.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Explore Page - Search @high", () => {
test("explore page renders with filter buttons", async ({ frigateApp }) => {
await frigateApp.goto("/explore");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
const buttons = frigateApp.page.locator("#pageRoot button");
await expect(buttons.first()).toBeVisible({ timeout: 10_000 });
});
test("search input accepts text and can be cleared", async ({
frigateApp,
}) => {
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(1000);
const searchInput = frigateApp.page.locator("input").first();
if (await searchInput.isVisible()) {
await searchInput.fill("person");
await expect(searchInput).toHaveValue("person");
await searchInput.fill("");
await expect(searchInput).toHaveValue("");
}
});
test("search input submits on Enter", async ({ frigateApp }) => {
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(1000);
const searchInput = frigateApp.page.locator("input").first();
if (await searchInput.isVisible()) {
await searchInput.fill("car in driveway");
await searchInput.press("Enter");
await frigateApp.page.waitForTimeout(1000);
// Page should not crash after search submit
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
}
});
});
test.describe("Explore Page - Filters @high", () => {
test("camera filter button opens popover with camera names (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(1000);
const camerasBtn = frigateApp.page.getByRole("button", {
name: /cameras/i,
});
if (await camerasBtn.isVisible().catch(() => false)) {
await camerasBtn.click();
await frigateApp.page.waitForTimeout(500);
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]",
);
await expect(popover.first()).toBeVisible({ timeout: 3_000 });
// Camera names from config should be in the popover
await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
}
});
test("filter button opens and closes overlay cleanly", async ({
frigateApp,
}) => {
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(1000);
const firstButton = frigateApp.page.locator("#pageRoot button").first();
await expect(firstButton).toBeVisible({ timeout: 5_000 });
await firstButton.click();
await frigateApp.page.waitForTimeout(500);
await frigateApp.page.keyboard.press("Escape");
await frigateApp.page.waitForTimeout(300);
// Page is still functional after open/close cycle
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
});
test.describe("Explore Page - Content @high", () => {
test("explore page shows content with mock events", async ({
frigateApp,
}) => {
await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(3000);
const pageText = await frigateApp.page.textContent("#pageRoot");
expect(pageText?.length).toBeGreaterThan(0);
});
});
+74
View File
@@ -0,0 +1,74 @@
/**
* Export page tests -- HIGH tier.
*
* Tests export card rendering with mock data, search filtering,
* and delete confirmation dialog.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Export Page - Cards @high", () => {
test("export page renders export cards from mock data", async ({
frigateApp,
}) => {
await frigateApp.goto("/export");
await frigateApp.page.waitForTimeout(2000);
// Should show export names from our mock data
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible({ timeout: 10_000 });
await expect(
frigateApp.page.getByText("Backyard - Car Detection"),
).toBeVisible();
});
test("export page shows in-progress indicator", async ({ frigateApp }) => {
await frigateApp.goto("/export");
await frigateApp.page.waitForTimeout(2000);
// "Garage - In Progress" export should be visible
await expect(frigateApp.page.getByText("Garage - In Progress")).toBeVisible(
{ timeout: 10_000 },
);
});
test("export page shows case grouping", async ({ frigateApp }) => {
await frigateApp.goto("/export");
await frigateApp.page.waitForTimeout(3000);
// Cases may render differently depending on API response shape
const pageText = await frigateApp.page.textContent("#pageRoot");
expect(pageText?.length).toBeGreaterThan(0);
});
});
test.describe("Export Page - Search @high", () => {
test("search input filters export list", async ({ frigateApp }) => {
await frigateApp.goto("/export");
await frigateApp.page.waitForTimeout(2000);
const searchInput = frigateApp.page.locator(
'#pageRoot input[type="text"], #pageRoot input',
);
if (
(await searchInput.count()) > 0 &&
(await searchInput.first().isVisible())
) {
// Type a search term that matches one export
await searchInput.first().fill("Front Door");
await frigateApp.page.waitForTimeout(500);
// "Front Door - Person Alert" should still be visible
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible();
}
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
});
test.describe("Export Page - Controls @high", () => {
test("export page filter controls are present", async ({ frigateApp }) => {
await frigateApp.goto("/export");
await frigateApp.page.waitForTimeout(1000);
const buttons = frigateApp.page.locator("#pageRoot button");
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
});
});
+32
View File
@@ -0,0 +1,32 @@
/**
* Face Library page tests -- MEDIUM tier.
*
* Tests face grid rendering, empty state, and interactive controls.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Face Library @medium", () => {
test("face library page renders without crash", async ({ frigateApp }) => {
await frigateApp.goto("/faces");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
test("face library shows empty state with no faces", async ({
frigateApp,
}) => {
await frigateApp.goto("/faces");
await frigateApp.page.waitForTimeout(2000);
// With empty faces mock, should show empty state or content
const text = await frigateApp.page.textContent("#pageRoot");
expect(text?.length).toBeGreaterThan(0);
});
test("face library has interactive buttons", async ({ frigateApp }) => {
await frigateApp.goto("/faces");
await frigateApp.page.waitForTimeout(2000);
const buttons = frigateApp.page.locator("#pageRoot button");
const count = await buttons.count();
expect(count).toBeGreaterThanOrEqual(0);
});
});
+253
View File
@@ -0,0 +1,253 @@
/**
* Live page tests -- CRITICAL tier.
*
* Tests camera dashboard rendering, camera card clicks, single camera view
* with named controls, feature toggle behavior, context menu, and mobile layout.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Live Dashboard @critical", () => {
test("dashboard renders all configured cameras by name", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
for (const cam of ["front_door", "backyard", "garage"]) {
await expect(
frigateApp.page.locator(`[data-camera='${cam}']`),
).toBeVisible({ timeout: 10_000 });
}
});
test("clicking camera card opens single camera view via hash", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.click({ timeout: 10_000 });
await expect(frigateApp.page).toHaveURL(/#front_door/);
});
test("back button returns from single camera to dashboard", async ({
frigateApp,
}) => {
// First navigate to dashboard so there's history to go back to
await frigateApp.goto("/");
await frigateApp.page.waitForTimeout(1000);
// Click a camera to enter single view
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.click({ timeout: 10_000 });
await frigateApp.page.waitForTimeout(2000);
// Now click Back to return to dashboard
const backBtn = frigateApp.page.getByText("Back", { exact: true });
if (await backBtn.isVisible().catch(() => false)) {
await backBtn.click();
await frigateApp.page.waitForTimeout(1000);
}
// Should be back on the dashboard with cameras visible
await expect(
frigateApp.page.locator("[data-camera='front_door']"),
).toBeVisible({ timeout: 10_000 });
});
test("birdseye view loads without crash", async ({ frigateApp }) => {
await frigateApp.goto("/#birdseye");
await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("body")).toBeVisible();
});
test("empty group shows fallback content", async ({ frigateApp }) => {
await frigateApp.page.goto("/?group=nonexistent");
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
});
test.describe("Live Single Camera - Controls @critical", () => {
test("single camera view shows Back and History buttons (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip(); // On mobile, buttons may show icons only
return;
}
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// Back and History are visible text buttons in the header
await expect(
frigateApp.page.getByText("Back", { exact: true }),
).toBeVisible({ timeout: 5_000 });
await expect(
frigateApp.page.getByText("History", { exact: true }),
).toBeVisible();
});
test("single camera view shows feature toggle icons (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// Feature toggles are CameraFeatureToggle components rendered as divs
// with bg-selected (active) or bg-secondary (inactive) classes
// Count the toggles - should have at least detect, recording, snapshots
const toggles = frigateApp.page.locator(
".flex.flex-col.items-center.justify-center.bg-selected, .flex.flex-col.items-center.justify-center.bg-secondary",
);
const count = await toggles.count();
expect(count).toBeGreaterThanOrEqual(3);
});
test("clicking a feature toggle changes its visual state (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// Find active toggles (bg-selected class = feature is ON)
const activeToggles = frigateApp.page.locator(
".flex.flex-col.items-center.justify-center.bg-selected",
);
const initialCount = await activeToggles.count();
if (initialCount > 0) {
// Click the first active toggle to disable it
await activeToggles.first().click();
await frigateApp.page.waitForTimeout(1000);
// After WS mock echoes back new state, count should decrease
const newCount = await activeToggles.count();
expect(newCount).toBeLessThan(initialCount);
}
});
test("settings gear button opens dropdown (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// Find the gear icon button (last button-like element in header)
// The settings gear opens a dropdown with Stream, Play in background, etc.
const gearButtons = frigateApp.page.locator("button:has(svg)");
const count = await gearButtons.count();
// Click the last one (gear icon is typically last in the header)
if (count > 0) {
await gearButtons.last().click();
await frigateApp.page.waitForTimeout(500);
// A dropdown or drawer should appear
const overlay = frigateApp.page.locator(
'[role="menu"], [data-radix-menu-content], [role="dialog"]',
);
const visible = await overlay
.first()
.isVisible()
.catch(() => false);
if (visible) {
await frigateApp.page.keyboard.press("Escape");
}
}
});
test("keyboard shortcut f does not crash on desktop", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
await frigateApp.page.keyboard.press("f");
await frigateApp.page.waitForTimeout(500);
await expect(frigateApp.page.locator("body")).toBeVisible();
});
});
test.describe("Live Single Camera - Mobile Controls @critical", () => {
test("mobile camera view has settings drawer trigger", async ({
frigateApp,
}) => {
if (!frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
// On mobile, settings gear opens a drawer
// The button has aria-label with the camera name like "front_door Settings"
const buttons = frigateApp.page.locator("button:has(svg)");
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
});
});
test.describe("Live Context Menu @critical", () => {
test("right-click on camera opens context menu on desktop", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.waitFor({ state: "visible", timeout: 10_000 });
await card.click({ button: "right" });
const contextMenu = frigateApp.page.locator(
'[role="menu"], [data-radix-menu-content]',
);
await expect(contextMenu.first()).toBeVisible({ timeout: 5_000 });
});
test("context menu closes on escape", async ({ frigateApp }) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.waitFor({ state: "visible", timeout: 10_000 });
await card.click({ button: "right" });
await frigateApp.page.waitForTimeout(500);
await frigateApp.page.keyboard.press("Escape");
await frigateApp.page.waitForTimeout(300);
const contextMenu = frigateApp.page.locator(
'[role="menu"], [data-radix-menu-content]',
);
await expect(contextMenu).not.toBeVisible();
});
});
test.describe("Live Mobile Layout @critical", () => {
test("mobile renders cameras without sidebar", async ({ frigateApp }) => {
if (!frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
await expect(frigateApp.page.locator("aside")).not.toBeVisible();
await expect(
frigateApp.page.locator("[data-camera='front_door']"),
).toBeVisible({ timeout: 10_000 });
});
test("mobile camera click opens single camera view", async ({
frigateApp,
}) => {
if (!frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.click({ timeout: 10_000 });
await expect(frigateApp.page).toHaveURL(/#front_door/);
});
});
+75
View File
@@ -0,0 +1,75 @@
/**
* Logs page tests -- MEDIUM tier.
*
* Tests service tab switching by name, copy/download buttons,
* and websocket message feed tab.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Logs Page - Service Tabs @medium", () => {
test("logs page renders with named service tabs", async ({ frigateApp }) => {
await frigateApp.goto("/logs");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
// Service tabs have aria-label="Select {service}"
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
});
test("switching to go2rtc tab changes active tab", async ({ frigateApp }) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(1000);
const go2rtcTab = frigateApp.page.getByLabel("Select go2rtc");
if (await go2rtcTab.isVisible().catch(() => false)) {
await go2rtcTab.click();
await frigateApp.page.waitForTimeout(1000);
await expect(go2rtcTab).toHaveAttribute("data-state", "on");
}
});
test("switching to websocket tab shows message feed", async ({
frigateApp,
}) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(1000);
const wsTab = frigateApp.page.getByLabel("Select websocket");
if (await wsTab.isVisible().catch(() => false)) {
await wsTab.click();
await frigateApp.page.waitForTimeout(1000);
await expect(wsTab).toHaveAttribute("data-state", "on");
}
});
});
test.describe("Logs Page - Actions @medium", () => {
test("copy to clipboard button is present and clickable", async ({
frigateApp,
}) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(1000);
const copyBtn = frigateApp.page.getByLabel("Copy to Clipboard");
if (await copyBtn.isVisible().catch(() => false)) {
await copyBtn.click();
await frigateApp.page.waitForTimeout(500);
// Should trigger clipboard copy (toast may appear)
}
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
test("download logs button is present", async ({ frigateApp }) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(1000);
const downloadBtn = frigateApp.page.getByLabel("Download Logs");
if (await downloadBtn.isVisible().catch(() => false)) {
await expect(downloadBtn).toBeVisible();
}
});
test("logs page displays log content text", async ({ frigateApp }) => {
await frigateApp.goto("/logs");
await frigateApp.page.waitForTimeout(2000);
const text = await frigateApp.page.textContent("#pageRoot");
expect(text?.length).toBeGreaterThan(0);
});
});
+227
View File
@@ -0,0 +1,227 @@
/**
* Navigation tests -- CRITICAL tier.
*
* Tests sidebar (desktop) and bottombar (mobile) navigation,
* conditional nav items, settings menus, and their actual behaviors.
*/
import { test, expect } from "../fixtures/frigate-test";
import { BasePage } from "../pages/base.page";
test.describe("Navigation @critical", () => {
test("app loads and renders page root", async ({ frigateApp }) => {
await frigateApp.goto("/");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
test("logo is visible and links to home", async ({ frigateApp }) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
const base = new BasePage(frigateApp.page, true);
const logo = base.sidebar.locator('a[href="/"]').first();
await expect(logo).toBeVisible();
});
test("all primary nav links are present and navigate", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const routes = ["/review", "/explore", "/export"];
for (const route of routes) {
await expect(
frigateApp.page.locator(`a[href="${route}"]`).first(),
).toBeVisible();
}
// Verify clicking each one actually navigates
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
for (const route of routes) {
await base.navigateTo(route);
await expect(frigateApp.page).toHaveURL(new RegExp(route));
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
}
});
test("desktop sidebar is visible, mobile bottombar is visible", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
if (!frigateApp.isMobile) {
await expect(base.sidebar).toBeVisible();
} else {
await expect(base.sidebar).not.toBeVisible();
}
});
test("navigate between all main pages without crash", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
const pageRoot = frigateApp.page.locator("#pageRoot");
await base.navigateTo("/review");
await expect(pageRoot).toBeVisible({ timeout: 10_000 });
await base.navigateTo("/explore");
await expect(pageRoot).toBeVisible({ timeout: 10_000 });
await base.navigateTo("/export");
await expect(pageRoot).toBeVisible({ timeout: 10_000 });
await base.navigateTo("/review");
await expect(pageRoot).toBeVisible({ timeout: 10_000 });
});
test("unknown route redirects to home", async ({ frigateApp }) => {
await frigateApp.page.goto("/nonexistent-route");
await frigateApp.page.waitForTimeout(2000);
const url = frigateApp.page.url();
const hasPageRoot = await frigateApp.page
.locator("#pageRoot")
.isVisible()
.catch(() => false);
expect(url.endsWith("/") || hasPageRoot).toBeTruthy();
});
});
test.describe("Navigation - Conditional Items @critical", () => {
test("Faces nav hidden when face_recognition disabled", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
await expect(frigateApp.page.locator('a[href="/faces"]')).not.toBeVisible();
});
test("Chat nav hidden when genai model is none", async ({ frigateApp }) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.installDefaults({
config: {
genai: {
enabled: false,
provider: "ollama",
model: "none",
base_url: "",
},
},
});
await frigateApp.goto("/");
await expect(frigateApp.page.locator('a[href="/chat"]')).not.toBeVisible();
});
test("Faces nav visible when face_recognition enabled on desktop", async ({
frigateApp,
page,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.installDefaults({
config: { face_recognition: { enabled: true } },
});
await frigateApp.goto("/");
await expect(page.locator('a[href="/faces"]')).toBeVisible();
});
test("Chat nav visible when genai model set on desktop", async ({
frigateApp,
page,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.installDefaults({
config: { genai: { enabled: true, model: "llava" } },
});
await frigateApp.goto("/");
await expect(page.locator('a[href="/chat"]')).toBeVisible();
});
test("Classification nav visible for admin on desktop", async ({
frigateApp,
page,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
await expect(page.locator('a[href="/classification"]')).toBeVisible();
});
});
test.describe("Navigation - Settings Menu @critical", () => {
test("settings gear opens menu with navigation items (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
// Settings gear is in the sidebar bottom section, a div with cursor-pointer
const sidebarBottom = frigateApp.page.locator("aside .mb-8");
const gearIcon = sidebarBottom
.locator("div[class*='cursor-pointer']")
.first();
await expect(gearIcon).toBeVisible({ timeout: 5_000 });
await gearIcon.click();
// Menu should open - look for the "Settings" menu item by aria-label
await expect(frigateApp.page.getByLabel("Settings")).toBeVisible({
timeout: 3_000,
});
});
test("settings menu items navigate to correct routes (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
const targets = [
{ label: "Settings", url: "/settings" },
{ label: "System metrics", url: "/system" },
{ label: "System logs", url: "/logs" },
{ label: "Configuration Editor", url: "/config" },
];
for (const target of targets) {
await frigateApp.goto("/");
const gearIcon = frigateApp.page
.locator("aside .mb-8 div[class*='cursor-pointer']")
.first();
await gearIcon.click();
await frigateApp.page.waitForTimeout(300);
const menuItem = frigateApp.page.getByLabel(target.label);
if (await menuItem.isVisible().catch(() => false)) {
await menuItem.click();
await expect(frigateApp.page).toHaveURL(
new RegExp(target.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
);
}
}
});
test("account button in sidebar is clickable (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
const sidebarBottom = frigateApp.page.locator("aside .mb-8");
const items = sidebarBottom.locator("div[class*='cursor-pointer']");
const count = await items.count();
if (count >= 2) {
await items.nth(1).click();
await frigateApp.page.waitForTimeout(500);
}
await expect(frigateApp.page.locator("body")).toBeVisible();
});
});
+23
View File
@@ -0,0 +1,23 @@
/**
* Replay page tests -- LOW tier.
*
* Tests replay page rendering and basic interactivity.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Replay Page @low", () => {
test("replay page renders without crash", async ({ frigateApp }) => {
await frigateApp.goto("/replay");
await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("body")).toBeVisible();
});
test("replay page has interactive controls", async ({ frigateApp }) => {
await frigateApp.goto("/replay");
await frigateApp.page.waitForTimeout(2000);
const buttons = frigateApp.page.locator("button");
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
});
});
+200
View File
@@ -0,0 +1,200 @@
/**
* Review/Events page tests -- CRITICAL tier.
*
* Tests severity tab switching by name (Alerts/Detections/Motion),
* filter popover opening with camera names, show reviewed toggle,
* calendar button, and filter button interactions.
*/
import { test, expect } from "../fixtures/frigate-test";
import { BasePage } from "../pages/base.page";
test.describe("Review Page - Severity Tabs @critical", () => {
test("severity tabs render with Alerts, Detections, Motion", async ({
frigateApp,
}) => {
await frigateApp.goto("/review");
await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({
timeout: 10_000,
});
await expect(frigateApp.page.getByLabel("Detections")).toBeVisible();
// Motion uses role="radio" to distinguish from other Motion elements
await expect(
frigateApp.page.getByRole("radio", { name: "Motion" }),
).toBeVisible();
});
test("Alerts tab is active by default", async ({ frigateApp }) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const alertsTab = frigateApp.page.getByLabel("Alerts");
await expect(alertsTab).toHaveAttribute("data-state", "on");
});
test("clicking Detections tab makes it active and deactivates Alerts", async ({
frigateApp,
}) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const alertsTab = frigateApp.page.getByLabel("Alerts");
const detectionsTab = frigateApp.page.getByLabel("Detections");
await detectionsTab.click();
await frigateApp.page.waitForTimeout(500);
await expect(detectionsTab).toHaveAttribute("data-state", "on");
await expect(alertsTab).toHaveAttribute("data-state", "off");
});
test("clicking Motion tab makes it active", async ({ frigateApp }) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const motionTab = frigateApp.page.getByRole("radio", { name: "Motion" });
await motionTab.click();
await frigateApp.page.waitForTimeout(500);
await expect(motionTab).toHaveAttribute("data-state", "on");
});
test("switching back to Alerts from Detections works", async ({
frigateApp,
}) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
await frigateApp.page.getByLabel("Detections").click();
await frigateApp.page.waitForTimeout(300);
await frigateApp.page.getByLabel("Alerts").click();
await frigateApp.page.waitForTimeout(300);
await expect(frigateApp.page.getByLabel("Alerts")).toHaveAttribute(
"data-state",
"on",
);
});
});
test.describe("Review Page - Filters @critical", () => {
test("All Cameras filter button opens popover with camera names", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const camerasBtn = frigateApp.page.getByRole("button", {
name: /cameras/i,
});
await expect(camerasBtn).toBeVisible({ timeout: 5_000 });
await camerasBtn.click();
await frigateApp.page.waitForTimeout(500);
// Popover should open with camera names from config
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]",
);
await expect(popover.first()).toBeVisible({ timeout: 3_000 });
// Camera names should be present
await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
});
test("Show Reviewed toggle is clickable", async ({ frigateApp }) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const showReviewed = frigateApp.page.getByRole("button", {
name: /reviewed/i,
});
if (await showReviewed.isVisible().catch(() => false)) {
await showReviewed.click();
await frigateApp.page.waitForTimeout(500);
// Toggle should change state
await expect(frigateApp.page.locator("body")).toBeVisible();
}
});
test("Last 24 Hours calendar button opens date picker", async ({
frigateApp,
}) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const calendarBtn = frigateApp.page.getByRole("button", {
name: /24 hours|calendar|date/i,
});
if (await calendarBtn.isVisible().catch(() => false)) {
await calendarBtn.click();
await frigateApp.page.waitForTimeout(500);
// Popover should open
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]",
);
if (
await popover
.first()
.isVisible()
.catch(() => false)
) {
await frigateApp.page.keyboard.press("Escape");
}
}
});
test("Filter button opens filter popover", async ({ frigateApp }) => {
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000);
const filterBtn = frigateApp.page.getByRole("button", {
name: /^filter$/i,
});
if (await filterBtn.isVisible().catch(() => false)) {
await filterBtn.click();
await frigateApp.page.waitForTimeout(500);
// Popover or dialog should open
const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper], [role='dialog']",
);
if (
await popover
.first()
.isVisible()
.catch(() => false)
) {
await frigateApp.page.keyboard.press("Escape");
}
}
});
});
test.describe("Review Page - Timeline @critical", () => {
test("review page has timeline with time markers (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(2000);
// Timeline renders time labels like "4:30 PM"
const pageText = await frigateApp.page.textContent("#pageRoot");
expect(pageText).toMatch(/[AP]M/);
});
});
test.describe("Review Page - Navigation @critical", () => {
test("navigate to review from live page works", async ({ frigateApp }) => {
await frigateApp.goto("/");
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
await base.navigateTo("/review");
await expect(frigateApp.page).toHaveURL(/\/review/);
// Severity tabs should be visible
await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({
timeout: 10_000,
});
});
});
@@ -0,0 +1,40 @@
/**
* Settings page tests -- HIGH tier.
*
* Tests settings page rendering with content, form controls,
* and section navigation.
*/
import { test, expect } from "../../fixtures/frigate-test";
test.describe("Settings Page @high", () => {
test("settings page renders with content", async ({ frigateApp }) => {
await frigateApp.goto("/settings");
await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
const text = await frigateApp.page.textContent("#pageRoot");
expect(text?.length).toBeGreaterThan(0);
});
test("settings page has clickable navigation items", async ({
frigateApp,
}) => {
await frigateApp.goto("/settings");
await frigateApp.page.waitForTimeout(2000);
const navItems = frigateApp.page.locator(
"#pageRoot button, #pageRoot [role='button'], #pageRoot a",
);
const count = await navItems.count();
expect(count).toBeGreaterThan(0);
});
test("settings page has form controls", async ({ frigateApp }) => {
await frigateApp.goto("/settings");
await frigateApp.page.waitForTimeout(2000);
const formElements = frigateApp.page.locator(
'#pageRoot input, #pageRoot button[role="switch"], #pageRoot button[role="combobox"]',
);
const count = await formElements.count();
expect(count).toBeGreaterThanOrEqual(0);
});
});
+90
View File
@@ -0,0 +1,90 @@
/**
* System page tests -- MEDIUM tier.
*
* Tests system page rendering with tabs and tab switching.
* Navigates to /system#general explicitly so useHashState resolves
* the tab state deterministically.
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("System Page @medium", () => {
test("system page renders with tab buttons", async ({ frigateApp }) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
await expect(frigateApp.page.getByLabel("Select storage")).toBeVisible();
await expect(frigateApp.page.getByLabel("Select cameras")).toBeVisible();
});
test("general tab is active when navigated via hash", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
});
test("clicking Storage tab activates it and deactivates General", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Select storage").click();
await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute(
"data-state",
"on",
{ timeout: 5_000 },
);
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"off",
);
});
test("clicking Cameras tab activates it and deactivates General", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
await frigateApp.page.getByLabel("Select cameras").click();
await expect(frigateApp.page.getByLabel("Select cameras")).toHaveAttribute(
"data-state",
"on",
{ timeout: 5_000 },
);
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"off",
);
});
test("system page shows version and last refreshed", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
await expect(frigateApp.page.getByText("0.15.0-test")).toBeVisible();
await expect(frigateApp.page.getByText(/Last refreshed/)).toBeVisible();
});
});