mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-26 16:34:43 +03:00
update zod to 4.6.7
zod 4 types the input of z.coerce.number() as unknown, so the zone, camera wizard, and classification edit forms pass number to keep their field types. The ONVIF port error showed zod's built-in English text, which zod 4 rewords, so it now uses a translated message. New e2e tests cover zone inertia validation and camera wizard step 1 validation.
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Add-camera wizard Step 1 validation -- MEDIUM tier.
|
||||||
|
*
|
||||||
|
* Continue validates every field, so an invalid ONVIF port or a non-RTSP
|
||||||
|
* custom URL must show its translated message and keep the wizard on Step 1.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../fixtures/frigate-test";
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
const PORT_ERROR = "ONVIF port must be a whole number from 1 to 65535";
|
||||||
|
|
||||||
|
async function openWizard(page: Page) {
|
||||||
|
await page.getByRole("button", { name: /Add New Camera/i }).click();
|
||||||
|
const dialog = page.getByRole("dialog");
|
||||||
|
await expect(dialog).toBeVisible();
|
||||||
|
await dialog.getByPlaceholder(/front_door/i).fill("step1_camera");
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("Camera wizard step 1 validation @medium @mobile", () => {
|
||||||
|
test.beforeEach(async ({ frigateApp }) => {
|
||||||
|
// not in the default mock; unmocked it 500s and trips the error collector
|
||||||
|
await frigateApp.page.route("**/api/config/raw_paths", (route) =>
|
||||||
|
route.fulfill({ json: {} }),
|
||||||
|
);
|
||||||
|
await frigateApp.page.route("**/api/onvif/probe**", (route) =>
|
||||||
|
route.fulfill({ json: { success: false } }),
|
||||||
|
);
|
||||||
|
await frigateApp.goto("/settings?page=cameraManagement");
|
||||||
|
await expect(
|
||||||
|
frigateApp.page.getByRole("heading", { name: /Manage Cameras/i }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an invalid ONVIF port blocks Continue with a translated message", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
|
const dialog = await openWizard(frigateApp.page);
|
||||||
|
await dialog.getByPlaceholder("192.168.1.100").fill("192.168.1.100");
|
||||||
|
const port = dialog.getByPlaceholder("80", { exact: true });
|
||||||
|
const next = dialog.getByRole("button", { name: /^Continue$/i });
|
||||||
|
|
||||||
|
for (const value of ["abc", "0", "70000", "80.5"]) {
|
||||||
|
await port.fill(value);
|
||||||
|
await next.click();
|
||||||
|
await expect(dialog.getByText(PORT_ERROR)).toBeVisible();
|
||||||
|
await expect(port).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
await port.fill("8080");
|
||||||
|
await next.click();
|
||||||
|
await expect(port).toBeHidden();
|
||||||
|
await expect(dialog.getByText(PORT_ERROR)).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a custom URL must use RTSP", async ({ frigateApp }) => {
|
||||||
|
const dialog = await openWizard(frigateApp.page);
|
||||||
|
await dialog.getByText("Manual selection", { exact: true }).click();
|
||||||
|
await dialog.getByRole("combobox").click();
|
||||||
|
await frigateApp.page.getByRole("option", { name: "Other" }).click();
|
||||||
|
const url = dialog.getByPlaceholder(
|
||||||
|
"rtsp://username:password@host:port/path",
|
||||||
|
);
|
||||||
|
const next = dialog.getByRole("button", { name: /^Continue$/i });
|
||||||
|
|
||||||
|
await url.fill("http://192.168.1.100/stream");
|
||||||
|
await next.click();
|
||||||
|
await expect(
|
||||||
|
dialog.getByText(/Custom URLs must begin with "rtsp:\/\/"/),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await url.fill("rtsp://192.168.1.100/stream");
|
||||||
|
await next.click();
|
||||||
|
await expect(url).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -248,4 +248,37 @@ test.describe("zone rename and delete @medium @mobile", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("inertia below 1 blocks saving and a valid value saves as a number", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
|
const requests = await installRoutes(frigateApp.page);
|
||||||
|
await frigateApp.goto(SETTINGS_URL);
|
||||||
|
|
||||||
|
await openZoneAction(frigateApp.page, frigateApp.isMobile, "Edit");
|
||||||
|
const inertia = frigateApp.page.getByRole("textbox", {
|
||||||
|
name: "Inertia",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
const save = frigateApp.page.getByRole("button", { name: "Save" });
|
||||||
|
|
||||||
|
await inertia.fill("0");
|
||||||
|
await expect(
|
||||||
|
frigateApp.page.getByText("Inertia must be above 0."),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(save).toBeDisabled();
|
||||||
|
|
||||||
|
await inertia.fill("5");
|
||||||
|
await expect(
|
||||||
|
frigateApp.page.getByText("Inertia must be above 0."),
|
||||||
|
).toBeHidden();
|
||||||
|
await save.click();
|
||||||
|
|
||||||
|
await expect.poll(() => requests.length).toBe(1);
|
||||||
|
expect(requests[0].body).toMatchObject({
|
||||||
|
config_data: {
|
||||||
|
cameras: { front_door: { zones: { driveway: { inertia: 5 } } } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+4
-32
@@ -83,7 +83,7 @@
|
|||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"virtua": "^0.53.6",
|
"virtua": "^0.53.6",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^4.6.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -3131,9 +3131,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3151,9 +3148,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3171,9 +3165,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3191,9 +3182,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3211,9 +3199,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3231,9 +3216,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5115,16 +5097,6 @@
|
|||||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
|
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-hooks/node_modules/zod": {
|
|
||||||
"version": "4.6.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
|
|
||||||
"integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error": {
|
"node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
|
||||||
@@ -10655,9 +10627,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "4.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@
|
|||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"virtua": "^0.53.6",
|
"virtua": "^0.53.6",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^4.6.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|||||||
@@ -367,7 +367,8 @@
|
|||||||
"nameLength": "Camera name must be 64 characters or less",
|
"nameLength": "Camera name must be 64 characters or less",
|
||||||
"invalidCharacters": "Camera name contains invalid characters",
|
"invalidCharacters": "Camera name contains invalid characters",
|
||||||
"nameExists": "Camera name already exists",
|
"nameExists": "Camera name already exists",
|
||||||
"customUrlRtspRequired": "Custom URLs must begin with \"rtsp://\" or \"rtsps://\". Manual configuration is required for non-RTSP camera streams."
|
"customUrlRtspRequired": "Custom URLs must begin with \"rtsp://\" or \"rtsps://\". Manual configuration is required for non-RTSP camera streams.",
|
||||||
|
"onvifPortInvalid": "ONVIF port must be a whole number from 1 to 65535"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"step2": {
|
"step2": {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function ClassificationModelEditDialog({
|
|||||||
const sharedFields = {
|
const sharedFields = {
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
saveAttempts: z.coerce
|
saveAttempts: z.coerce
|
||||||
.number({ message: t("edit.errors.saveAttemptsInvalid") })
|
.number<number>({ message: t("edit.errors.saveAttemptsInvalid") })
|
||||||
.int(t("edit.errors.saveAttemptsInvalid"))
|
.int(t("edit.errors.saveAttemptsInvalid"))
|
||||||
.min(0, t("edit.errors.saveAttemptsInvalid")),
|
.min(0, t("edit.errors.saveAttemptsInvalid")),
|
||||||
};
|
};
|
||||||
@@ -136,7 +136,7 @@ export default function ClassificationModelEditDialog({
|
|||||||
(val) =>
|
(val) =>
|
||||||
val === "" || val === null || val === undefined ? undefined : val,
|
val === "" || val === null || val === undefined ? undefined : val,
|
||||||
z.coerce
|
z.coerce
|
||||||
.number({ message: t("edit.errors.intervalInvalid") })
|
.number<number>({ message: t("edit.errors.intervalInvalid") })
|
||||||
.int(t("edit.errors.intervalInvalid"))
|
.int(t("edit.errors.intervalInvalid"))
|
||||||
.positive(t("edit.errors.intervalInvalid"))
|
.positive(t("edit.errors.intervalInvalid"))
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -171,7 +171,6 @@ export default function ClassificationModelEditDialog({
|
|||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
formSchema as z.ZodType<
|
formSchema as z.ZodType<
|
||||||
ObjectFormData | StateFormData,
|
ObjectFormData | StateFormData,
|
||||||
z.ZodTypeDef,
|
|
||||||
ObjectFormData | StateFormData
|
ObjectFormData | StateFormData
|
||||||
>,
|
>,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -200,13 +200,13 @@ export default function ZoneEditPane({
|
|||||||
),
|
),
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
inertia: z.coerce
|
inertia: z.coerce
|
||||||
.number()
|
.number<number>()
|
||||||
.min(1, {
|
.min(1, {
|
||||||
message: t("masksAndZones.form.inertia.error.mustBeAboveZero"),
|
message: t("masksAndZones.form.inertia.error.mustBeAboveZero"),
|
||||||
})
|
})
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
loitering_time: z.coerce
|
loitering_time: z.coerce
|
||||||
.number()
|
.number<number>()
|
||||||
.min(0, {
|
.min(0, {
|
||||||
message: t(
|
message: t(
|
||||||
"masksAndZones.form.loiteringTime.error.mustBeGreaterOrEqualZero",
|
"masksAndZones.form.loiteringTime.error.mustBeGreaterOrEqualZero",
|
||||||
@@ -222,35 +222,35 @@ export default function ZoneEditPane({
|
|||||||
review_detections: z.boolean().default(false).optional(),
|
review_detections: z.boolean().default(false).optional(),
|
||||||
speedEstimation: z.boolean().default(false),
|
speedEstimation: z.boolean().default(false),
|
||||||
lineA: z.coerce
|
lineA: z.coerce
|
||||||
.number()
|
.number<number>()
|
||||||
.min(0.1, {
|
.min(0.1, {
|
||||||
message: t("masksAndZones.form.distance.error.text"),
|
message: t("masksAndZones.form.distance.error.text"),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
lineB: z.coerce
|
lineB: z.coerce
|
||||||
.number()
|
.number<number>()
|
||||||
.min(0.1, {
|
.min(0.1, {
|
||||||
message: t("masksAndZones.form.distance.error.text"),
|
message: t("masksAndZones.form.distance.error.text"),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
lineC: z.coerce
|
lineC: z.coerce
|
||||||
.number()
|
.number<number>()
|
||||||
.min(0.1, {
|
.min(0.1, {
|
||||||
message: t("masksAndZones.form.distance.error.text"),
|
message: t("masksAndZones.form.distance.error.text"),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
lineD: z.coerce
|
lineD: z.coerce
|
||||||
.number()
|
.number<number>()
|
||||||
.min(0.1, {
|
.min(0.1, {
|
||||||
message: t("masksAndZones.form.distance.error.text"),
|
message: t("masksAndZones.form.distance.error.text"),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
speed_threshold: z.coerce
|
speed_threshold: z.coerce
|
||||||
.number()
|
.number<number>()
|
||||||
.min(0.1, {
|
.min(0.1, {
|
||||||
message: t("masksAndZones.form.speed.error.mustBeGreaterOrEqualTo"),
|
message: t("masksAndZones.form.speed.error.mustBeGreaterOrEqualTo"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -81,7 +81,14 @@ export default function Step1NameCamera({
|
|||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
brandTemplate: z.enum(CAMERA_BRAND_VALUES).optional(),
|
brandTemplate: z.enum(CAMERA_BRAND_VALUES).optional(),
|
||||||
onvifPort: z.coerce.number().int().min(1).max(65535).optional(),
|
onvifPort: z.coerce
|
||||||
|
.number<number>({
|
||||||
|
message: t("cameraWizard.step1.errors.onvifPortInvalid"),
|
||||||
|
})
|
||||||
|
.int(t("cameraWizard.step1.errors.onvifPortInvalid"))
|
||||||
|
.min(1, t("cameraWizard.step1.errors.onvifPortInvalid"))
|
||||||
|
.max(65535, t("cameraWizard.step1.errors.onvifPortInvalid"))
|
||||||
|
.optional(),
|
||||||
useDigestAuth: z.boolean().optional(),
|
useDigestAuth: z.boolean().optional(),
|
||||||
customUrl: z
|
customUrl: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
Reference in New Issue
Block a user