mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-14 16:01:13 +03:00
formatting
This commit is contained in:
parent
c6138a973e
commit
7fcf0bb827
@ -14,10 +14,7 @@ import { expect, type Page } from "@playwright/test";
|
||||
* lines. For short configs (our mocks) that's the full content.
|
||||
*/
|
||||
export async function getMonacoVisibleText(page: Page): Promise<string> {
|
||||
return page
|
||||
.locator(".monaco-editor .view-lines")
|
||||
.first()
|
||||
.innerText();
|
||||
return page.locator(".monaco-editor .view-lines").first().innerText();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -9,14 +9,6 @@
|
||||
import { test, expect } from "../fixtures/frigate-test";
|
||||
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("admin /system renders general tab", async ({ frigateApp }) => {
|
||||
await frigateApp.goto("/system");
|
||||
@ -92,10 +84,7 @@ test.describe("Auth — viewer restrictions @high", () => {
|
||||
});
|
||||
|
||||
test.describe("Auth — viewer nav restrictions (desktop) @high", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => frigateApp.isMobile,
|
||||
"Sidebar only on desktop",
|
||||
);
|
||||
test.skip(({ frigateApp }) => frigateApp.isMobile, "Sidebar only on desktop");
|
||||
|
||||
test("viewer sidebar hides admin routes", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({ profile: viewerProfile() });
|
||||
|
||||
@ -7,13 +7,6 @@
|
||||
|
||||
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
|
||||
* 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.press("Enter");
|
||||
|
||||
await expect(
|
||||
frigateApp.page.getByText(/Hello, world!/i),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(frigateApp.page.getByText(/Hello, world!/i)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
// normalizeName(). Match the rendered display label instead.
|
||||
await expect(
|
||||
frigateApp.page.getByText(/search objects/i),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(frigateApp.page.getByText(/search objects/i)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(
|
||||
frigateApp.page.getByText(/searching for people/i),
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
@ -210,9 +203,9 @@ test.describe("Chat — stop @medium", () => {
|
||||
await input.press("Enter");
|
||||
|
||||
// Wait for the first chunk to render
|
||||
await expect(
|
||||
frigateApp.page.getByText(/First chunk\./),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(frigateApp.page.getByText(/First chunk\./)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// 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
|
||||
@ -228,9 +221,7 @@ test.describe("Chat — stop @medium", () => {
|
||||
});
|
||||
|
||||
// Third chunk should never appear.
|
||||
await expect(
|
||||
frigateApp.page.getByText(/Third chunk\./),
|
||||
).toHaveCount(0);
|
||||
await expect(frigateApp.page.getByText(/Third chunk\./)).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -308,10 +299,7 @@ test.describe("Chat — attachment chip @medium", () => {
|
||||
});
|
||||
|
||||
test.describe("Chat — mobile @medium @mobile", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => !frigateApp.isMobile,
|
||||
"Mobile-only",
|
||||
);
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("chat input is focusable at mobile viewport", async ({ frigateApp }) => {
|
||||
await frigateApp.goto("/chat");
|
||||
|
||||
@ -49,9 +49,9 @@ test.describe("Classification — model list @medium", () => {
|
||||
await installDatasetRoute(frigateApp, "state_classifier");
|
||||
await frigateApp.goto("/classification");
|
||||
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
|
||||
await expect(
|
||||
frigateApp.page.getByText("object_classifier"),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
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 frigateApp.goto("/classification");
|
||||
// Objects is default — object_classifier visible, state_classifier hidden.
|
||||
await expect(
|
||||
frigateApp.page.getByText("object_classifier"),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(
|
||||
frigateApp.page.getByText("state_classifier"),
|
||||
).toHaveCount(0);
|
||||
await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(frigateApp.page.getByText("state_classifier")).toHaveCount(0);
|
||||
|
||||
// Click the "states" toggle. Radix ToggleGroup type="single" uses role="radio".
|
||||
const statesToggle = frigateApp.page
|
||||
@ -88,12 +86,10 @@ test.describe("Classification — model list @medium", () => {
|
||||
await expect(statesToggle).toBeVisible({ timeout: 5_000 });
|
||||
await statesToggle.click();
|
||||
|
||||
await expect(
|
||||
frigateApp.page.getByText("state_classifier"),
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
await expect(
|
||||
frigateApp.page.getByText("object_classifier"),
|
||||
).toHaveCount(0);
|
||||
await expect(frigateApp.page.getByText("state_classifier")).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
await expect(frigateApp.page.getByText("object_classifier")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -109,9 +105,7 @@ test.describe("Classification — model detail navigation @medium", () => {
|
||||
await installTrainRoute(frigateApp, "object_classifier");
|
||||
await frigateApp.goto("/classification");
|
||||
|
||||
const objectCard = frigateApp.page
|
||||
.getByText("object_classifier")
|
||||
.first();
|
||||
const objectCard = frigateApp.page.getByText("object_classifier").first();
|
||||
await expect(objectCard).toBeVisible({ timeout: 10_000 });
|
||||
await objectCard.click();
|
||||
|
||||
@ -123,9 +117,7 @@ test.describe("Classification — model detail navigation @medium", () => {
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// The model grid is no longer shown; state_classifier card is gone.
|
||||
await expect(
|
||||
frigateApp.page.getByText("state_classifier"),
|
||||
).toHaveCount(0);
|
||||
await expect(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 frigateApp.goto("/classification");
|
||||
await expect(
|
||||
frigateApp.page.getByText("object_classifier"),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// The card-level actions menu (FiMoreVertical three-dot icon) is a
|
||||
// DropdownMenuTrigger with asChild on a BlurredIconButton div.
|
||||
// Radix forwards aria-haspopup="menu" to the child element.
|
||||
// Scope the selector to the model card grid to avoid hitting the
|
||||
// settings sidebar trigger.
|
||||
const cardGrid = frigateApp.page.locator(
|
||||
".grid.auto-rows-max",
|
||||
);
|
||||
const cardGrid = frigateApp.page.locator(".grid.auto-rows-max");
|
||||
await expect(cardGrid).toBeVisible({ timeout: 5_000 });
|
||||
const trigger = cardGrid.locator('[aria-haspopup="menu"]').first();
|
||||
await expect(trigger).toBeVisible({ timeout: 5_000 });
|
||||
@ -192,7 +182,10 @@ test.describe("Classification — delete model (desktop) @medium", () => {
|
||||
// Confirm the AlertDialog.
|
||||
const alert = frigateApp.page.getByRole("alertdialog");
|
||||
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(() => configSetCalled, { timeout: 5_000 }).toBe(true);
|
||||
@ -219,10 +212,7 @@ test.describe("Classification — admin only @medium", () => {
|
||||
});
|
||||
|
||||
test.describe("Classification — mobile @medium @mobile", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => !frigateApp.isMobile,
|
||||
"Mobile-only",
|
||||
);
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("page renders at mobile viewport", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({
|
||||
|
||||
@ -7,27 +7,25 @@
|
||||
*/
|
||||
|
||||
import { test, expect } from "../fixtures/frigate-test";
|
||||
import {
|
||||
installWsFrameCapture,
|
||||
waitForWsFrame,
|
||||
} from "../helpers/ws-frames";
|
||||
import {
|
||||
grantClipboardPermissions,
|
||||
readClipboard,
|
||||
} from "../helpers/clipboard";
|
||||
import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames";
|
||||
import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard";
|
||||
import {
|
||||
getMonacoVisibleText,
|
||||
replaceMonacoValue,
|
||||
waitForErrorMarker,
|
||||
} 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(
|
||||
app: { page: import("@playwright/test").Page },
|
||||
status: number,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<{ capturedUrl: () => string | null; capturedBody: () => string | null }> {
|
||||
): Promise<{
|
||||
capturedUrl: () => string | null;
|
||||
capturedBody: () => string | null;
|
||||
}> {
|
||||
let lastUrl: string | null = null;
|
||||
let lastBody: string | null = null;
|
||||
await app.page.route("**/api/config/save**", async (route) => {
|
||||
@ -42,14 +40,12 @@ async function installSaveRoute(
|
||||
}
|
||||
|
||||
test.describe("Config Editor — Monaco @medium", () => {
|
||||
test("editor loads with mocked configRaw content", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
test("editor loads with mocked configRaw content", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
|
||||
await frigateApp.goto("/config");
|
||||
await expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
// Assert via DOM-rendered visible text (Monaco virtualizes — works
|
||||
// for short configs which covers our mocked content).
|
||||
await expect
|
||||
@ -72,17 +68,17 @@ test.describe("Config Editor — Save @medium", () => {
|
||||
message: "Config saved",
|
||||
});
|
||||
await frigateApp.goto("/config");
|
||||
await expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
await frigateApp.page.getByLabel("Save Only").click();
|
||||
await expect.poll(() => capture.capturedUrl(), { timeout: 5_000 }).toMatch(
|
||||
/config\/save\?save_option=saveonly/,
|
||||
);
|
||||
await expect
|
||||
.poll(() => capture.capturedUrl(), { timeout: 5_000 })
|
||||
.toMatch(/config\/save\?save_option=saveonly/);
|
||||
// Body is the raw YAML as text/plain
|
||||
await expect.poll(() => capture.capturedBody(), { timeout: 5_000 }).toContain(
|
||||
"front_door",
|
||||
);
|
||||
await expect
|
||||
.poll(() => capture.capturedBody(), { timeout: 5_000 })
|
||||
.toContain("front_door");
|
||||
});
|
||||
|
||||
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`",
|
||||
});
|
||||
await frigateApp.goto("/config");
|
||||
await expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
await frigateApp.page.getByLabel("Save Only").click();
|
||||
await expect(
|
||||
frigateApp.page.getByText(/Invalid field/i),
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
await expect(frigateApp.page.getByText(/Invalid field/i)).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -117,9 +113,9 @@ test.describe("Config Editor — Save and Restart @medium", () => {
|
||||
await installWsFrameCapture(frigateApp.page);
|
||||
|
||||
await frigateApp.goto("/config");
|
||||
await expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
|
||||
await frigateApp.page.getByLabel("Save & Restart").click();
|
||||
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 frigateApp.goto("/config");
|
||||
await expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
|
||||
await frigateApp.page.getByLabel("Save & Restart").click();
|
||||
const dialog = frigateApp.page.getByRole("alertdialog");
|
||||
@ -168,14 +164,14 @@ test.describe("Config Editor — Copy @medium", () => {
|
||||
await grantClipboardPermissions(context);
|
||||
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
|
||||
await frigateApp.goto("/config");
|
||||
await expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
|
||||
await frigateApp.page.getByLabel("Copy Config").click();
|
||||
await expect.poll(() => readClipboard(frigateApp.page), { timeout: 5_000 }).toContain(
|
||||
"front_door",
|
||||
);
|
||||
await expect
|
||||
.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.goto("/config");
|
||||
await expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
|
||||
// Replace editor contents with clearly invalid YAML via keyboard.
|
||||
await replaceMonacoValue(
|
||||
@ -219,17 +215,17 @@ test.describe("Config Editor — Cmd+S keyboard shortcut @medium", () => {
|
||||
message: "Saved",
|
||||
});
|
||||
await frigateApp.goto("/config");
|
||||
await expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
|
||||
// Focus the editor so Monaco's keybinding receives the shortcut.
|
||||
await frigateApp.page.locator(".monaco-editor").first().click();
|
||||
await frigateApp.page.keyboard.press("ControlOrMeta+s");
|
||||
|
||||
await expect.poll(() => capture.capturedUrl(), { timeout: 5_000 }).toMatch(
|
||||
/config\/save\?save_option=saveonly/,
|
||||
);
|
||||
await expect
|
||||
.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 expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
await expect.poll(() => autoSaveCalled, { timeout: 10_000 }).toBe(true);
|
||||
await expect(
|
||||
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.skip(
|
||||
({ frigateApp }) => !frigateApp.isMobile,
|
||||
"Mobile-only",
|
||||
);
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("editor renders at narrow viewport", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
|
||||
await frigateApp.goto("/config");
|
||||
await expect(
|
||||
frigateApp.page.locator(".monaco-editor").first(),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -78,10 +78,7 @@ test.describe("Explore — search @high", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe("Explore — filters (desktop) @high", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => frigateApp.isMobile,
|
||||
"Desktop popovers",
|
||||
);
|
||||
test.skip(({ frigateApp }) => frigateApp.isMobile, "Desktop popovers");
|
||||
|
||||
test("Cameras popover lists configured cameras", async ({ frigateApp }) => {
|
||||
// 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 });
|
||||
// "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");
|
||||
});
|
||||
|
||||
@ -251,10 +250,7 @@ test.describe("Explore — similarity search (desktop) @high", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe("Explore — mobile @high @mobile", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => !frigateApp.isMobile,
|
||||
"Mobile-only",
|
||||
);
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("search input is focusable at mobile viewport", async ({
|
||||
frigateApp,
|
||||
|
||||
@ -190,9 +190,9 @@ test.describe("Face Library — delete flow (desktop) @high", () => {
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect.poll(() => deleteUrl, { timeout: 5_000 }).toMatch(
|
||||
/\/faces\/alice\/delete/,
|
||||
);
|
||||
await expect
|
||||
.poll(() => deleteUrl, { timeout: 5_000 })
|
||||
.toMatch(/\/faces\/alice\/delete/);
|
||||
expect(deleteBody).toMatchObject({ ids: expect.any(Array) });
|
||||
});
|
||||
});
|
||||
@ -245,9 +245,9 @@ test.describe("Face Library — rename flow (desktop) @high", () => {
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect.poll(() => renameUrl, { timeout: 5_000 }).toMatch(
|
||||
/\/faces\/alice\/rename/,
|
||||
);
|
||||
await expect
|
||||
.poll(() => renameUrl, { timeout: 5_000 })
|
||||
.toMatch(/\/faces\/alice\/rename/);
|
||||
expect(renameBody).toEqual({ new_name: "alice_renamed" });
|
||||
});
|
||||
});
|
||||
@ -361,10 +361,7 @@ test.describe("FaceSelectionDialog @high", () => {
|
||||
});
|
||||
|
||||
test.describe("Face Library — mobile @high @mobile", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => !frigateApp.isMobile,
|
||||
"Mobile-only",
|
||||
);
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("mobile library selector dropdown closes cleanly on Escape", async ({
|
||||
frigateApp,
|
||||
|
||||
@ -9,10 +9,7 @@
|
||||
|
||||
import { test, expect } from "../fixtures/frigate-test";
|
||||
import { LivePage } from "../pages/live.page";
|
||||
import {
|
||||
installWsFrameCapture,
|
||||
waitForWsFrame,
|
||||
} from "../helpers/ws-frames";
|
||||
import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames";
|
||||
import {
|
||||
expectBodyInteractive,
|
||||
waitForBodyInteractive,
|
||||
@ -86,7 +83,9 @@ test.describe("Live Single Camera — desktop controls @critical", () => {
|
||||
await frigateApp.goto("/#front_door");
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
// 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();
|
||||
expect(activeBefore).toBeGreaterThan(0);
|
||||
|
||||
@ -96,9 +95,11 @@ test.describe("Live Single Camera — desktop controls @critical", () => {
|
||||
// front_door/ prefix + /set suffix (any feature).
|
||||
await waitForWsFrame(
|
||||
frigateApp.page,
|
||||
(frame) =>
|
||||
frame.includes("front_door/") && frame.includes("/set"),
|
||||
{ message: "feature toggle should dispatch a <camera>/<feature>/set frame" },
|
||||
(frame) => frame.includes("front_door/") && frame.includes("/set"),
|
||||
{
|
||||
message:
|
||||
"feature toggle should dispatch a <camera>/<feature>/set frame",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -206,10 +207,7 @@ test.describe("Live PTZ preset dropdown @critical", () => {
|
||||
.first();
|
||||
await expect(menu).toBeVisible({ timeout: 3_000 });
|
||||
|
||||
await menu
|
||||
.getByRole("menuitem", { name: PRESET_NAMES[0] })
|
||||
.first()
|
||||
.click();
|
||||
await menu.getByRole("menuitem", { name: PRESET_NAMES[0] }).first().click();
|
||||
await expect(menu).not.toBeVisible({ timeout: 3_000 });
|
||||
|
||||
await waitForWsFrame(
|
||||
@ -251,7 +249,9 @@ test.describe("Live mobile layout @critical @mobile", () => {
|
||||
await frigateApp.goto("/");
|
||||
await expect(frigateApp.page.locator("aside")).toHaveCount(0);
|
||||
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 }) => {
|
||||
|
||||
@ -7,10 +7,7 @@
|
||||
*/
|
||||
|
||||
import { test, expect } from "../fixtures/frigate-test";
|
||||
import {
|
||||
grantClipboardPermissions,
|
||||
readClipboard,
|
||||
} from "../helpers/clipboard";
|
||||
import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard";
|
||||
|
||||
function logsJsonBody(lines: string[]) {
|
||||
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.
|
||||
await frigateApp.page.route(
|
||||
/\/api\/logs\/frigate\?stream=true/,
|
||||
(route) => route.fulfill({ status: 200, body: "" }),
|
||||
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
|
||||
route.fulfill({ status: 200, body: "" }),
|
||||
);
|
||||
await frigateApp.goto("/logs");
|
||||
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
await expect(
|
||||
frigateApp.page.getByText(/Frigate started/),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("switching to go2rtc fires a GET to /logs/go2rtc", async ({
|
||||
@ -85,14 +81,13 @@ test.describe("Logs — actions @medium", () => {
|
||||
]),
|
||||
}),
|
||||
);
|
||||
await frigateApp.page.route(
|
||||
/\/api\/logs\/frigate\?stream=true/,
|
||||
(route) => route.fulfill({ status: 200, body: "" }),
|
||||
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
|
||||
route.fulfill({ status: 200, body: "" }),
|
||||
);
|
||||
await frigateApp.goto("/logs");
|
||||
await expect(
|
||||
frigateApp.page.getByText(/Frigate started/),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const copyBtn = frigateApp.page.getByLabel("Copy to Clipboard");
|
||||
await expect(copyBtn).toBeVisible({ timeout: 5_000 });
|
||||
@ -112,9 +107,8 @@ test.describe("Logs — actions @medium", () => {
|
||||
}
|
||||
return route.fulfill({ json: logsJsonBody(["frigate line"]) });
|
||||
});
|
||||
await frigateApp.page.route(
|
||||
/\/api\/logs\/frigate\?stream=true/,
|
||||
(route) => route.fulfill({ status: 200, body: "" }),
|
||||
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
|
||||
route.fulfill({ status: 200, body: "" }),
|
||||
);
|
||||
|
||||
await frigateApp.goto("/logs");
|
||||
@ -132,9 +126,8 @@ test.describe("Logs — websocket tab @medium", () => {
|
||||
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
|
||||
route.fulfill({ json: logsJsonBody(["frigate line"]) }),
|
||||
);
|
||||
await frigateApp.page.route(
|
||||
/\/api\/logs\/frigate\?stream=true/,
|
||||
(route) => route.fulfill({ status: 200, body: "" }),
|
||||
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
|
||||
route.fulfill({ status: 200, body: "" }),
|
||||
);
|
||||
await frigateApp.goto("/logs");
|
||||
const wsTab = frigateApp.page.getByLabel("Select websocket");
|
||||
@ -152,9 +145,7 @@ test.describe("Logs — streaming @medium", () => {
|
||||
return route.fallback();
|
||||
}
|
||||
return route.fulfill({
|
||||
json: logsJsonBody([
|
||||
"[2026-04-06 10:00:00] INFO: initial batch line",
|
||||
]),
|
||||
json: logsJsonBody(["[2026-04-06 10:00:00] INFO: initial batch line"]),
|
||||
});
|
||||
});
|
||||
|
||||
@ -179,11 +170,15 @@ test.describe("Logs — streaming @medium", () => {
|
||||
async start(controller) {
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
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));
|
||||
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();
|
||||
},
|
||||
@ -197,35 +192,31 @@ test.describe("Logs — streaming @medium", () => {
|
||||
await frigateApp.goto("/logs");
|
||||
// The initial batch line is parsed by LogLineData and its content is
|
||||
// rendered in a .log-content cell — assert against that element.
|
||||
await expect(
|
||||
frigateApp.page.getByText("initial batch line"),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(
|
||||
frigateApp.page.getByText(/streamed line one/),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(
|
||||
frigateApp.page.getByText(/streamed line two/),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(frigateApp.page.getByText("initial batch line")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(frigateApp.page.getByText(/streamed line one/)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(frigateApp.page.getByText(/streamed line two/)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Logs — mobile @medium @mobile", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => !frigateApp.isMobile,
|
||||
"Mobile-only",
|
||||
);
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("service tabs render at mobile viewport", async ({ frigateApp }) => {
|
||||
await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
|
||||
route.fulfill({ json: logsJsonBody(["frigate line"]) }),
|
||||
);
|
||||
await frigateApp.page.route(
|
||||
/\/api\/logs\/frigate\?stream=true/,
|
||||
(route) => route.fulfill({ status: 200, body: "" }),
|
||||
await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
|
||||
route.fulfill({ status: 200, body: "" }),
|
||||
);
|
||||
await frigateApp.goto("/logs");
|
||||
await expect(
|
||||
frigateApp.page.getByLabel("Select frigate"),
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -75,7 +75,12 @@ test.describe("Navigation — conditional items @critical", () => {
|
||||
test.skip(frigateApp.isMobile, "Desktop sidebar");
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
genai: { enabled: false, provider: "ollama", model: "none", base_url: "" },
|
||||
genai: {
|
||||
enabled: false,
|
||||
provider: "ollama",
|
||||
model: "none",
|
||||
base_url: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/");
|
||||
|
||||
@ -68,9 +68,7 @@ test.describe("Replay — active session @medium", () => {
|
||||
frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
// Three tabs (Debug / Objects / Messages) in TabsList
|
||||
await expect(
|
||||
frigateApp.page.locator('[role="tab"]'),
|
||||
).toHaveCount(3);
|
||||
await expect(frigateApp.page.locator('[role="tab"]')).toHaveCount(3);
|
||||
});
|
||||
|
||||
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).
|
||||
// It is the first button inside the right-side action group div
|
||||
// (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();
|
||||
|
||||
const dialog = frigateApp.page.getByRole("dialog");
|
||||
@ -172,9 +172,9 @@ test.describe("Replay — active session @medium", () => {
|
||||
});
|
||||
|
||||
// The object row renders the label.
|
||||
await expect(
|
||||
frigateApp.page.getByText(/person/i).first(),
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
await expect(frigateApp.page.getByText(/person/i).first()).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
});
|
||||
|
||||
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.skip(
|
||||
({ frigateApp }) => !frigateApp.isMobile,
|
||||
"Mobile-only",
|
||||
);
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("no-session state renders at mobile viewport", async ({
|
||||
frigateApp,
|
||||
|
||||
@ -163,9 +163,7 @@ test.describe("Review — filters (desktop) @critical", () => {
|
||||
// Radix popper wrapper), so scope by the grid role instead.
|
||||
const calendarGrid = frigateApp.page.locator('[role="grid"]').first();
|
||||
await expect(calendarGrid).toBeVisible({ timeout: 3_000 });
|
||||
const dayButton = calendarGrid
|
||||
.locator('[role="gridcell"] button')
|
||||
.first();
|
||||
const dayButton = calendarGrid.locator('[role="gridcell"] button').first();
|
||||
await expect(dayButton).toBeVisible({ timeout: 3_000 });
|
||||
await frigateApp.page.keyboard.press("Escape");
|
||||
});
|
||||
@ -183,7 +181,8 @@ test.describe("Review — filters (desktop) @critical", () => {
|
||||
await expect(showReviewedSwitch).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// 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();
|
||||
const flippedChecked = initialChecked === "true" ? "false" : "true";
|
||||
await expect(showReviewedSwitch).toHaveAttribute(
|
||||
@ -211,10 +210,7 @@ test.describe("Review — timeline (desktop) @critical", () => {
|
||||
});
|
||||
|
||||
test.describe("Review — mobile @critical @mobile", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => !frigateApp.isMobile,
|
||||
"Mobile-only",
|
||||
);
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("severity tabs render on mobile", async ({ frigateApp }) => {
|
||||
await frigateApp.goto("/review");
|
||||
|
||||
@ -104,11 +104,9 @@ 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(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
|
||||
"data-state",
|
||||
"off",
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
await expect(
|
||||
frigateApp.page.getByLabel("Select general"),
|
||||
).toHaveAttribute("data-state", "off", { timeout: 5_000 });
|
||||
}
|
||||
});
|
||||
|
||||
@ -129,7 +127,9 @@ test.describe("System — tabs @medium", () => {
|
||||
// mock has front_door, backyard, garage.
|
||||
for (const cam of ["front_door", "backyard", "garage"]) {
|
||||
await expect(
|
||||
frigateApp.page.getByText(new RegExp(cam.replace("_", ".?"), "i")).first(),
|
||||
frigateApp.page
|
||||
.getByText(new RegExp(cam.replace("_", ".?"), "i"))
|
||||
.first(),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
});
|
||||
@ -211,16 +211,13 @@ test.describe("System — RestartDialog @medium", () => {
|
||||
});
|
||||
|
||||
test.describe("System — mobile @medium @mobile", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => !frigateApp.isMobile,
|
||||
"Mobile-only",
|
||||
);
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("tabs render at mobile viewport", async ({ frigateApp }) => {
|
||||
await frigateApp.goto("/system#general");
|
||||
await expect(
|
||||
frigateApp.page.getByLabel("Select general"),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("switching tabs works at mobile viewport", async ({ frigateApp }) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user