mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-09 08:37:37 +03:00
254 lines
8.6 KiB
TypeScript
254 lines
8.6 KiB
TypeScript
|
|
/**
|
||
|
|
* 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/);
|
||
|
|
});
|
||
|
|
});
|