From 06ed433e9a9adef62019007dbfa4807d0aad87ed Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Fri, 2 Oct 2026 07:13:30 -0500
Subject: [PATCH] add default icon, observe measure div, fix tests
---
web/e2e/helpers/system-tabs.ts | 11 ++++
web/e2e/specs/auth.spec.ts | 5 +-
web/e2e/specs/live.spec.ts | 19 +++++++
web/e2e/specs/system-health.spec.ts | 5 +-
web/e2e/specs/system.spec.ts | 51 +++++++++----------
.../components/filter/CameraGroupSelector.tsx | 25 +++++----
web/src/components/mobile/OverflowStrip.tsx | 1 +
7 files changed, 76 insertions(+), 41 deletions(-)
create mode 100644 web/e2e/helpers/system-tabs.ts
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]);