more cases

This commit is contained in:
Josh Hawkins 2026-04-06 12:04:51 -05:00
parent c42f28b60e
commit 0ca825d91c
4 changed files with 316 additions and 167 deletions

View File

@ -2,77 +2,111 @@
* Auth and cross-cutting tests -- HIGH tier. * Auth and cross-cutting tests -- HIGH tier.
* *
* Tests protected route access for admin/viewer roles, * Tests protected route access for admin/viewer roles,
* redirect behavior, and all routes smoke test. * access denied page rendering, viewer nav restrictions,
* and all routes smoke test.
*/ */
import { test, expect } from "../fixtures/frigate-test"; import { test, expect } from "../fixtures/frigate-test";
import { viewerProfile } from "../fixtures/mock-data/profile"; import { viewerProfile } from "../fixtures/mock-data/profile";
test.describe("Auth - Admin Access @high", () => { test.describe("Auth - Admin Access @high", () => {
test("admin can access /system and it renders", async ({ frigateApp }) => { test("admin can access /system and sees system tabs", async ({
frigateApp,
}) => {
await frigateApp.goto("/system"); await frigateApp.goto("/system");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
// Wait for lazy-loaded system content
await frigateApp.page.waitForTimeout(3000); await frigateApp.page.waitForTimeout(3000);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); // System page should have named tab buttons
await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
timeout: 5_000,
});
}); });
test("admin can access /config and editor loads", async ({ frigateApp }) => { test("admin can access /config and Monaco editor loads", async ({
frigateApp,
}) => {
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await frigateApp.page.waitForTimeout(3000); await frigateApp.page.waitForTimeout(5000);
// Monaco editor or at least page content should render const editor = frigateApp.page.locator(
await expect(frigateApp.page.locator("body")).toBeVisible(); ".monaco-editor, [data-keybinding-context]",
);
await expect(editor.first()).toBeVisible({ timeout: 10_000 });
}); });
test("admin can access /logs and service tabs render", async ({ test("admin can access /logs and sees service tabs", async ({
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/logs"); await frigateApp.goto("/logs");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
// Should have service toggle group await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
const toggleGroup = frigateApp.page.locator('[role="group"]'); timeout: 5_000,
await expect(toggleGroup.first()).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.describe("Auth - Viewer Restrictions @high", () => {
test("viewer is denied access to /system", async ({ frigateApp, page }) => { test("viewer sees Access Denied on /system", async ({ frigateApp, page }) => {
await frigateApp.installDefaults({ profile: viewerProfile() }); await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/system"); await page.goto("/system");
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
const bodyText = await page.textContent("body"); // Should show "Access Denied" text
expect( await expect(page.getByText("Access Denied")).toBeVisible({
bodyText?.includes("Access Denied") || timeout: 5_000,
bodyText?.includes("permission") || });
page.url().includes("unauthorized"),
).toBeTruthy();
}); });
test("viewer is denied access to /config", async ({ frigateApp, page }) => { test("viewer sees Access Denied on /config", async ({ frigateApp, page }) => {
await frigateApp.installDefaults({ profile: viewerProfile() }); await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/config"); await page.goto("/config");
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
const bodyText = await page.textContent("body"); await expect(page.getByText("Access Denied")).toBeVisible({
expect( timeout: 5_000,
bodyText?.includes("Access Denied") || });
bodyText?.includes("permission") ||
page.url().includes("unauthorized"),
).toBeTruthy();
}); });
test("viewer is denied access to /logs", async ({ frigateApp, page }) => { test("viewer sees Access Denied on /logs", async ({ frigateApp, page }) => {
await frigateApp.installDefaults({ profile: viewerProfile() }); await frigateApp.installDefaults({ profile: viewerProfile() });
await page.goto("/logs"); await page.goto("/logs");
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
const bodyText = await page.textContent("body"); await expect(page.getByText("Access Denied")).toBeVisible({
expect( timeout: 5_000,
bodyText?.includes("Access Denied") || });
bodyText?.includes("permission") ||
page.url().includes("unauthorized"),
).toBeTruthy();
}); });
test("viewer can access all main user routes", async ({ 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, frigateApp,
page, page,
}) => { }) => {
@ -81,7 +115,6 @@ test.describe("Auth - Viewer Restrictions @high", () => {
for (const route of routes) { for (const route of routes) {
await page.goto(route); await page.goto(route);
await page.waitForSelector("#pageRoot", { timeout: 10_000 }); await page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(page.locator("#pageRoot")).toBeVisible();
} }
}); });
}); });
@ -97,13 +130,18 @@ test.describe("Auth - All Routes Smoke @high", () => {
} }
}); });
test("all admin routes render without crash", async ({ frigateApp }) => { test("admin routes render with specific content", async ({ frigateApp }) => {
const routes = ["/system", "/logs"]; // System page should have tab controls
for (const route of routes) { await frigateApp.goto("/system");
await frigateApp.goto(route); await frigateApp.page.waitForTimeout(3000);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({ await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
timeout: 10_000, timeout: 5_000,
});
// Logs page should have service tabs
await frigateApp.goto("/logs");
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
}); });
}
}); });
}); });

View File

@ -1,19 +1,16 @@
/** /**
* Explore page tests -- HIGH tier. * Explore page tests -- HIGH tier.
* *
* Tests search input, filter button opening popovers, * Tests search input with text entry and clearing, camera filter popover
* search result thumbnails rendering, and detail dialog. * opening with camera names, and content rendering with mock events.
*/ */
import { test, expect } from "../fixtures/frigate-test"; import { test, expect } from "../fixtures/frigate-test";
test.describe("Explore Page - Search @high", () => { test.describe("Explore Page - Search @high", () => {
test("explore page renders with search and filter controls", async ({ test("explore page renders with filter buttons", async ({ frigateApp }) => {
frigateApp,
}) => {
await frigateApp.goto("/explore"); await frigateApp.goto("/explore");
const pageRoot = frigateApp.page.locator("#pageRoot"); await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await expect(pageRoot).toBeVisible();
const buttons = frigateApp.page.locator("#pageRoot button"); const buttons = frigateApp.page.locator("#pageRoot button");
await expect(buttons.first()).toBeVisible({ timeout: 10_000 }); await expect(buttons.first()).toBeVisible({ timeout: 10_000 });
}); });
@ -31,38 +28,48 @@ test.describe("Explore Page - Search @high", () => {
await expect(searchInput).toHaveValue(""); 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.describe("Explore Page - Filters @high", () => {
test("camera filter button opens selector and escape closes it", async ({ test("camera filter button opens popover with camera names (desktop)", async ({
frigateApp, frigateApp,
}) => { }) => {
if (frigateApp.isMobile) { if (frigateApp.isMobile) {
test.skip(); // Mobile uses drawer-based filters test.skip();
return; return;
} }
await frigateApp.goto("/explore"); await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(1000); await frigateApp.page.waitForTimeout(1000);
// Find the cameras filter button
const camerasBtn = frigateApp.page.getByRole("button", { const camerasBtn = frigateApp.page.getByRole("button", {
name: /cameras/i, name: /cameras/i,
}); });
if (await camerasBtn.isVisible().catch(() => false)) { if (await camerasBtn.isVisible().catch(() => false)) {
await camerasBtn.click(); await camerasBtn.click();
await frigateApp.page.waitForTimeout(500); await frigateApp.page.waitForTimeout(500);
// Popover should open with camera names
const popover = frigateApp.page.locator( const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]", "[data-radix-popper-content-wrapper]",
); );
await expect(popover.first()).toBeVisible({ timeout: 3_000 }); await expect(popover.first()).toBeVisible({ timeout: 3_000 });
// Dismiss // Camera names from config should be in the popover
await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
await frigateApp.page.keyboard.press("Escape"); await frigateApp.page.keyboard.press("Escape");
await frigateApp.page.waitForTimeout(300);
} }
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
}); });
test("filter button opens overlay and page remains stable", async ({ test("filter button opens and closes overlay cleanly", async ({
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/explore"); await frigateApp.goto("/explore");
@ -73,17 +80,17 @@ test.describe("Explore Page - Filters @high", () => {
await frigateApp.page.waitForTimeout(500); await frigateApp.page.waitForTimeout(500);
await frigateApp.page.keyboard.press("Escape"); await frigateApp.page.keyboard.press("Escape");
await frigateApp.page.waitForTimeout(300); await frigateApp.page.waitForTimeout(300);
// Page is still functional after open/close cycle
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
}); });
}); });
test.describe("Explore Page - Content @high", () => { test.describe("Explore Page - Content @high", () => {
test("explore page shows summary content with mock events", async ({ test("explore page shows content with mock events", async ({
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/explore"); await frigateApp.goto("/explore");
await frigateApp.page.waitForTimeout(3000); await frigateApp.page.waitForTimeout(3000);
// With mock events, the summary view should render thumbnails or content
const pageText = await frigateApp.page.textContent("#pageRoot"); const pageText = await frigateApp.page.textContent("#pageRoot");
expect(pageText?.length).toBeGreaterThan(0); expect(pageText?.length).toBeGreaterThan(0);
}); });

View File

@ -1,16 +1,17 @@
/** /**
* Live page tests -- CRITICAL tier. * Live page tests -- CRITICAL tier.
* *
* Tests camera dashboard rendering, camera card clicks opening single view, * Tests camera dashboard rendering, camera card clicks, single camera view
* feature toggles sending WS messages, context menu behavior, and mobile layout. * with named controls, feature toggle behavior, context menu, and mobile layout.
*/ */
import { test, expect } from "../fixtures/frigate-test"; import { test, expect } from "../fixtures/frigate-test";
test.describe("Live Dashboard @critical", () => { test.describe("Live Dashboard @critical", () => {
test("dashboard renders all configured cameras", async ({ frigateApp }) => { test("dashboard renders all configured cameras by name", async ({
frigateApp,
}) => {
await frigateApp.goto("/"); await frigateApp.goto("/");
// All 3 mock cameras should have data-camera elements
for (const cam of ["front_door", "backyard", "garage"]) { for (const cam of ["front_door", "backyard", "garage"]) {
await expect( await expect(
frigateApp.page.locator(`[data-camera='${cam}']`), frigateApp.page.locator(`[data-camera='${cam}']`),
@ -30,39 +31,23 @@ test.describe("Live Dashboard @critical", () => {
test("back button returns from single camera to dashboard", async ({ test("back button returns from single camera to dashboard", async ({
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/#front_door"); // First navigate to dashboard so there's history to go back to
await frigateApp.goto("/");
await frigateApp.page.waitForTimeout(1000); await frigateApp.page.waitForTimeout(1000);
// Click the back button (first button with SVG icon) // Click a camera to enter single view
const backBtn = frigateApp.page const card = frigateApp.page.locator("[data-camera='front_door']").first();
.locator("button") await card.click({ timeout: 10_000 });
.filter({ has: frigateApp.page.locator("svg") }) await frigateApp.page.waitForTimeout(2000);
.first(); // 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 backBtn.click();
await frigateApp.page.waitForTimeout(1000); await frigateApp.page.waitForTimeout(1000);
// Should return to dashboard - hash cleared or page root visible
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
test("fullscreen button is present on desktop", async ({ frigateApp }) => {
if (frigateApp.isMobile) {
test.skip();
return;
} }
await frigateApp.goto("/"); // Should be back on the dashboard with cameras visible
const fullscreenBtn = frigateApp.page.locator("button:has(svg)").last(); await expect(
await expect(fullscreenBtn).toBeVisible({ timeout: 10_000 }); frigateApp.page.locator("[data-camera='front_door']"),
}); ).toBeVisible({ timeout: 10_000 });
test("camera group selector is in sidebar on live page", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
// Camera group selector renders in the sidebar below the Live nav icon
await expect(frigateApp.page.locator("aside")).toBeVisible();
}); });
test("birdseye view loads without crash", async ({ frigateApp }) => { test("birdseye view loads without crash", async ({ frigateApp }) => {
@ -78,29 +63,96 @@ test.describe("Live Dashboard @critical", () => {
}); });
}); });
test.describe("Live Single Camera @critical", () => { test.describe("Live Single Camera - Controls @critical", () => {
test("single camera view has control buttons", async ({ frigateApp }) => { test("single camera view shows Back and History buttons (desktop)", async ({
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
const buttons = frigateApp.page.locator("button");
const count = await buttons.count();
// Should have back, fullscreen, settings, and toggle buttons
expect(count).toBeGreaterThanOrEqual(2);
});
test("camera feature toggles are clickable without crash", async ({
frigateApp, frigateApp,
}) => { }) => {
if (frigateApp.isMobile) {
test.skip(); // On mobile, buttons may show icons only
return;
}
await frigateApp.goto("/#front_door"); await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000); await frigateApp.page.waitForTimeout(2000);
const switches = frigateApp.page.locator('button[role="switch"]'); // Back and History are visible text buttons in the header
const count = await switches.count(); 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) { if (count > 0) {
// Click the first toggle (e.g., detect toggle) await gearButtons.last().click();
await switches.first().click();
await frigateApp.page.waitForTimeout(500); await frigateApp.page.waitForTimeout(500);
// Page should still be functional // A dropdown or drawer should appear
await expect(frigateApp.page.locator("body")).toBeVisible(); 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");
}
} }
}); });
@ -118,6 +170,24 @@ test.describe("Live Single Camera @critical", () => {
}); });
}); });
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.describe("Live Context Menu @critical", () => {
test("right-click on camera opens context menu on desktop", async ({ test("right-click on camera opens context menu on desktop", async ({
frigateApp, frigateApp,
@ -130,7 +200,6 @@ test.describe("Live Context Menu @critical", () => {
const card = frigateApp.page.locator("[data-camera='front_door']").first(); const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.waitFor({ state: "visible", timeout: 10_000 }); await card.waitFor({ state: "visible", timeout: 10_000 });
await card.click({ button: "right" }); await card.click({ button: "right" });
// Context menu should appear (Radix ContextMenu renders a portal)
const contextMenu = frigateApp.page.locator( const contextMenu = frigateApp.page.locator(
'[role="menu"], [data-radix-menu-content]', '[role="menu"], [data-radix-menu-content]',
); );
@ -156,16 +225,14 @@ test.describe("Live Context Menu @critical", () => {
}); });
}); });
test.describe("Live Mobile @critical", () => { test.describe("Live Mobile Layout @critical", () => {
test("mobile renders camera list (not sidebar)", async ({ frigateApp }) => { test("mobile renders cameras without sidebar", async ({ frigateApp }) => {
if (!frigateApp.isMobile) { if (!frigateApp.isMobile) {
test.skip(); test.skip();
return; return;
} }
await frigateApp.goto("/"); await frigateApp.goto("/");
// No sidebar on mobile
await expect(frigateApp.page.locator("aside")).not.toBeVisible(); await expect(frigateApp.page.locator("aside")).not.toBeVisible();
// Cameras should still be visible
await expect( await expect(
frigateApp.page.locator("[data-camera='front_door']"), frigateApp.page.locator("[data-camera='front_door']"),
).toBeVisible({ timeout: 10_000 }); ).toBeVisible({ timeout: 10_000 });

View File

@ -1,9 +1,9 @@
/** /**
* Review/Events page tests -- CRITICAL tier. * Review/Events page tests -- CRITICAL tier.
* *
* Tests severity toggle switching between alerts/detections/motion, * Tests severity tab switching by name (Alerts/Detections/Motion),
* filter buttons opening popovers, show reviewed toggle, * filter popover opening with camera names, show reviewed toggle,
* and page content rendering with mock review data. * calendar button, and filter button interactions.
*/ */
import { test, expect } from "../fixtures/frigate-test"; import { test, expect } from "../fixtures/frigate-test";
@ -14,85 +14,106 @@ test.describe("Review Page - Severity Tabs @critical", () => {
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/review"); await frigateApp.goto("/review");
// Severity toggle group should have 3 items
await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({ await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({
timeout: 10_000, timeout: 10_000,
}); });
await expect(frigateApp.page.getByLabel("Detections")).toBeVisible(); await expect(frigateApp.page.getByLabel("Detections")).toBeVisible();
await expect(frigateApp.page.getByLabel("Motion")).toBeVisible(); // Motion uses role="radio" to distinguish from other Motion elements
await expect(
frigateApp.page.getByRole("radio", { name: "Motion" }),
).toBeVisible();
}); });
test("clicking Detections tab switches active severity", async ({ test("Alerts tab is active by default", async ({ frigateApp }) => {
frigateApp,
}) => {
await frigateApp.goto("/review"); await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000); await frigateApp.page.waitForTimeout(1000);
// Initially Alerts is active (aria-checked="true")
const alertsTab = frigateApp.page.getByLabel("Alerts"); const alertsTab = frigateApp.page.getByLabel("Alerts");
await expect(alertsTab).toHaveAttribute("aria-checked", "true"); await expect(alertsTab).toHaveAttribute("data-state", "on");
// Click Detections
await frigateApp.page.getByLabel("Detections").click();
await frigateApp.page.waitForTimeout(500);
// Detections should now be active
const detectionsTab = frigateApp.page.getByLabel("Detections");
await expect(detectionsTab).toHaveAttribute("aria-checked", "true");
// Alerts should no longer be active
await expect(alertsTab).toHaveAttribute("aria-checked", "false");
}); });
test("clicking Motion tab switches to motion view", async ({ test("clicking Detections tab makes it active and deactivates Alerts", async ({
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/review"); await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000); await frigateApp.page.waitForTimeout(1000);
// Use getByRole to target the specific radio button, not the switch 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" }); const motionTab = frigateApp.page.getByRole("radio", { name: "Motion" });
await motionTab.click(); await motionTab.click();
await frigateApp.page.waitForTimeout(500); await frigateApp.page.waitForTimeout(500);
await expect(motionTab).toHaveAttribute("data-state", "on"); 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.describe("Review Page - Filters @critical", () => {
test("All Cameras filter button opens camera selector", async ({ test("All Cameras filter button opens popover with camera names", async ({
frigateApp, frigateApp,
}) => { }) => {
if (frigateApp.isMobile) { if (frigateApp.isMobile) {
test.skip(); // Mobile uses drawer-based camera selector test.skip();
return; return;
} }
await frigateApp.goto("/review"); await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000); await frigateApp.page.waitForTimeout(1000);
// Click "All Cameras" button
const camerasBtn = frigateApp.page.getByRole("button", { const camerasBtn = frigateApp.page.getByRole("button", {
name: /cameras/i, name: /cameras/i,
}); });
await expect(camerasBtn).toBeVisible({ timeout: 5_000 }); await expect(camerasBtn).toBeVisible({ timeout: 5_000 });
await camerasBtn.click(); await camerasBtn.click();
await frigateApp.page.waitForTimeout(500); await frigateApp.page.waitForTimeout(500);
// A popover/dropdown with camera names should appear
// Popover should open with camera names from config
const popover = frigateApp.page.locator( const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]", "[data-radix-popper-content-wrapper]",
); );
await expect(popover.first()).toBeVisible({ timeout: 3_000 }); await expect(popover.first()).toBeVisible({ timeout: 3_000 });
// Should contain camera names from config // Camera names should be present
await expect(frigateApp.page.getByText("Front Door")).toBeVisible(); await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
// Close
await frigateApp.page.keyboard.press("Escape"); await frigateApp.page.keyboard.press("Escape");
}); });
test("Show Reviewed toggle is clickable", async ({ frigateApp }) => { test("Show Reviewed toggle is clickable", async ({ frigateApp }) => {
await frigateApp.goto("/review"); await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000); await frigateApp.page.waitForTimeout(1000);
// Find the Show Reviewed toggle/switch
const showReviewed = frigateApp.page.getByRole("button", { const showReviewed = frigateApp.page.getByRole("button", {
name: /reviewed/i, name: /reviewed/i,
}); });
if (await showReviewed.isVisible().catch(() => false)) { if (await showReviewed.isVisible().catch(() => false)) {
await showReviewed.click(); await showReviewed.click();
await frigateApp.page.waitForTimeout(500); await frigateApp.page.waitForTimeout(500);
// Page should still be functional // Toggle should change state
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); await expect(frigateApp.page.locator("body")).toBeVisible();
} }
}); });
@ -101,52 +122,56 @@ test.describe("Review Page - Filters @critical", () => {
}) => { }) => {
await frigateApp.goto("/review"); await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000); await frigateApp.page.waitForTimeout(1000);
const calendarBtn = frigateApp.page.getByRole("button", { const calendarBtn = frigateApp.page.getByRole("button", {
name: /24 hours|calendar|date/i, name: /24 hours|calendar|date/i,
}); });
if (await calendarBtn.isVisible().catch(() => false)) { if (await calendarBtn.isVisible().catch(() => false)) {
await calendarBtn.click(); await calendarBtn.click();
await frigateApp.page.waitForTimeout(500); await frigateApp.page.waitForTimeout(500);
// A popover with calendar should appear // Popover should open
const popover = frigateApp.page.locator( const popover = frigateApp.page.locator(
"[data-radix-popper-content-wrapper]", "[data-radix-popper-content-wrapper]",
); );
const visible = await popover if (
await popover
.first() .first()
.isVisible() .isVisible()
.catch(() => false); .catch(() => false)
if (visible) { ) {
await frigateApp.page.keyboard.press("Escape"); await frigateApp.page.keyboard.press("Escape");
} }
} }
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
}); });
test("Filter button opens filter popover", async ({ frigateApp }) => { test("Filter button opens filter popover", async ({ frigateApp }) => {
await frigateApp.goto("/review"); await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(1000); await frigateApp.page.waitForTimeout(1000);
const filterBtn = frigateApp.page.getByRole("button", { const filterBtn = frigateApp.page.getByRole("button", {
name: /^filter$/i, name: /^filter$/i,
}); });
if (await filterBtn.isVisible().catch(() => false)) { if (await filterBtn.isVisible().catch(() => false)) {
await filterBtn.click(); await filterBtn.click();
await frigateApp.page.waitForTimeout(500); 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"); await frigateApp.page.keyboard.press("Escape");
} }
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); }
}); });
}); });
test.describe("Review Page - Navigation @critical", () => { test.describe("Review Page - Timeline @critical", () => {
test("navigate to review from live page", async ({ frigateApp }) => { test("review page has timeline with time markers (desktop)", async ({
await frigateApp.goto("/");
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
await base.navigateTo("/review");
await expect(frigateApp.page).toHaveURL(/\/review/);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
test("review page has timeline on right side (desktop)", async ({
frigateApp, frigateApp,
}) => { }) => {
if (frigateApp.isMobile) { if (frigateApp.isMobile) {
@ -155,9 +180,21 @@ test.describe("Review Page - Navigation @critical", () => {
} }
await frigateApp.goto("/review"); await frigateApp.goto("/review");
await frigateApp.page.waitForTimeout(2000); await frigateApp.page.waitForTimeout(2000);
// Timeline renders time labels on the right // Timeline renders time labels like "4:30 PM"
const pageText = await frigateApp.page.textContent("#pageRoot"); const pageText = await frigateApp.page.textContent("#pageRoot");
// Should have time markers like "PM" or "AM"
expect(pageText).toMatch(/[AP]M/); 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,
});
});
});