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 efe7a4143a..e71a7b1363 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx @@ -1,9 +1,20 @@ +import { + getPreviouslySubmittedData, + makeData, +} from "@planx/components/shared/utils"; +import { PublicProps } from "@planx/components/ui"; import { FormikProps, useFormik } from "formik"; -import React, { createContext, ReactNode, useContext, useState } from "react"; +import React, { + createContext, + PropsWithChildren, + useContext, + useState, +} from "react"; import { generateInitialValues, generateValidationSchema, + List, Schema, UserData, } from "../model"; @@ -17,7 +28,8 @@ interface ListContextValue { editItem: (index: number) => void; cancelEditItem: () => void; formik: FormikProps; - handleSubmit: () => void; + validateAndSubmitForm: () => void; + listProps: PublicProps; errors: { addItem: boolean; unsavedItem: boolean; @@ -26,18 +38,16 @@ interface ListContextValue { }; } -interface ListProviderProps { - children: ReactNode; - schema: Schema; -} +type ListProviderProps = PropsWithChildren>; const ListContext = createContext(undefined); -export const ListProvider: React.FC = ({ - children, - schema, -}) => { - const [activeIndex, setActiveIndex] = useState(0); +export const ListProvider: React.FC = (props) => { + const { schema, children, handleSubmit } = props; + + const [activeIndex, setActiveIndex] = useState( + props.previouslySubmittedData ? -1 : 0, + ); const [addItemError, setAddItemError] = useState(false); const [unsavedItemError, setUnsavedItemError] = useState(false); @@ -91,7 +101,7 @@ export const ListProvider: React.FC = ({ ); }; - const handleSubmit = () => { + const validateAndSubmitForm = () => { // Do not allow submissions with an unsaved item if (activeIndex !== -1) return setUnsavedItemError(true); @@ -106,15 +116,22 @@ export const ListProvider: React.FC = ({ }; const cancelEditItem = () => setActiveIndex(-1); + const editItem = (index: number) => setActiveIndex(index); + const getInitialValues = () => { + const previousValues = getPreviouslySubmittedData(props); + if (previousValues) return previousValues; + + return schema.min ? [generateInitialValues(schema)] : []; + }; + const formik = useFormik({ initialValues: { - userData: schema.min ? [generateInitialValues(schema)] : [], + userData: getInitialValues(), }, onSubmit: (values) => { - console.log("Submit!"); - console.log({ values }); + handleSubmit?.(makeData(props, values.userData)); }, validateOnBlur: false, validateOnChange: false, @@ -128,11 +145,12 @@ export const ListProvider: React.FC = ({ addNewItem, saveItem, schema, + listProps: props, editItem, removeItem, cancelEditItem, formik, - handleSubmit, + validateAndSubmitForm, errors: { addItem: addItemError, unsavedItem: unsavedItemError, diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx index 2bae5ecf6f..ee9178be5c 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx @@ -8,13 +8,34 @@ import ListComponent, { Props } from "../Public"; import { Zoo } from "../schemas/Zoo"; const mockProps: Props = { - fn: "mock", + fn: "mockFn", schema: Zoo, schemaName: "Zoo", title: "Mock Title", description: "Mock description", }; +const mockPayload = { + data: { + mockFn: [ + { + age: 10, + cuteness: "Very", + email: "richard.parker@pi.com", + name: "Richard Parker", + size: "Medium", + }, + { + age: 10, + cuteness: "Very", + email: "richard.parker@pi.com", + name: "Richard Parker", + size: "Medium", + }, + ], + }, +}; + jest.setTimeout(20_000); describe("Basic UI", () => { @@ -102,7 +123,7 @@ describe("Basic UI", () => { describe("Building a list", () => { it("does not display a default item if the schema has no required minimum", () => { const mockWithMinZero = merge(cloneDeep(mockProps), { schema: { min: 0 } }); - const { queryByRole, getByRole } = setup( + const { queryByRole, getByTestId } = setup( , ); @@ -114,9 +135,7 @@ describe("Building a list", () => { expect(activeListHeading).toBeNull(); // Button is present allow additional items to be added - const addItemButton = getByRole("button", { - name: /Add a new animal type/, - }); + const addItemButton = getByTestId("list-add-button"); expect(addItemButton).toBeInTheDocument(); expect(addItemButton).not.toBeDisabled(); }); @@ -140,26 +159,20 @@ describe("Building a list", () => { }); test("Adding an item", async () => { - const { getAllByRole, getByRole, user } = setup( + const { getAllByTestId, getByTestId, user } = setup( , ); - let cards = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + let cards = getAllByTestId(/list-card/); expect(cards).toHaveLength(1); await fillInResponse(user); - const addItemButton = getByRole("button", { - name: /Add a new animal type/, - }); + const addItemButton = getByTestId("list-add-button"); await user.click(addItemButton); // Item successfully added - cards = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + cards = getAllByTestId(/list-card/); expect(cards).toHaveLength(2); // Old item is inactive @@ -178,15 +191,13 @@ describe("Building a list", () => { test("Editing an item", async () => { // Setup three cards - const { getAllByRole, getByRole, user } = setup( + const { getAllByTestId, getByTestId, user } = setup( , ); await fillInResponse(user); - const addItemButton = getByRole("button", { - name: /Add a new animal type/, - }); + const addItemButton = getByTestId("list-add-button"); await user.click(addItemButton); await fillInResponse(user); @@ -194,9 +205,7 @@ describe("Building a list", () => { await user.click(addItemButton); await fillInResponse(user); - let cards = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + const cards = getAllByTestId(/list-card/); expect(cards).toHaveLength(3); let [firstCard, secondCard, thirdCard] = cards; @@ -223,10 +232,7 @@ describe("Building a list", () => { }); await user.click(secondCardEditButton); - cards = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); - [firstCard, secondCard, thirdCard] = cards; + [firstCard, secondCard, thirdCard] = getAllByTestId(/list-card/); // Second card now editable expect( @@ -236,14 +242,17 @@ describe("Building a list", () => { test("Removing an item when all cards are inactive", async () => { // Setup three cards - const { getAllByRole, getByRole, user, getByLabelText, queryAllByRole } = - setup(); + const { + getByTestId, + getAllByTestId, + user, + getByLabelText, + queryAllByTestId, + } = setup(); await fillInResponse(user); - const addItemButton = getByRole("button", { - name: /Add a new animal type/, - }); + const addItemButton = getByTestId("list-add-button"); await user.click(addItemButton); await fillInResponse(user); @@ -251,13 +260,10 @@ describe("Building a list", () => { await user.click(addItemButton); await fillInResponse(user); - let cards = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + let cards = getAllByTestId(/list-card/); expect(cards).toHaveLength(3); - let [firstCard, secondCard] = cards; - const thirdCard = cards[2]; + let [firstCard, secondCard, thirdCard] = cards; // Remove third card const thirdCardRemoveButton = within(thirdCard!).getByRole("button", { @@ -265,14 +271,10 @@ describe("Building a list", () => { }); await user.click(thirdCardRemoveButton); - cards = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + cards = getAllByTestId(/list-card/); expect(cards).toHaveLength(2); - [firstCard, secondCard] = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + [firstCard, secondCard, thirdCard] = getAllByTestId(/list-card/); // Previous items remain inactive expect( @@ -287,14 +289,10 @@ describe("Building a list", () => { name: /Remove/, }); await user.click(secondCardRemoveButton); - cards = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + cards = getAllByTestId(/list-card/); expect(cards).toHaveLength(1); - [firstCard] = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + [firstCard] = getAllByTestId(/list-card/); // Previous items remain inactive expect( @@ -306,9 +304,7 @@ describe("Building a list", () => { name: /Remove/, }); await user.click(firstCardRemoveButton); - cards = queryAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + cards = queryAllByTestId(/list-card/); expect(cards).toHaveLength(0); // Add item back @@ -321,21 +317,17 @@ describe("Building a list", () => { test("Removing an item when another card is active", async () => { // Setup two cards - const { getAllByRole, getByRole, user } = setup( + const { getAllByTestId, getByTestId, user } = setup( , ); await fillInResponse(user); - const addItemButton = getByRole("button", { - name: /Add a new animal type/, - }); + const addItemButton = getByTestId("list-add-button"); await user.click(addItemButton); - const [firstCard, secondCard] = getAllByRole("heading", { level: 2 }).map( - (el) => el.closest("div"), - ); + const [firstCard, secondCard] = getAllByTestId(/list-card/); // Second card is active expect( @@ -347,9 +339,7 @@ describe("Building a list", () => { name: /Remove/, }); await user.click(firstCardRemoveButton); - const cards = getAllByRole("heading", { level: 2 }).map((el) => - el.closest("div"), - ); + const cards = getAllByTestId(/list-card/); expect(cards).toHaveLength(1); // First card is active @@ -376,11 +366,43 @@ describe("Form validation and error handling", () => { }); describe("Payload generation", () => { - it.todo("generates a valid payload on submission"); + it("generates a valid payload on submission", async () => { + const handleSubmit = jest.fn(); + const { getByTestId, user } = setup( + , + ); + const addItemButton = getByTestId("list-add-button"); + + await fillInResponse(user); + + await user.click(addItemButton); + await fillInResponse(user); + + await user.click(screen.getByTestId("continue-button")); + + expect(handleSubmit).toHaveBeenCalled(); + expect(handleSubmit.mock.calls[0][0]).toMatchObject(mockPayload); + }); }); describe("Navigating back", () => { - test.todo("it pre-populates list correctly"); + test("it pre-populates list correctly", async () => { + const { getAllByText, queryByLabelText, getAllByTestId } = setup( + , + ); + + const cards = getAllByTestId(/list-card/); + + // Two cards + expect(cards).toHaveLength(2); + + // Both inactive + expect(queryByLabelText(/What's their name?/)).toBeNull(); + expect(getAllByText(/What's their name?/)).toHaveLength(2); + + // With the correct previous data + expect(getAllByText(/Richard Parker/)).toHaveLength(2); + }); }); /** 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 e860ad8573..c10e02cad5 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.tsx @@ -70,7 +70,7 @@ const ActiveListCard: React.FC<{ - + {schema.type} {index + 1} @@ -100,7 +100,7 @@ const InactiveListCard: React.FC<{ const { schema, formik, removeItem, editItem } = useListContext(); return ( - + {schema.type} {i + 1} @@ -131,9 +131,18 @@ const InactiveListCard: React.FC<{ ); }; -const Root = ({ title, description, info, policyRef, howMeasured }: Props) => { - const { formik, handleSubmit, activeIndex, schema, addNewItem, errors } = - useListContext(); +const Root = () => { + const { + formik, + validateAndSubmitForm, + activeIndex, + schema, + addNewItem, + errors, + listProps, + } = useListContext(); + + const { title, description, info, policyRef, howMeasured } = listProps; const rootError: string = (errors.min && `You must provide at least ${schema.min} response(s)`) || @@ -141,7 +150,7 @@ const Root = ({ title, description, info, policyRef, howMeasured }: Props) => { ""; return ( - + { color="secondary" onClick={addNewItem} sx={{ width: "100%" }} + data-testid="list-add-button" > + Add a new {schema.type.toLowerCase()} type @@ -181,11 +191,9 @@ const Root = ({ title, description, info, policyRef, howMeasured }: Props) => { }; function ListComponent(props: Props) { - // TODO: On submit generate a payload - return ( - - + + ); }