Skip to content

Commit

Permalink
test: Complete remaining \test.todo()s\ for List component validation
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed Jun 27, 2024
1 parent be6a176 commit 4f6287a
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 26 deletions.
11 changes: 7 additions & 4 deletions editor.planx.uk/src/@planx/components/List/Public/Context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export const ListProvider: React.FC<ListProviderProps> = (props) => {
// Do not allow a new item to be added if there's still an active item
if (activeIndex !== -1) return setAddItemError(true);

// Do not allow new item to be added if it will exceed max
if (schema.max && formik.values.userData.length === schema.max) {
return setMaxError(true);
}

// Add new item, and set to active
setAddItemError(false);
formik.values.userData.push(generateInitialValues(schema));
Expand Down Expand Up @@ -120,13 +125,11 @@ export const ListProvider: React.FC<ListProviderProps> = (props) => {
// Do not allow submissions with an unsaved item
if (activeIndex !== -1) return setUnsavedItemError(true);

// Manually validate min/max
// Manually validate minimum number of items
if (formik.values.userData.length < schema.min) {
return setMinError(true);
}
if (schema.max && formik.values.userData.length > schema.max) {
return setMaxError(true);
}

formik.handleSubmit();
};

Expand Down
18 changes: 9 additions & 9 deletions editor.planx.uk/src/@planx/components/List/Public/Fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import FormLabel from "@mui/material/FormLabel";
import MenuItem from "@mui/material/MenuItem";
import RadioGroup from "@mui/material/RadioGroup";
import { Option } from "@planx/components/shared";
import { getIn } from "formik";
import React from "react";
import SelectInput from "ui/editor/SelectInput";
import InputLabel from "ui/public/InputLabel";
Expand All @@ -16,6 +15,7 @@ import { DESCRIPTION_TEXT, ERROR_MESSAGE } from "../../shared/constants";
import BasicRadio from "../../shared/Radio/BasicRadio";
import type { NumberField, QuestionField, TextField } from "../model";
import { useListContext } from "./Context";
import { get } from "lodash";

type Props<T> = T & { id: string };

Expand All @@ -41,9 +41,9 @@ export const TextFieldInput: React.FC<Props<TextField>> = ({
bordered
value={formik.values.userData[activeIndex][data.fn]}
onChange={formik.handleChange}
errorMessage={getIn(
errorMessage={get(
formik.errors,
`userData[${activeIndex}][${data.fn}]`,
["userData", activeIndex, data.fn],
)}
id={id}
rows={
Expand All @@ -54,7 +54,7 @@ export const TextFieldInput: React.FC<Props<TextField>> = ({
inputProps={{
"aria-describedby": [
data.description ? DESCRIPTION_TEXT : "",
getIn(formik.errors, `userData[${activeIndex}][${data.fn}]`)
get(formik.errors, ["userData", activeIndex, data.fn])
? `${ERROR_MESSAGE}-${id}`
: "",
]
Expand Down Expand Up @@ -86,14 +86,14 @@ export const NumberFieldInput: React.FC<Props<NumberField>> = ({
type="number"
value={formik.values.userData[activeIndex][data.fn]}
onChange={formik.handleChange}
errorMessage={getIn(
errorMessage={get(
formik.errors,
`userData[${activeIndex}][${data.fn}]`,
["userData", activeIndex, data.fn],
)}
inputProps={{
"aria-describedby": [
data.description ? DESCRIPTION_TEXT : "",
getIn(formik.errors, `userData[${activeIndex}][${data.fn}]`)
get(formik.errors, ["userData", activeIndex, data.fn])
? `${ERROR_MESSAGE}-${id}`
: "",
]
Expand Down Expand Up @@ -128,7 +128,7 @@ export const RadioFieldInput: React.FC<Props<QuestionField>> = (props) => {
</FormLabel>
<ErrorWrapper
id={`${id}-error`}
error={getIn(formik.errors, `userData[${activeIndex}][${data.fn}]`)}
error={get(formik.errors, ["userData", activeIndex, data.fn])}
>
<RadioGroup
aria-labelledby={`radio-buttons-group-label-${id}`}
Expand Down Expand Up @@ -173,7 +173,7 @@ export const SelectFieldInput: React.FC<Props<QuestionField>> = (props) => {
>
<ErrorWrapper
id={`${id}-error`}
error={getIn(formik.errors, `userData[${activeIndex}][${data.fn}]`)}
error={get(formik.errors, ["userData", activeIndex, data.fn])}
>
<SelectInput
bordered
Expand Down
159 changes: 147 additions & 12 deletions editor.planx.uk/src/@planx/components/List/Public/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,19 +378,154 @@ describe("Building a list", () => {
});

describe("Form validation and error handling", () => {
test.todo("form validation is triggered when saving an item");
test.todo("text fields use existing validation schemas");
test.todo("number fields use existing validation schemas");
test.todo("question fields use validation schema");
test.todo("unique constraints are enforced on question where this is set");
test.todo("optional fields can be empty when saving an item");
test.todo("an error displays if the minimum number of items is not met");
test.todo("an error displays if the maximum number of items is exceeded");
test.todo(
"an error displays if you add a new item, without saving the active item",
test("form validation is triggered when saving an item", async () => {
const { user, getByRole, getAllByTestId } = setup(<ListComponent {...mockZooProps} />);

let errorMessages = getAllByTestId(/error-message-input/);

// Each field has an ErrorWrapper
expect(errorMessages).toHaveLength(mockZooProps.schema.fields.length)

// All are empty initially
errorMessages.forEach(message => {
expect(message).toBeEmptyDOMElement();
});

await user.click(getByRole("button", { name: /Save/ }));

// Error wrappers persist
errorMessages = getAllByTestId(/error-message-input/);
expect(errorMessages).toHaveLength(mockZooProps.schema.fields.length)

// Each field is in an error state
errorMessages.forEach(message => {
expect(message).not.toBeEmptyDOMElement();
});
});

/**
* These tests are not exhaustive tests of validation schemas, these can be tested in their respective model.test.ts files
* We are testing that the validation schemas are correctly "wired up" to out List component fields
*/
describe("existing validation schemas are correctly referenced", () => {
test("text fields", async () => {
const { user, getByRole, getByTestId } = setup(<ListComponent {...mockZooProps} />);

const nameInput = screen.getByLabelText(/name/);
await user.type(nameInput, "This is a long string of text over one hundred and twenty characters, which should trigger the 'short' text validation warning");
await user.click(getByRole("button", { name: /Save/ }));

const nameInputErrorMessage = getByTestId(/error-message-input-text-name/);

expect(nameInputErrorMessage).toHaveTextContent(/Your answer must be 120 characters or fewer/);
});

test("number fields", async () => {
const { user, getByRole, getByTestId } = setup(<ListComponent {...mockZooProps} />);

const ageInput = screen.getByLabelText(/old/);
await user.type(ageInput, "-35");
await user.click(getByRole("button", { name: /Save/ }));

const ageInputErrorMessage = getByTestId(/error-message-input-number-age/);

expect(ageInputErrorMessage).toHaveTextContent(/Enter a positive number/);
});

test("question fields", async () => {
const { user, getByRole, getByTestId } = setup(<ListComponent {...mockZooProps} />);

await user.click(getByRole("button", { name: /Save/ }));

const sizeInputErrorMessage = getByTestId(/error-message-input-question-size/);

expect(sizeInputErrorMessage).toHaveTextContent(/Select your answer before continuing/);
});

test("radio fields", async () => {
const { user, getByRole, getByTestId } = setup(<ListComponent {...mockZooProps} />);

await user.click(getByRole("button", { name: /Save/ }));

const cuteInputErrorMessage = getByTestId(/error-message-input-question-cute/);

expect(cuteInputErrorMessage).toHaveTextContent(/Select your answer before continuing/);
});

test.todo("checklist fields")
});

test("an error displays if the minimum number of items is not met", async () => {
const { user, getByRole, getByTestId, getByText } = setup(<ListComponent {...mockZooProps} />);

const minNumberOfItems = mockZooProps.schema.min;
expect(minNumberOfItems).toEqual(1);

await user.click(getByRole("button", { name: /Cancel/ }));
await user.click(getByTestId("continue-button"));

const minItemsErrorMessage = getByText(`You must provide at least ${minNumberOfItems} response(s)`)
expect(minItemsErrorMessage).toBeVisible();
});

test("an error displays if the maximum number of items is exceeded", async () => {
const { user, getAllByTestId, getByTestId, getByText } = setup(<ListComponent {...mockZooProps} />);
const addItemButton = getByTestId(/list-add-button/);

const maxNumberOfItems = mockZooProps.schema.max;
expect(maxNumberOfItems).toEqual(3);

// Complete three items
await fillInResponse(user);
await user.click(addItemButton);
await fillInResponse(user);
await user.click(addItemButton);
await fillInResponse(user);

const cards = getAllByTestId(/list-card/);
expect(cards).toHaveLength(3);

// Try to add a fourth
await user.click(getByTestId(/list-add-button/));

const maxItemsErrorMessage = getByText(`You can provide at most ${maxNumberOfItems} response(s)`)
expect(maxItemsErrorMessage).toBeVisible();
});

test(
"an error displays if you add a new item, without saving the active item", async () => {
const { user, getByTestId, getByText, getByLabelText } = setup(<ListComponent {...mockZooProps} />);
// Start filling out item
const nameInput = getByLabelText(/name/);
await user.type(nameInput, "Richard Parker");

const emailInput = getByLabelText(/email/);
await user.type(emailInput, "[email protected]");

// Try to add a new item
await user.click(getByTestId(/list-add-button/));

const activeItemErrorMessage = getByText(/Please save all responses before adding another/)
expect(activeItemErrorMessage).toBeVisible();
}
);
test.todo(
"an error displays if you continue, without saving the active item",

test(
"an error displays if you continue, without saving the active item", async () => {
const { user, getByTestId, getByText, getByLabelText } = setup(<ListComponent {...mockZooProps} />);
// Start filling out item
const nameInput = getByLabelText(/name/);
await user.type(nameInput, "Richard Parker");

const emailInput = getByLabelText(/email/);
await user.type(emailInput, "[email protected]");

// Try to continue
await user.click(getByTestId(/continue-button/));

const unsavedItemErrorMessage = getByText(/Please save in order to continue/)
expect(unsavedItemErrorMessage).toBeVisible();
}
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const Zoo: Schema = {
},
],
min: 1,
max: 10,
max: 3,
} as const;

export const mockZooProps: Props = {
Expand Down

0 comments on commit 4f6287a

Please sign in to comment.