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 { 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,
});
});
+19
View File
@@ -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 });
});
});
+3 -2
View File
@@ -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 },
+23 -28
View File
@@ -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 },
@@ -181,9 +181,14 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
setGroup(name, group != "default");
}}
>
{config && config.icon && isValidIconName(config.icon) && (
<IconRenderer icon={LuIcons[config.icon]} className="size-5" />
)}
<IconRenderer
icon={
isValidIconName(config.icon)
? LuIcons[config.icon]
: LuIcons.LuFolder
}
className="size-5"
/>
</Button>
)),
];
@@ -264,12 +269,14 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
onMouseEnter={() => showTooltip(name)}
onMouseLeave={() => showTooltip(undefined)}
>
{config && config.icon && isValidIconName(config.icon) && (
<IconRenderer
icon={LuIcons[config.icon]}
className="size-4"
/>
)}
<IconRenderer
icon={
isValidIconName(config.icon)
? LuIcons[config.icon]
: LuIcons.LuFolder
}
className="size-4"
/>
</Button>
</TooltipTrigger>
<TooltipPortal>
@@ -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]);