frigate/web/e2e/specs/live.spec.ts

254 lines
8.6 KiB
TypeScript
Raw Normal View History

2026-04-06 19:15:22 +03:00
/**
* Live page tests -- CRITICAL tier.
*
2026-04-06 20:04:51 +03:00
* Tests camera dashboard rendering, camera card clicks, single camera view
* with named controls, feature toggle behavior, context menu, and mobile layout.
2026-04-06 19:15:22 +03:00
*/
import { test, expect } from "../fixtures/frigate-test";
test.describe("Live Dashboard @critical", () => {
2026-04-06 20:04:51 +03:00
test("dashboard renders all configured cameras by name", async ({
frigateApp,
}) => {
2026-04-06 19:15:22 +03:00
await frigateApp.goto("/");
for (const cam of ["front_door", "backyard", "garage"]) {
await expect(
frigateApp.page.locator(`[data-camera='${cam}']`),
).toBeVisible({ timeout: 10_000 });
}
2026-04-06 19:15:22 +03:00
});
test("clicking camera card opens single camera view via hash", async ({
frigateApp,
}) => {
2026-04-06 19:15:22 +03:00
await frigateApp.goto("/");
const card = frigateApp.page.locator("[data-camera='front_door']").first();
await card.click({ timeout: 10_000 });
2026-04-06 19:15:22 +03:00
await expect(frigateApp.page).toHaveURL(/#front_door/);
});
test("back button returns from single camera to dashboard", async ({
2026-04-06 19:15:22 +03:00
frigateApp,
}) => {
2026-04-06 20:04:51 +03:00
// First navigate to dashboard so there's history to go back to
await frigateApp.goto("/");
2026-04-06 19:15:22 +03:00
await frigateApp.page.waitForTimeout(1000);
2026-04-06 20:04:51 +03:00
// 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 });
2026-04-06 19:15:22 +03:00
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
2026-04-06 20:04:51 +03:00
});
2026-04-06 19:15:22 +03:00
2026-04-06 20:04:51 +03:00
test.describe("Live Single Camera - Controls @critical", () => {
test("single camera view shows Back and History buttons (desktop)", async ({
frigateApp,
}) => {
2026-04-06 19:15:22 +03:00
if (frigateApp.isMobile) {
2026-04-06 20:04:51 +03:00
test.skip(); // On mobile, buttons may show icons only
2026-04-06 19:15:22 +03:00
return;
}
2026-04-06 20:04:51 +03:00
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();
2026-04-06 19:15:22 +03:00
});
2026-04-06 20:04:51 +03:00
test("single camera view shows feature toggle icons (desktop)", async ({
2026-04-06 19:15:22 +03:00
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
2026-04-06 19:15:22 +03:00
return;
}
2026-04-06 20:04:51 +03:00
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
2026-04-06 20:04:51 +03:00
// 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);
2026-04-06 19:15:22 +03:00
});
2026-04-06 20:04:51 +03:00
test("clicking a feature toggle changes its visual state (desktop)", async ({
frigateApp,
}) => {
if (frigateApp.isMobile) {
test.skip();
return;
}
2026-04-06 19:15:22 +03:00
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
2026-04-06 20:04:51 +03:00
// 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);
}
2026-04-06 19:15:22 +03:00
});
2026-04-06 20:04:51 +03:00
test("settings gear button opens dropdown (desktop)", async ({
frigateApp,
}) => {
2026-04-06 20:04:51 +03:00
if (frigateApp.isMobile) {
test.skip();
return;
}
2026-04-06 19:15:22 +03:00
await frigateApp.goto("/#front_door");
await frigateApp.page.waitForTimeout(2000);
2026-04-06 20:04:51 +03:00
// 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)
2026-04-06 19:15:22 +03:00
if (count > 0) {
2026-04-06 20:04:51 +03:00
await gearButtons.last().click();
await frigateApp.page.waitForTimeout(500);
2026-04-06 20:04:51 +03:00
// 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");
}
2026-04-06 19:15:22 +03:00
}
});
test("keyboard shortcut f does not crash on desktop", async ({
frigateApp,
}) => {
2026-04-06 19:15:22 +03:00
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();
});
});
2026-04-06 20:04:51 +03:00
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);
});
});
2026-04-06 19:15:22 +03:00
test.describe("Live Context Menu @critical", () => {
test("right-click on camera opens context menu on desktop", async ({
2026-04-06 19:15:22 +03:00
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" });
2026-04-06 19:15:22 +03:00
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();
});
2026-04-06 19:15:22 +03:00
});
2026-04-06 20:04:51 +03:00
test.describe("Live Mobile Layout @critical", () => {
test("mobile renders cameras without sidebar", async ({ frigateApp }) => {
2026-04-06 19:15:22 +03:00
if (!frigateApp.isMobile) {
test.skip();
return;
}
await frigateApp.goto("/");
await expect(frigateApp.page.locator("aside")).not.toBeVisible();
2026-04-06 19:15:22 +03:00
await expect(
frigateApp.page.locator("[data-camera='front_door']"),
).toBeVisible({ timeout: 10_000 });
});
test("mobile camera click opens single camera view", async ({
frigateApp,
}) => {
2026-04-06 19:15:22 +03:00
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 });
2026-04-06 19:15:22 +03:00
await expect(frigateApp.page).toHaveURL(/#front_door/);
});
});