Skip to content

Commit

Permalink
Update range tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Etheryte committed Oct 2, 2024
1 parent d50da32 commit 7df4c73
Show file tree
Hide file tree
Showing 16 changed files with 98 additions and 104 deletions.
1 change: 1 addition & 0 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 3 additions & 1 deletion web/html/src/components/input/InputBase.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ describe("InputBase", () => {

beforeEach(() => {
model = {};
onChange = () => {};
onChange = (newModel) => {
model = newModel;
};
});

function renderWithForm(content) {
Expand Down
14 changes: 7 additions & 7 deletions web/html/src/components/input/range/Range.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Form
model={model}
onChange={(newModel) => {
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"
Expand Down
65 changes: 35 additions & 30 deletions web/html/src/components/input/range/Range.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Form model={model} onChange={onChange}>
{React.Children.toArray(content)}
</Form>
);
function renderWithForm(content, initialModel = {}, onChange?, onSubmit?) {
const Wrapper = () => {
const [model, setModel] = React.useState(initialModel);
return (
<Form
model={model}
onChange={(newModel) => {
setModel(newModel);
onChange?.(newModel);
}}
onSubmit={(newModel) => {
onSubmit?.(newModel);
}}
>
{React.Children.toArray(content)}
</Form>
);
};
return render(<Wrapper />);
}

test("renders with minimal props", () => {
Expand All @@ -30,7 +34,6 @@ describe("Range", () => {
});

test("renders with default values", () => {
model = {};
renderWithForm(<Range name="range" prefix="port" label="Port range" defaultStart="1000" defaultEnd="1100" />);
const startInput = screen.getByRole("textbox", { name: "Port range start" }) as HTMLInputElement;
expect(startInput.value).toBe("1000");
Expand All @@ -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(<Range name="range" prefix="port" label="Port range" />);
renderWithForm(<Range name="range" prefix="port" label="Port range" />, 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(
<Range
Expand All @@ -85,26 +88,28 @@ describe("Range", () => {
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();
});
});
15 changes: 1 addition & 14 deletions web/html/src/components/input/validation/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
});
});
81 changes: 39 additions & 42 deletions web/html/src/components/input/validation/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,131 +6,129 @@ export type Validator = (...args: any[]) => SyncOrAsync<ValidationResult>;

/** 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 === "") {
return;
}

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, string> | 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, string> | 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;
}

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 = {
Expand All @@ -142,6 +140,5 @@ export const Validation = {
gt,
lt,
isInt,
intRange,
floatRange,
range,
};
2 changes: 1 addition & 1 deletion web/html/src/manager/proxy/container-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function fieldValuesByName(name: string) {
return getFieldValuesByName("network properties", name);
}

let onSubmit;
let onSubmit: (model?: any) => any = () => {};

beforeEach(() => {
onSubmit = () => {};
Expand Down
Loading

0 comments on commit 7df4c73

Please sign in to comment.