diff --git a/web/e2e/helpers/system-tabs.ts b/web/e2e/helpers/system-tabs.ts new file mode 100644 index 0000000000..17862b1e5f --- /dev/null +++ b/web/e2e/helpers/system-tabs.ts @@ -0,0 +1,11 @@ +import type { FrigateApp } from "../fixtures/frigate-test"; + +// On mobile the System tabs sit in an OverflowStrip, which keeps an inert copy +// of every tab for measurement and hides the ones that do not fit behind a +// kebab. The selected tab always stays in the strip. + +export function systemTab(frigateApp: FrigateApp, name: string) { + return frigateApp.page + .locator(`[aria-label="Select ${name}" i]:not([inert] *)`) + .first(); +} diff --git a/web/e2e/specs/auth.spec.ts b/web/e2e/specs/auth.spec.ts index f0326a5d02..80aca00b99 100644 --- a/web/e2e/specs/auth.spec.ts +++ b/web/e2e/specs/auth.spec.ts @@ -7,12 +7,13 @@ */ import { test, expect } from "../fixtures/frigate-test"; +import { systemTab } from "../helpers/system-tabs"; import { viewerProfile } from "../fixtures/mock-data/profile"; test.describe("Auth — admin access @high", () => { test("admin /system renders general tab", async ({ frigateApp }) => { await frigateApp.goto("/system"); - await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({ + await expect(systemTab(frigateApp, "general")).toBeVisible({ timeout: 15_000, }); }); @@ -28,7 +29,7 @@ test.describe("Auth — admin access @high", () => { test("admin /logs renders frigate tab", async ({ frigateApp }) => { await frigateApp.goto("/logs"); - await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({ + await expect(systemTab(frigateApp, "frigate")).toBeVisible({ timeout: 5_000, }); }); diff --git a/web/e2e/specs/live.spec.ts b/web/e2e/specs/live.spec.ts index af28f00307..20265072c5 100644 --- a/web/e2e/specs/live.spec.ts +++ b/web/e2e/specs/live.spec.ts @@ -287,3 +287,22 @@ test.describe("Live mobile layout @critical @mobile", () => { await expect(frigateApp.page.locator("body")).toBeVisible(); }); }); + +test.describe("Live camera groups @medium", () => { + test("a group with an invalid icon renders a fallback icon", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ + config: { + camera_groups: { + outdoor: { cameras: ["front_door"], icon: "generic" }, + }, + }, + }); + await frigateApp.goto("/"); + const group = frigateApp.page + .locator('[aria-label="Camera Groups"]:not([inert] *)') + .first(); + await expect(group.locator("svg")).toBeVisible({ timeout: 10_000 }); + }); +}); diff --git a/web/e2e/specs/system-health.spec.ts b/web/e2e/specs/system-health.spec.ts index b4de9839f7..0bb1cc1a6b 100644 --- a/web/e2e/specs/system-health.spec.ts +++ b/web/e2e/specs/system-health.spec.ts @@ -6,6 +6,7 @@ */ import { test, expect } from "../fixtures/frigate-test"; +import { systemTab } from "../helpers/system-tabs"; import { viewerProfile } from "../fixtures/mock-data/profile"; const NOW = Math.floor(Date.now() / 1000); @@ -55,7 +56,7 @@ test.describe("System — Health tab @medium", () => { }); await frigateApp.goto("/system"); - await expect(frigateApp.page.getByLabel("Select health")).toHaveAttribute( + await expect(systemTab(frigateApp, "health")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, @@ -801,7 +802,7 @@ test.describe("System — Health notices sources @medium", () => { stats: QUIET_STATS, }); await frigateApp.goto("/system#health"); - await expect(frigateApp.page.getByLabel("Select health")).toHaveAttribute( + await expect(systemTab(frigateApp, "health")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, diff --git a/web/e2e/specs/system.spec.ts b/web/e2e/specs/system.spec.ts index e5d45d03bd..7823368c2b 100644 --- a/web/e2e/specs/system.spec.ts +++ b/web/e2e/specs/system.spec.ts @@ -11,16 +11,7 @@ import { expectBodyInteractive, waitForBodyInteractive, } from "../helpers/overlay-interaction"; - -// On mobile the tabs sit in an OverflowStrip, which keeps an inert copy of -// every tab for measurement and hides the ones that do not fit behind a kebab. -// The selected tab always stays in the strip. - -function tab(frigateApp: FrigateApp, name: string) { - return frigateApp.page - .locator(`[aria-label="Select ${name}" i]:not([inert] *)`) - .first(); -} +import { systemTab } from "../helpers/system-tabs"; async function selectTab(frigateApp: FrigateApp, name: string) { const kebab = frigateApp.page.getByLabel("Show all tabs"); @@ -34,13 +25,17 @@ async function selectTab(frigateApp: FrigateApp, name: string) { return; } - await tab(frigateApp, name).click(); + await systemTab(frigateApp, name).click(); } async function expectTabActive(frigateApp: FrigateApp, name: string) { - await expect(tab(frigateApp, name)).toHaveAttribute("data-state", "on", { - timeout: 5_000, - }); + await expect(systemTab(frigateApp, name)).toHaveAttribute( + "data-state", + "on", + { + timeout: 5_000, + }, + ); } test.describe("System — tabs @medium", () => { @@ -48,14 +43,14 @@ test.describe("System — tabs @medium", () => { frigateApp, }) => { await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, ); if (!frigateApp.isMobile) { - await expect(tab(frigateApp, "storage")).toBeVisible(); - await expect(tab(frigateApp, "cameras")).toBeVisible(); + await expect(systemTab(frigateApp, "storage")).toBeVisible(); + await expect(systemTab(frigateApp, "cameras")).toBeVisible(); } }); @@ -63,14 +58,14 @@ test.describe("System — tabs @medium", () => { frigateApp, }) => { await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, ); await selectTab(frigateApp, "storage"); await expectTabActive(frigateApp, "storage"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "off", ); @@ -78,7 +73,7 @@ test.describe("System — tabs @medium", () => { test("Cameras tab activates", async ({ frigateApp }) => { await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, @@ -91,7 +86,7 @@ test.describe("System — tabs @medium", () => { frigateApp, }) => { await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, @@ -112,7 +107,7 @@ test.describe("System — tabs @medium", () => { frigateApp, }) => { await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, @@ -133,7 +128,7 @@ test.describe("System — tabs @medium", () => { } else { // Mobile: tab activation (data-state "on") already asserted above. // Additionally confirm general tab is no longer the active tab. - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "off", { timeout: 5_000 }, @@ -143,7 +138,7 @@ test.describe("System — tabs @medium", () => { test("cameras tab renders each configured camera", async ({ frigateApp }) => { await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, @@ -170,7 +165,7 @@ test.describe("System — tabs @medium", () => { config: { semantic_search: { enabled: true } }, }); await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, @@ -238,14 +233,14 @@ test.describe("System — mobile @medium @mobile", () => { test("tabs render at mobile viewport", async ({ frigateApp }) => { await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toBeVisible({ + await expect(systemTab(frigateApp, "general")).toBeVisible({ timeout: 15_000, }); }); test("switching tabs works at mobile viewport", async ({ frigateApp }) => { await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, @@ -258,7 +253,7 @@ test.describe("System — mobile @medium @mobile", () => { frigateApp, }) => { await frigateApp.goto("/system#general"); - await expect(tab(frigateApp, "general")).toHaveAttribute( + await expect(systemTab(frigateApp, "general")).toHaveAttribute( "data-state", "on", { timeout: 15_000 }, diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index 2537dadfe4..b75a34eb0a 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -181,9 +181,14 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { setGroup(name, group != "default"); }} > - {config && config.icon && isValidIconName(config.icon) && ( - - )} + )), ]; @@ -264,12 +269,14 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { onMouseEnter={() => showTooltip(name)} onMouseLeave={() => showTooltip(undefined)} > - {config && config.icon && isValidIconName(config.icon) && ( - - )} + diff --git a/web/src/components/mobile/OverflowStrip.tsx b/web/src/components/mobile/OverflowStrip.tsx index 9403b8a627..6775108449 100644 --- a/web/src/components/mobile/OverflowStrip.tsx +++ b/web/src/components/mobile/OverflowStrip.tsx @@ -72,6 +72,7 @@ export default function OverflowStrip({ const observer = new ResizeObserver(compute); observer.observe(wrapper); + observer.observe(measure); return () => observer.disconnect(); }, [items.length, gapClassName]);