From 91275d74baf5269a5188cd23af0ba89291ad7d38 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?=
Date: Wed, 18 Sep 2024 08:10:55 +0100
Subject: [PATCH 01/10] fix: Move "invalid graph" warning to `MapFieldInput`
(#3689)
---
.../@planx/components/List/Public/index.tsx | 44 +---------
.../components/MapAndLabel/Public/index.tsx | 22 +----
.../PlanningConstraints/Public.test.tsx | 74 +++++++++--------
.../components/PlanningConstraints/Public.tsx | 30 +------
.../PropertyInformation/Public.test.tsx | 12 ++-
.../components/PropertyInformation/Public.tsx | 23 ++----
.../Schema/InputFields/MapFieldInput.tsx | 11 +++
.../{ => Error}/ErrorFallback.stories.tsx | 0
.../components/{ => Error}/ErrorFallback.tsx | 15 ++--
.../src/components/Error/GraphError.test.tsx | 81 +++++++++++++++++++
.../src/components/Error/GraphError.tsx | 43 ++++++++++
.../Settings/Submissions/EventsLog.tsx | 2 +-
.../FlowEditor/components/forms/FormModal.tsx | 2 +-
.../src/pages/Preview/Questions.tsx | 2 +-
.../src/pages/layout/FlowEditorLayout.tsx | 2 +-
.../src/pages/layout/PublicLayout.tsx | 2 +-
16 files changed, 212 insertions(+), 153 deletions(-)
rename editor.planx.uk/src/components/{ => Error}/ErrorFallback.stories.tsx (100%)
rename editor.planx.uk/src/components/{ => Error}/ErrorFallback.tsx (70%)
create mode 100644 editor.planx.uk/src/components/Error/GraphError.test.tsx
create mode 100644 editor.planx.uk/src/components/Error/GraphError.tsx
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 aef0b30a2c..6b7f1f6888 100644
--- a/editor.planx.uk/src/@planx/components/List/Public/index.tsx
+++ b/editor.planx.uk/src/@planx/components/List/Public/index.tsx
@@ -8,11 +8,8 @@ import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
-import { SiteAddress } from "@planx/components/FindProperty/model";
-import { ErrorSummaryContainer } from "@planx/components/shared/Preview/ErrorSummaryContainer";
import { SchemaFields } from "@planx/components/shared/Schema/SchemaFields";
import { PublicProps } from "@planx/components/ui";
-import { useStore } from "pages/FlowEditor/lib/store";
import React, { useEffect, useRef } from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";
import FullWidthWrapper from "ui/public/FullWidthWrapper";
@@ -56,14 +53,8 @@ const InactiveListCardLayout = styled(Box)(({ theme }) => ({
const ActiveListCard: React.FC<{
index: number;
}> = ({ index: i }) => {
- const {
- schema,
- saveItem,
- cancelEditItem,
- errors,
- formik,
- activeIndex,
- } = useListContext();
+ const { schema, saveItem, cancelEditItem, errors, formik, activeIndex } =
+ useListContext();
const ref = useRef(null);
useEffect(() => {
@@ -119,8 +110,7 @@ const ActiveListCard: React.FC<{
const InactiveListCard: React.FC<{
index: number;
}> = ({ index: i }) => {
- const { schema, formik, removeItem, editItem } =
- useListContext();
+ const { schema, formik, removeItem, editItem } = useListContext();
const mapPreview = schema.fields.find((field) => field.type === "map");
@@ -189,8 +179,7 @@ const Root = () => {
listProps,
} = useListContext();
- const { title, description, info, policyRef, howMeasured, handleSubmit } =
- listProps;
+ const { title, description, info, policyRef, howMeasured } = listProps;
const rootError: string =
(errors.min && `You must provide at least ${schema.min} response(s)`) ||
@@ -201,32 +190,7 @@ const Root = () => {
const shouldShowAddAnotherButton =
schema.max !== 1 || formik.values.schemaData.length < 1;
- // If the selected schema has a "map" field, ensure there's a FindProperty component preceding it (eg address data in state to position map view)
const hasMapField = schema.fields.some((field) => field.type === "map");
- const { longitude, latitude } = useStore(
- (state) =>
- (state.computePassport()?.data?.["_address"] as SiteAddress) || {},
- );
-
- if (hasMapField && (!longitude || !latitude)) {
- return (
-
-
-
-
- Invalid graph
-
-
- Edit this flow so that "List" is positioned after "FindProperty"; an
- address is required for schemas that include a "map" field.
-
-
-
- );
- }
const listContent = (
diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx
index 4b260271a7..f55921d07f 100644
--- a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx
+++ b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx
@@ -8,8 +8,8 @@ import Tab, { tabClasses, TabProps } from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
import Typography from "@mui/material/Typography";
import { SiteAddress } from "@planx/components/FindProperty/model";
-import { ErrorSummaryContainer } from "@planx/components/shared/Preview/ErrorSummaryContainer";
import { SchemaFields } from "@planx/components/shared/Schema/SchemaFields";
+import { GraphError } from "components/Error/GraphError";
import { GeoJsonObject } from "geojson";
import sortBy from "lodash/sortBy";
import { useStore } from "pages/FlowEditor/lib/store";
@@ -305,24 +305,6 @@ export const Presentational: React.FC = (props) => (
);
-const GraphError = (props: Props) => (
-
-
-
-
- Invalid graph
-
-
- Edit this flow so that "MapAndLabel" is positioned after "FindProperty";
- an initial address is required to correctly display the map.
-
-
-
-);
-
function MapAndLabelComponent(props: Props) {
const teamSettings = useStore.getState().teamSettings;
const passport = useStore((state) => state.computePassport());
@@ -330,7 +312,7 @@ function MapAndLabelComponent(props: Props) {
(passport?.data?._address as SiteAddress) || {};
if (!latitude || !longitude) {
- return ;
+ throw new GraphError("nodeMustFollowFindProperty");
}
return (
diff --git a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx
index 2eefae94fe..256dff7063 100644
--- a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx
+++ b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx
@@ -1,5 +1,6 @@
-import { screen } from "@testing-library/react";
+import ErrorFallback from "components/Error/ErrorFallback";
import React from "react";
+import { ErrorBoundary } from "react-error-boundary";
import { setup } from "testUtils";
import { vi } from "vitest";
import { axe } from "vitest-axe";
@@ -9,7 +10,7 @@ import digitalLandResponseMock from "./mocks/digitalLandResponseMock";
import PlanningConstraints from "./Public";
vi.mock("swr", () => ({
- default: vi.fn((url: any) => {
+ default: vi.fn((url: () => string) => {
const isGISRequest = url()?.startsWith(
`${import.meta.env.VITE_APP_API_URL}/gis/`,
);
@@ -17,47 +18,54 @@ vi.mock("swr", () => ({
`${import.meta.env.VITE_APP_API_URL}/roads/`,
);
- if (isGISRequest) return { data: digitalLandResponseMock } as any;
- if (isRoadsRequest) return { data: classifiedRoadsResponseMock } as any;
+ if (isGISRequest) return { data: digitalLandResponseMock };
+ if (isRoadsRequest) return { data: classifiedRoadsResponseMock };
return { data: null };
}),
}));
-it("renders correctly", async () => {
- const handleSubmit = vi.fn();
+describe("error state", () => {
+ it("renders an error if no addres is present in the passport", async () => {
+ const handleSubmit = vi.fn();
- const { user } = setup(
- ,
- );
-
- expect(screen.getByText("Planning constraints")).toBeInTheDocument();
+ const { getByRole, getByTestId } = setup(
+
+
+ ,
+ ,
+ );
- // TODO mock passport _address so that SWR request is actually triggered to return mock response
- expect(screen.getByTestId("error-summary-invalid-graph")).toBeInTheDocument();
+ expect(getByTestId("error-summary-invalid-graph")).toBeInTheDocument();
+ expect(getByRole("heading", { name: "Invalid graph" })).toBeInTheDocument();
+ });
- await user.click(screen.getByTestId("continue-button"));
- expect(handleSubmit).toHaveBeenCalledTimes(1);
+ it("should not have any accessibility violations", async () => {
+ const { container } = setup(
+
+
+ ,
+ ,
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
});
-it("should not have any accessibility violations", async () => {
- const { container } = setup(
- ,
- );
- const results = await axe(container);
- expect(results).toHaveNoViolations();
-});
+it.todo("renders correctly");
+
+it.todo("should not have any accessibility violations");
it.todo("fetches classified roads only when we have a siteBoundary"); // using expect(spy).toHaveBeenCalled() ??
diff --git a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx
index f93b0a1edc..513961d632 100644
--- a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx
+++ b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx
@@ -9,6 +9,7 @@ import Card from "@planx/components/shared/Preview/Card";
import CardHeader from "@planx/components/shared/Preview/CardHeader";
import type { PublicProps } from "@planx/components/ui";
import DelayedLoadingIndicator from "components/DelayedLoadingIndicator";
+import { GraphError } from "components/Error/GraphError";
import capitalize from "lodash/capitalize";
import { useStore } from "pages/FlowEditor/lib/store";
import { HandleSubmit } from "pages/Preview/Node";
@@ -63,6 +64,8 @@ function Component(props: Props) {
// PlanningConstraints must come after at least a FindProperty in the graph
const showGraphError = !x || !y || !longitude || !latitude;
+ if (showGraphError)
+ throw new GraphError("mapInputFieldMustFollowFindProperty");
// Even though this component will fetch fresh GIS data when coming "back",
// still prepopulate any previously marked inaccurateConstraints
@@ -145,8 +148,6 @@ function Component(props: Props) {
...roads?.metadata,
};
- if (showGraphError) return ;
-
const isLoading = isValidating || isValidatingRoads;
if (isLoading)
return (
@@ -396,28 +397,3 @@ const ConstraintsFetchError = (props: ConstraintsFetchErrorProps) => (
);
-
-interface ConstraintsGraphErrorProps {
- title: string;
- description: string;
- handleSubmit?: HandleSubmit;
-}
-
-const ConstraintsGraphError = (props: ConstraintsGraphErrorProps) => (
-
-
-
-
- Invalid graph
-
-
- Edit this flow so that "Planning constraints" is positioned after "Find
- property"; an address or site boundary drawing is required to fetch
- data.
-
-
-
-);
diff --git a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.test.tsx b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.test.tsx
index 318aaf37f2..2e35f47189 100644
--- a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.test.tsx
+++ b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.test.tsx
@@ -1,6 +1,8 @@
import { MockedProvider } from "@apollo/client/testing";
import { screen } from "@testing-library/react";
+import ErrorFallback from "components/Error/ErrorFallback";
import React from "react";
+import { ErrorBoundary } from "react-error-boundary";
import { setup } from "testUtils";
import { vi } from "vitest";
@@ -18,10 +20,12 @@ const defaultPresentationalProps: PresentationalProps = {
test("renders a warning for editors if address data is not in state", async () => {
setup(
-
+
+
+
,
);
diff --git a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx
index 5502fc42aa..6b6bfc17d9 100644
--- a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx
+++ b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx
@@ -1,12 +1,12 @@
import { useQuery } from "@apollo/client";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
-import Typography from "@mui/material/Typography";
import { visuallyHidden } from "@mui/utils";
import Card from "@planx/components/shared/Preview/Card";
import CardHeader from "@planx/components/shared/Preview/CardHeader";
import { SummaryListTable } from "@planx/components/shared/Preview/SummaryList";
import type { PublicProps } from "@planx/components/ui";
+import { GraphError } from "components/Error/GraphError";
import { Feature } from "geojson";
import { publicClient } from "lib/graphql";
import find from "lodash/find";
@@ -17,7 +17,6 @@ import React from "react";
import type { SiteAddress } from "../FindProperty/model";
import { FETCH_BLPU_CODES } from "../FindProperty/Public";
-import { ErrorSummaryContainer } from "../shared/Preview/ErrorSummaryContainer";
import { MapContainer } from "../shared/Preview/MapContainer";
import type { PropertyInformation } from "./model";
@@ -32,7 +31,10 @@ function Component(props: PublicProps) {
client: publicClient,
});
- return passport.data?._address ? (
+ if (!passport.data?._address)
+ throw new GraphError("nodeMustFollowFindProperty");
+
+ return (
) {
});
}}
/>
- ) : (
-
-
-
- Invalid graph
-
-
- Edit this flow so that "Property information" is positioned after
- "Find property"; an address is required to render.
-
-
-
);
}
diff --git a/editor.planx.uk/src/@planx/components/shared/Schema/InputFields/MapFieldInput.tsx b/editor.planx.uk/src/@planx/components/shared/Schema/InputFields/MapFieldInput.tsx
index 765ef75489..7ddc054bc9 100644
--- a/editor.planx.uk/src/@planx/components/shared/Schema/InputFields/MapFieldInput.tsx
+++ b/editor.planx.uk/src/@planx/components/shared/Schema/InputFields/MapFieldInput.tsx
@@ -1,6 +1,8 @@
import Box from "@mui/material/Box";
+import { SiteAddress } from "@opensystemslab/planx-core/types";
import { MapContainer } from "@planx/components/shared/Preview/MapContainer";
import type { MapField } from "@planx/components/shared/Schema/model";
+import { GraphError } from "components/Error/GraphError";
import { Feature } from "geojson";
import { useStore } from "pages/FlowEditor/lib/store";
import React, { useEffect, useState } from "react";
@@ -11,6 +13,15 @@ import { getFieldProps, Props } from ".";
import { FieldInputDescription } from "./shared";
export const MapFieldInput: React.FC> = (props) => {
+ // Ensure there's a FindProperty component preceding this field (eg address data in state to position map view)
+ const { longitude, latitude } = useStore(
+ (state) =>
+ (state.computePassport()?.data?.["_address"] as SiteAddress) || {},
+ );
+
+ if (!longitude || !latitude)
+ throw new GraphError("mapInputFieldMustFollowFindProperty");
+
const {
formik,
data: { title, description, mapOptions },
diff --git a/editor.planx.uk/src/components/ErrorFallback.stories.tsx b/editor.planx.uk/src/components/Error/ErrorFallback.stories.tsx
similarity index 100%
rename from editor.planx.uk/src/components/ErrorFallback.stories.tsx
rename to editor.planx.uk/src/components/Error/ErrorFallback.stories.tsx
diff --git a/editor.planx.uk/src/components/ErrorFallback.tsx b/editor.planx.uk/src/components/Error/ErrorFallback.tsx
similarity index 70%
rename from editor.planx.uk/src/components/ErrorFallback.tsx
rename to editor.planx.uk/src/components/Error/ErrorFallback.tsx
index 68a85ff170..ad9e822d91 100644
--- a/editor.planx.uk/src/components/ErrorFallback.tsx
+++ b/editor.planx.uk/src/components/Error/ErrorFallback.tsx
@@ -1,12 +1,15 @@
import Typography from "@mui/material/Typography";
import Card from "@planx/components/shared/Preview/Card";
import { ErrorSummaryContainer } from "@planx/components/shared/Preview/ErrorSummaryContainer";
+import { logger } from "airbrake";
import React from "react";
-import { logger } from "../airbrake";
+import { GraphErrorComponent, isGraphError } from "./GraphError";
-function ErrorFallback(props: { error: Error }) {
- logger.notify(props.error);
+const ErrorFallback: React.FC<{ error: Error }> = ({ error }) => {
+ if (isGraphError(error)) return ;
+
+ logger.notify(error);
return (
@@ -15,9 +18,9 @@ function ErrorFallback(props: { error: Error }) {
Something went wrong
- {props.error?.message && (
+ {error.message && (
- {props.error.message}
+ {error.message}
)}
@@ -27,6 +30,6 @@ function ErrorFallback(props: { error: Error }) {
);
-}
+};
export default ErrorFallback;
diff --git a/editor.planx.uk/src/components/Error/GraphError.test.tsx b/editor.planx.uk/src/components/Error/GraphError.test.tsx
new file mode 100644
index 0000000000..ec79de8dfc
--- /dev/null
+++ b/editor.planx.uk/src/components/Error/GraphError.test.tsx
@@ -0,0 +1,81 @@
+import { logger } from "airbrake";
+import ErrorFallback from "components/Error/ErrorFallback";
+import React from "react";
+import { ErrorBoundary } from "react-error-boundary";
+import { setup } from "testUtils";
+import { vi } from "vitest";
+import { axe } from "vitest-axe";
+
+import { GraphError } from "./GraphError";
+
+vi.mock("airbrake", () => ({
+ logger: {
+ notify: vi.fn(),
+ },
+}));
+
+const ThrowError: React.FC = () => {
+ throw new Error("Something broke");
+};
+
+const ThrowGraphError: React.FC = () => {
+ throw new GraphError("nodeMustFollowFindProperty");
+};
+
+it("does not render if a child does not throw an error", () => {
+ const { queryByRole } = setup(
+
+ No error
+ ,
+ );
+ expect(
+ queryByRole("heading", { name: /Invalid graph/ }),
+ ).not.toBeInTheDocument();
+});
+
+it("does not render if a child throws a non-Graph error", () => {
+ const { queryByRole, getByText } = setup(
+
+
+ ,
+ );
+ // ErrorFallback displays...
+ expect(getByText(/Something went wrong/)).toBeInTheDocument();
+ // ...but does not show a GraphError
+ expect(
+ queryByRole("heading", { name: /Invalid graph/ }),
+ ).not.toBeInTheDocument();
+});
+
+it("renders if a child throws an error", () => {
+ const { queryByText, getByRole } = setup(
+
+
+ ,
+ );
+
+ expect(queryByText(/Something went wrong/)).not.toBeInTheDocument();
+ expect(getByRole("heading", { name: /Invalid graph/ })).toBeInTheDocument();
+});
+
+it("does not call Airbrake", () => {
+ const loggerSpy = vi.spyOn(logger, "notify");
+
+ setup(
+
+
+ ,
+ );
+
+ expect(loggerSpy).not.toHaveBeenCalled();
+});
+
+it("should not have accessability violations", async () => {
+ const { container } = setup(
+
+
+ ,
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+});
diff --git a/editor.planx.uk/src/components/Error/GraphError.tsx b/editor.planx.uk/src/components/Error/GraphError.tsx
new file mode 100644
index 0000000000..2b86e7aaef
--- /dev/null
+++ b/editor.planx.uk/src/components/Error/GraphError.tsx
@@ -0,0 +1,43 @@
+import Typography from "@mui/material/Typography";
+import Card from "@planx/components/shared/Preview/Card";
+import { ErrorSummaryContainer } from "@planx/components/shared/Preview/ErrorSummaryContainer";
+import React from "react";
+
+type GraphErrorType =
+ | "nodeMustFollowFindProperty"
+ | "mapInputFieldMustFollowFindProperty";
+
+const GRAPH_ERROR_MESSAGES: Record = {
+ nodeMustFollowFindProperty:
+ 'Edit this flow so that this node is positioned after "Find property"; an address or site boundary drawing is required to fetch data',
+ mapInputFieldMustFollowFindProperty:
+ 'Edit this flow so that this component is positioned after "FindProperty"; an address is required for schemas that include a "map" field.',
+};
+
+export class GraphError extends Error {
+ constructor(public type: GraphErrorType) {
+ super();
+ this.type = type;
+ }
+}
+
+export const isGraphError = (error: unknown): error is GraphError =>
+ error instanceof GraphError;
+
+export const GraphErrorComponent: React.FC<{ error: GraphError }> = ({
+ error,
+}) => (
+
+
+
+ Invalid graph
+
+
+ {GRAPH_ERROR_MESSAGES[error.type]}
+
+
+
+);
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx
index 4de7b76aac..29d1994471 100644
--- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx
@@ -15,7 +15,7 @@ import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import DelayedLoadingIndicator from "components/DelayedLoadingIndicator";
-import ErrorFallback from "components/ErrorFallback";
+import ErrorFallback from "components/Error/ErrorFallback";
import { format } from "date-fns";
import React, { useState } from "react";
import ErrorSummary from "ui/shared/ErrorSummary";
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx b/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx
index 9e400b8267..a937b8d259 100644
--- a/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx
+++ b/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx
@@ -9,7 +9,7 @@ import IconButton from "@mui/material/IconButton";
import { styled } from "@mui/material/styles";
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import { parseFormValues } from "@planx/components/shared";
-import ErrorFallback from "components/ErrorFallback";
+import ErrorFallback from "components/Error/ErrorFallback";
import { hasFeatureFlag } from "lib/featureFlags";
import React from "react";
import { ErrorBoundary } from "react-error-boundary";
diff --git a/editor.planx.uk/src/pages/Preview/Questions.tsx b/editor.planx.uk/src/pages/Preview/Questions.tsx
index f287939af9..1819922734 100644
--- a/editor.planx.uk/src/pages/Preview/Questions.tsx
+++ b/editor.planx.uk/src/pages/Preview/Questions.tsx
@@ -12,7 +12,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { ApplicationPath, Session } from "types";
-import ErrorFallback from "../../components/ErrorFallback";
+import ErrorFallback from "../../components/Error/ErrorFallback";
import { useStore } from "../FlowEditor/lib/store";
import Node, { HandleSubmit } from "./Node";
diff --git a/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx b/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx
index 327d8da975..d1d9e6eada 100644
--- a/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx
+++ b/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx
@@ -1,4 +1,4 @@
-import ErrorFallback from "components/ErrorFallback";
+import ErrorFallback from "components/Error/ErrorFallback";
import FlowEditor from "pages/FlowEditor";
import React, { PropsWithChildren } from "react";
import { ErrorBoundary } from "react-error-boundary";
diff --git a/editor.planx.uk/src/pages/layout/PublicLayout.tsx b/editor.planx.uk/src/pages/layout/PublicLayout.tsx
index 23df5c9232..f7b21a23b1 100644
--- a/editor.planx.uk/src/pages/layout/PublicLayout.tsx
+++ b/editor.planx.uk/src/pages/layout/PublicLayout.tsx
@@ -6,7 +6,7 @@ import {
ThemeProvider,
} from "@mui/material/styles";
import Typography from "@mui/material/Typography";
-import ErrorFallback from "components/ErrorFallback";
+import ErrorFallback from "components/Error/ErrorFallback";
import Feedback from "components/Feedback";
import { useStore } from "pages/FlowEditor/lib/store";
import React, { PropsWithChildren } from "react";
From b248eccb2e7d6cd02ecf4319a84b4a00ff5ef37f Mon Sep 17 00:00:00 2001
From: Rory Doak <138574807+RODO94@users.noreply.github.com>
Date: Wed, 18 Sep 2024 09:11:44 +0100
Subject: [PATCH 02/10] feat: Map and Label Copy Tests (#3695)
---
.../MapAndLabel/Public/index.test.tsx | 104 ++++++++++++++++--
.../MapAndLabel/test/mocks/GenericValues.ts | 3 +-
2 files changed, 93 insertions(+), 14 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 c79f02117e..58ce130ef2 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
@@ -18,6 +18,7 @@ import {
fillOutForm,
fillOutSecondHalfOfForm,
} from "../test/utils";
+import { mockTreeData } from "../test/mocks/GenericValues";
beforeAll(() => {
if (!window.customElements.get("my-map")) {
@@ -332,18 +333,97 @@ describe("basic interactions - happy path", () => {
});
describe("copy feature select", () => {
- it.todo("is disabled if only a single feature is present");
- // no copy select if only one feature
- 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)"
- );
- // current tree is not an option in the copy select
- it.todo("copies all data from one feature to another");
- // all data fields are populated from one field to another
- it.todo("should not have any accessibility violations");
- // axe checks
+ it("is disabled if only a single feature is present", async () => {
+ const { getByTestId, getByTitle } = setup();
+ const map = getByTestId("map-and-label-map");
+
+ addFeaturesToMap(map, [point1]);
+
+ const copyTitle = getByTitle("Copy from");
+
+ const copyInput = within(copyTitle).getByRole("combobox");
+
+ expect(copyInput).toHaveAttribute("aria-disabled", "true");
+ });
+
+ it("is enabled once multiple features are present", async () => {
+ const { getByTitle } = setup();
+
+ addMultipleFeatures([point1, point2]);
+
+ const copyTitle = getByTitle("Copy from");
+
+ const copyInput = within(copyTitle).getByRole("combobox");
+
+ expect(copyInput).not.toHaveAttribute("aria-disabled", "true");
+ });
+
+ it("lists all other features as options (the current feature is not listed)", async () => {
+ const { getByTitle, user, queryByRole } = setup();
+ addMultipleFeatures([point1, point2]);
+
+ const copyTitle = getByTitle("Copy from");
+
+ const copyInput = within(copyTitle).getByRole("combobox");
+
+ expect(copyInput).not.toHaveAttribute("aria-disabled", "true");
+
+ await user.click(copyInput);
+
+ // Current item would be Tree 2 since we added two points
+ const listItemTwo = queryByRole("option", { name: "Tree 2" });
+
+ expect(listItemTwo).not.toBeInTheDocument();
+ });
+
+ it("copies all data from one feature to another", async () => {
+ const { getByTitle, user, getByLabelText, getByRole } = setup(
+
+ );
+ addMultipleFeatures([point1, point2]);
+ const tabOne = getByRole("tab", { name: /Tree 1/ });
+
+ await fillOutForm(user);
+
+ await user.click(tabOne);
+
+ const copyTitle = getByTitle("Copy from");
+ const copyInput = within(copyTitle).getByRole("combobox", {
+ name: "Copy from",
+ });
+
+ await user.click(copyInput);
+
+ const listItemTwo = getByRole("option", { name: "Tree 2" });
+
+ await user.click(listItemTwo);
+
+ const urgencyDiv = getByTitle("Urgency");
+ const urgencySelect = within(urgencyDiv).getByRole("combobox");
+
+ expect(getByLabelText("Species")).toHaveDisplayValue(mockTreeData.species);
+ expect(getByLabelText("Proposed work")).toHaveDisplayValue(
+ mockTreeData.work
+ );
+ expect(getByLabelText("Justification")).toHaveDisplayValue(
+ mockTreeData.justification
+ );
+ expect(urgencySelect).toHaveTextContent(mockTreeData.urgency);
+ });
+
+ it("should not have any accessibility violations", async () => {
+ const { getByTitle, user, container } = setup();
+ addMultipleFeatures([point1, point2]);
+
+ const copyTitle = getByTitle("Copy from");
+
+ const copyInput = within(copyTitle).getByRole("combobox");
+
+ await user.click(copyInput);
+
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
});
describe("remove feature button", () => {
diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/GenericValues.ts b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/GenericValues.ts
index 61912b194d..a67af1c009 100644
--- a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/GenericValues.ts
+++ b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/GenericValues.ts
@@ -2,6 +2,5 @@ export const mockTreeData = {
species: "Larch",
work: "Chopping it down",
justification: "Cause I can",
- urgency: "High",
- completionDate: { day: "", month: "", year: "" },
+ urgency: "Low",
};
From 1e61b3d44af199c3d0a68e727a126f7acd6c30a4 Mon Sep 17 00:00:00 2001
From: Rory Doak <138574807+RODO94@users.noreply.github.com>
Date: Wed, 18 Sep 2024 09:12:01 +0100
Subject: [PATCH 03/10] feat: Map and Label Remove Tests (#3696)
---
.../MapAndLabel/Public/index.test.tsx | 72 +++++++++++++++++--
.../MapAndLabel/test/mocks/geojson.ts | 2 +
2 files changed, 67 insertions(+), 7 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 58ce130ef2..5d0cdcd880 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,7 +6,12 @@ import { setup } from "testUtils";
import { vi } from "vitest";
import { axe } from "vitest-axe";
-import { point1, point2, point3 } from "../test/mocks/geojson";
+import {
+ mockFeaturePointObj,
+ point1,
+ point2,
+ point3,
+} from "../test/mocks/geojson";
import { props } from "../test/mocks/Trees";
import {
addFeaturesToMap,
@@ -206,13 +211,16 @@ test.todo("an error displays if the maximum number of items is exceeded");
describe("basic interactions - happy path", () => {
it("adding an item to the map adds a feature tab", async () => {
const { getByTestId } = setup();
- const map = getByTestId("map-and-label-map");
+ let map = getByTestId("map-and-label-map");
addFeaturesToMap(map, [point1]);
const firstTabPanel = getByTestId("vertical-tabpanel-0");
expect(firstTabPanel).toBeVisible();
+
+ map = getByTestId("map-and-label-map");
+ expect(map).toHaveAttribute("drawgeojsondata", mockFeaturePointObj);
});
it("a user can input details on a single feature and submit", async () => {
@@ -425,12 +433,62 @@ describe("copy feature select", () => {
expect(results).toHaveNoViolations();
});
});
-
describe("remove feature button", () => {
- it.todo("removes a feature from the form");
- // click remove - feature is removed
- // not tab
- it.todo("removes a feature from the map");
+ it("removes a feature from the form - single feature", async () => {
+ const { getByTestId, getByRole, user } = setup();
+ const map = getByTestId("map-and-label-map");
+
+ addFeaturesToMap(map, [point1]);
+
+ const tabOne = getByRole("tab", { name: /Tree 1/ });
+ const tabOnePanel = getByRole("tabpanel", { name: /Tree 1/ });
+
+ const removeButton = getByRole("button", { name: "Remove" });
+
+ await user.click(removeButton);
+
+ expect(tabOne).not.toBeInTheDocument();
+ expect(tabOnePanel).not.toBeInTheDocument();
+ });
+ it("removes a feature from the form - multiple features", async () => {
+ const { getByRole, user } = setup();
+
+ addMultipleFeatures([point1, point2]);
+
+ const tabOne = getByRole("tab", { name: /Tree 1/ });
+ const tabTwo = getByRole("tab", { name: /Tree 2/ });
+ const tabTwoPanel = getByRole("tabpanel", { name: /Tree 2/ });
+
+ const removeButton = getByRole("button", { name: "Remove" });
+
+ await user.click(removeButton);
+
+ expect(tabTwo).not.toBeInTheDocument();
+ expect(tabTwoPanel).not.toBeInTheDocument();
+
+ const tabOnePanel = getByRole("tabpanel", { name: /Tree 1/ });
+
+ // Ensure tab one remains
+ expect(tabOne).toBeInTheDocument();
+ expect(tabOnePanel).toBeInTheDocument();
+ });
+ it("removes a feature from the map", async () => {
+ const { getByTestId, getByRole, user } = setup();
+ let map = getByTestId("map-and-label-map");
+
+ addFeaturesToMap(map, [point1]);
+
+ const removeButton = getByRole("button", { name: "Remove" });
+
+ await user.click(removeButton);
+
+ map = getByTestId("map-and-label-map");
+
+ expect(map).toHaveAttribute(
+ "drawgeojsondata",
+ `{"type":"FeatureCollection","features":[]}`
+ );
+ });
// click remove - feature is removed
// no map icon
});
diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts
index 7aff35a382..6fdd9fb026 100644
--- a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts
+++ b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts
@@ -32,3 +32,5 @@ export const point3: Feature = {
coordinates: [-3.68689607119201, 57.15310833687542],
},
};
+
+export const mockFeaturePointObj = `{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"label":"1"},"geometry":{"type":"Point","coordinates":[-3.685929607119201,57.15301433687542]}}]}`;
From 75abcf410c9fce7171cfaf68e7c7c0951575a206 Mon Sep 17 00:00:00 2001
From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com>
Date: Wed, 18 Sep 2024 10:49:23 +0100
Subject: [PATCH 04/10] test[e2e]: refactor, and add to public-facing editor
flow test (#3683)
---
api.planx.uk/.husky/pre-commit | 2 +-
.../src/create-flow/create-flow.spec.ts | 275 +++++++-----------
.../ui-driven/src/helpers/userActions.ts | 112 ++++++-
.../ui-driven/src/mocks/osPlacesResponse.ts | 13 +
e2e/tests/ui-driven/src/pages/Editor.ts | 174 +++++++++++
e2e/tests/ui-driven/src/refresh-page.spec.ts | 85 ++++++
.../MapAndLabel/Public/index.test.tsx | 18 +-
.../components/MapAndLabel/test/utils.ts | 4 +-
.../src/components/RouteLoadingIndicator.tsx | 2 +-
.../components/Team/TeamMembers.tsx | 13 +-
.../tests/TeamMembers.addNewEditor.test.tsx | 10 +-
.../tests/TeamMembers.updateEditor.test.tsx | 22 +-
.../src/pages/FlowEditor/index.tsx | 3 +-
editor.planx.uk/src/routes/team.tsx | 4 +-
14 files changed, 526 insertions(+), 211 deletions(-)
create mode 100644 e2e/tests/ui-driven/src/pages/Editor.ts
create mode 100644 e2e/tests/ui-driven/src/refresh-page.spec.ts
diff --git a/api.planx.uk/.husky/pre-commit b/api.planx.uk/.husky/pre-commit
index 58eb265738..2dca158161 100755
--- a/api.planx.uk/.husky/pre-commit
+++ b/api.planx.uk/.husky/pre-commit
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
cd api.planx.uk
-pnpm dlx lint-staged
\ No newline at end of file
+pnpm dlx lint-staged
diff --git a/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts b/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts
index 0db5895bcc..d3eda04740 100644
--- a/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts
+++ b/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts
@@ -1,21 +1,4 @@
import { Browser, expect, test } from "@playwright/test";
-import {
- createAddressInput,
- createChecklist,
- createContactInput,
- createDateInput,
- createDrawBoundary,
- createFileUpload,
- createFindProperty,
- createNextSteps,
- createNotice,
- createNumberInput,
- createPlanningConstraints,
- createQuestionWithOptions,
- createReview,
- createTaskList,
- createTextInput,
-} from "../helpers/addComponent";
import type { Context } from "../helpers/context";
import {
contextDefaults,
@@ -23,13 +6,21 @@ import {
tearDownTestContext,
} from "../helpers/context";
import { getTeamPage } from "../helpers/getPage";
+import { createAuthenticatedSession } from "../helpers/globalHelpers";
import {
- createAuthenticatedSession,
- isGetUserRequest,
-} from "../helpers/globalHelpers";
-import { answerQuestion, clickContinue } from "../helpers/userActions";
-
-test.describe("Navigation", () => {
+ answerAddressInput,
+ answerChecklist,
+ answerContactInput,
+ answerDateInput,
+ answerFindProperty,
+ answerNumberInput,
+ answerQuestion,
+ answerTextInput,
+ clickContinue,
+} from "../helpers/userActions";
+import { PlaywrightEditor } from "../pages/Editor";
+
+test.describe("Flow creation, publish and preview", () => {
let context: Context = {
...contextDefaults,
};
@@ -52,60 +43,6 @@ test.describe("Navigation", () => {
await tearDownTestContext(context);
});
- test("user data persists on page refresh @regression", async ({
- browser,
- }) => {
- const page = await createAuthenticatedSession({
- browser,
- userId: context.user!.id!,
- });
-
- const initialRequest = page.waitForRequest(isGetUserRequest);
-
- Promise.all([await page.goto("/"), await initialRequest]);
-
- const team = page.locator("h3", { hasText: context.team.name });
-
- let isRepeatedRequestMade = false;
- page.on(
- "request",
- (req) => (isRepeatedRequestMade = isGetUserRequest(req)),
- );
-
- Promise.all([
- await team.click(),
- expect(isRepeatedRequestMade).toBe(false),
- ]);
-
- const reloadRequest = page.waitForRequest(isGetUserRequest);
-
- Promise.all([await page.reload(), await reloadRequest]);
- });
-
- test("team data persists on page refresh @regression", async ({
- browser,
- }) => {
- const page = await createAuthenticatedSession({
- browser,
- userId: context.user!.id!,
- });
-
- await page.goto("/");
- const team = page.locator("h3", { hasText: context.team.name });
- await team.click();
-
- const teamSlugInHeader = page.getByRole("link", {
- name: context.team.slug,
- });
- await expect(teamSlugInHeader).toBeVisible();
-
- await page.reload();
- await expect(teamSlugInHeader).toBeVisible();
-
- await page.goBack();
- await expect(teamSlugInHeader).toBeHidden();
- });
-
test("Create a flow", async ({ browser }) => {
const page = await getTeamPage({
browser,
@@ -113,105 +50,48 @@ test.describe("Navigation", () => {
teamName: context.team.name,
});
+ const editor = new PlaywrightEditor(page);
+
page.on("dialog", (dialog) => dialog.accept(serviceProps.name));
- await page.locator("button", { hasText: "Add a new service" }).click();
+ await editor.addNewService();
// update context to allow flow to be torn down
context.flow = { ...serviceProps };
- const firstNode = page.locator("li.hanger > a").first();
-
- const questionText = "Is this a test?";
- await createQuestionWithOptions(page, firstNode, questionText, [
- "Yes",
- "No",
- ]);
- await expect(
- page.locator("a").filter({ hasText: questionText }),
- ).toBeVisible();
-
- // Add a notice to the "Yes" path
- const yesBranch = page.locator("#flow .card .options .option").nth(0);
-
- const yesBranchNoticeText = "Yes! this is a test";
- await createNotice(
- page,
- yesBranch.locator(".hanger > a"),
- yesBranchNoticeText,
- );
-
- // Add a notice to the "No" path
- const noBranch = page.locator("#flow .card .options .option").nth(1);
- const noBranchNoticeText = "Sorry, this is a test";
- await createNotice(
- page,
- noBranch.locator(".hanger > a"),
- noBranchNoticeText,
- );
-
- const getNextNode = () => page.locator(".hanger > a").last();
-
- await createChecklist(page, getNextNode(), "A checklist title", [
+ await editor.createQuestion();
+ await editor.createNoticeOnEachBranch();
+ await editor.createChecklist();
+ await editor.createTextInput();
+ await editor.createNumberInput();
+ await editor.createDateInput();
+ await editor.createAddressInput();
+ await editor.createContactInput();
+ await editor.createTaskList();
+ await editor.createFindProperty();
+ await editor.createDrawBoundary();
+ await editor.createPlanningConstraints();
+ await editor.createFileUpload();
+ await editor.createNextSteps();
+ await editor.createReview();
+
+ await expect(editor.nodeList).toContainText([
+ "Is this a test?",
+ "Yes! this is a test",
+ "Sorry, this is a test",
"Checklist item 1",
- "Second checklist item",
- "The third checklist item",
- ]);
-
- await createTextInput(page, getNextNode(), "Tell us about your trees.");
- await createNumberInput(page, getNextNode(), "How old are you?", "years");
- await createDateInput(page, getNextNode(), "When is your birthday?");
-
- await createAddressInput(
- page,
- getNextNode(),
+ "Tell us about your trees.",
+ "How old are you?",
+ "When is your birthday?",
"What is your address?",
- "some data field",
- );
-
- await createContactInput(
- page,
- getNextNode(),
"What is your contact info?",
- "some data field",
- );
-
- await createTaskList(page, getNextNode(), "What you should do next", [
- "Have a cup of tea",
- "Continue through this flow",
- ]);
-
- await createFindProperty(page, getNextNode());
- await createDrawBoundary(page, getNextNode());
- await createPlanningConstraints(page, getNextNode());
- await createFileUpload(page, getNextNode(), "some data field");
-
- await createNextSteps(page, getNextNode(), [
- "A possible next step",
- "Another option",
+ "What you should do next",
+ "Find property",
+ "Confirm your location plan",
+ "Planning constraints",
+ "File upload",
+ "Next steps",
+ "Check your answers before sending your application",
]);
-
- await createReview(page, getNextNode());
-
- const nodes = page.locator(".card");
- await expect(nodes.getByText(questionText)).toBeVisible();
- await expect(nodes.getByText(yesBranchNoticeText)).toBeVisible();
- await expect(nodes.getByText(noBranchNoticeText)).toBeVisible();
- await expect(nodes.getByText("Checklist item 1")).toBeVisible();
- await expect(nodes.getByText("Tell us about your trees.")).toBeVisible();
- await expect(nodes.getByText("How old are you?")).toBeVisible();
- await expect(nodes.getByText("When is your birthday?")).toBeVisible();
- await expect(nodes.getByText("What is your address?")).toBeVisible();
- await expect(nodes.getByText("What is your contact info?")).toBeVisible();
- await expect(nodes.getByText("What you should do next")).toBeVisible();
- await expect(nodes.getByText("Find property")).toBeVisible();
- await expect(nodes.getByText("Confirm your location plan")).toBeVisible();
- await expect(nodes.getByText("Planning constraints")).toBeVisible();
- await expect(nodes.getByText("File upload")).toBeVisible();
-
- await expect(nodes.getByText("Next steps")).toBeVisible();
- await expect(
- nodes.getByText("Check your answers before sending your application"),
- ).toBeVisible();
});
test("Cannot preview an unpublished flow", async ({
@@ -321,5 +201,66 @@ test.describe("Navigation", () => {
await expect(
page.locator("h1", { hasText: "Sorry, this is a test" }),
).toBeVisible();
+ await clickContinue({ page });
+
+ await answerChecklist({
+ page,
+ title: "A checklist title",
+ answers: ["Checklist item 1", "Second checklist item"],
+ });
+ await clickContinue({ page });
+
+ await answerTextInput(page, {
+ expectedQuestion: "Tell us about your trees.",
+ answer: "My trees are lovely",
+ continueToNext: true,
+ });
+
+ await answerNumberInput(page, {
+ expectedQuestion: "How old are you?",
+ answer: 30,
+ continueToNext: true,
+ });
+
+ await answerDateInput(page, {
+ expectedQuestion: "When is your birthday?",
+ day: 30,
+ month: 12,
+ year: 1980,
+ continueToNext: true,
+ });
+
+ await answerAddressInput(page, {
+ expectedQuestion: "What is your address?",
+ addressLineOne: "1 Silver Street",
+ town: "Bamburgh",
+ postcode: "BG1 2SS",
+ continueToNext: true,
+ });
+
+ await expect(
+ page.locator("h1", { hasText: "What is your contact info?" }),
+ ).toBeVisible();
+ await answerContactInput(page, {
+ firstName: "Freddie",
+ lastName: "Mercury",
+ phoneNumber: "01234 555555",
+ email: "freddie@queen.com",
+ });
+ await clickContinue({ page });
+
+ await expect(
+ page.locator("h1", { hasText: "What you should do next" }),
+ ).toBeVisible();
+ await expect(
+ page.locator("h2", { hasText: "Have a cup of tea" }),
+ ).toBeVisible();
+ await expect(
+ page.locator("h2", { hasText: "Continue through this flow" }),
+ ).toBeVisible();
+ await clickContinue({ page });
+
+ await answerFindProperty(page);
+ await clickContinue({ page });
});
});
diff --git a/e2e/tests/ui-driven/src/helpers/userActions.ts b/e2e/tests/ui-driven/src/helpers/userActions.ts
index bb2a048125..51c7cbe7ca 100644
--- a/e2e/tests/ui-driven/src/helpers/userActions.ts
+++ b/e2e/tests/ui-driven/src/helpers/userActions.ts
@@ -1,6 +1,6 @@
import type { Locator, Page } from "@playwright/test";
import { expect } from "@playwright/test";
-import { mockOSPlacesResponse } from "../mocks/osPlacesResponse";
+import { setupOSMockResponse } from "../mocks/osPlacesResponse";
import type { Context } from "./context";
import { findSessionId, getGraphQLClient } from "./context";
import { TEST_EMAIL, log, waitForDebugLog } from "./globalHelpers";
@@ -121,7 +121,7 @@ export async function answerChecklist({
title: string;
answers: string[];
}) {
- const checklist = await page.getByRole("heading").filter({
+ const checklist = page.getByRole("heading").filter({
hasText: title,
});
await expect(checklist).toBeVisible();
@@ -196,23 +196,14 @@ export async function submitCardDetails(page: Page) {
export async function answerFindProperty(page: Page) {
await setupOSMockResponse(page);
+ await expect(
+ page.locator("h1", { hasText: "Find the property" }),
+ ).toBeVisible();
await page.getByLabel("Postcode").fill("SW1 1AA");
await page.getByLabel("Select an address").click();
await page.getByRole("option").first().click();
}
-async function setupOSMockResponse(page: Page) {
- const ordnanceSurveryPlacesEndpoint = new RegExp(
- /proxy\/ordnance-survey\/search\/places\/v1\/postcode\/*/,
- );
- await page.route(ordnanceSurveryPlacesEndpoint, async (route) => {
- await route.fulfill({
- status: 200,
- body: JSON.stringify(mockOSPlacesResponse),
- });
- });
-}
-
export async function answerContactInput(
page: Page,
{
@@ -232,3 +223,96 @@ export async function answerContactInput(
await page.getByLabel("Phone number").fill(phoneNumber);
await page.getByLabel("Email address").fill(email);
}
+
+export async function answerTextInput(
+ page: Page,
+ {
+ expectedQuestion,
+ answer,
+ continueToNext,
+ }: {
+ expectedQuestion: string;
+ answer: string;
+ continueToNext: boolean;
+ },
+) {
+ await expect(page.locator("p", { hasText: expectedQuestion })).toBeVisible();
+ await page.locator("label div input[type='text']").fill(answer);
+ if (continueToNext) {
+ await clickContinue({ page });
+ }
+}
+
+export async function answerNumberInput(
+ page: Page,
+ {
+ expectedQuestion,
+ answer,
+ continueToNext,
+ }: {
+ expectedQuestion: string;
+ answer: number;
+ continueToNext: boolean;
+ },
+) {
+ await expect(page.locator("p", { hasText: expectedQuestion })).toBeVisible();
+ await page.locator("label div input[type='number']").fill(answer.toString());
+ if (continueToNext) {
+ await clickContinue({ page });
+ }
+}
+
+export async function answerDateInput(
+ page: Page,
+ {
+ expectedQuestion,
+
+ day,
+ month,
+ year,
+ continueToNext,
+ }: {
+ expectedQuestion: string;
+
+ day: number;
+ month: number;
+ year: number;
+ continueToNext: boolean;
+ },
+) {
+ await expect(page.locator("h1", { hasText: expectedQuestion })).toBeVisible();
+ await page.getByLabel("Day").fill(day.toString());
+ await page.getByLabel("Month").fill(month.toString());
+ await page.getByLabel("Year").fill(year.toString());
+
+ if (continueToNext) {
+ await clickContinue({ page });
+ }
+}
+
+export async function answerAddressInput(
+ page: Page,
+ {
+ expectedQuestion,
+
+ addressLineOne,
+ town,
+ postcode,
+ continueToNext,
+ }: {
+ expectedQuestion: string;
+
+ addressLineOne: string;
+ town: string;
+ postcode: string;
+ continueToNext: boolean;
+ },
+) {
+ await expect(page.locator("h1", { hasText: expectedQuestion })).toBeVisible();
+ await page.getByLabel("Address line 1").fill(addressLineOne);
+ await page.getByLabel("Town").fill(town);
+ await page.getByLabel("Postcode").fill(postcode);
+ if (continueToNext) {
+ await clickContinue({ page });
+ }
+}
diff --git a/e2e/tests/ui-driven/src/mocks/osPlacesResponse.ts b/e2e/tests/ui-driven/src/mocks/osPlacesResponse.ts
index 6b6baf7d4b..5f4547be28 100644
--- a/e2e/tests/ui-driven/src/mocks/osPlacesResponse.ts
+++ b/e2e/tests/ui-driven/src/mocks/osPlacesResponse.ts
@@ -1,3 +1,5 @@
+import { Page } from "@playwright/test";
+
export const mockOSPlacesResponse = {
header: {
uri: "https://api.os.uk/search/places/v1/postcode?postcode=SW1%201AA&dataset=LPI&maxResults=100&output_srs=EPSG%3A4326&lr=EN&offset=0",
@@ -58,3 +60,14 @@ export const mockOSPlacesResponse = {
},
],
};
+export async function setupOSMockResponse(page: Page) {
+ const ordnanceSurveryPlacesEndpoint = new RegExp(
+ /proxy\/ordnance-survey\/search\/places\/v1\/postcode\/*/,
+ );
+ await page.route(ordnanceSurveryPlacesEndpoint, async (route) => {
+ await route.fulfill({
+ status: 200,
+ body: JSON.stringify(mockOSPlacesResponse),
+ });
+ });
+}
diff --git a/e2e/tests/ui-driven/src/pages/Editor.ts b/e2e/tests/ui-driven/src/pages/Editor.ts
new file mode 100644
index 0000000000..accbd03265
--- /dev/null
+++ b/e2e/tests/ui-driven/src/pages/Editor.ts
@@ -0,0 +1,174 @@
+import { expect, type Locator, type Page } from "@playwright/test";
+import {
+ createAddressInput,
+ createChecklist,
+ createContactInput,
+ createDateInput,
+ createDrawBoundary,
+ createFileUpload,
+ createFindProperty,
+ createNextSteps,
+ createNotice,
+ createNumberInput,
+ createPlanningConstraints,
+ createQuestionWithOptions,
+ createReview,
+ createTaskList,
+ createTextInput,
+} from "../helpers/addComponent";
+
+export class PlaywrightEditor {
+ readonly page: Page;
+ readonly addNewServiceButton: Locator;
+ readonly firstNode: Locator;
+ readonly yesBranch: Locator;
+ readonly noBranch: Locator;
+ readonly nodeList: Locator;
+ readonly answers: {
+ questionText: string;
+ yesBranchNoticeText: string;
+ noBranchNoticeText: string;
+ };
+
+ constructor(page: Page) {
+ this.page = page;
+ this.addNewServiceButton = page.locator("button", {
+ hasText: "Add a new service",
+ });
+ this.firstNode = page.locator("li.hanger > a").first();
+ this.yesBranch = page.locator("#flow .card .options .option").nth(0);
+ this.noBranch = page.locator("#flow .card .options .option").nth(1);
+ this.nodeList = page.locator(".card");
+ this.answers = {
+ questionText: "Is this a test?",
+ yesBranchNoticeText: "Yes! this is a test",
+ noBranchNoticeText: "Sorry, this is a test",
+ };
+ }
+
+ async addNewService() {
+ await this.addNewServiceButton.click();
+ }
+
+ async createQuestion() {
+ await createQuestionWithOptions(
+ this.page,
+ this.firstNode,
+ this.answers.questionText,
+ ["Yes", "No"],
+ );
+ await expect(
+ this.page.locator("a").filter({ hasText: this.answers.questionText }),
+ ).toBeVisible();
+ }
+
+ async createNoticeOnEachBranch() {
+ // Add a notice to the "Yes" path
+ await createNotice(
+ this.page,
+ this.yesBranch.locator(".hanger > a"),
+ this.answers.yesBranchNoticeText,
+ );
+ // Add a notice to the "No" path
+ await createNotice(
+ this.page,
+ this.noBranch.locator(".hanger > a"),
+ this.answers.noBranchNoticeText,
+ );
+
+ await expect(
+ this.page.locator("a").filter({ hasText: this.answers.questionText }),
+ ).toBeVisible();
+ }
+
+ getNextNode() {
+ return this.page.locator(".hanger > a").last();
+ }
+
+ async createChecklist() {
+ await createChecklist(this.page, this.getNextNode(), "A checklist title", [
+ "Checklist item 1",
+ "Second checklist item",
+ "The third checklist item",
+ ]);
+ }
+
+ async createTextInput() {
+ await createTextInput(
+ this.page,
+ this.getNextNode(),
+ "Tell us about your trees.",
+ );
+ }
+
+ async createNumberInput() {
+ await createNumberInput(
+ this.page,
+ this.getNextNode(),
+ "How old are you?",
+ "years",
+ );
+ }
+
+ async createDateInput() {
+ await createDateInput(
+ this.page,
+ this.getNextNode(),
+ "When is your birthday?",
+ );
+ }
+
+ async createAddressInput() {
+ await createAddressInput(
+ this.page,
+ this.getNextNode(),
+ "What is your address?",
+ "some data field",
+ );
+ }
+
+ async createContactInput() {
+ await createContactInput(
+ this.page,
+ this.getNextNode(),
+ "What is your contact info?",
+ "some data field",
+ );
+ }
+
+ async createTaskList() {
+ await createTaskList(
+ this.page,
+ this.getNextNode(),
+ "What you should do next",
+ ["Have a cup of tea", "Continue through this flow"],
+ );
+ }
+
+ async createFindProperty() {
+ await createFindProperty(this.page, this.getNextNode());
+ }
+
+ async createDrawBoundary() {
+ await createDrawBoundary(this.page, this.getNextNode());
+ }
+
+ async createPlanningConstraints() {
+ await createPlanningConstraints(this.page, this.getNextNode());
+ }
+
+ async createFileUpload() {
+ await createFileUpload(this.page, this.getNextNode(), "some data field");
+ }
+
+ async createNextSteps() {
+ await createNextSteps(this.page, this.getNextNode(), [
+ "A possible next step",
+ "Another option",
+ ]);
+ }
+
+ async createReview() {
+ await createReview(this.page, this.getNextNode());
+ }
+}
diff --git a/e2e/tests/ui-driven/src/refresh-page.spec.ts b/e2e/tests/ui-driven/src/refresh-page.spec.ts
new file mode 100644
index 0000000000..309d09c0f4
--- /dev/null
+++ b/e2e/tests/ui-driven/src/refresh-page.spec.ts
@@ -0,0 +1,85 @@
+import { expect, test } from "@playwright/test";
+import type { Context } from "./helpers/context";
+import {
+ contextDefaults,
+ setUpTestContext,
+ tearDownTestContext,
+} from "./helpers/context";
+import {
+ createAuthenticatedSession,
+ isGetUserRequest,
+} from "./helpers/globalHelpers";
+
+test.describe("Refresh page", () => {
+ let context: Context = {
+ ...contextDefaults,
+ };
+
+ test.beforeAll(async () => {
+ try {
+ context = await setUpTestContext(context);
+ } catch (error) {
+ // ensure proper teardown if setup fails
+ await tearDownTestContext(context);
+ throw error;
+ }
+ });
+
+ test.afterAll(async () => {
+ await tearDownTestContext(context);
+ });
+
+ test("user data persists on page refresh @regression", async ({
+ browser,
+ }) => {
+ const page = await createAuthenticatedSession({
+ browser,
+ userId: context.user!.id!,
+ });
+
+ const initialRequest = page.waitForRequest(isGetUserRequest);
+
+ Promise.all([await page.goto("/"), await initialRequest]);
+
+ const team = page.locator("h3", { hasText: context.team.name });
+
+ let isRepeatedRequestMade = false;
+ page.on(
+ "request",
+ (req) => (isRepeatedRequestMade = isGetUserRequest(req)),
+ );
+
+ Promise.all([
+ await team.click(),
+ expect(isRepeatedRequestMade).toBe(false),
+ ]);
+
+ const reloadRequest = page.waitForRequest(isGetUserRequest);
+
+ Promise.all([await page.reload(), await reloadRequest]);
+ });
+
+ test("team data persists on page refresh @regression", async ({
+ browser,
+ }) => {
+ const page = await createAuthenticatedSession({
+ browser,
+ userId: context.user!.id!,
+ });
+
+ await page.goto("/");
+ const team = page.locator("h3", { hasText: context.team.name });
+ await team.click();
+
+ const teamSlugInHeader = page.getByRole("link", {
+ name: context.team.slug,
+ });
+ await expect(teamSlugInHeader).toBeVisible();
+
+ await page.reload();
+ await expect(teamSlugInHeader).toBeVisible();
+
+ await page.goBack();
+ await expect(teamSlugInHeader).toBeHidden();
+ });
+});
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 5d0cdcd880..5addd9b4c3 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
@@ -60,14 +60,14 @@ describe("Basic UI", () => {
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();
@@ -81,7 +81,7 @@ describe("Basic UI", () => {
it("should not have any accessibility violations", async () => {
const { queryByText, getByTestId, container } = setup(
-
+ ,
);
expect(queryByText(/Tree 1/)).not.toBeInTheDocument();
@@ -98,7 +98,7 @@ describe("Basic UI", () => {
describe("validation and error handling", () => {
it("shows all fields are required", async () => {
const { getByTestId, user, queryByRole, getAllByTestId } = setup(
-
+ ,
);
const map = getByTestId("map-and-label-map");
@@ -172,7 +172,7 @@ describe("validation and error handling", () => {
// ??
it("an error state is applied to a tabpanel button, when it's associated feature is invalid", async () => {
const { getByTestId, user, queryByRole } = setup(
-
+ ,
);
const map = getByTestId("map-and-label-map");
@@ -194,7 +194,7 @@ describe("validation and error handling", () => {
it("does not trigger handleSubmit when errors exist", async () => {
const handleSubmit = vi.fn();
const { getByTestId, user } = setup(
-
+ ,
);
const map = getByTestId("map-and-label-map");
@@ -497,11 +497,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 1873246808..0b2424e603 100644
--- a/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts
+++ b/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts
@@ -11,7 +11,7 @@ import { mockTreeData } from "./mocks/GenericValues";
*/
export const addFeaturesToMap = async (
map: HTMLElement,
- features: Feature[]
+ features: Feature[],
) => {
const mockEvent = new CustomEvent("geojsonChange", {
detail: {
@@ -22,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[] = [];
diff --git a/editor.planx.uk/src/components/RouteLoadingIndicator.tsx b/editor.planx.uk/src/components/RouteLoadingIndicator.tsx
index 72c6852ce2..4417471a18 100644
--- a/editor.planx.uk/src/components/RouteLoadingIndicator.tsx
+++ b/editor.planx.uk/src/components/RouteLoadingIndicator.tsx
@@ -33,4 +33,4 @@ const RouteLoadingIndicator: React.FC<{
);
};
-export default RouteLoadingIndicator;
\ No newline at end of file
+export default RouteLoadingIndicator;
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx
index 1ae158e124..e6b0535e30 100644
--- a/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx
@@ -16,7 +16,7 @@ import { TeamMember } from "./types";
export const TeamMembers = () => {
const [teamMembers, teamSlug] = useStore((state) => [
- state.teamMembers,
+ state.teamMembers,
state.teamSlug,
]);
@@ -48,7 +48,11 @@ export const TeamMembers = () => {
Editors have access to edit your services.
-
+
@@ -57,7 +61,10 @@ export const TeamMembers = () => {
Admins have editor access across all teams.
-
+
{archivedMembers.length > 0 && (
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx
index 1c9a54c6a6..c82b41cf71 100644
--- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx
@@ -94,14 +94,18 @@ describe("when the addNewEditor modal is rendered", () => {
});
describe("'add a new editor' button is hidden from Templates team", () => {
- beforeEach(async() => {
- useStore.setState({ teamMembers: mockTeamMembersData, teamSlug: "templates" });
+ beforeEach(async () => {
+ useStore.setState({
+ teamMembers: mockTeamMembersData,
+ teamSlug: "templates",
+ });
});
it("hides the button on the Templates team", async () => {
const { user: _user } = await setupTeamMembersScreen();
const teamEditorsTable = screen.getByTestId("team-editors");
- const addEditorButton = within(teamEditorsTable).queryByText("Add a new editor");
+ const addEditorButton =
+ within(teamEditorsTable).queryByText("Add a new editor");
expect(addEditorButton).not.toBeInTheDocument();
});
});
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.updateEditor.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.updateEditor.test.tsx
index 7cba480610..7527470705 100644
--- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.updateEditor.test.tsx
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.updateEditor.test.tsx
@@ -24,7 +24,7 @@ describe("when a user presses 'edit button'", () => {
const teamEditorsTable = screen.getByTestId("team-editors");
const addEditorButton = await within(teamEditorsTable).findByTestId(
- "edit-button-0"
+ "edit-button-0",
);
user.click(addEditorButton);
@@ -64,7 +64,7 @@ describe("when a user deletes an input value", () => {
const teamEditorsTable = screen.getByTestId("team-editors");
const addEditorButton = await within(teamEditorsTable).findByTestId(
- "edit-button-0"
+ "edit-button-0",
);
await user.click(addEditorButton);
@@ -98,7 +98,7 @@ describe("when a user updates a field correctly", () => {
const teamEditorsTable = screen.getByTestId("team-editors");
const addEditorButton = await within(teamEditorsTable).findByTestId(
- "edit-button-0"
+ "edit-button-0",
);
await user.click(addEditorButton);
@@ -112,7 +112,7 @@ describe("when a user updates a field correctly", () => {
it("updates the field", async () => {
const firstNameInput = await screen.findByLabelText("First name");
expect(firstNameInput).toHaveDisplayValue(
- mockTeamMembersData[1].firstName + "bo"
+ mockTeamMembersData[1].firstName + "bo",
);
});
it("enables the update user button", async () => {
@@ -130,7 +130,7 @@ describe("when a user correctly updates an Editor", () => {
const teamEditorsTable = screen.getByTestId("team-editors");
const addEditorButton = await within(teamEditorsTable).findByTestId(
- "edit-button-0"
+ "edit-button-0",
);
await user.click(addEditorButton);
@@ -150,7 +150,7 @@ describe("when a user correctly updates an Editor", () => {
expect(within(membersTable).getByText(/Billbo/)).toBeInTheDocument();
});
expect(
- await screen.findByText(/Successfully updated a user/)
+ await screen.findByText(/Successfully updated a user/),
).toBeInTheDocument();
});
it("closes the modal", async () => {
@@ -160,14 +160,18 @@ describe("when a user correctly updates an Editor", () => {
});
it("shows a success message", async () => {
expect(
- await screen.findByText(/Successfully updated a user/)
+ await screen.findByText(/Successfully updated a user/),
).toBeInTheDocument();
});
});
describe("'edit' button is hidden from Templates team", () => {
- beforeEach(async() => {
- useStore.setState({ teamMembers: mockTeamMembersData, user: mockPlatformAdminUser, teamSlug: "templates" });
+ beforeEach(async () => {
+ useStore.setState({
+ teamMembers: mockTeamMembersData,
+ user: mockPlatformAdminUser,
+ teamSlug: "templates",
+ });
});
it("hides the button on the Templates team", async () => {
diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx
index 4dd0e5a36c..9e580d5800 100644
--- a/editor.planx.uk/src/pages/FlowEditor/index.tsx
+++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx
@@ -20,7 +20,8 @@ const EditorContainer = styled(Box)(() => ({
}));
const FlowEditor = () => {
- const [ flow, ...breadcrumbs ] = useCurrentRoute().url.pathname.split("/").at(-1)?.split(",") || [];
+ const [flow, ...breadcrumbs] =
+ useCurrentRoute().url.pathname.split("/").at(-1)?.split(",") || [];
const scrollContainerRef = useRef(null);
useScrollControlsAndRememberPosition(scrollContainerRef);
diff --git a/editor.planx.uk/src/routes/team.tsx b/editor.planx.uk/src/routes/team.tsx
index 02e14e96b4..864d158a59 100644
--- a/editor.planx.uk/src/routes/team.tsx
+++ b/editor.planx.uk/src/routes/team.tsx
@@ -85,7 +85,9 @@ const routes = compose(
"/:flow/service": setFlowAndLazyLoad(() => import("./serviceSettings")),
- "/:flow/submissions-log": setFlowAndLazyLoad(() => import("./submissionsLog")),
+ "/:flow/submissions-log": setFlowAndLazyLoad(
+ () => import("./submissionsLog"),
+ ),
"/members": lazy(() => import("./teamMembers")),
"/design": compose(
From b15c023a13fd05f4663c3f12d5acd654f27d77ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?=
Date: Wed, 18 Sep 2024 12:15:18 +0100
Subject: [PATCH 05/10] test: Basic coverage for `PlanningConstraints` public
interface (#3700)
---
.../PlanningConstraints/Public.test.tsx | 186 ++++++++++++++++--
.../components/PlanningConstraints/Public.tsx | 1 +
.../PlanningConstraints/mocks/simpleFlow.ts | 98 +++++++++
.../shared/Preview/SimpleExpand.tsx | 2 +-
4 files changed, 275 insertions(+), 12 deletions(-)
create mode 100644 editor.planx.uk/src/@planx/components/PlanningConstraints/mocks/simpleFlow.ts
diff --git a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx
index 256dff7063..86504baec4 100644
--- a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx
+++ b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx
@@ -1,21 +1,31 @@
import ErrorFallback from "components/Error/ErrorFallback";
+import { useStore } from "pages/FlowEditor/lib/store";
import React from "react";
+import { act } from "react-dom/test-utils";
import { ErrorBoundary } from "react-error-boundary";
+import swr from "swr";
import { setup } from "testUtils";
import { vi } from "vitest";
import { axe } from "vitest-axe";
import classifiedRoadsResponseMock from "./mocks/classifiedRoadsResponseMock";
import digitalLandResponseMock from "./mocks/digitalLandResponseMock";
+import { breadcrumbsWithoutUSRN, simpleBreadcrumbs, simpleFlow } from "./mocks/simpleFlow";
import PlanningConstraints from "./Public";
+const { setState } = useStore;
+
+beforeEach(() => vi.clearAllMocks());
+
+const swrMock = (swr as jest.Mock).mock;
+
vi.mock("swr", () => ({
default: vi.fn((url: () => string) => {
const isGISRequest = url()?.startsWith(
- `${import.meta.env.VITE_APP_API_URL}/gis/`,
+ `${import.meta.env.VITE_APP_API_URL}/gis`,
);
const isRoadsRequest = url()?.startsWith(
- `${import.meta.env.VITE_APP_API_URL}/roads/`,
+ `${import.meta.env.VITE_APP_API_URL}/roads`,
);
if (isGISRequest) return { data: digitalLandResponseMock };
@@ -27,8 +37,6 @@ vi.mock("swr", () => ({
describe("error state", () => {
it("renders an error if no addres is present in the passport", async () => {
- const handleSubmit = vi.fn();
-
const { getByRole, getByTestId } = setup(
{
description="Things that might affect your project"
fn="property.constraints.planning"
disclaimer="This page does not include information about historic planning conditions that may apply to this property."
- handleSubmit={handleSubmit}
+ handleSubmit={vi.fn()}
/>
- ,
,
);
@@ -55,7 +62,6 @@ describe("error state", () => {
fn="property.constraints.planning"
disclaimer="This page does not include information about historic planning conditions that may apply to this property."
/>
- ,
,
);
const results = await axe(container);
@@ -63,10 +69,168 @@ describe("error state", () => {
});
});
-it.todo("renders correctly");
+describe("following a FindProperty component", () => {
+ beforeEach(() => {
+ act(() =>
+ setState({
+ breadcrumbs: simpleBreadcrumbs,
+ flow: simpleFlow,
+ teamIntegrations: {
+ hasPlanningData: true,
+ },
+ }),
+ );
+ });
+
+ it("renders correctly", async () => {
+ const handleSubmit = vi.fn();
+
+ const { user, getByRole, getByTestId } = setup(
+ ,
+ );
+
+ expect(
+ getByRole("heading", { name: "Planning constraints" }),
+ ).toBeInTheDocument();
+
+ await user.click(getByTestId("continue-button"));
-it.todo("should not have any accessibility violations");
+ expect(handleSubmit).toHaveBeenCalled();
+ });
-it.todo("fetches classified roads only when we have a siteBoundary"); // using expect(spy).toHaveBeenCalled() ??
+ it("should not have any accessibility violations", async () => {
+ const { container } = setup(
+ ,
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+
+ it("fetches planning constraints when we have lng, lat or siteBoundary", async () => {
+ setup(
+ ,
+ );
+
+ expect(swr).toHaveBeenCalled();
+
+ // Planning data is called first
+ const swrURL = swrMock.calls[0][0]();
+ const swrResponse = swrMock.results[0].value;
+
+ expect(swrURL).toContain("/gis");
+ expect(swrResponse).toEqual({ data: digitalLandResponseMock });
+ });
-it.todo("fetches planning constraints when we have lng,lat or siteBoundary");
+ it("fetches classified roads when a USRN is provided", () => {
+ setup(
+ ,
+ );
+
+ expect(swr).toHaveBeenCalled();
+
+ // Classified roads are called second
+ const swrURL = swrMock.calls[1][0]();
+ const swrResponse = swrMock.results[1].value;
+
+ expect(swrURL).toContain("/roads");
+ expect(swrResponse).toEqual({ data: classifiedRoadsResponseMock });
+ });
+
+ it("does not fetch classified roads when a USRN is not provided", async () => {
+ act(() =>
+ setState({
+ breadcrumbs: breadcrumbsWithoutUSRN,
+ flow: simpleFlow,
+ teamIntegrations: {
+ hasPlanningData: true,
+ },
+ })
+ );
+
+ setup(
+ ,
+ );
+
+ expect(swr).toHaveBeenCalled();
+
+ // Planning constraints API still called
+ const planingConstraintsURL = swrMock.calls[0][0]();
+ const planingConstraintsResponse = swrMock.results[0].value;
+
+ expect(planingConstraintsURL).toContain("/gis");
+ expect(planingConstraintsResponse).toEqual({ data: digitalLandResponseMock });
+
+ // Classified roads API not called due to missing USRN
+ const swrURL = swrMock.calls[1][0]();
+ const swrResponse = swrMock.results[1].value;
+
+ expect(swrURL).toBeNull();
+ expect(swrResponse).toEqual({ data: null });
+ });
+
+ test("basic layout and interactions", async () => {
+ const { user, getByRole, queryByRole, getByTestId } = setup(
+ ,
+ );
+
+ // Positive constraints visible by default
+ expect(
+ getByRole("heading", { name: /These are the planning constraints/ }),
+ ).toBeVisible();
+ expect(getByRole("button", { name: /Parks and gardens/ })).toBeVisible();
+
+ // Negative constraints hidden by default
+ const showNegativeConstraintsButton = getByRole("button", {
+ name: /Constraints that don't apply/,
+ });
+ expect(showNegativeConstraintsButton).toBeVisible();
+
+ const negativeConstraintsContainer = getByTestId(
+ "negative-constraints-list",
+ );
+ expect(negativeConstraintsContainer).not.toBeVisible();
+
+ expect(queryByRole("heading", { name: /Ecology/ })).not.toBeInTheDocument();
+
+ // Negative constraints viewable on toggle
+ await user.click(showNegativeConstraintsButton);
+
+ expect(negativeConstraintsContainer).toBeVisible();
+ expect(getByRole("heading", { name: /Ecology/ })).toBeVisible();
+ });
+});
\ No newline at end of file
diff --git a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx
index 513961d632..2cfcf8fbbd 100644
--- a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx
+++ b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx
@@ -302,6 +302,7 @@ export function PlanningConstraintsContent(
{negativeConstraints.length > 0 && (
This page does not include information about historic planning conditions that may apply to this property.
",
+ },
+ },
+};
+
+export const simpleBreadcrumbs: Store.Breadcrumbs = {
+ findProperty: {
+ auto: false,
+ data: {
+ _address: {
+ uprn: "100071417680",
+ usrn: "2702440",
+ blpu_code: "2",
+ latitude: 52.4804358,
+ longitude: -1.9034539,
+ organisation: null,
+ sao: "",
+ saoEnd: "",
+ pao: "COUNCIL HOUSE",
+ paoEnd: "",
+ street: "VICTORIA SQUARE",
+ town: "BIRMINGHAM",
+ postcode: "B1 1BB",
+ ward: "E05011151",
+ x: 406653.64,
+ y: 286948.41,
+ planx_description: "Local Government Service",
+ planx_value: "commercial.office.workspace.gov.local",
+ single_line_address:
+ "COUNCIL HOUSE, VICTORIA SQUARE, BIRMINGHAM, B1 1BB",
+ title: "COUNCIL HOUSE, VICTORIA SQUARE",
+ source: "os",
+ },
+ "property.type": ["commercial.office.workspace.gov.local"],
+ "property.localAuthorityDistrict": ["Birmingham"],
+ "property.region": ["West Midlands"],
+ "property.boundary.title": {
+ geometry: {
+ type: "MultiPolygon",
+ coordinates: [
+ [
+ [
+ [-1.903955, 52.480237],
+ [-1.903881, 52.480179],
+ [-1.903955, 52.480237],
+ ],
+ ],
+ ],
+ },
+ type: "Feature",
+ properties: {
+ "entry-date": "2024-05-06",
+ "start-date": "2021-03-25",
+ "end-date": "",
+ entity: 12001049997,
+ name: "",
+ dataset: "title-boundary",
+ typology: "geography",
+ reference: "61385289",
+ prefix: "title-boundary",
+ "organisation-entity": "13",
+ },
+ },
+ "property.boundary.title.area": 8242.37,
+ "property.boundary.title.area.hectares": 0.8242370000000001,
+ "findProperty.action": "Selected an existing address",
+ },
+ },
+};
+
+export const breadcrumbsWithoutUSRN = merge(cloneDeep(simpleBreadcrumbs), { findProperty: { data: { _address: { usrn: null }}}});
\ No newline at end of file
diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx
index beb2475701..2bc82c9396 100644
--- a/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx
+++ b/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx
@@ -48,7 +48,7 @@ const SimpleExpand: React.FC> = ({
/>
-
+
{children}
>
From 0c2dd87bf5cabe160a464bf815e9f51248e927bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?=
Date: Wed, 18 Sep 2024 12:29:20 +0100
Subject: [PATCH 06/10] chore(page): Drop `"PAGE"` feature flag (#3682)
---
editor.planx.uk/src/lib/featureFlags.ts | 2 +-
.../src/pages/FlowEditor/components/forms/FormModal.tsx | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/editor.planx.uk/src/lib/featureFlags.ts b/editor.planx.uk/src/lib/featureFlags.ts
index e065d941b1..b1e478c760 100644
--- a/editor.planx.uk/src/lib/featureFlags.ts
+++ b/editor.planx.uk/src/lib/featureFlags.ts
@@ -1,5 +1,5 @@
// add/edit/remove feature flags in array below
-const AVAILABLE_FEATURE_FLAGS = ["SEARCH", "ADD_NEW_EDITOR", "PAGE"] as const;
+const AVAILABLE_FEATURE_FLAGS = ["SEARCH", "ADD_NEW_EDITOR"] as const;
type FeatureFlag = (typeof AVAILABLE_FEATURE_FLAGS)[number];
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx b/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx
index a937b8d259..91ccc8e191 100644
--- a/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx
+++ b/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx
@@ -10,7 +10,6 @@ import { styled } from "@mui/material/styles";
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import { parseFormValues } from "@planx/components/shared";
import ErrorFallback from "components/Error/ErrorFallback";
-import { hasFeatureFlag } from "lib/featureFlags";
import React from "react";
import { ErrorBoundary } from "react-error-boundary";
import { useNavigation } from "react-navi";
@@ -62,7 +61,7 @@ const NodeTypeSelect: React.FC<{
- {hasFeatureFlag("PAGE") && }
+