diff --git a/web/e2e/helpers/api-mocker.ts b/web/e2e/helpers/api-mocker.ts index 54718e63ae..0663250c4e 100644 --- a/web/e2e/helpers/api-mocker.ts +++ b/web/e2e/helpers/api-mocker.ts @@ -47,6 +47,7 @@ export interface ApiMockOverrides { recommended: string; available?: { key: string; presets: Record }[]; }; + users?: { username: string; role: string }[]; } export class ApiMocker { @@ -200,6 +201,16 @@ export class ApiMocker { }), ); + // Users. GET lists them; POST/PUT (create, password) just succeed, so + // tests assert on the intercepted request body instead of a response. + await this.page.route("**/api/users**", (route) => + route.request().method() === "GET" + ? route.fulfill({ + json: overrides?.users ?? [{ username: "admin", role: "admin" }], + }) + : route.fulfill({ json: { message: "ok" } }), + ); + // Go2RTC streams await this.page.route("**/api/go2rtc/streams**", (route) => route.fulfill({ json: {} }), diff --git a/web/e2e/helpers/setup-wizard.ts b/web/e2e/helpers/setup-wizard.ts new file mode 100644 index 0000000000..4c86d5ef20 --- /dev/null +++ b/web/e2e/helpers/setup-wizard.ts @@ -0,0 +1,51 @@ +/** + * Shared setup-wizard e2e helpers. + * + * The wizard shows when config has no cameras, so a first run is mocked by + * serving a camera-less config until the returned callback is fired. + */ + +import type { Page } from "@playwright/test"; +import { expect } from "../fixtures/frigate-test"; +import { configFactory } from "../fixtures/mock-data/config"; +import type { ApiMockOverrides } from "./api-mocker"; + +export async function installFirstRun( + frigateApp: { installDefaults: (o?: ApiMockOverrides) => Promise }, + page: Page, + overrides?: ApiMockOverrides, +): Promise<() => void> { + await frigateApp.installDefaults(overrides); + + const full = configFactory(overrides?.config); + let cameras: unknown = {}; + + await page.route("**/api/config", (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: { ...full, cameras } }); + } + return route.fulfill({ json: { success: true } }); + }); + + return () => { + cameras = full.cameras; + }; +} + +export async function gotoDetectorStep(page: Page, addCamera: () => void) { + await page.getByRole("button", { name: "Get Started" }).click(); + await expect(page.getByText("Add Your First Camera")).toBeVisible(); + + // the camera step's Next only renders once its local addedCameras fills, + // which SetupCamera does by refetching config when the dialog closes, so + // opening and cancelling the dialog is what reveals Next + await page.getByRole("button", { name: "Add Camera" }).click(); + addCamera(); + await page.getByRole("button", { name: "Cancel" }).click(); + + await expect(page.getByRole("button", { name: "Next" })).toBeVisible({ + timeout: 10_000, + }); + await page.getByRole("button", { name: "Next" }).click(); + await expect(page.getByText("Object Detection")).toBeVisible(); +} diff --git a/web/e2e/specs/setup/wizard-hardware.spec.ts b/web/e2e/specs/setup/wizard-hardware.spec.ts index c1ae05b96b..57d0e43b3d 100644 --- a/web/e2e/specs/setup/wizard-hardware.spec.ts +++ b/web/e2e/specs/setup/wizard-hardware.spec.ts @@ -9,8 +9,7 @@ import { test, expect } from "../../fixtures/frigate-test"; import type { Page } from "@playwright/test"; -import { configFactory } from "../../fixtures/mock-data/config"; -import type { ApiMockOverrides } from "../../helpers/api-mocker"; +import { gotoDetectorStep, installFirstRun } from "../../helpers/setup-wizard"; const NVIDIA_HARDWARE = [ { @@ -57,51 +56,6 @@ async function captureRestarts(page: Page): Promise { return calls; } -/** - * Install mocks for a first run: the wizard shows when config has no cameras, - * so the config route starts empty and only reports cameras once the returned - * callback is fired, standing in for the camera the user adds. - */ -async function installFirstRun( - frigateApp: { installDefaults: (o?: ApiMockOverrides) => Promise }, - page: Page, - overrides?: ApiMockOverrides, -): Promise<() => void> { - await frigateApp.installDefaults(overrides); - - const full = configFactory(overrides?.config); - let cameras: unknown = {}; - - await page.route("**/api/config", (route) => { - if (route.request().method() === "GET") { - return route.fulfill({ json: { ...full, cameras } }); - } - return route.fulfill({ json: { success: true } }); - }); - - return () => { - cameras = full.cameras; - }; -} - -async function gotoDetectorStep(page: Page, addCamera: () => void) { - await page.getByRole("button", { name: "Get Started" }).click(); - await expect(page.getByText("Add Your First Camera")).toBeVisible(); - - // the camera step's Next only renders once its local addedCameras fills, - // which SetupCamera does by refetching config when the dialog closes, so - // opening and cancelling the dialog is what reveals Next - await page.getByRole("button", { name: "Add Camera" }).click(); - addCamera(); - await page.getByRole("button", { name: "Cancel" }).click(); - - await expect(page.getByRole("button", { name: "Next" })).toBeVisible({ - timeout: 10_000, - }); - await page.getByRole("button", { name: "Next" }).click(); - await expect(page.getByText("Object Detection")).toBeVisible(); -} - test.describe("setup wizard hardware @high @mobile", () => { test("lists probed hardware and writes a models config", async ({ frigateApp,