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