formatting

This commit is contained in:
Josh Hawkins 2026-04-21 16:51:12 -05:00
parent c6138a973e
commit 7fcf0bb827
13 changed files with 180 additions and 244 deletions

View File

@ -14,10 +14,7 @@ import { expect, type Page } from "@playwright/test";
* lines. For short configs (our mocks) that's the full content. * lines. For short configs (our mocks) that's the full content.
*/ */
export async function getMonacoVisibleText(page: Page): Promise<string> { export async function getMonacoVisibleText(page: Page): Promise<string> {
return page return page.locator(".monaco-editor .view-lines").first().innerText();
.locator(".monaco-editor .view-lines")
.first()
.innerText();
} }
/** /**

View File

@ -9,14 +9,6 @@
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";
const ADMIN_ROUTES = [
{ path: "/system", marker: () => ({ label: "Select general" }) },
{
path: "/logs",
marker: () => ({ label: "Select frigate" }),
},
] as const;
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");
@ -92,10 +84,7 @@ test.describe("Auth — viewer restrictions @high", () => {
}); });
test.describe("Auth — viewer nav restrictions (desktop) @high", () => { test.describe("Auth — viewer nav restrictions (desktop) @high", () => {
test.skip( test.skip(({ frigateApp }) => frigateApp.isMobile, "Sidebar only on desktop");
({ frigateApp }) => frigateApp.isMobile,
"Sidebar only on desktop",
);
test("viewer sidebar hides admin routes", async ({ frigateApp }) => { test("viewer sidebar hides admin routes", async ({ frigateApp }) => {
await frigateApp.installDefaults({ profile: viewerProfile() }); await frigateApp.installDefaults({ profile: viewerProfile() });

View File

@ -7,13 +7,6 @@
import { test, expect, type FrigateApp } from "../fixtures/frigate-test"; import { test, expect, type FrigateApp } from "../fixtures/frigate-test";
/**
* Join NDJSON chunks into a single body (for non-streaming tests).
*/
function ndjsonBody(chunks: Array<Record<string, unknown>>): string {
return chunks.map((c) => JSON.stringify(c)).join("\n") + "\n";
}
/** /**
* Install a window.fetch override on the page so that POSTs to * Install a window.fetch override on the page so that POSTs to
* chat/completion resolve with a real ReadableStream that emits the * chat/completion resolve with a real ReadableStream that emits the
@ -153,9 +146,9 @@ test.describe("Chat — streaming @medium", () => {
await input.fill("greet me"); await input.fill("greet me");
await input.press("Enter"); await input.press("Enter");
await expect( await expect(frigateApp.page.getByText(/Hello, world!/i)).toBeVisible({
frigateApp.page.getByText(/Hello, world!/i), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
}); });
test("tool_calls chunks render a ToolCallsGroup", async ({ frigateApp }) => { test("tool_calls chunks render a ToolCallsGroup", async ({ frigateApp }) => {
@ -180,9 +173,9 @@ test.describe("Chat — streaming @medium", () => {
// ToolCallsGroup normalizes "search_objects" → "Search Objects" via // ToolCallsGroup normalizes "search_objects" → "Search Objects" via
// normalizeName(). Match the rendered display label instead. // normalizeName(). Match the rendered display label instead.
await expect( await expect(frigateApp.page.getByText(/search objects/i)).toBeVisible({
frigateApp.page.getByText(/search objects/i), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
await expect( await expect(
frigateApp.page.getByText(/searching for people/i), frigateApp.page.getByText(/searching for people/i),
).toBeVisible({ timeout: 5_000 }); ).toBeVisible({ timeout: 5_000 });
@ -210,9 +203,9 @@ test.describe("Chat — stop @medium", () => {
await input.press("Enter"); await input.press("Enter");
// Wait for the first chunk to render // Wait for the first chunk to render
await expect( await expect(frigateApp.page.getByText(/First chunk\./)).toBeVisible({
frigateApp.page.getByText(/First chunk\./), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
// The Stop button is a destructive rounded button shown while isLoading. // The Stop button is a destructive rounded button shown while isLoading.
// It contains only an FaStop SVG icon (no visible text). Find it by the // It contains only an FaStop SVG icon (no visible text). Find it by the
@ -228,9 +221,7 @@ test.describe("Chat — stop @medium", () => {
}); });
// Third chunk should never appear. // Third chunk should never appear.
await expect( await expect(frigateApp.page.getByText(/Third chunk\./)).toHaveCount(0);
frigateApp.page.getByText(/Third chunk\./),
).toHaveCount(0);
}); });
}); });
@ -308,10 +299,7 @@ test.describe("Chat — attachment chip @medium", () => {
}); });
test.describe("Chat — mobile @medium @mobile", () => { test.describe("Chat — mobile @medium @mobile", () => {
test.skip( test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only",
);
test("chat input is focusable at mobile viewport", async ({ frigateApp }) => { test("chat input is focusable at mobile viewport", async ({ frigateApp }) => {
await frigateApp.goto("/chat"); await frigateApp.goto("/chat");

View File

@ -49,9 +49,9 @@ test.describe("Classification — model list @medium", () => {
await installDatasetRoute(frigateApp, "state_classifier"); await installDatasetRoute(frigateApp, "state_classifier");
await frigateApp.goto("/classification"); await frigateApp.goto("/classification");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await expect( await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
frigateApp.page.getByText("object_classifier"), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
}); });
test("empty custom map renders without crash", async ({ frigateApp }) => { test("empty custom map renders without crash", async ({ frigateApp }) => {
@ -74,12 +74,10 @@ test.describe("Classification — model list @medium", () => {
await installDatasetRoute(frigateApp, "state_classifier"); await installDatasetRoute(frigateApp, "state_classifier");
await frigateApp.goto("/classification"); await frigateApp.goto("/classification");
// Objects is default — object_classifier visible, state_classifier hidden. // Objects is default — object_classifier visible, state_classifier hidden.
await expect( await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
frigateApp.page.getByText("object_classifier"), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
await expect( await expect(frigateApp.page.getByText("state_classifier")).toHaveCount(0);
frigateApp.page.getByText("state_classifier"),
).toHaveCount(0);
// Click the "states" toggle. Radix ToggleGroup type="single" uses role="radio". // Click the "states" toggle. Radix ToggleGroup type="single" uses role="radio".
const statesToggle = frigateApp.page const statesToggle = frigateApp.page
@ -88,12 +86,10 @@ test.describe("Classification — model list @medium", () => {
await expect(statesToggle).toBeVisible({ timeout: 5_000 }); await expect(statesToggle).toBeVisible({ timeout: 5_000 });
await statesToggle.click(); await statesToggle.click();
await expect( await expect(frigateApp.page.getByText("state_classifier")).toBeVisible({
frigateApp.page.getByText("state_classifier"), timeout: 5_000,
).toBeVisible({ timeout: 5_000 }); });
await expect( await expect(frigateApp.page.getByText("object_classifier")).toHaveCount(0);
frigateApp.page.getByText("object_classifier"),
).toHaveCount(0);
}); });
}); });
@ -109,9 +105,7 @@ test.describe("Classification — model detail navigation @medium", () => {
await installTrainRoute(frigateApp, "object_classifier"); await installTrainRoute(frigateApp, "object_classifier");
await frigateApp.goto("/classification"); await frigateApp.goto("/classification");
const objectCard = frigateApp.page const objectCard = frigateApp.page.getByText("object_classifier").first();
.getByText("object_classifier")
.first();
await expect(objectCard).toBeVisible({ timeout: 10_000 }); await expect(objectCard).toBeVisible({ timeout: 10_000 });
await objectCard.click(); await objectCard.click();
@ -123,9 +117,7 @@ test.describe("Classification — model detail navigation @medium", () => {
).toBeVisible({ timeout: 5_000 }); ).toBeVisible({ timeout: 5_000 });
// The model grid is no longer shown; state_classifier card is gone. // The model grid is no longer shown; state_classifier card is gone.
await expect( await expect(frigateApp.page.getByText("state_classifier")).toHaveCount(0);
frigateApp.page.getByText("state_classifier"),
).toHaveCount(0);
}); });
}); });
@ -167,18 +159,16 @@ test.describe("Classification — delete model (desktop) @medium", () => {
await route.fulfill({ json: { success: true, require_restart: false } }); await route.fulfill({ json: { success: true, require_restart: false } });
}); });
await frigateApp.goto("/classification"); await frigateApp.goto("/classification");
await expect( await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
frigateApp.page.getByText("object_classifier"), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
// The card-level actions menu (FiMoreVertical three-dot icon) is a // The card-level actions menu (FiMoreVertical three-dot icon) is a
// DropdownMenuTrigger with asChild on a BlurredIconButton div. // DropdownMenuTrigger with asChild on a BlurredIconButton div.
// Radix forwards aria-haspopup="menu" to the child element. // Radix forwards aria-haspopup="menu" to the child element.
// Scope the selector to the model card grid to avoid hitting the // Scope the selector to the model card grid to avoid hitting the
// settings sidebar trigger. // settings sidebar trigger.
const cardGrid = frigateApp.page.locator( const cardGrid = frigateApp.page.locator(".grid.auto-rows-max");
".grid.auto-rows-max",
);
await expect(cardGrid).toBeVisible({ timeout: 5_000 }); await expect(cardGrid).toBeVisible({ timeout: 5_000 });
const trigger = cardGrid.locator('[aria-haspopup="menu"]').first(); const trigger = cardGrid.locator('[aria-haspopup="menu"]').first();
await expect(trigger).toBeVisible({ timeout: 5_000 }); await expect(trigger).toBeVisible({ timeout: 5_000 });
@ -192,7 +182,10 @@ test.describe("Classification — delete model (desktop) @medium", () => {
// Confirm the AlertDialog. // Confirm the AlertDialog.
const alert = frigateApp.page.getByRole("alertdialog"); const alert = frigateApp.page.getByRole("alertdialog");
await expect(alert).toBeVisible({ timeout: 5_000 }); await expect(alert).toBeVisible({ timeout: 5_000 });
await alert.getByRole("button", { name: /delete|confirm/i }).first().click(); await alert
.getByRole("button", { name: /delete|confirm/i })
.first()
.click();
await expect.poll(() => deleteCalled, { timeout: 5_000 }).toBe(true); await expect.poll(() => deleteCalled, { timeout: 5_000 }).toBe(true);
await expect.poll(() => configSetCalled, { timeout: 5_000 }).toBe(true); await expect.poll(() => configSetCalled, { timeout: 5_000 }).toBe(true);
@ -219,10 +212,7 @@ test.describe("Classification — admin only @medium", () => {
}); });
test.describe("Classification — mobile @medium @mobile", () => { test.describe("Classification — mobile @medium @mobile", () => {
test.skip( test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only",
);
test("page renders at mobile viewport", async ({ frigateApp }) => { test("page renders at mobile viewport", async ({ frigateApp }) => {
await frigateApp.installDefaults({ await frigateApp.installDefaults({

View File

@ -7,27 +7,25 @@
*/ */
import { test, expect } from "../fixtures/frigate-test"; import { test, expect } from "../fixtures/frigate-test";
import { import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames";
installWsFrameCapture, import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard";
waitForWsFrame,
} from "../helpers/ws-frames";
import {
grantClipboardPermissions,
readClipboard,
} from "../helpers/clipboard";
import { import {
getMonacoVisibleText, getMonacoVisibleText,
replaceMonacoValue, replaceMonacoValue,
waitForErrorMarker, waitForErrorMarker,
} from "../helpers/monaco"; } from "../helpers/monaco";
const SAMPLE_CONFIG = "mqtt:\n host: mqtt\ncameras:\n front_door:\n enabled: true\n"; const SAMPLE_CONFIG =
"mqtt:\n host: mqtt\ncameras:\n front_door:\n enabled: true\n";
async function installSaveRoute( async function installSaveRoute(
app: { page: import("@playwright/test").Page }, app: { page: import("@playwright/test").Page },
status: number, status: number,
body: Record<string, unknown>, body: Record<string, unknown>,
): Promise<{ capturedUrl: () => string | null; capturedBody: () => string | null }> { ): Promise<{
capturedUrl: () => string | null;
capturedBody: () => string | null;
}> {
let lastUrl: string | null = null; let lastUrl: string | null = null;
let lastBody: string | null = null; let lastBody: string | null = null;
await app.page.route("**/api/config/save**", async (route) => { await app.page.route("**/api/config/save**", async (route) => {
@ -42,14 +40,12 @@ async function installSaveRoute(
} }
test.describe("Config Editor — Monaco @medium", () => { test.describe("Config Editor — Monaco @medium", () => {
test("editor loads with mocked configRaw content", async ({ test("editor loads with mocked configRaw content", async ({ frigateApp }) => {
frigateApp,
}) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
// Assert via DOM-rendered visible text (Monaco virtualizes — works // Assert via DOM-rendered visible text (Monaco virtualizes — works
// for short configs which covers our mocked content). // for short configs which covers our mocked content).
await expect await expect
@ -72,17 +68,17 @@ test.describe("Config Editor — Save @medium", () => {
message: "Config saved", message: "Config saved",
}); });
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
await frigateApp.page.getByLabel("Save Only").click(); await frigateApp.page.getByLabel("Save Only").click();
await expect.poll(() => capture.capturedUrl(), { timeout: 5_000 }).toMatch( await expect
/config\/save\?save_option=saveonly/, .poll(() => capture.capturedUrl(), { timeout: 5_000 })
); .toMatch(/config\/save\?save_option=saveonly/);
// Body is the raw YAML as text/plain // Body is the raw YAML as text/plain
await expect.poll(() => capture.capturedBody(), { timeout: 5_000 }).toContain( await expect
"front_door", .poll(() => capture.capturedBody(), { timeout: 5_000 })
); .toContain("front_door");
}); });
test("Save error shows the server message in the error area", async ({ test("Save error shows the server message in the error area", async ({
@ -93,13 +89,13 @@ test.describe("Config Editor — Save @medium", () => {
message: "Invalid field `cameras.front_door`", message: "Invalid field `cameras.front_door`",
}); });
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
await frigateApp.page.getByLabel("Save Only").click(); await frigateApp.page.getByLabel("Save Only").click();
await expect( await expect(frigateApp.page.getByText(/Invalid field/i)).toBeVisible({
frigateApp.page.getByText(/Invalid field/i), timeout: 5_000,
).toBeVisible({ timeout: 5_000 }); });
}); });
}); });
@ -117,9 +113,9 @@ test.describe("Config Editor — Save and Restart @medium", () => {
await installWsFrameCapture(frigateApp.page); await installWsFrameCapture(frigateApp.page);
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
await frigateApp.page.getByLabel("Save & Restart").click(); await frigateApp.page.getByLabel("Save & Restart").click();
const dialog = frigateApp.page.getByRole("alertdialog"); const dialog = frigateApp.page.getByRole("alertdialog");
@ -140,9 +136,9 @@ test.describe("Config Editor — Save and Restart @medium", () => {
await installSaveRoute(frigateApp, 200, { message: "Saved" }); await installSaveRoute(frigateApp, 200, { message: "Saved" });
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
await frigateApp.page.getByLabel("Save & Restart").click(); await frigateApp.page.getByLabel("Save & Restart").click();
const dialog = frigateApp.page.getByRole("alertdialog"); const dialog = frigateApp.page.getByRole("alertdialog");
@ -168,14 +164,14 @@ test.describe("Config Editor — Copy @medium", () => {
await grantClipboardPermissions(context); await grantClipboardPermissions(context);
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
await frigateApp.page.getByLabel("Copy Config").click(); await frigateApp.page.getByLabel("Copy Config").click();
await expect.poll(() => readClipboard(frigateApp.page), { timeout: 5_000 }).toContain( await expect
"front_door", .poll(() => readClipboard(frigateApp.page), { timeout: 5_000 })
); .toContain("front_door");
}); });
}); });
@ -190,9 +186,9 @@ test.describe("Config Editor — schema markers @medium", () => {
}) => { }) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
// Replace editor contents with clearly invalid YAML via keyboard. // Replace editor contents with clearly invalid YAML via keyboard.
await replaceMonacoValue( await replaceMonacoValue(
@ -219,17 +215,17 @@ test.describe("Config Editor — Cmd+S keyboard shortcut @medium", () => {
message: "Saved", message: "Saved",
}); });
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
// Focus the editor so Monaco's keybinding receives the shortcut. // Focus the editor so Monaco's keybinding receives the shortcut.
await frigateApp.page.locator(".monaco-editor").first().click(); await frigateApp.page.locator(".monaco-editor").first().click();
await frigateApp.page.keyboard.press("ControlOrMeta+s"); await frigateApp.page.keyboard.press("ControlOrMeta+s");
await expect.poll(() => capture.capturedUrl(), { timeout: 5_000 }).toMatch( await expect
/config\/save\?save_option=saveonly/, .poll(() => capture.capturedUrl(), { timeout: 5_000 })
); .toMatch(/config\/save\?save_option=saveonly/);
}); });
}); });
@ -257,9 +253,9 @@ test.describe("Config Editor — Safe Mode auto-validation @medium", () => {
}); });
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
await expect.poll(() => autoSaveCalled, { timeout: 10_000 }).toBe(true); await expect.poll(() => autoSaveCalled, { timeout: 10_000 }).toBe(true);
await expect( await expect(
frigateApp.page.getByText(/safe-mode validation failure/i), frigateApp.page.getByText(/safe-mode validation failure/i),
@ -268,16 +264,13 @@ test.describe("Config Editor — Safe Mode auto-validation @medium", () => {
}); });
test.describe("Config Editor — mobile @medium @mobile", () => { test.describe("Config Editor — mobile @medium @mobile", () => {
test.skip( test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only",
);
test("editor renders at narrow viewport", async ({ frigateApp }) => { test("editor renders at narrow viewport", async ({ frigateApp }) => {
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG }); await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await frigateApp.goto("/config"); await frigateApp.goto("/config");
await expect( await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
frigateApp.page.locator(".monaco-editor").first(), { timeout: 15_000 },
).toBeVisible({ timeout: 15_000 }); );
}); });
}); });

View File

@ -78,10 +78,7 @@ test.describe("Explore — search @high", () => {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
test.describe("Explore — filters (desktop) @high", () => { test.describe("Explore — filters (desktop) @high", () => {
test.skip( test.skip(({ frigateApp }) => frigateApp.isMobile, "Desktop popovers");
({ frigateApp }) => frigateApp.isMobile,
"Desktop popovers",
);
test("Cameras popover lists configured cameras", async ({ frigateApp }) => { test("Cameras popover lists configured cameras", async ({ frigateApp }) => {
// Navigate with a labels filter param so the filter bar renders. // Navigate with a labels filter param so the filter bar renders.
@ -136,7 +133,9 @@ test.describe("Explore — filters (desktop) @high", () => {
); );
await expect(overlay.first()).toBeVisible({ timeout: 3_000 }); await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
// "Sub Labels" section heading always renders inside the dialog. // "Sub Labels" section heading always renders inside the dialog.
await expect(frigateApp.page.getByText(/sub.?label/i).first()).toBeVisible(); await expect(
frigateApp.page.getByText(/sub.?label/i).first(),
).toBeVisible();
await frigateApp.page.keyboard.press("Escape"); await frigateApp.page.keyboard.press("Escape");
}); });
@ -251,10 +250,7 @@ test.describe("Explore — similarity search (desktop) @high", () => {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
test.describe("Explore — mobile @high @mobile", () => { test.describe("Explore — mobile @high @mobile", () => {
test.skip( test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only",
);
test("search input is focusable at mobile viewport", async ({ test("search input is focusable at mobile viewport", async ({
frigateApp, frigateApp,

View File

@ -190,9 +190,9 @@ test.describe("Face Library — delete flow (desktop) @high", () => {
.first() .first()
.click(); .click();
await expect.poll(() => deleteUrl, { timeout: 5_000 }).toMatch( await expect
/\/faces\/alice\/delete/, .poll(() => deleteUrl, { timeout: 5_000 })
); .toMatch(/\/faces\/alice\/delete/);
expect(deleteBody).toMatchObject({ ids: expect.any(Array) }); expect(deleteBody).toMatchObject({ ids: expect.any(Array) });
}); });
}); });
@ -245,9 +245,9 @@ test.describe("Face Library — rename flow (desktop) @high", () => {
.first() .first()
.click(); .click();
await expect.poll(() => renameUrl, { timeout: 5_000 }).toMatch( await expect
/\/faces\/alice\/rename/, .poll(() => renameUrl, { timeout: 5_000 })
); .toMatch(/\/faces\/alice\/rename/);
expect(renameBody).toEqual({ new_name: "alice_renamed" }); expect(renameBody).toEqual({ new_name: "alice_renamed" });
}); });
}); });
@ -361,10 +361,7 @@ test.describe("FaceSelectionDialog @high", () => {
}); });
test.describe("Face Library — mobile @high @mobile", () => { test.describe("Face Library — mobile @high @mobile", () => {
test.skip( test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only",
);
test("mobile library selector dropdown closes cleanly on Escape", async ({ test("mobile library selector dropdown closes cleanly on Escape", async ({
frigateApp, frigateApp,

View File

@ -9,10 +9,7 @@
import { test, expect } from "../fixtures/frigate-test"; import { test, expect } from "../fixtures/frigate-test";
import { LivePage } from "../pages/live.page"; import { LivePage } from "../pages/live.page";
import { import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames";
installWsFrameCapture,
waitForWsFrame,
} from "../helpers/ws-frames";
import { import {
expectBodyInteractive, expectBodyInteractive,
waitForBodyInteractive, waitForBodyInteractive,
@ -86,7 +83,9 @@ test.describe("Live Single Camera — desktop controls @critical", () => {
await frigateApp.goto("/#front_door"); await frigateApp.goto("/#front_door");
const live = new LivePage(frigateApp.page, true); const live = new LivePage(frigateApp.page, true);
// Wait for feature toggles to render (WS camera_activity must arrive first). // Wait for feature toggles to render (WS camera_activity must arrive first).
await expect(live.activeFeatureToggles.first()).toBeVisible({ timeout: 5_000 }); await expect(live.activeFeatureToggles.first()).toBeVisible({
timeout: 5_000,
});
const activeBefore = await live.activeFeatureToggles.count(); const activeBefore = await live.activeFeatureToggles.count();
expect(activeBefore).toBeGreaterThan(0); expect(activeBefore).toBeGreaterThan(0);
@ -96,9 +95,11 @@ test.describe("Live Single Camera — desktop controls @critical", () => {
// front_door/ prefix + /set suffix (any feature). // front_door/ prefix + /set suffix (any feature).
await waitForWsFrame( await waitForWsFrame(
frigateApp.page, frigateApp.page,
(frame) => (frame) => frame.includes("front_door/") && frame.includes("/set"),
frame.includes("front_door/") && frame.includes("/set"), {
{ message: "feature toggle should dispatch a <camera>/<feature>/set frame" }, message:
"feature toggle should dispatch a <camera>/<feature>/set frame",
},
); );
}); });
@ -206,10 +207,7 @@ test.describe("Live PTZ preset dropdown @critical", () => {
.first(); .first();
await expect(menu).toBeVisible({ timeout: 3_000 }); await expect(menu).toBeVisible({ timeout: 3_000 });
await menu await menu.getByRole("menuitem", { name: PRESET_NAMES[0] }).first().click();
.getByRole("menuitem", { name: PRESET_NAMES[0] })
.first()
.click();
await expect(menu).not.toBeVisible({ timeout: 3_000 }); await expect(menu).not.toBeVisible({ timeout: 3_000 });
await waitForWsFrame( await waitForWsFrame(
@ -251,7 +249,9 @@ test.describe("Live mobile layout @critical @mobile", () => {
await frigateApp.goto("/"); await frigateApp.goto("/");
await expect(frigateApp.page.locator("aside")).toHaveCount(0); await expect(frigateApp.page.locator("aside")).toHaveCount(0);
const live = new LivePage(frigateApp.page, false); const live = new LivePage(frigateApp.page, false);
await expect(live.cameraCard("front_door")).toBeVisible({ timeout: 10_000 }); await expect(live.cameraCard("front_door")).toBeVisible({
timeout: 10_000,
});
}); });
test("mobile camera tap opens single view", async ({ frigateApp }) => { test("mobile camera tap opens single view", async ({ frigateApp }) => {

View File

@ -7,10 +7,7 @@
*/ */
import { test, expect } from "../fixtures/frigate-test"; import { test, expect } from "../fixtures/frigate-test";
import { import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard";
grantClipboardPermissions,
readClipboard,
} from "../helpers/clipboard";
function logsJsonBody(lines: string[]) { function logsJsonBody(lines: string[]) {
return { lines, totalLines: lines.length }; return { lines, totalLines: lines.length };
@ -29,17 +26,16 @@ test.describe("Logs — service tabs @medium", () => {
}), }),
); );
// Silence the streaming fetch so it doesn't hang the test. // Silence the streaming fetch so it doesn't hang the test.
await frigateApp.page.route( await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
/\/api\/logs\/frigate\?stream=true/, route.fulfill({ status: 200, body: "" }),
(route) => route.fulfill({ status: 200, body: "" }),
); );
await frigateApp.goto("/logs"); await frigateApp.goto("/logs");
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({ await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000, timeout: 5_000,
}); });
await expect( await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({
frigateApp.page.getByText(/Frigate started/), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
}); });
test("switching to go2rtc fires a GET to /logs/go2rtc", async ({ test("switching to go2rtc fires a GET to /logs/go2rtc", async ({
@ -85,14 +81,13 @@ test.describe("Logs — actions @medium", () => {
]), ]),
}), }),
); );
await frigateApp.page.route( await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
/\/api\/logs\/frigate\?stream=true/, route.fulfill({ status: 200, body: "" }),
(route) => route.fulfill({ status: 200, body: "" }),
); );
await frigateApp.goto("/logs"); await frigateApp.goto("/logs");
await expect( await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({
frigateApp.page.getByText(/Frigate started/), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
const copyBtn = frigateApp.page.getByLabel("Copy to Clipboard"); const copyBtn = frigateApp.page.getByLabel("Copy to Clipboard");
await expect(copyBtn).toBeVisible({ timeout: 5_000 }); await expect(copyBtn).toBeVisible({ timeout: 5_000 });
@ -112,9 +107,8 @@ test.describe("Logs — actions @medium", () => {
} }
return route.fulfill({ json: logsJsonBody(["frigate line"]) }); return route.fulfill({ json: logsJsonBody(["frigate line"]) });
}); });
await frigateApp.page.route( await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
/\/api\/logs\/frigate\?stream=true/, route.fulfill({ status: 200, body: "" }),
(route) => route.fulfill({ status: 200, body: "" }),
); );
await frigateApp.goto("/logs"); await frigateApp.goto("/logs");
@ -132,9 +126,8 @@ test.describe("Logs — websocket tab @medium", () => {
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
route.fulfill({ json: logsJsonBody(["frigate line"]) }), route.fulfill({ json: logsJsonBody(["frigate line"]) }),
); );
await frigateApp.page.route( await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
/\/api\/logs\/frigate\?stream=true/, route.fulfill({ status: 200, body: "" }),
(route) => route.fulfill({ status: 200, body: "" }),
); );
await frigateApp.goto("/logs"); await frigateApp.goto("/logs");
const wsTab = frigateApp.page.getByLabel("Select websocket"); const wsTab = frigateApp.page.getByLabel("Select websocket");
@ -152,9 +145,7 @@ test.describe("Logs — streaming @medium", () => {
return route.fallback(); return route.fallback();
} }
return route.fulfill({ return route.fulfill({
json: logsJsonBody([ json: logsJsonBody(["[2026-04-06 10:00:00] INFO: initial batch line"]),
"[2026-04-06 10:00:00] INFO: initial batch line",
]),
}); });
}); });
@ -179,11 +170,15 @@ test.describe("Logs — streaming @medium", () => {
async start(controller) { async start(controller) {
await new Promise((r) => setTimeout(r, 30)); await new Promise((r) => setTimeout(r, 30));
controller.enqueue( controller.enqueue(
encoder.encode("[2026-04-06 10:00:02] INFO: streamed line one\n"), encoder.encode(
"[2026-04-06 10:00:02] INFO: streamed line one\n",
),
); );
await new Promise((r) => setTimeout(r, 30)); await new Promise((r) => setTimeout(r, 30));
controller.enqueue( controller.enqueue(
encoder.encode("[2026-04-06 10:00:03] INFO: streamed line two\n"), encoder.encode(
"[2026-04-06 10:00:03] INFO: streamed line two\n",
),
); );
controller.close(); controller.close();
}, },
@ -197,35 +192,31 @@ test.describe("Logs — streaming @medium", () => {
await frigateApp.goto("/logs"); await frigateApp.goto("/logs");
// The initial batch line is parsed by LogLineData and its content is // The initial batch line is parsed by LogLineData and its content is
// rendered in a .log-content cell — assert against that element. // rendered in a .log-content cell — assert against that element.
await expect( await expect(frigateApp.page.getByText("initial batch line")).toBeVisible({
frigateApp.page.getByText("initial batch line"), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
await expect( await expect(frigateApp.page.getByText(/streamed line one/)).toBeVisible({
frigateApp.page.getByText(/streamed line one/), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
await expect( await expect(frigateApp.page.getByText(/streamed line two/)).toBeVisible({
frigateApp.page.getByText(/streamed line two/), timeout: 10_000,
).toBeVisible({ timeout: 10_000 }); });
}); });
}); });
test.describe("Logs — mobile @medium @mobile", () => { test.describe("Logs — mobile @medium @mobile", () => {
test.skip( test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only",
);
test("service tabs render at mobile viewport", async ({ frigateApp }) => { test("service tabs render at mobile viewport", async ({ frigateApp }) => {
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
route.fulfill({ json: logsJsonBody(["frigate line"]) }), route.fulfill({ json: logsJsonBody(["frigate line"]) }),
); );
await frigateApp.page.route( await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
/\/api\/logs\/frigate\?stream=true/, route.fulfill({ status: 200, body: "" }),
(route) => route.fulfill({ status: 200, body: "" }),
); );
await frigateApp.goto("/logs"); await frigateApp.goto("/logs");
await expect( await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
frigateApp.page.getByLabel("Select frigate"), timeout: 5_000,
).toBeVisible({ timeout: 5_000 }); });
}); });
}); });

View File

@ -75,7 +75,12 @@ test.describe("Navigation — conditional items @critical", () => {
test.skip(frigateApp.isMobile, "Desktop sidebar"); test.skip(frigateApp.isMobile, "Desktop sidebar");
await frigateApp.installDefaults({ await frigateApp.installDefaults({
config: { config: {
genai: { enabled: false, provider: "ollama", model: "none", base_url: "" }, genai: {
enabled: false,
provider: "ollama",
model: "none",
base_url: "",
},
}, },
}); });
await frigateApp.goto("/"); await frigateApp.goto("/");

View File

@ -68,9 +68,7 @@ test.describe("Replay — active session @medium", () => {
frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }), frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
).toBeVisible({ timeout: 10_000 }); ).toBeVisible({ timeout: 10_000 });
// Three tabs (Debug / Objects / Messages) in TabsList // Three tabs (Debug / Objects / Messages) in TabsList
await expect( await expect(frigateApp.page.locator('[role="tab"]')).toHaveCount(3);
frigateApp.page.locator('[role="tab"]'),
).toHaveCount(3);
}); });
test("debug toggles render with bbox ON by default", async ({ test("debug toggles render with bbox ON by default", async ({
@ -126,7 +124,9 @@ test.describe("Replay — active session @medium", () => {
// On mobile the Configuration button text span is hidden (md:inline). // On mobile the Configuration button text span is hidden (md:inline).
// It is the first button inside the right-side action group div // It is the first button inside the right-side action group div
// (the flex container that holds Config + Stop, sibling of the Back button). // (the flex container that holds Config + Stop, sibling of the Back button).
const actionGroup = frigateApp.page.locator(".flex.items-center.gap-2 button"); const actionGroup = frigateApp.page.locator(
".flex.items-center.gap-2 button",
);
await actionGroup.first().click(); await actionGroup.first().click();
const dialog = frigateApp.page.getByRole("dialog"); const dialog = frigateApp.page.getByRole("dialog");
@ -172,9 +172,9 @@ test.describe("Replay — active session @medium", () => {
}); });
// The object row renders the label. // The object row renders the label.
await expect( await expect(frigateApp.page.getByText(/person/i).first()).toBeVisible({
frigateApp.page.getByText(/person/i).first(), timeout: 5_000,
).toBeVisible({ timeout: 5_000 }); });
}); });
test("Messages tab renders WsMessageFeed container", async ({ test("Messages tab renders WsMessageFeed container", async ({
@ -287,10 +287,7 @@ test.describe("Replay — stop button (mobile) @medium @mobile", () => {
}); });
test.describe("Replay — mobile @medium @mobile", () => { test.describe("Replay — mobile @medium @mobile", () => {
test.skip( test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only",
);
test("no-session state renders at mobile viewport", async ({ test("no-session state renders at mobile viewport", async ({
frigateApp, frigateApp,

View File

@ -163,9 +163,7 @@ test.describe("Review — filters (desktop) @critical", () => {
// Radix popper wrapper), so scope by the grid role instead. // Radix popper wrapper), so scope by the grid role instead.
const calendarGrid = frigateApp.page.locator('[role="grid"]').first(); const calendarGrid = frigateApp.page.locator('[role="grid"]').first();
await expect(calendarGrid).toBeVisible({ timeout: 3_000 }); await expect(calendarGrid).toBeVisible({ timeout: 3_000 });
const dayButton = calendarGrid const dayButton = calendarGrid.locator('[role="gridcell"] button').first();
.locator('[role="gridcell"] button')
.first();
await expect(dayButton).toBeVisible({ timeout: 3_000 }); await expect(dayButton).toBeVisible({ timeout: 3_000 });
await frigateApp.page.keyboard.press("Escape"); await frigateApp.page.keyboard.press("Escape");
}); });
@ -183,7 +181,8 @@ test.describe("Review — filters (desktop) @critical", () => {
await expect(showReviewedSwitch).toBeVisible({ timeout: 5_000 }); await expect(showReviewedSwitch).toBeVisible({ timeout: 5_000 });
// Record initial checked state and click to toggle // Record initial checked state and click to toggle
const initialChecked = await showReviewedSwitch.getAttribute("aria-checked"); const initialChecked =
await showReviewedSwitch.getAttribute("aria-checked");
await showReviewedSwitch.click(); await showReviewedSwitch.click();
const flippedChecked = initialChecked === "true" ? "false" : "true"; const flippedChecked = initialChecked === "true" ? "false" : "true";
await expect(showReviewedSwitch).toHaveAttribute( await expect(showReviewedSwitch).toHaveAttribute(
@ -211,10 +210,7 @@ test.describe("Review — timeline (desktop) @critical", () => {
}); });
test.describe("Review — mobile @critical @mobile", () => { test.describe("Review — mobile @critical @mobile", () => {
test.skip( test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only",
);
test("severity tabs render on mobile", async ({ frigateApp }) => { test("severity tabs render on mobile", async ({ frigateApp }) => {
await frigateApp.goto("/review"); await frigateApp.goto("/review");

View File

@ -104,11 +104,9 @@ 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(frigateApp.page.getByLabel("Select general")).toHaveAttribute( await expect(
"data-state", frigateApp.page.getByLabel("Select general"),
"off", ).toHaveAttribute("data-state", "off", { timeout: 5_000 });
{ timeout: 5_000 },
);
} }
}); });
@ -129,7 +127,9 @@ test.describe("System — tabs @medium", () => {
// mock has front_door, backyard, garage. // mock has front_door, backyard, garage.
for (const cam of ["front_door", "backyard", "garage"]) { for (const cam of ["front_door", "backyard", "garage"]) {
await expect( await expect(
frigateApp.page.getByText(new RegExp(cam.replace("_", ".?"), "i")).first(), frigateApp.page
.getByText(new RegExp(cam.replace("_", ".?"), "i"))
.first(),
).toBeVisible({ timeout: 10_000 }); ).toBeVisible({ timeout: 10_000 });
} }
}); });
@ -211,16 +211,13 @@ test.describe("System — RestartDialog @medium", () => {
}); });
test.describe("System — mobile @medium @mobile", () => { test.describe("System — mobile @medium @mobile", () => {
test.skip( test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
({ frigateApp }) => !frigateApp.isMobile,
"Mobile-only",
);
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( await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
frigateApp.page.getByLabel("Select general"), timeout: 15_000,
).toBeVisible({ timeout: 15_000 }); });
}); });
test("switching tabs works at mobile viewport", async ({ frigateApp }) => { test("switching tabs works at mobile viewport", async ({ frigateApp }) => {