Skip to content

Commit

Permalink
feat: can toggle feedback required and show validation errors (#3939)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamdelion authored Nov 12, 2024
1 parent 48d795d commit 2c8eaf3
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<DndProvider backend={HTML5Backend}>
<FeedbackEditor id="test-feedback-editor" />
</DndProvider>,
);
});
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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export default meta;
export const Basic = {
args: defaultContent,
} satisfies Story;

export const WithFeedbackRequired = {
args: { ...defaultContent, feedbackRequired: true },
} satisfies Story;

This file was deleted.

102 changes: 56 additions & 46 deletions editor.planx.uk/src/@planx/components/Feedback/Public/Public.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Feedback>): FCReturn => {
const feedbackDataSchema = createFeedbackSchema(props.feedbackRequired);

const handleSubmitFeedback = async (values: FormProps) => {
const metadata = await getInternalFeedbackMetadata();
const data = {
Expand All @@ -52,6 +55,9 @@ const FeedbackComponent = (props: PublicProps<Feedback>): FCReturn => {
userComment: "",
},
onSubmit: handleSubmitFeedback,
validateOnBlur: false,
validateOnChange: false,
validationSchema: feedbackDataSchema,
});

const handleFeedbackChange = (
Expand Down Expand Up @@ -88,52 +94,54 @@ const FeedbackComponent = (props: PublicProps<Feedback>): FCReturn => {
}
/>
)}
<StyledToggleButtonGroup
value={formik.values.feedbackScore}
exclusive
id="feedbackButtonGroup"
onChange={handleFeedbackChange}
aria-label="feedback score"
>
<Grid
container
columnSpacing={2}
component="fieldset"
direction={{ xs: "column", formWrap: "row" }}
<ErrorWrapper error={formik.errors.feedbackScore}>
<StyledToggleButtonGroup
value={formik.values.feedbackScore}
exclusive
id="feedbackButtonGroup"
onChange={handleFeedbackChange}
aria-label="feedback score"
>
<FaceBox
value={1}
testId="feedback-button-terrible"
icon={TerribleFace}
label="Terrible"
altText="very unhappy face"
/>
<FaceBox
value={2}
icon={PoorFace}
label="Poor"
altText="slightly unhappy face"
/>
<FaceBox
value={3}
icon={NeutralFace}
label="Neutral"
altText="neutral face"
/>
<FaceBox
value={4}
icon={GoodFace}
label="Good"
altText="smiling face"
/>
<FaceBox
value={5}
icon={ExcellentFace}
label="Excellent"
altText="very happy face"
/>
</Grid>
</StyledToggleButtonGroup>
<Grid
container
columnSpacing={2}
component="fieldset"
direction={{ xs: "column", formWrap: "row" }}
>
<FaceBox
value={1}
testId="feedback-button-terrible"
icon={TerribleFace}
label="Terrible"
altText="very unhappy face"
/>
<FaceBox
value={2}
icon={PoorFace}
label="Poor"
altText="slightly unhappy face"
/>
<FaceBox
value={3}
icon={NeutralFace}
label="Neutral"
altText="neutral face"
/>
<FaceBox
value={4}
icon={GoodFace}
label="Good"
altText="smiling face"
/>
<FaceBox
value={5}
icon={ExcellentFace}
label="Excellent"
altText="very happy face"
/>
</Grid>
</StyledToggleButtonGroup>
</ErrorWrapper>
{props.freeformQuestion && (
<InputLabel
label={
Expand All @@ -153,6 +161,8 @@ const FeedbackComponent = (props: PublicProps<Feedback>): FCReturn => {
bordered
onChange={formik.handleChange}
aria-label="user comment"
data-testid="user-comment"
errorMessage={formik.errors.userComment}
/>
</Box>
{props.disclaimer && <Disclaimer text={props.disclaimer} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<FeedbackComponent feedbackRequired={false} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("should be navigable by keyboard", async () => {
const { user } = setup(<FeedbackComponent feedbackRequired={false} />);

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: "",
});
});
});
Original file line number Diff line number Diff line change
@@ -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(
<FeedbackComponent
handleSubmit={handleSubmit}
feedbackRequired={false}
/>,
);

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(
<FeedbackComponent handleSubmit={handleSubmit} feedbackRequired={true} />,
);
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();
});
});
});
14 changes: 14 additions & 0 deletions editor.planx.uk/src/@planx/components/Feedback/model.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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(),
});
};

0 comments on commit 2c8eaf3

Please sign in to comment.