From 8317b638a0af2c00c025fbd12d7136be0ff0e0f9 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:17:37 -0500 Subject: [PATCH] e2e test --- .../settings/camera-ffmpeg-streams.spec.ts | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts diff --git a/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts new file mode 100644 index 0000000000..526b419ca8 --- /dev/null +++ b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts @@ -0,0 +1,203 @@ +/** + * Camera ffmpeg streams settings tests -- MEDIUM tier. + * + * Covers the input-path source toggle: each ffmpeg input can either point at a + * go2rtc restream (picked from a dropdown, which writes the rtsp://127.0.0.1:8554 + * path plus the preset-rtsp-restream input_args) or use a manually typed path. + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { test, expect } from "../../fixtures/frigate-test"; +import type { Page } from "@playwright/test"; +import { configFactory } from "../../fixtures/mock-data/config"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CONFIG_SCHEMA = JSON.parse( + readFileSync( + resolve(__dirname, "../../fixtures/mock-data/config-schema.json"), + "utf-8", + ), +); + +const GO2RTC_STREAMS = { + dome_main: ["rtsp://user:pass@192.168.0.20:554/Stream1"], + dome_sub: ["rtsp://user:pass@192.168.0.20:554/Stream2"], +}; + +type CameraInput = { + path: string; + roles: string[]; + input_args?: string; +}; + +async function installRoutes(page: Page, frontDoorInputs: CameraInput[]) { + const config = configFactory({ + go2rtc: { streams: GO2RTC_STREAMS }, + cameras: { + front_door: { + ffmpeg: { inputs: frontDoorInputs }, + }, + }, + }); + + let lastSavedConfig: unknown = null; + + await page.route("**/api/config/schema.json", (route) => + route.fulfill({ json: CONFIG_SCHEMA }), + ); + await page.route("**/api/config", (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: config }); + } + return route.fulfill({ json: { success: true } }); + }); + await page.route("**/api/config/raw_paths", (route) => + route.fulfill({ + json: { + cameras: { front_door: { ffmpeg: { inputs: frontDoorInputs } } }, + go2rtc: { streams: GO2RTC_STREAMS }, + }, + }), + ); + await page.route("**/api/config/set", async (route) => { + lastSavedConfig = route.request().postDataJSON(); + await route.fulfill({ json: { success: true, require_restart: false } }); + }); + await page.route("**/api/ffmpeg/presets", (route) => + route.fulfill({ + json: { + hwaccel_args: [], + input_args: ["preset-rtsp-restream", "preset-rtsp-generic"], + output_args: { record: [], detect: [] }, + }, + }), + ); + + return { capturedConfig: () => lastSavedConfig }; +} + +const RESTREAM_RADIO = "Restream (go2rtc)"; +const MANUAL_RADIO = "Manual input path"; + +test.describe("camera ffmpeg input source toggle @medium", () => { + test("manual input defaults to the manual text field", async ({ + frigateApp, + }) => { + await installRoutes(frigateApp.page, [ + { path: "rtsp://10.0.0.1:554/video", roles: ["detect"] }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await expect( + frigateApp.page.getByRole("radio", { name: MANUAL_RADIO }), + ).toBeChecked(); + await expect( + frigateApp.page.getByRole("textbox", { name: "Input path" }), + ).toHaveValue("rtsp://10.0.0.1:554/video"); + }); + + test("an existing restream path auto-detects into restream mode", async ({ + frigateApp, + }) => { + await installRoutes(frigateApp.page, [ + { + path: "rtsp://127.0.0.1:8554/dome_main", + roles: ["detect"], + input_args: "preset-rtsp-restream", + }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await expect( + frigateApp.page.getByRole("radio", { name: RESTREAM_RADIO }), + ).toBeChecked(); + // The dropdown is preselected to the matching go2rtc stream. + await expect( + frigateApp.page.getByRole("combobox", { name: /go2rtc stream/i }), + ).toContainText("dome_main"); + }); + + test("selecting a restream writes the path and preset", async ({ + frigateApp, + }) => { + const capture = await installRoutes(frigateApp.page, [ + { path: "rtsp://10.0.0.1:554/video", roles: ["detect"] }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await frigateApp.page.getByRole("radio", { name: RESTREAM_RADIO }).click(); + await frigateApp.page + .getByRole("combobox", { name: /go2rtc stream/i }) + .click(); + + // The dropdown is searchable: typing narrows the list to matches only, + // with no option to enter a custom stream name. + await frigateApp.page.getByPlaceholder("Search streams…").fill("sub"); + await expect( + frigateApp.page.getByRole("option", { name: "dome_main" }), + ).toBeHidden(); + await frigateApp.page.getByRole("option", { name: "dome_sub" }).click(); + + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .toMatchObject({ + config_data: { + cameras: { + front_door: { + ffmpeg: { + inputs: [ + { + path: "rtsp://127.0.0.1:8554/dome_sub", + input_args: "preset-rtsp-restream", + }, + ], + }, + }, + }, + }, + }); + }); + + test("switching a restream back to manual reverts the preset", async ({ + frigateApp, + }) => { + const capture = await installRoutes(frigateApp.page, [ + { + path: "rtsp://127.0.0.1:8554/dome_main", + roles: ["detect"], + input_args: "preset-rtsp-restream", + }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await frigateApp.page.getByRole("radio", { name: MANUAL_RADIO }).click(); + + // The restream path stays editable in the manual text field. + await expect( + frigateApp.page.getByRole("textbox", { name: "Input path" }), + ).toHaveValue("rtsp://127.0.0.1:8554/dome_main"); + + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .not.toBeNull(); + + const payload = capture.capturedConfig() as { + config_data?: { + cameras?: { + front_door?: { + ffmpeg?: { inputs?: Array<{ input_args?: unknown }> }; + }; + }; + }; + }; + const input = + payload?.config_data?.cameras?.front_door?.ffmpeg?.inputs?.[0]; + expect(input?.input_args).not.toBe("preset-rtsp-restream"); + }); +});