From f45fefdfcaadccaf1b8ac8259db15a651969c19f Mon Sep 17 00:00:00 2001 From: Rory Doak <138574807+RODO94@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:44:45 +0100 Subject: [PATCH] feat: Map and Label Happy Path Tests (#3690) --- .../MapAndLabel/Public/index.test.tsx | 263 ++++++++++++------ .../components/MapAndLabel/test/utils.ts | 53 +++- 2 files changed, 234 insertions(+), 82 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx index ee15ba20a9..c79f02117e 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx @@ -6,9 +6,18 @@ import { setup } from "testUtils"; import { vi } from "vitest"; import { axe } from "vitest-axe"; -import { point1, point2 } from "../test/mocks/geojson"; +import { point1, point2, point3 } from "../test/mocks/geojson"; import { props } from "../test/mocks/Trees"; -import { addFeaturesToMap, addMultipleFeatures } from "../test/utils"; +import { + addFeaturesToMap, + addMultipleFeatures, + checkErrorMessagesEmpty, + checkErrorMessagesPopulated, + clickContinue, + fillOutFirstHalfOfForm, + fillOutForm, + fillOutSecondHalfOfForm, +} from "../test/utils"; beforeAll(() => { if (!window.customElements.get("my-map")) { @@ -40,25 +49,23 @@ describe("Basic UI", () => { it("removes the prompt once a feature is added", async () => { const { queryByText, getByTestId } = setup(); const map = getByTestId("map-and-label-map"); - expect(map).toBeInTheDocument(); addFeaturesToMap(map, [point1]); await waitFor(() => expect( - queryByText("Plot a feature on the map to begin"), - ).not.toBeInTheDocument(), + queryByText("Plot a feature on the map to begin") + ).not.toBeInTheDocument() ); }); it("renders the schema name as the tab title", async () => { const { queryByText, getByRole, getByTestId } = setup( - , + ); expect(queryByText(/Tree 1/)).not.toBeInTheDocument(); const map = getByTestId("map-and-label-map"); - expect(map).toBeInTheDocument(); addFeaturesToMap(map, [point1]); @@ -68,12 +75,11 @@ describe("Basic UI", () => { it("should not have any accessibility violations", async () => { const { queryByText, getByTestId, container } = setup( - , + ); expect(queryByText(/Tree 1/)).not.toBeInTheDocument(); const map = getByTestId("map-and-label-map"); - expect(map).toBeInTheDocument(); addFeaturesToMap(map, [point1]); @@ -85,23 +91,26 @@ describe("Basic UI", () => { // Schema and field validation is handled in both List and Schema folders - here we're only testing the MapAndLabel specific error handling describe("validation and error handling", () => { it("shows all fields are required", async () => { - const { getAllByTestId, getByTestId, getByRole, user } = setup( - , + const { getByTestId, user, queryByRole, getAllByTestId } = setup( + ); const map = getByTestId("map-and-label-map"); - expect(map).toBeInTheDocument(); addFeaturesToMap(map, [point1]); - expect(getByRole("tab", { name: /Tree 1/ })).toBeInTheDocument(); + const tabOne = queryByRole("tab", { name: /Tree 1/ }); + expect(tabOne).toBeInTheDocument(); + + const firstTabPanel = getByTestId("vertical-tabpanel-0"); + const firstSpeciesInput = within(firstTabPanel).getByLabelText("Species"); - const continueButton = getByRole("button", { name: /Continue/ }); - expect(continueButton).toBeInTheDocument(); - await user.click(continueButton); + // check input is empty + expect(firstSpeciesInput).toHaveDisplayValue(""); + + await clickContinue(user); const errorMessages = getAllByTestId(/error-message-input/); - // Date field has been removed so only 4 inputs expect(errorMessages).toHaveLength(4); errorMessages.forEach((message) => { @@ -111,28 +120,18 @@ describe("validation and error handling", () => { // it shows all fields are required in a tab it("should show all fields are required, for all feature tabs", async () => { - const { getByTestId, getByRole, user, debug } = setup( - , - ); - const map = getByTestId("map-and-label-map"); - expect(map).toBeInTheDocument(); - debug(); + const { getByTestId, getByRole, user } = setup(); addMultipleFeatures([point1, point2]); // vertical side tab query const firstTab = getByRole("tab", { name: /Tree 1/ }); - const secondTab = getByRole("tab", { name: /Tree 2/ }); - - // side tab validation - expect(firstTab).toBeInTheDocument(); - expect(secondTab).toBeInTheDocument(); // form for each tab const firstTabPanel = getByTestId("vertical-tabpanel-0"); const secondTabPanel = getByTestId("vertical-tabpanel-1"); - // default is to start on seond tab panel since we add two points + // default is to start on second tab panel since we add two points expect(firstTabPanel).not.toBeVisible(); expect(secondTabPanel).toBeVisible(); @@ -140,90 +139,196 @@ describe("validation and error handling", () => { expect(secondTabPanel.childElementCount).toBeGreaterThan(0); expect(firstTabPanel.childElementCount).toBe(0); - const continueButton = getByRole("button", { name: /Continue/ }); - await user.click(continueButton); + await clickContinue(user); // error messages appear - const errorMessagesTabTwo = - within(secondTabPanel).getAllByTestId(/error-message-input/); - expect(errorMessagesTabTwo).toHaveLength(4); - - // error messages are empty but visible before error state induced - // this ensures they contain the error message text - errorMessagesTabTwo.forEach((input) => { - expect(input).not.toBeEmptyDOMElement(); - }); + await checkErrorMessagesPopulated(); await user.click(firstTab); expect(firstTabPanel).toBeVisible(); // error messages persist - const errorMessagesTabOne = - within(firstTabPanel).getAllByTestId(/error-message-input/); - expect(errorMessagesTabOne).toHaveLength(4); + await checkErrorMessagesPopulated(); }); // it shows all fields are required across different tabs it("should show an error if the minimum number of items is not met", async () => { - const { getByTestId, getByRole, user } = setup(); - const map = getByTestId("map-and-label-map"); - expect(map).toBeInTheDocument(); - - const continueButton = getByRole("button", { name: /Continue/ }); + const { getByTestId, user } = setup(); - await user.click(continueButton); + await clickContinue(user); const errorWrapper = getByTestId(/error-wrapper/); const errorMessage = within(errorWrapper).getByText(/You must plot /); - - expect(errorMessage).toBeInTheDocument(); + expect(errorMessage).toBeVisible(); }); // ?? it("an error state is applied to a tabpanel button, when it's associated feature is invalid", async () => { - const { getByTestId, getByRole, user, getAllByTestId } = setup( - , + const { getByTestId, user, queryByRole } = setup( + ); const map = getByTestId("map-and-label-map"); - expect(map).toBeInTheDocument(); addFeaturesToMap(map, [point1]); - const tabOne = getByRole("tab", { name: /Tree 1/ }); + const tabOne = queryByRole("tab", { name: /Tree 1/ }); expect(tabOne).toBeInTheDocument(); - const continueButton = getByRole("button", { name: /Continue/ }); - expect(continueButton).toBeInTheDocument(); - await user.click(continueButton); + await clickContinue(user); - const errorMessages = getAllByTestId(/error-message-input/); - - // check error messages are correct amount and contain info - expect(errorMessages).toHaveLength(4); - - errorMessages.forEach((message) => { - expect(message).not.toBeEmptyDOMElement(); - }); + await checkErrorMessagesPopulated(); expect(tabOne).toHaveStyle("border-left: 5px solid #D4351C"); }); // shows the error state on a tab when it's invalid }); + +it("does not trigger handleSubmit when errors exist", async () => { + const handleSubmit = vi.fn(); + const { getByTestId, user } = setup( + + ); + const map = getByTestId("map-and-label-map"); + + addFeaturesToMap(map, [point1]); + + await clickContinue(user); + + await checkErrorMessagesPopulated(); + + expect(handleSubmit).not.toBeCalled(); +}); test.todo("an error displays if the maximum number of items is exceeded"); describe("basic interactions - happy path", () => { - test.todo("adding an item to the map adds a feature tab"); - // add feature, see a tab (one feature only) - test.todo("a user can input details on a single feature and submit"); - // only one feature, fill out form, submit - test.todo("adding multiple features to the map adds multiple feature tabs"); - // add more than one feature, see multiple tabs - test.todo("a user can input details on multiple features and submit"); - // add details to more than one tab, submit - test.todo("a user can input details on feature tabs in any order"); - // ?? + it("adding an item to the map adds a feature tab", async () => { + const { getByTestId } = setup(); + const map = getByTestId("map-and-label-map"); + + addFeaturesToMap(map, [point1]); + + const firstTabPanel = getByTestId("vertical-tabpanel-0"); + + expect(firstTabPanel).toBeVisible(); + }); + + it("a user can input details on a single feature and submit", async () => { + const { getByTestId, user } = setup(); + + const map = getByTestId("map-and-label-map"); + + addFeaturesToMap(map, [point1]); + + const firstTabPanel = getByTestId("vertical-tabpanel-0"); + + expect(firstTabPanel).toBeVisible(); + + await fillOutForm(user); + + await clickContinue(user); + + await checkErrorMessagesEmpty(); + }); + + it("adding multiple features to the map adds multiple feature tabs", async () => { + const { queryByRole } = setup(); + + addMultipleFeatures([point1, point2, point3]); + + // vertical side tab query + const firstTab = queryByRole("tab", { name: /Tree 1/ }); + const secondTab = queryByRole("tab", { name: /Tree 2/ }); + const thirdTab = queryByRole("tab", { name: /Tree 3/ }); + const fourthTab = queryByRole("tab", { name: /Tree 4/ }); + + // check the right amount are in the document + expect(firstTab).toBeInTheDocument(); + expect(secondTab).toBeInTheDocument(); + expect(thirdTab).toBeInTheDocument(); + expect(fourthTab).not.toBeInTheDocument(); + + expect(firstTab).toHaveAttribute("aria-selected", "false"); + expect(secondTab).toHaveAttribute("aria-selected", "false"); + expect(thirdTab).toHaveAttribute("aria-selected", "true"); + }); + + it("a user can input details on multiple features and submit", async () => { + const { getByTestId, getByRole, user } = setup(); + getByTestId("map-and-label-map"); + + addMultipleFeatures([point1, point2]); + + // vertical side tab query + const firstTab = getByRole("tab", { name: /Tree 1/ }); + const firstTabPanel = getByTestId("vertical-tabpanel-0"); + const secondTabPanel = getByTestId("vertical-tabpanel-1"); + + await fillOutForm(user); + const secondSpeciesInput = within(secondTabPanel).getByLabelText("Species"); + + expect(secondSpeciesInput).toHaveDisplayValue("Larch"); + + await user.click(firstTab); + + // check form on screen is reset + const firstSpeciesInput = within(firstTabPanel).getByLabelText("Species"); + expect(secondSpeciesInput).not.toBeInTheDocument(); + expect(firstSpeciesInput).not.toHaveDisplayValue("Larch"); + + await fillOutForm(user); + + await clickContinue(user); + + await checkErrorMessagesEmpty(); + }); + it("a user can input details on feature tabs in any order", async () => { + const { getByTestId, getByRole, user } = setup(); + + addMultipleFeatures([point1, point2]); + + const firstTab = getByRole("tab", { name: /Tree 1/ }); + const secondTab = getByRole("tab", { name: /Tree 2/ }); + + const firstTabPanel = getByTestId("vertical-tabpanel-0"); + const secondTabPanel = getByTestId("vertical-tabpanel-1"); + + await user.click(firstTab); + + const firstSpeciesInput = within(firstTabPanel).getByLabelText("Species"); + expect(firstSpeciesInput).not.toHaveDisplayValue("Larch"); + + // partially fill out firstTabPanel + await fillOutFirstHalfOfForm(user); + + await user.click(secondTab); + const secondSpeciesInput = within(secondTabPanel).getByLabelText("Species"); + expect(secondSpeciesInput).not.toHaveDisplayValue("Larch"); + + // partially fill out secondTabPanel + await fillOutFirstHalfOfForm(user); + + await user.click(firstTab); + + // check that the data stays within the firstTabPanel + expect(firstSpeciesInput).toHaveDisplayValue("Larch"); + + // Complete the filling out of the firstTabPanel + await fillOutSecondHalfOfForm(user); + + await user.click(secondTab); + + // check that the data stays within the secondTabPanel + expect(secondSpeciesInput).toHaveDisplayValue("Larch"); + + // Complete the filling out of the secondTabPanel + await fillOutSecondHalfOfForm(user); + + await clickContinue(user); + + await checkErrorMessagesEmpty(); + }); }); describe("copy feature select", () => { @@ -232,7 +337,7 @@ describe("copy feature select", () => { it.todo("is enabled once multiple features are present"); // copy select enabled once you add more features it.todo( - "lists all other features as options (the current feature is not listed)", + "lists all other features as options (the current feature is not listed)" ); // current tree is not an option in the copy select it.todo("copies all data from one feature to another"); @@ -254,11 +359,11 @@ describe("payload generation", () => { test.todo("a submitted payload contains a GeoJSON feature collection"); // check payload contains GeoJSON feature collection test.todo( - "the feature collection contains all geospatial data inputted by the user", + "the feature collection contains all geospatial data inputted by the user" ); // feature collection matches the mocked data test.todo( - "each feature's properties correspond with the details entered for that feature", + "each feature's properties correspond with the details entered for that feature" ); // feature properties contain the answers to inputs }); diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts b/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts index 9baaeea2d1..1873246808 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts @@ -1,14 +1,17 @@ -import { screen } from "@testing-library/react"; +import { screen, within } from "@testing-library/react"; +import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup"; import { Feature, Point, Polygon } from "geojson"; import { act } from "react-dom/test-utils"; +import { mockTreeData } from "./mocks/GenericValues"; + /** * Helper to mock a user's interaction with the @opensystemslab/map element * We aren't able to mock a user's click interaction, so instead we dispatch an event which matches the one generated by the webcomponent */ export const addFeaturesToMap = async ( map: HTMLElement, - features: Feature[], + features: Feature[] ) => { const mockEvent = new CustomEvent("geojsonChange", { detail: { @@ -19,7 +22,7 @@ export const addFeaturesToMap = async ( }; export const addMultipleFeatures = ( - featureArray: Feature[], + featureArray: Feature[] ) => { const map = screen.getByTestId("map-and-label-map"); const pointsAddedArray: Feature[] = []; @@ -28,3 +31,47 @@ export const addMultipleFeatures = ( addFeaturesToMap(map, pointsAddedArray); }); }; + +export const fillOutFirstHalfOfForm = async (user: UserEvent) => { + const speciesInput = screen.getByLabelText("Species"); + await user.type(speciesInput, mockTreeData.species); + const workInput = screen.getByLabelText("Proposed work"); + await user.type(workInput, mockTreeData.work); +}; + +export const fillOutSecondHalfOfForm = async (user: UserEvent) => { + const justificationInput = screen.getByLabelText("Justification"); + await user.type(justificationInput, mockTreeData.justification); + const urgencyDiv = screen.getByTitle("Urgency"); + const urgencySelect = within(urgencyDiv).getByRole("combobox"); + await user.click(urgencySelect); + await user.click(screen.getByRole("option", { name: /low/i })); +}; + +export const fillOutForm = async (user: UserEvent) => { + await fillOutFirstHalfOfForm(user); + await fillOutSecondHalfOfForm(user); +}; + +export const clickContinue = async (user: UserEvent) => { + const continueButton = screen.getByRole("button", { name: /Continue/ }); + await user.click(continueButton); +}; + +export const checkErrorMessagesEmpty = async () => { + const errorMessages = screen.getAllByTestId(/error-message-input/); + + errorMessages.forEach((message) => { + expect(message.textContent).toBeFalsy(); + }); +}; + +export const checkErrorMessagesPopulated = async () => { + const errorMessages = screen.getAllByTestId(/error-message-input/); + + expect(errorMessages).toHaveLength(4); + + errorMessages.forEach((message) => { + expect(message).not.toBeEmptyDOMElement(); + }); +};