Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: can toggle feedback required and show validation errors #3939

Merged
merged 6 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 provide a feedback score",
"Enter your feedback",
];
errorMessages.map((error) => {
expect(screen.getByText(error)).toBeVisible();
});
});
});
Loading
Loading