From 7df4c7372084d5c4fc2e501ca176be41a48915ed Mon Sep 17 00:00:00 2001 From: Etheryte Date: Wed, 2 Oct 2024 18:42:01 +0200 Subject: [PATCH] Update range tests --- web/README.md | 1 + .../src/components/input/InputBase.test.tsx | 4 +- .../components/input/range/Range.stories.tsx | 14 ++-- .../src/components/input/range/Range.test.tsx | 65 ++++++++------- .../input/validation/validation.test.ts | 15 +--- .../components/input/validation/validation.ts | 81 +++++++++---------- .../src/manager/proxy/container-config.tsx | 2 +- .../nets/network-properties.test.tsx | 2 +- .../nets/network-properties.tsx | 2 +- .../nets/properties/NetworkAddress.tsx | 2 +- .../virtualization/nets/properties/Vlans.tsx | 2 +- .../nets/properties/utils.test.ts | 6 +- web/html/src/utils/test-utils/index.ts | 1 + web/html/src/utils/test-utils/timeout.ts | 1 + .../src/vendors/npm.licenses.structured.js | 2 +- web/html/src/vendors/npm.licenses.txt | 2 +- 16 files changed, 98 insertions(+), 104 deletions(-) create mode 100644 web/html/src/utils/test-utils/timeout.ts diff --git a/web/README.md b/web/README.md index 160d7ec4c041..d55c9daa7d5a 100644 --- a/web/README.md +++ b/web/README.md @@ -24,6 +24,7 @@ The following scripts cover most day-to-day uses, see `web/html/src/package.json - Run lint with autofixer: `yarn lint` - Run unit tests: `yarn test` + - Run a single set of tests: `yarn test path/to/file.test.ts` - Run the Typescript checker: `yarn tsc` - Build the web UI: `yarn build` - Run lint, tests, Typescript checker, and build the application: `yarn all` diff --git a/web/html/src/components/input/InputBase.test.tsx b/web/html/src/components/input/InputBase.test.tsx index bfb7706719e1..4d9c48441ec7 100644 --- a/web/html/src/components/input/InputBase.test.tsx +++ b/web/html/src/components/input/InputBase.test.tsx @@ -16,7 +16,9 @@ describe("InputBase", () => { beforeEach(() => { model = {}; - onChange = () => {}; + onChange = (newModel) => { + model = newModel; + }; }); function renderWithForm(content) { diff --git a/web/html/src/components/input/range/Range.stories.tsx b/web/html/src/components/input/range/Range.stories.tsx index 5a1cc28cc424..49b41115b8a6 100644 --- a/web/html/src/components/input/range/Range.stories.tsx +++ b/web/html/src/components/input/range/Range.stories.tsx @@ -1,20 +1,20 @@ -import { SubmitButton } from "components/buttons"; +import { useState } from "react"; -import { Form } from "../form/Form"; -import { Range } from "./Range"; +import { SubmitButton } from "components/buttons"; +import { Form, Range } from "components/input"; export default () => { - const model = { + const [model, setModel] = useState({ port_start: "1000", port_end: "1100", - }; + }); return (
{ - model["port_start"] = newModel["port_start"]; - model["port_end"] = newModel["port_end"]; + console.log(newModel); + setModel(newModel); }} onSubmit={() => Loggerhead.info(model)} divClass="col-md-12" diff --git a/web/html/src/components/input/range/Range.test.tsx b/web/html/src/components/input/range/Range.test.tsx index 57b34d8d5493..c4c802cbf228 100644 --- a/web/html/src/components/input/range/Range.test.tsx +++ b/web/html/src/components/input/range/Range.test.tsx @@ -6,21 +6,25 @@ import { Form } from "../form/Form"; import { Range } from "./Range"; describe("Range", () => { - // Use these to test model changes in tests - let model; - let onChange; - - beforeEach(() => { - model = {}; - onChange = () => {}; - }); - - function renderWithForm(content) { - return render( - - {React.Children.toArray(content)} -
- ); + function renderWithForm(content, initialModel = {}, onChange?, onSubmit?) { + const Wrapper = () => { + const [model, setModel] = React.useState(initialModel); + return ( +
{ + setModel(newModel); + onChange?.(newModel); + }} + onSubmit={(newModel) => { + onSubmit?.(newModel); + }} + > + {React.Children.toArray(content)} +
+ ); + }; + return render(); } test("renders with minimal props", () => { @@ -30,7 +34,6 @@ describe("Range", () => { }); test("renders with default values", () => { - model = {}; renderWithForm(); const startInput = screen.getByRole("textbox", { name: "Port range start" }) as HTMLInputElement; expect(startInput.value).toBe("1000"); @@ -39,29 +42,29 @@ describe("Range", () => { }); test("change values", async () => { - model = { + const initialModel = { port_start: "10", port_end: "1100", }; - onChange = () => {}; + const onChange = jest.fn(); - renderWithForm(); + renderWithForm(, initialModel, onChange); const startInput = screen.getByRole("textbox", { name: "Port range start" }); clear(startInput); - expect(model).toStrictEqual({ port_start: "", port_end: "1100" }); + expect(onChange).toHaveBeenCalledWith({ port_start: "", port_end: "1100" }); await type(startInput, "900"); - expect(model).toStrictEqual({ port_start: "900", port_end: "1100" }); + expect(onChange).toHaveBeenCalledWith({ port_start: "900", port_end: "1100" }); const endInput = screen.getByRole("textbox", { name: "Port range end" }); clear(endInput); await type(endInput, "903"); - expect(model).toStrictEqual({ port_start: "900", port_end: "903" }); + expect(onChange).toHaveBeenCalledWith({ port_start: "900", port_end: "903" }); }); - test("validation", async (done) => { - onChange = () => done(); + test("validation", async () => { + const onChange = jest.fn(); renderWithForm( { return message; } }} - /> + />, + {}, + onChange ); const startInput = screen.getByRole("textbox", { name: "Port range start" }); const endInput = screen.getByRole("textbox", { name: "Port range end" }); await type(startInput, "900"); - expect(model).toStrictEqual({ port_start: "900" }); + expect(onChange).toHaveBeenCalledWith({ port_start: "900", port_end: "" }); screen.findByText(/Both values need to be positive integers/); await type(endInput, "800"); - expect(model).toStrictEqual({ port_start: "900", port_end: "800" }); + expect(onChange).toHaveBeenCalledWith({ port_start: "900", port_end: "800" }); screen.findByText(/Both values need to be positive integers/); await type(endInput, "NaN"); - expect(model).toStrictEqual({ port_start: "900", port_end: "NaN" }); + expect(onChange).toHaveBeenCalledWith({ port_start: "900", port_end: "NaN" }); screen.findByText(/Both values need to be positive integers/); await type(endInput, "901"); - expect(model).toStrictEqual({ port_start: "900", port_end: "901" }); - await waitForElementToBeRemoved(() => screen.queryByText(/Both values need to be positive integers/)); + expect(onChange).toHaveBeenCalledWith({ port_start: "900", port_end: "901" }); + expect(screen.queryByText(/Both values need to be positive integers/)).toBeNull(); }); }); diff --git a/web/html/src/components/input/validation/validation.test.ts b/web/html/src/components/input/validation/validation.test.ts index 6c84056962f6..67221d86a886 100644 --- a/web/html/src/components/input/validation/validation.test.ts +++ b/web/html/src/components/input/validation/validation.test.ts @@ -101,7 +101,7 @@ describe("validation", () => { }); test("intRange", () => { - const validator = Validation.intRange(3, 5, errorMessage); + const validator = Validation.range(3, 5, errorMessage); expect(validator("")).toEqual(undefined); expect(validator("1.5")).toEqual(errorMessage); @@ -112,17 +112,4 @@ describe("validation", () => { expect(validator("5")).toEqual(undefined); expect(validator("6")).toEqual(errorMessage); }); - - test("floatRange", () => { - const validator = Validation.floatRange(3, 5, errorMessage); - - expect(validator("")).toEqual(undefined); - expect(validator("1.5")).toEqual(errorMessage); - expect(validator("2")).toEqual(errorMessage); - expect(validator("3")).toEqual(undefined); - expect(validator("4")).toEqual(undefined); - expect(validator("4.5")).toEqual(undefined); - expect(validator("5")).toEqual(undefined); - expect(validator("6")).toEqual(errorMessage); - }); }); diff --git a/web/html/src/components/input/validation/validation.ts b/web/html/src/components/input/validation/validation.ts index b955ebacdfe3..649796c7d5b4 100644 --- a/web/html/src/components/input/validation/validation.ts +++ b/web/html/src/components/input/validation/validation.ts @@ -6,7 +6,7 @@ export type Validator = (...args: any[]) => SyncOrAsync; /** String must match `regex` */ const matches = - (regex: RegExp, message?: string): Validator => + (regex: RegExp, message = t("Doesn't match expected format")): Validator => (value: string) => { // Here and elsewhere, if you want the value to be required, set the `required` flag instead if (value === "") { @@ -14,44 +14,42 @@ const matches = } if (!regex.test(value)) { - return message ?? t("Doesn't match expected format"); + return message; } }; // TODO: Some places that use this are off by one from what they want to be /** String must be at least `length` chars long */ const minLength = - (length: number, message?: string): Validator => + (length: number, message = t(`Must be at least ${length} characters long`)): Validator => (value: Record | string) => { - const defaultMessage = t(`Must be at least ${length} characters long`); if (typeof value === "object") { const isInvalid = Object.values(value).some((item) => item.length !== 0 && item.length < length); if (isInvalid) { - return message ?? defaultMessage; + return message; } } else if (value.length !== 0 && value.length < length) { - return message ?? defaultMessage; + return message; } }; /** String must be no more than `length` chars long */ const maxLength = - (length: number, message?: string): Validator => + (length: number, message = t(`Must be no more than ${length} characters long`)): Validator => (value: Record | string) => { - const defaultMessage = t(`Must be no more than ${length} characters long`); if (typeof value === "object") { const isInvalid = Object.values(value).some((item) => item.length !== 0 && item.length > length); if (isInvalid) { - return message ?? defaultMessage; + return message; } } else if (value.length !== 0 && value.length > length) { - return message ?? defaultMessage; + return message; } }; /** String is integer */ const isInt = - (message?: string): Validator => + (message = t(`Must be an integer`)): Validator => (value: string) => { if (value === "") { return; @@ -59,78 +57,78 @@ const isInt = const parsed = parseInt(value, 10); if (isNaN(parsed) || parsed.toString() !== value) { - return message ?? t(`Must be an integer`); + return message; } }; -/** Value is no smaller than `minValue` */ +/** Value is an integer no smaller than `minValue` */ const min = - (minValue: number, message?: string): Validator => + (minValue: number, message = t(`Must be an integer no smaller than ${minValue}`)): Validator => (value: string) => { if (value === "") { return; } - const parsed = parseFloat(value); - if (isNaN(parsed) || parsed < minValue) { - return message ?? t(`Must be no smaller than ${minValue}`); + const parsed = parseInt(value, 10); + if (isNaN(parsed) || parsed.toString() !== value || parsed < minValue) { + return message; } }; -/** Value is no larger than `maxValue` */ +/** Value is an integer no larger than `maxValue` */ const max = - (maxValue: number, message?: string): Validator => + (maxValue: number, message = t(`Must be an integer no larger than ${maxValue}`)): Validator => (value: string) => { if (value === "") { return; } - const parsed = parseFloat(value); - if (isNaN(parsed) || parsed > maxValue) { - return message ?? t(`Must be no larger than ${maxValue}`); + const parsed = parseInt(value, 10); + if (isNaN(parsed) || parsed.toString() !== value || parsed > maxValue) { + return message; } }; -/** Value is greater than `gtValue` */ +/** Value is an integer greater than `gtValue` */ const gt = - (gtValue: number, message?: string): Validator => + (gtValue: number, message = t(`Must be an integer greater than ${gtValue}`)): Validator => (value: string) => { if (value === "") { return; } - const parsed = parseFloat(value); - if (isNaN(parsed) || parsed <= gtValue) { - return message ?? t(`Must be greater than ${gtValue}`); + const parsed = parseInt(value, 10); + if (isNaN(parsed) || parsed.toString() !== value || parsed <= gtValue) { + return message; } }; -/** Value is smaller than `ltValue` */ +/** Value is an integer smaller than `ltValue` */ const lt = - (ltValue: number, message?: string): Validator => + (ltValue: number, message = t(`Must be an integer greater than ${ltValue}`)): Validator => (value: string) => { if (value === "") { return; } - const parsed = parseFloat(value); - if (isNaN(parsed) || parsed >= ltValue) { - return message ?? t(`Must be greater than ${ltValue}`); + const parsed = parseInt(value, 10); + if (isNaN(parsed) || parsed.toString() !== value || parsed >= ltValue) { + return message; } }; /** Value is an integer that is no smaller than `minValue` and no larger than `maxValue` */ -const intRange = +const range = (minValue: number, maxValue: number, message?: string): Validator => (value: string) => { - return isInt(message)(value) || min(minValue, message)(value) || max(maxValue, message)(value); - }; + if (value === "") { + return; + } -/** Value is a number that is no smaller than `minValue` and no larger than `maxValue` */ -const floatRange = - (minValue: number, maxValue: number, message?: string): Validator => - (value: string) => { - return min(minValue, message)(value) || max(maxValue, message)(value); + const parsed = parseInt(value, 10); + if (isNaN(parsed) || parsed.toString() !== value || parsed < minValue || parsed > maxValue) { + return message; + } }; export const Validation = { @@ -142,6 +140,5 @@ export const Validation = { gt, lt, isInt, - intRange, - floatRange, + range, }; diff --git a/web/html/src/manager/proxy/container-config.tsx b/web/html/src/manager/proxy/container-config.tsx index 992ad08da58e..f3c181d12721 100644 --- a/web/html/src/manager/proxy/container-config.tsx +++ b/web/html/src/manager/proxy/container-config.tsx @@ -220,7 +220,7 @@ export function ProxyConfig() { name="proxyPort" label={t("Proxy SSH port")} hint={t("Port range: 1 - 65535")} - validate={[Validation.intRange(0, 65536)]} + validate={[Validation.range(0, 65536)]} defaultValue="8022" labelClass="col-md-3" divClass="col-md-6" diff --git a/web/html/src/manager/virtualization/nets/network-properties.test.tsx b/web/html/src/manager/virtualization/nets/network-properties.test.tsx index 0e1c7001982c..2abf05ed5361 100644 --- a/web/html/src/manager/virtualization/nets/network-properties.test.tsx +++ b/web/html/src/manager/virtualization/nets/network-properties.test.tsx @@ -20,7 +20,7 @@ function fieldValuesByName(name: string) { return getFieldValuesByName("network properties", name); } -let onSubmit; +let onSubmit: (model?: any) => any = () => {}; beforeEach(() => { onSubmit = () => {}; diff --git a/web/html/src/manager/virtualization/nets/network-properties.tsx b/web/html/src/manager/virtualization/nets/network-properties.tsx index 14df36afda79..6cc85a31a12b 100644 --- a/web/html/src/manager/virtualization/nets/network-properties.tsx +++ b/web/html/src/manager/virtualization/nets/network-properties.tsx @@ -379,7 +379,7 @@ export function NetworkProperties(props: Props) { label={t("VLAN tag")} labelClass="col-md-3" divClass="col-md-6" - validate={[Validation.intRange(0, 4095)]} + validate={[Validation.range(0, 4095)]} invalidHint={t("Integer between 0 and 4095")} /> )} diff --git a/web/html/src/manager/virtualization/nets/properties/NetworkAddress.tsx b/web/html/src/manager/virtualization/nets/properties/NetworkAddress.tsx index e9ca32bc9714..723bd899e59a 100644 --- a/web/html/src/manager/virtualization/nets/properties/NetworkAddress.tsx +++ b/web/html/src/manager/virtualization/nets/properties/NetworkAddress.tsx @@ -57,7 +57,7 @@ export const NetworkAddress = (props: Props) => { const errorMessage = t(`Value needs to be a valid IPv${ipVersion} address with prefix`); return ( Validation.matches(ipv6 ? utils.ipv6Pattern : utils.ipv4Pattern, errorMessage)(address) || - Validation.intRange(0, ipv6 ? 128 : 32, errorMessage)(prefix) + Validation.range(0, ipv6 ? 128 : 32, errorMessage)(prefix) ); }, ]} diff --git a/web/html/src/manager/virtualization/nets/properties/Vlans.tsx b/web/html/src/manager/virtualization/nets/properties/Vlans.tsx index 5a5afedbf898..7a21d1f77887 100644 --- a/web/html/src/manager/virtualization/nets/properties/Vlans.tsx +++ b/web/html/src/manager/virtualization/nets/properties/Vlans.tsx @@ -53,7 +53,7 @@ export function Vlans(props: Props) { divClass="col-md-12" className="col-md-6" required - validate={[Validation.intRange(0, 4095)]} + validate={[Validation.range(0, 4095)]} invalidHint={t("Integer between 0 and 4095")} />