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();
+ });
+};