mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-09 16:47:37 +03:00
more cases
This commit is contained in:
parent
c42f28b60e
commit
0ca825d91c
@ -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,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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.page.waitForTimeout(1000);
|
|
||||||
// Click the back button (first button with SVG icon)
|
|
||||||
const backBtn = frigateApp.page
|
|
||||||
.locator("button")
|
|
||||||
.filter({ has: frigateApp.page.locator("svg") })
|
|
||||||
.first();
|
|
||||||
await backBtn.click();
|
|
||||||
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("/");
|
await frigateApp.goto("/");
|
||||||
const fullscreenBtn = frigateApp.page.locator("button:has(svg)").last();
|
await frigateApp.page.waitForTimeout(1000);
|
||||||
await expect(fullscreenBtn).toBeVisible({ timeout: 10_000 });
|
// Click a camera to enter single view
|
||||||
});
|
const card = frigateApp.page.locator("[data-camera='front_door']").first();
|
||||||
|
await card.click({ timeout: 10_000 });
|
||||||
test("camera group selector is in sidebar on live page", async ({
|
await frigateApp.page.waitForTimeout(2000);
|
||||||
frigateApp,
|
// Now click Back to return to dashboard
|
||||||
}) => {
|
const backBtn = frigateApp.page.getByText("Back", { exact: true });
|
||||||
if (frigateApp.isMobile) {
|
if (await backBtn.isVisible().catch(() => false)) {
|
||||||
test.skip();
|
await backBtn.click();
|
||||||
return;
|
await frigateApp.page.waitForTimeout(1000);
|
||||||
}
|
}
|
||||||
await frigateApp.goto("/");
|
// Should be back on the dashboard with cameras visible
|
||||||
// Camera group selector renders in the sidebar below the Live nav icon
|
await expect(
|
||||||
await expect(frigateApp.page.locator("aside")).toBeVisible();
|
frigateApp.page.locator("[data-camera='front_door']"),
|
||||||
|
).toBeVisible({ timeout: 10_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
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 });
|
||||||
|
|||||||
@ -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 (
|
||||||
.first()
|
await popover
|
||||||
.isVisible()
|
.first()
|
||||||
.catch(() => false);
|
.isVisible()
|
||||||
if (visible) {
|
.catch(() => false)
|
||||||
|
) {
|
||||||
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);
|
||||||
await frigateApp.page.keyboard.press("Escape");
|
// 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 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user