diff --git a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx index 38bb5b1ec4..8d7f033828 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx @@ -129,7 +129,7 @@ export const ListProvider: React.FC = (props) => { if (formik.values.userData.length < schema.min) { return setMinError(true); } - + formik.handleSubmit(); }; diff --git a/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx b/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx index 8fad6c35db..961e219c8c 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx @@ -6,6 +6,7 @@ import MenuItem from "@mui/material/MenuItem"; import RadioGroup from "@mui/material/RadioGroup"; import { visuallyHidden } from "@mui/utils"; import { getIn } from "formik"; +import { get } from "lodash"; import React from "react"; import SelectInput from "ui/editor/SelectInput"; import InputLabel from "ui/public/InputLabel"; @@ -23,21 +24,14 @@ import type { TextField, } from "../model"; import { useListContext } from "./Context"; -import { get } from "lodash"; type Props = T & { id: string }; -export const TextFieldInput: React.FC> = ({ - id, - data, -}) => { +export const TextFieldInput: React.FC> = ({ id, data }) => { const { formik, activeIndex } = useListContext(); return ( - + { if (type === "email") return "email"; @@ -48,10 +42,7 @@ export const TextFieldInput: React.FC> = ({ bordered value={formik.values.userData[activeIndex][data.fn]} onChange={formik.handleChange} - errorMessage={get( - formik.errors, - ["userData", activeIndex, data.fn], - )} + errorMessage={get(formik.errors, ["userData", activeIndex, data.fn])} id={id} rows={ data.type && ["long", "extraLong"].includes(data.type) ? 5 : undefined @@ -80,10 +71,7 @@ export const NumberFieldInput: React.FC> = ({ const { formik, activeIndex } = useListContext(); return ( - + > = ({ type="number" value={formik.values.userData[activeIndex][data.fn]} onChange={formik.handleChange} - errorMessage={get( - formik.errors, - ["userData", activeIndex, data.fn], - )} + errorMessage={get(formik.errors, ["userData", activeIndex, data.fn])} inputProps={{ "aria-describedby": [ data.description ? DESCRIPTION_TEXT : "", @@ -161,10 +146,7 @@ export const SelectFieldInput: React.FC> = (props) => { const { id, data } = props; return ( - + { describe("Form validation and error handling", () => { test("form validation is triggered when saving an item", async () => { - const { user, getByRole, getAllByTestId } = setup(); + const { user, getByRole, getAllByTestId } = setup( + , + ); let errorMessages = getAllByTestId(/error-message-input/); - + // Each field has an ErrorWrapper - expect(errorMessages).toHaveLength(mockZooProps.schema.fields.length) + expect(errorMessages).toHaveLength(mockZooProps.schema.fields.length); // All are empty initially - errorMessages.forEach(message => { + 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) + expect(errorMessages).toHaveLength(mockZooProps.schema.fields.length); // Each field is in an error state - errorMessages.forEach(message => { + errorMessages.forEach((message) => { expect(message).not.toBeEmptyDOMElement(); - }); + }); }); /** @@ -410,62 +412,95 @@ describe("Form validation and error handling", () => { */ describe("existing validation schemas are correctly referenced", () => { test("text fields", async () => { - const { user, getByRole, getByTestId } = setup(); + const { user, getByRole, getByTestId } = setup( + , + ); 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.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/); + const nameInputErrorMessage = getByTestId( + /error-message-input-text-name/, + ); - expect(nameInputErrorMessage).toHaveTextContent(/Your answer must be 120 characters or fewer/); + expect(nameInputErrorMessage).toHaveTextContent( + /Your answer must be 120 characters or fewer/, + ); }); test("number fields", async () => { - const { user, getByRole, getByTestId } = setup(); + const { user, getByRole, getByTestId } = setup( + , + ); 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/); + const ageInputErrorMessage = getByTestId( + /error-message-input-number-age/, + ); expect(ageInputErrorMessage).toHaveTextContent(/Enter a positive number/); }); test("question fields", async () => { - const { user, getByRole, getByTestId } = setup(); + const { user, getByRole, getByTestId } = setup( + , + ); await user.click(getByRole("button", { name: /Save/ })); - const sizeInputErrorMessage = getByTestId(/error-message-input-question-size/); + const sizeInputErrorMessage = getByTestId( + /error-message-input-question-size/, + ); - expect(sizeInputErrorMessage).toHaveTextContent(/Select your answer before continuing/); + expect(sizeInputErrorMessage).toHaveTextContent( + /Select your answer before continuing/, + ); }); test("radio fields", async () => { - const { user, getByRole, getByTestId } = setup(); + const { user, getByRole, getByTestId } = setup( + , + ); await user.click(getByRole("button", { name: /Save/ })); - const cuteInputErrorMessage = getByTestId(/error-message-input-question-cute/); + const cuteInputErrorMessage = getByTestId( + /error-message-input-question-cute/, + ); - expect(cuteInputErrorMessage).toHaveTextContent(/Select your answer before continuing/); + expect(cuteInputErrorMessage).toHaveTextContent( + /Select your answer before continuing/, + ); }); test("checklist fields", async () => { - const { user, getByRole, getByTestId } = setup(); + const { user, getByRole, getByTestId } = setup( + , + ); await user.click(getByRole("button", { name: /Save/ })); - const foodInputErrorMessage = getByTestId(/error-message-input-checklist-food/); + const foodInputErrorMessage = getByTestId( + /error-message-input-checklist-food/, + ); - expect(foodInputErrorMessage).toHaveTextContent(/Select at least one option/); - }) + expect(foodInputErrorMessage).toHaveTextContent( + /Select at least one option/, + ); + }); }); test("an error displays if the minimum number of items is not met", async () => { - const { user, getByRole, getByTestId, getByText } = setup(); + const { user, getByRole, getByTestId, getByText } = setup( + , + ); const minNumberOfItems = mockZooProps.schema.min; expect(minNumberOfItems).toEqual(1); @@ -473,12 +508,16 @@ describe("Form validation and error handling", () => { await user.click(getByRole("button", { name: /Cancel/ })); await user.click(getByTestId("continue-button")); - const minItemsErrorMessage = getByText(`You must provide at least ${minNumberOfItems} response(s)`) + 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(); + const { user, getAllByTestId, getByTestId, getByText } = setup( + , + ); const addItemButton = getByTestId(/list-add-button/); const maxNumberOfItems = mockZooProps.schema.max; @@ -497,45 +536,51 @@ describe("Form validation and error handling", () => { // Try to add a fourth await user.click(getByTestId(/list-add-button/)); - const maxItemsErrorMessage = getByText(`You can provide at most ${maxNumberOfItems} response(s)`) + 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(); - // Start filling out item - const nameInput = getByLabelText(/name/); - await user.type(nameInput, "Richard Parker"); - - const emailInput = getByLabelText(/email/); - await user.type(emailInput, "richard.parker@pi.com"); - - // 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( - "an error displays if you continue, without saving the active item", async () => { - const { user, getByTestId, getByText, getByLabelText } = setup(); - // Start filling out item - const nameInput = getByLabelText(/name/); - await user.type(nameInput, "Richard Parker"); - - const emailInput = getByLabelText(/email/); - await user.type(emailInput, "richard.parker@pi.com"); - - // Try to continue - await user.click(getByTestId(/continue-button/)); - - const unsavedItemErrorMessage = getByText(/Please save in order to continue/) - expect(unsavedItemErrorMessage).toBeVisible(); - } - ); + test("an error displays if you add a new item, without saving the active item", async () => { + const { user, getByTestId, getByText, getByLabelText } = setup( + , + ); + // Start filling out item + const nameInput = getByLabelText(/name/); + await user.type(nameInput, "Richard Parker"); + + const emailInput = getByLabelText(/email/); + await user.type(emailInput, "richard.parker@pi.com"); + + // 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("an error displays if you continue, without saving the active item", async () => { + const { user, getByTestId, getByText, getByLabelText } = setup( + , + ); + // Start filling out item + const nameInput = getByLabelText(/name/); + await user.type(nameInput, "Richard Parker"); + + const emailInput = getByLabelText(/email/); + await user.type(emailInput, "richard.parker@pi.com"); + + // Try to continue + await user.click(getByTestId(/continue-button/)); + + const unsavedItemErrorMessage = getByText( + /Please save in order to continue/, + ); + expect(unsavedItemErrorMessage).toBeVisible(); + }); }); describe("Payload generation", () => { @@ -559,9 +604,8 @@ describe("Payload generation", () => { it("generates a valid payload with summary stats on submission (Units)", async () => { const handleSubmit = jest.fn(); - const { getByTestId, user, getByRole, getAllByRole, getByLabelText } = setup( - , - ); + const { getByTestId, user, getByRole, getAllByRole, getByLabelText } = + setup(); const addItemButton = getByTestId("list-add-button"); @@ -601,7 +645,7 @@ describe("Payload generation", () => { gardenYesRadio = getAllByRole("radio")[0]; gardenNoRadio = getAllByRole("radio")[1]; unitsNumberInput = getByLabelText(/identical units/); - + await user.click(developmentSelect); await user.click(getByRole("option", { name: /Change of use to a home/ })); await user.click(gardenNoRadio); @@ -611,7 +655,7 @@ describe("Payload generation", () => { await user.click(getByTestId("continue-button")); expect(handleSubmit).toHaveBeenCalled(); - const output = handleSubmit.mock.calls[0][0] + const output = handleSubmit.mock.calls[0][0]; expect(output).toMatchObject(mockUnitsPayload); }); }); diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.tsx index c0d3136c29..52ae77b23e 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.tsx @@ -70,14 +70,14 @@ const ActiveListCard: React.FC<{ }> = ({ index: i }) => { const { schema, saveItem, cancelEditItem, errors, isPageComponent } = useListContext(); - + const ref = useRef(null); useEffect(() => { if (ref.current) { - ref.current.scrollIntoView({ behavior: "smooth" }) + ref.current.scrollIntoView({ behavior: "smooth" }); } }, []); - + return ( = ({ } if ( - destinations.includes(Destination.Idox) && - isReady && + destinations.includes(Destination.Idox) && + isReady && props.handleSubmit ) { props.handleSubmit( - makeData(props, request.value.idox?.event_id, "idoxSendEventId") + makeData(props, request.value.idox?.event_id, "idoxSendEventId"), ); } diff --git a/editor.planx.uk/src/@planx/components/SetValue/Editor.tsx b/editor.planx.uk/src/@planx/components/SetValue/Editor.tsx index f3926067ee..b36c1e37f0 100644 --- a/editor.planx.uk/src/@planx/components/SetValue/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/SetValue/Editor.tsx @@ -106,7 +106,7 @@ function SetValueComponent(props: Props) { /> - {formik.values.operation !== "removeAll" && + {formik.values.operation !== "removeAll" && ( - } + )} diff --git a/editor.planx.uk/src/ui/shared/Checkbox.tsx b/editor.planx.uk/src/ui/shared/Checkbox.tsx index 37969b1e90..db2fe034f2 100644 --- a/editor.planx.uk/src/ui/shared/Checkbox.tsx +++ b/editor.planx.uk/src/ui/shared/Checkbox.tsx @@ -57,9 +57,9 @@ export default function Checkbox({ inputProps, }: Props): FCReturn { const handleChange = (e: React.MouseEvent) => { - e.preventDefault() + e.preventDefault(); onChange && onChange(); - } + }; return (