-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
test: Outstanding List component validation tests #3323
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
@@ -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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change was required as the formik's Docs: https://formik.org/docs/api/fieldarray#fieldarray-validation-gotchas There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 |
||
|
||
type Props<T> = T & { id: string }; | ||
|
||
|
@@ -40,9 +40,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={ | ||
|
@@ -53,7 +53,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}` | ||
: "", | ||
] | ||
|
@@ -84,14 +84,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}` | ||
: "", | ||
] | ||
|
@@ -126,7 +126,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}`} | ||
|
@@ -159,7 +159,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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -379,17 +379,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("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(); | ||
} | ||
); | ||
}); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving this here feels like a better user experience than just checking this when a user hits "Continue" (and may have added too many items).