diff --git a/editor.planx.uk/src/@planx/components/Feedback/Editor/Editor.test.tsx b/editor.planx.uk/src/@planx/components/Feedback/Editor/Editor.test.tsx
index e005ed1193..dfad5c904e 100644
--- a/editor.planx.uk/src/@planx/components/Feedback/Editor/Editor.test.tsx
+++ b/editor.planx.uk/src/@planx/components/Feedback/Editor/Editor.test.tsx
@@ -7,12 +7,19 @@ import { setup } from "testUtils";
import { FeedbackEditor } from "./Editor";
describe("When the Feedback editor modal is rendered", () => {
- it("does not throw an error", () => {
+ beforeEach(() => {
setup(
,
);
+ });
+ it("does not throw an error", () => {
expect(screen.getByText("Feedback")).toBeInTheDocument();
});
+ it("displays the default title if no edits are made", () => {
+ expect(
+ screen.getByPlaceholderText("Tell us what you think"),
+ ).toBeInTheDocument();
+ });
});
diff --git a/editor.planx.uk/src/@planx/components/Feedback/Public/Feedback.stories.tsx b/editor.planx.uk/src/@planx/components/Feedback/Public/Feedback.stories.tsx
index e8f0b3d8bb..2bc537e27f 100644
--- a/editor.planx.uk/src/@planx/components/Feedback/Public/Feedback.stories.tsx
+++ b/editor.planx.uk/src/@planx/components/Feedback/Public/Feedback.stories.tsx
@@ -15,3 +15,7 @@ export default meta;
export const Basic = {
args: defaultContent,
} satisfies Story;
+
+export const WithFeedbackRequired = {
+ args: { ...defaultContent, feedbackRequired: true },
+} satisfies Story;
diff --git a/editor.planx.uk/src/@planx/components/Feedback/Public/Public.test.tsx b/editor.planx.uk/src/@planx/components/Feedback/Public/Public.test.tsx
deleted file mode 100644
index 4f50a17f86..0000000000
--- a/editor.planx.uk/src/@planx/components/Feedback/Public/Public.test.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { screen } from "@testing-library/react";
-import {
- getInternalFeedbackMetadata,
- insertFeedbackMutation,
-} from "lib/feedback";
-import React from "react";
-import { setup } from "testUtils";
-import { vi } from "vitest";
-import { axe } from "vitest-axe";
-
-import FeedbackComponent from "./Public";
-
-const handleSubmit = vi.fn();
-vi.mock("lib/feedback", () => ({
- getInternalFeedbackMetadata: vi.fn(),
- insertFeedbackMutation: vi.fn(),
-}));
-
-describe("when the Feedback component is rendered", async () => {
- it("should not have any accessibility violations", async () => {
- const { container } = setup(
- ,
- );
- const results = await axe(container);
- expect(results).toHaveNoViolations();
- });
- it("should call handleSubmit when the continue button is pressed", async () => {
- const { user } = setup(
- ,
- );
-
- await user.click(screen.getByTestId("feedback-button-terrible"));
- await user.click(screen.getByTestId("continue-button"));
-
- expect(getInternalFeedbackMetadata).toBeCalled();
- expect(insertFeedbackMutation).toBeCalled();
- });
-});
diff --git a/editor.planx.uk/src/@planx/components/Feedback/Public/Public.tsx b/editor.planx.uk/src/@planx/components/Feedback/Public/Public.tsx
index f5d64d39d7..975455b3c7 100644
--- a/editor.planx.uk/src/@planx/components/Feedback/Public/Public.tsx
+++ b/editor.planx.uk/src/@planx/components/Feedback/Public/Public.tsx
@@ -17,17 +17,20 @@ import NeutralFace from "ui/images/feedback_filled-03.svg";
import GoodFace from "ui/images/feedback_filled-04.svg";
import ExcellentFace from "ui/images/feedback_filled-05.svg";
import InputLabel from "ui/public/InputLabel";
+import ErrorWrapper from "ui/shared/ErrorWrapper";
import Input from "ui/shared/Input/Input";
import ReactMarkdownOrHtml from "ui/shared/ReactMarkdownOrHtml/ReactMarkdownOrHtml";
import { getPreviouslySubmittedData, makeData } from "../../shared/utils";
import { FaceBox } from "../components/FaceBox";
-import { Feedback, FormProps } from "../model";
+import { createFeedbackSchema, Feedback, FormProps } from "../model";
import { StyledToggleButtonGroup } from "../styled";
export const PASSPORT_FEEDBACK_KEY = "_feedback";
const FeedbackComponent = (props: PublicProps): FCReturn => {
+ const feedbackDataSchema = createFeedbackSchema(props.feedbackRequired);
+
const handleSubmitFeedback = async (values: FormProps) => {
const metadata = await getInternalFeedbackMetadata();
const data = {
@@ -52,6 +55,9 @@ const FeedbackComponent = (props: PublicProps): FCReturn => {
userComment: "",
},
onSubmit: handleSubmitFeedback,
+ validateOnBlur: false,
+ validateOnChange: false,
+ validationSchema: feedbackDataSchema,
});
const handleFeedbackChange = (
@@ -88,52 +94,54 @@ const FeedbackComponent = (props: PublicProps): FCReturn => {
}
/>
)}
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
{props.freeformQuestion && (
): FCReturn => {
bordered
onChange={formik.handleChange}
aria-label="user comment"
+ data-testid="user-comment"
+ errorMessage={formik.errors.userComment}
/>
{props.disclaimer && }
diff --git a/editor.planx.uk/src/@planx/components/Feedback/Public/tests/Public.accessibility.test.tsx b/editor.planx.uk/src/@planx/components/Feedback/Public/tests/Public.accessibility.test.tsx
new file mode 100644
index 0000000000..1501cc1f5e
--- /dev/null
+++ b/editor.planx.uk/src/@planx/components/Feedback/Public/tests/Public.accessibility.test.tsx
@@ -0,0 +1,56 @@
+import { screen } from "@testing-library/react";
+import {
+ getInternalFeedbackMetadata,
+ insertFeedbackMutation,
+} from "lib/feedback";
+import React from "react";
+import { setup } from "testUtils";
+import { vi } from "vitest";
+import { axe } from "vitest-axe";
+
+import FeedbackComponent from "../Public";
+
+vi.mock("lib/feedback", () => ({
+ getInternalFeedbackMetadata: vi.fn(),
+ insertFeedbackMutation: vi.fn(),
+}));
+
+describe("when the Feedback component is rendered", async () => {
+ it("should not have any accessibility violations", async () => {
+ const { container } = setup();
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+ it("should be navigable by keyboard", async () => {
+ const { user } = setup();
+
+ const ratingButtons = screen.getAllByRole("button");
+
+ // user tabs through all rating buttons and selects the last one
+ await user.tab();
+ expect(ratingButtons[0]).toHaveFocus();
+ await user.tab();
+ expect(ratingButtons[1]).toHaveFocus();
+ await user.tab();
+ expect(ratingButtons[2]).toHaveFocus();
+ await user.tab();
+ expect(ratingButtons[3]).toHaveFocus();
+ await user.tab();
+ expect(ratingButtons[4]).toHaveFocus();
+ await user.keyboard("[Space]"); // select 'Excellent' button
+
+ await user.tab();
+ expect(screen.getByRole("textbox")).toHaveFocus();
+
+ await user.tab();
+ expect(screen.getByTestId("continue-button")).toHaveFocus();
+ await user.keyboard("[Space]"); // submits
+
+ expect(getInternalFeedbackMetadata).toBeCalled();
+ expect(insertFeedbackMutation).toBeCalledWith({
+ feedbackScore: 5,
+ feedbackType: "component",
+ userComment: "",
+ });
+ });
+});
diff --git a/editor.planx.uk/src/@planx/components/Feedback/Public/tests/Public.submit.test.tsx b/editor.planx.uk/src/@planx/components/Feedback/Public/tests/Public.submit.test.tsx
new file mode 100644
index 0000000000..abbf2b084f
--- /dev/null
+++ b/editor.planx.uk/src/@planx/components/Feedback/Public/tests/Public.submit.test.tsx
@@ -0,0 +1,97 @@
+import { screen } from "@testing-library/react";
+import {
+ getInternalFeedbackMetadata,
+ insertFeedbackMutation,
+} from "lib/feedback";
+import React from "react";
+import { setup } from "testUtils";
+import { vi } from "vitest";
+
+import FeedbackComponent from "../Public";
+
+const handleSubmit = vi.fn();
+vi.mock("lib/feedback", () => ({
+ getInternalFeedbackMetadata: vi.fn(),
+ insertFeedbackMutation: vi.fn(),
+}));
+
+describe.each([
+ {
+ dataType: "a rating",
+ expectedData: {
+ feedbackScore: 1,
+ feedbackType: "component",
+ userComment: "",
+ },
+ },
+ {
+ dataType: "a comment",
+ expectedData: {
+ feedbackScore: "",
+ feedbackType: "component",
+ userComment: "I had a great time",
+ },
+ },
+ {
+ dataType: "nothing",
+ expectedData: {
+ feedbackScore: "",
+ feedbackType: "component",
+ userComment: "",
+ },
+ },
+])(
+ "when a user submits $dataType on a feedback component where feedback is not required",
+ ({ dataType, expectedData }) => {
+ beforeEach(async () => {
+ const { user } = setup(
+ ,
+ );
+
+ switch (dataType) {
+ case "a rating":
+ await user.click(screen.getByTestId("feedback-button-terrible"));
+
+ break;
+ case "a comment":
+ await user.type(
+ screen.getByTestId("user-comment"),
+ "I had a great time",
+ );
+ break;
+ case "nothing":
+ default:
+ break;
+ }
+
+ await user.click(screen.getByTestId("continue-button"));
+ });
+
+ it("should call the handleSubmit function with the correct data", async () => {
+ expect(getInternalFeedbackMetadata).toBeCalled();
+ expect(insertFeedbackMutation).toBeCalledWith(expectedData);
+ });
+ },
+);
+
+describe("when feedback is required but the user does not submit any data", async () => {
+ beforeEach(async () => {
+ const { user } = setup(
+ ,
+ );
+ await user.click(screen.getByTestId("continue-button"));
+ });
+
+ it("displays an appropriate error message for each missing field", async () => {
+ const errorMessages = [
+ "Please rate your experience",
+ "Enter your feedback",
+ ];
+ errorMessages.map((error) => {
+ expect(screen.getByText(error)).toBeVisible();
+ });
+ });
+});
diff --git a/editor.planx.uk/src/@planx/components/Feedback/model.ts b/editor.planx.uk/src/@planx/components/Feedback/model.ts
index ee1b1cd94f..daeaa81124 100644
--- a/editor.planx.uk/src/@planx/components/Feedback/model.ts
+++ b/editor.planx.uk/src/@planx/components/Feedback/model.ts
@@ -1,5 +1,8 @@
+import { number, object, string } from "yup";
+
import { BaseNodeData, parseBaseNodeData } from "../shared";
import { defaultContent } from "./components/defaultContent";
+
export interface Feedback extends BaseNodeData {
title?: string;
description?: string;
@@ -24,3 +27,14 @@ export const parseFeedback = (
feedbackRequired: data?.feedbackRequired || defaultContent.feedbackRequired,
...parseBaseNodeData(data),
});
+
+export const createFeedbackSchema = (feedbackRequired: boolean) => {
+ return object().shape({
+ userComment: feedbackRequired
+ ? string().required("Enter your feedback")
+ : string(),
+ feedbackScore: feedbackRequired
+ ? number().integer().required("Please rate your experience")
+ : number().integer(),
+ });
+};