add default icon, observe measure div, fix tests

This commit is contained in:
Josh Hawkins
2026-10-02 07:13:30 -05:00
parent 5ab8666d4a
commit 06ed433e9a
7 changed files with 76 additions and 41 deletions
+11
View File
@@ -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();
}
+3 -2
View File
@@ -7,12 +7,13 @@
*/ */
import { test, expect } from "../fixtures/frigate-test"; import { test, expect } from "../fixtures/frigate-test";
import { systemTab } from "../helpers/system-tabs";
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 /system renders general tab", async ({ frigateApp }) => { test("admin /system renders general tab", async ({ frigateApp }) => {
await frigateApp.goto("/system"); await frigateApp.goto("/system");
await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({ await expect(systemTab(frigateApp, "general")).toBeVisible({
timeout: 15_000, timeout: 15_000,
}); });
}); });
@@ -28,7 +29,7 @@ test.describe("Auth — admin access @high", () => {
test("admin /logs renders frigate tab", async ({ frigateApp }) => { test("admin /logs renders frigate tab", async ({ frigateApp }) => {
await frigateApp.goto("/logs"); await frigateApp.goto("/logs");
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({ await expect(systemTab(frigateApp, "frigate")).toBeVisible({
timeout: 5_000, timeout: 5_000,
}); });
}); });
+19
View File
@@ -287,3 +287,22 @@ test.describe("Live mobile layout @critical @mobile", () => {
await expect(frigateApp.page.locator("body")).toBeVisible(); 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 });
});
});
+3 -2
View File
@@ -6,6 +6,7 @@
*/ */
import { test, expect } from "../fixtures/frigate-test"; import { test, expect } from "../fixtures/frigate-test";
import { systemTab } from "../helpers/system-tabs";
import { viewerProfile } from "../fixtures/mock-data/profile"; import { viewerProfile } from "../fixtures/mock-data/profile";
const NOW = Math.floor(Date.now() / 1000); const NOW = Math.floor(Date.now() / 1000);
@@ -55,7 +56,7 @@ test.describe("System — Health tab @medium", () => {
}); });
await frigateApp.goto("/system"); await frigateApp.goto("/system");
await expect(frigateApp.page.getByLabel("Select health")).toHaveAttribute( await expect(systemTab(frigateApp, "health")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
@@ -801,7 +802,7 @@ test.describe("System — Health notices sources @medium", () => {
stats: QUIET_STATS, stats: QUIET_STATS,
}); });
await frigateApp.goto("/system#health"); await frigateApp.goto("/system#health");
await expect(frigateApp.page.getByLabel("Select health")).toHaveAttribute( await expect(systemTab(frigateApp, "health")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
+23 -28
View File
@@ -11,16 +11,7 @@ import {
expectBodyInteractive, expectBodyInteractive,
waitForBodyInteractive, waitForBodyInteractive,
} from "../helpers/overlay-interaction"; } from "../helpers/overlay-interaction";
import { systemTab } from "../helpers/system-tabs";
// 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();
}
async function selectTab(frigateApp: FrigateApp, name: string) { async function selectTab(frigateApp: FrigateApp, name: string) {
const kebab = frigateApp.page.getByLabel("Show all tabs"); const kebab = frigateApp.page.getByLabel("Show all tabs");
@@ -34,13 +25,17 @@ async function selectTab(frigateApp: FrigateApp, name: string) {
return; return;
} }
await tab(frigateApp, name).click(); await systemTab(frigateApp, name).click();
} }
async function expectTabActive(frigateApp: FrigateApp, name: string) { async function expectTabActive(frigateApp: FrigateApp, name: string) {
await expect(tab(frigateApp, name)).toHaveAttribute("data-state", "on", { await expect(systemTab(frigateApp, name)).toHaveAttribute(
timeout: 5_000, "data-state",
}); "on",
{
timeout: 5_000,
},
);
} }
test.describe("System — tabs @medium", () => { test.describe("System — tabs @medium", () => {
@@ -48,14 +43,14 @@ test.describe("System — tabs @medium", () => {
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
); );
if (!frigateApp.isMobile) { if (!frigateApp.isMobile) {
await expect(tab(frigateApp, "storage")).toBeVisible(); await expect(systemTab(frigateApp, "storage")).toBeVisible();
await expect(tab(frigateApp, "cameras")).toBeVisible(); await expect(systemTab(frigateApp, "cameras")).toBeVisible();
} }
}); });
@@ -63,14 +58,14 @@ test.describe("System — tabs @medium", () => {
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
); );
await selectTab(frigateApp, "storage"); await selectTab(frigateApp, "storage");
await expectTabActive(frigateApp, "storage"); await expectTabActive(frigateApp, "storage");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"off", "off",
); );
@@ -78,7 +73,7 @@ test.describe("System — tabs @medium", () => {
test("Cameras tab activates", async ({ frigateApp }) => { test("Cameras tab activates", async ({ frigateApp }) => {
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
@@ -91,7 +86,7 @@ test.describe("System — tabs @medium", () => {
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
@@ -112,7 +107,7 @@ test.describe("System — tabs @medium", () => {
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
@@ -133,7 +128,7 @@ test.describe("System — tabs @medium", () => {
} else { } else {
// Mobile: tab activation (data-state "on") already asserted above. // Mobile: tab activation (data-state "on") already asserted above.
// Additionally confirm general tab is no longer the active tab. // Additionally confirm general tab is no longer the active tab.
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"off", "off",
{ timeout: 5_000 }, { timeout: 5_000 },
@@ -143,7 +138,7 @@ test.describe("System — tabs @medium", () => {
test("cameras tab renders each configured camera", async ({ frigateApp }) => { test("cameras tab renders each configured camera", async ({ frigateApp }) => {
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
@@ -170,7 +165,7 @@ test.describe("System — tabs @medium", () => {
config: { semantic_search: { enabled: true } }, config: { semantic_search: { enabled: true } },
}); });
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
@@ -238,14 +233,14 @@ test.describe("System — mobile @medium @mobile", () => {
test("tabs render at mobile viewport", async ({ frigateApp }) => { test("tabs render at mobile viewport", async ({ frigateApp }) => {
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toBeVisible({ await expect(systemTab(frigateApp, "general")).toBeVisible({
timeout: 15_000, timeout: 15_000,
}); });
}); });
test("switching tabs works at mobile viewport", async ({ frigateApp }) => { test("switching tabs works at mobile viewport", async ({ frigateApp }) => {
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
@@ -258,7 +253,7 @@ test.describe("System — mobile @medium @mobile", () => {
frigateApp, frigateApp,
}) => { }) => {
await frigateApp.goto("/system#general"); await frigateApp.goto("/system#general");
await expect(tab(frigateApp, "general")).toHaveAttribute( await expect(systemTab(frigateApp, "general")).toHaveAttribute(
"data-state", "data-state",
"on", "on",
{ timeout: 15_000 }, { timeout: 15_000 },
@@ -181,9 +181,14 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
setGroup(name, group != "default"); setGroup(name, group != "default");
}} }}
> >
{config && config.icon && isValidIconName(config.icon) && ( <IconRenderer
<IconRenderer icon={LuIcons[config.icon]} className="size-5" /> icon={
)} isValidIconName(config.icon)
? LuIcons[config.icon]
: LuIcons.LuFolder
}
className="size-5"
/>
</Button> </Button>
)), )),
]; ];
@@ -264,12 +269,14 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
onMouseEnter={() => showTooltip(name)} onMouseEnter={() => showTooltip(name)}
onMouseLeave={() => showTooltip(undefined)} onMouseLeave={() => showTooltip(undefined)}
> >
{config && config.icon && isValidIconName(config.icon) && ( <IconRenderer
<IconRenderer icon={
icon={LuIcons[config.icon]} isValidIconName(config.icon)
className="size-4" ? LuIcons[config.icon]
/> : LuIcons.LuFolder
)} }
className="size-4"
/>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipPortal> <TooltipPortal>
@@ -72,6 +72,7 @@ export default function OverflowStrip({
const observer = new ResizeObserver(compute); const observer = new ResizeObserver(compute);
observer.observe(wrapper); observer.observe(wrapper);
observer.observe(measure);
return () => observer.disconnect(); return () => observer.disconnect();
}, [items.length, gapClassName]); }, [items.length, gapClassName]);