mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 20:18:58 +03:00
* add onboarding wizard for new users * resolve hwaccel per camera and clarify recording retention The hwaccel step listed every preset Frigate ships, so an Intel box was offered Raspberry Pi and Rockchip decoding, and the codec specific presets (`preset-intel-qsv-h264` vs `-h265`) were offered as global values that break as soon as two cameras use different codecs. `/hardware/hwaccel` now returns the decoding families the probed hardware can actually use, each carrying a preset per codec, and the wizard resolves the family against the detect stream codec the camera wizard already probed: one global `ffmpeg.hwaccel_args` when every camera agrees, per-camera `cameras.<name>.ffmpeg.hwaccel_args` when they don't. The global stays on `auto` in that case so cameras added later still resolve at startup. A gen13+ Intel machine keeps its QuickSync recommendation with mixed h264 and h265 cameras instead of dropping to vaapi. The recording step's "Days to retain recordings" only wrote alert and detection retention, and the storage estimate under it assumed continuous recording. It now asks what to record in plain language, writes `record.continuous.days` to match, shows the estimate only for continuous, and drops the spinner arrows on the number input. * clean up * add light/dark mode icon switcher * use yml as default config file extension when not found * i18n tweaks * gate the setup wizard on cameras instead of a config key * render setup wizard steps by key * share the setup wizard e2e helpers and mock users * add an account step to the setup wizard * add setup wizard account step e2e coverage * cover the account step's restart behavior * button consistency * fix test * docs * fixes
197 lines
6.6 KiB
TypeScript
197 lines
6.6 KiB
TypeScript
/**
|
|
* Setup wizard account step -- HIGH tier.
|
|
*
|
|
* Covers the step's placement and gating, the password and user payloads it
|
|
* sends, the copy it shows when nobody is signed in (the internal port), and
|
|
* that skipping it writes nothing.
|
|
*/
|
|
|
|
import { test, expect } from "../../fixtures/frigate-test";
|
|
import type { Page } from "@playwright/test";
|
|
import { installFirstRun } from "../../helpers/setup-wizard";
|
|
|
|
type Sent = {
|
|
method: string;
|
|
url: string;
|
|
body: Record<string, unknown> | null;
|
|
};
|
|
|
|
async function captureUserCalls(page: Page): Promise<Sent[]> {
|
|
const sent: Sent[] = [];
|
|
|
|
await page.route("**/api/users**", (route) => {
|
|
const request = route.request();
|
|
|
|
if (request.method() === "GET") {
|
|
return route.fulfill({ json: [{ username: "admin", role: "admin" }] });
|
|
}
|
|
|
|
sent.push({
|
|
method: request.method(),
|
|
url: request.url(),
|
|
body: request.postDataJSON(),
|
|
});
|
|
return route.fulfill({ json: { message: "ok" } });
|
|
});
|
|
|
|
return sent;
|
|
}
|
|
|
|
async function gotoAccountStep(page: Page) {
|
|
await page.getByRole("button", { name: "Get Started" }).click();
|
|
await expect(
|
|
page.getByRole("heading", { name: "Secure your account" }),
|
|
).toBeVisible();
|
|
}
|
|
|
|
test.describe("setup wizard account @high @mobile", () => {
|
|
test("sets the admin password without an old password", async ({
|
|
frigateApp,
|
|
page,
|
|
}) => {
|
|
await installFirstRun(frigateApp, page);
|
|
const sent = await captureUserCalls(page);
|
|
|
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
|
await gotoAccountStep(page);
|
|
|
|
await page.getByRole("button", { name: "Change password" }).click();
|
|
|
|
const dialog = page.getByRole("dialog");
|
|
await expect(dialog).toBeVisible();
|
|
// the dialog is in set-password mode, so it asks for no current password
|
|
await expect(
|
|
dialog.getByPlaceholder("Enter your current password"),
|
|
).toBeHidden();
|
|
await dialog
|
|
.getByPlaceholder("Enter new password", { exact: true })
|
|
.fill("a-long-enough-password");
|
|
await dialog
|
|
.getByPlaceholder("Re-enter new password")
|
|
.fill("a-long-enough-password");
|
|
await dialog.getByRole("button", { name: "Save" }).click();
|
|
|
|
await expect(page.getByText("Password set")).toBeVisible();
|
|
|
|
const passwordCall = sent.find((call) => call.method === "PUT");
|
|
expect(passwordCall?.url).toContain("/users/admin/password");
|
|
// admins are exempt from the current-password check, so it must not be sent
|
|
expect(passwordCall?.body).toEqual({ password: "a-long-enough-password" });
|
|
});
|
|
|
|
test("creates a user with a role", async ({ frigateApp, page }) => {
|
|
await installFirstRun(frigateApp, page);
|
|
const sent = await captureUserCalls(page);
|
|
|
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
|
await gotoAccountStep(page);
|
|
|
|
await page.getByRole("button", { name: "Add user" }).click();
|
|
|
|
await page.getByPlaceholder("Enter username").fill("family");
|
|
await page
|
|
.getByPlaceholder("Enter password")
|
|
.fill("a-long-enough-password");
|
|
await page
|
|
.getByPlaceholder("Confirm Password")
|
|
.fill("a-long-enough-password");
|
|
await page.getByRole("button", { name: "Save" }).click();
|
|
|
|
const createCall = sent.find((call) => call.method === "POST");
|
|
expect(createCall?.body).toEqual({
|
|
username: "family",
|
|
password: "a-long-enough-password",
|
|
role: "viewer",
|
|
});
|
|
});
|
|
|
|
test("shows anonymous copy when nobody is signed in", async ({
|
|
frigateApp,
|
|
page,
|
|
}) => {
|
|
await installFirstRun(frigateApp, page, {
|
|
profile: { username: "anonymous", role: "admin", allowed_cameras: null },
|
|
});
|
|
await captureUserCalls(page);
|
|
|
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
|
await gotoAccountStep(page);
|
|
|
|
await expect(page.getByText("doesn't require a login")).toBeVisible();
|
|
await expect(page.getByText("You're signed in as")).toBeHidden();
|
|
});
|
|
|
|
test("is absent when native auth is disabled", async ({
|
|
frigateApp,
|
|
page,
|
|
}) => {
|
|
await installFirstRun(frigateApp, page, {
|
|
config: { auth: { enabled: false } } as never,
|
|
});
|
|
await captureUserCalls(page);
|
|
|
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
|
await page.getByRole("button", { name: "Get Started" }).click();
|
|
|
|
// straight from welcome to the camera step, with no gap in the indicator
|
|
await expect(page.getByText("Add Your First Camera")).toBeVisible();
|
|
await expect(
|
|
page.getByRole("heading", { name: "Secure your account" }),
|
|
).toBeHidden();
|
|
});
|
|
|
|
test("skipping sends nothing", async ({ frigateApp, page }) => {
|
|
await installFirstRun(frigateApp, page);
|
|
const sent = await captureUserCalls(page);
|
|
|
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
|
await gotoAccountStep(page);
|
|
await page.getByRole("button", { name: "Skip" }).click();
|
|
|
|
await expect(page.getByText("Add Your First Camera")).toBeVisible();
|
|
expect(sent).toHaveLength(0);
|
|
});
|
|
test("an account change alone needs no restart", async ({
|
|
frigateApp,
|
|
page,
|
|
}) => {
|
|
await installFirstRun(frigateApp, page);
|
|
await captureUserCalls(page);
|
|
|
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
|
await gotoAccountStep(page);
|
|
|
|
await page.getByRole("button", { name: "Change password" }).click();
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog
|
|
.getByPlaceholder("Enter new password", { exact: true })
|
|
.fill("a-long-enough-password");
|
|
await dialog
|
|
.getByPlaceholder("Re-enter new password")
|
|
.fill("a-long-enough-password");
|
|
await dialog.getByRole("button", { name: "Save" }).click();
|
|
await expect(page.getByText("Password set")).toBeVisible();
|
|
await page.getByRole("button", { name: "Next" }).click();
|
|
|
|
await expect(page.getByText("Add Your First Camera")).toBeVisible();
|
|
await page.getByRole("button", { name: "Skip" }).click();
|
|
|
|
// every remaining step is passed without writing config: Skip on the
|
|
// detector, then Auto on hwaccel, which has nothing to derive and so
|
|
// saves nothing, then Skip on recording
|
|
await expect(page.getByText("Object Detection")).toBeVisible();
|
|
await page.getByRole("button", { name: "Skip" }).click();
|
|
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
|
|
await page.getByRole("button", { name: "Next" }).click();
|
|
await page.getByRole("button", { name: "Skip" }).click();
|
|
|
|
await expect(page.getByText("You're done!")).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Go to Live View" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByText("Frigate needs to restart to apply your settings"),
|
|
).toBeHidden();
|
|
});
|
|
});
|