Introduce new page for rating feedback (#12581)

This commit is contained in:
William Durand 2023-11-13 17:26:34 +01:00 коммит произвёл GitHub
Родитель dfdfa1b49e
Коммит 940f7ce1eb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 784 добавлений и 39 удалений

Просмотреть файл

@ -257,34 +257,47 @@ export class FeedbackFormBase extends React.Component<InternalProps, State> {
return ['illegal'].includes(this.state.category);
}
renderCategories(categories: Array<Reason>): React.Node {
return categories
.filter((category) => this.props.categories.includes(category.value))
.map((category) => (
<li
className="FeedbackForm-checkbox-wrapper"
key={`FeedbackForm-category-${category.value}`}
>
<input
type="radio"
className="FeedbackForm-catgeory"
id={`feedbackCategory${category.value}`}
name="category"
onChange={this.onFieldChange}
value={category.value}
selected={this.state.category === category.value}
/>
<label
className="FeedbackForm-label"
htmlFor={`feedbackCategory${category.value}`}
>
{category.label}
</label>
{category.help && (
<p className="FeedbackForm--help">{category.help}</p>
)}
</li>
));
renderCategories(title: string, categories: Array<Reason>): React.Node {
const filteredCategories = categories.filter((category) =>
this.props.categories.includes(category.value),
);
if (!filteredCategories.length) {
return null;
}
return (
<>
<h3>{title}</h3>
<ul>
{filteredCategories.map((category) => (
<li
className="FeedbackForm-checkbox-wrapper"
key={`FeedbackForm-category-${category.value}`}
>
<input
type="radio"
className="FeedbackForm-catgeory"
id={`feedbackCategory${category.value}`}
name="category"
onChange={this.onFieldChange}
value={category.value}
selected={this.state.category === category.value}
/>
<label
className="FeedbackForm-label"
htmlFor={`feedbackCategory${category.value}`}
>
{category.label}
</label>
{category.help && (
<p className="FeedbackForm--help">{category.help}</p>
)}
</li>
))}
</ul>
</>
);
}
render(): React.Node {
@ -326,11 +339,8 @@ export class FeedbackFormBase extends React.Component<InternalProps, State> {
<div className="FeedbackForm-form-messages">{errorMessage}</div>
<Card className="FeedbackForm--Card" header={categoryHeader}>
<h3>{feedbackTitle}</h3>
<ul>{this.renderCategories(feedback)}</ul>
<h3>{reportTitle}</h3>
<ul>{this.renderCategories(report)}</ul>
{this.renderCategories(feedbackTitle, feedback)}
{this.renderCategories(reportTitle, report)}
</Card>
<Card

Просмотреть файл

@ -3,12 +3,14 @@ import invariant from 'invariant';
import * as React from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
import config from 'config';
import {
REVIEW_FLAG_REASON_BUG_SUPPORT,
REVIEW_FLAG_REASON_LANGUAGE,
REVIEW_FLAG_REASON_SPAM,
} from 'amo/constants';
import Link from 'amo/components/Link';
import FlagReview from 'amo/components/FlagReview';
import AuthenticateButton from 'amo/components/AuthenticateButton';
import { getCurrentUser } from 'amo/reducers/users';
@ -91,12 +93,20 @@ export class FlagReviewMenuBase extends React.Component<InternalProps> {
className="FlagReviewMenu-flag-language-item"
key="flag-language"
>
<FlagReview
reason={REVIEW_FLAG_REASON_LANGUAGE}
review={review}
buttonText={i18n.gettext('This contains inappropriate language')}
wasFlaggedText={i18n.gettext('Flagged for inappropriate language')}
/>
{config.get('enableFeatureFeedbackFormLinks') ? (
<Link to={`/feedback/review/${review.id}/`}>
{i18n.gettext('This contains inappropriate language')}
</Link>
) : (
<FlagReview
reason={REVIEW_FLAG_REASON_LANGUAGE}
review={review}
buttonText={i18n.gettext('This contains inappropriate language')}
wasFlaggedText={i18n.gettext(
'Flagged for inappropriate language',
)}
/>
)}
</ListItem>,
// Only reviews (not developer responses) can be flagged as
// misplaced bug reports or support requests.

Просмотреть файл

@ -18,6 +18,7 @@ import CollectionEdit from 'amo/pages/CollectionEdit';
import CollectionList from 'amo/pages/CollectionList';
import AddonFeedback from 'amo/pages/AddonFeedback';
import UserFeedback from 'amo/pages/UserFeedback';
import RatingFeedback from 'amo/pages/RatingFeedback';
import NotAuthorizedPage from 'amo/pages/ErrorPages/NotAuthorizedPage';
import UnavailableForLegalReasonsPage from 'amo/pages/ErrorPages/UnavailableForLegalReasonsPage';
import NotFoundPage from 'amo/pages/ErrorPages/NotFoundPage';
@ -189,6 +190,12 @@ const Routes = ({ _config = config }: Props = {}): React.Node => (
path="/:lang/:application(firefox|android)/feedback/user/:userId/"
component={UserFeedback}
/>,
<Route
key="rating-feedback"
exact
path="/:lang/:application(firefox|android)/feedback/review/:ratingId/"
component={RatingFeedback}
/>,
]}
{/* See: https://github.com/mozilla/addons-frontend/issues/5150 */}

Просмотреть файл

@ -0,0 +1,210 @@
/* @flow */
import * as React from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { Helmet } from 'react-helmet';
import invariant from 'invariant';
import { fetchReview, sendRatingAbuseReport } from 'amo/actions/reviews';
import { selectReview } from 'amo/reducers/reviews';
import FeedbackForm, {
CATEGORY_HATEFUL_VIOLENT_DECEPTIVE,
CATEGORY_ILLEGAL,
CATEGORY_OTHER,
} from 'amo/components/FeedbackForm';
import LoadingText from 'amo/components/LoadingText';
import Card from 'amo/components/Card';
import UserReview from 'amo/components/UserReview';
import log from 'amo/logger';
import translate from 'amo/i18n/translate';
import NotFoundPage from 'amo/pages/ErrorPages/NotFoundPage';
import Page from 'amo/components/Page';
import { withFixedErrorHandler } from 'amo/errorHandler';
import fallbackIcon from 'amo/img/icons/default.svg';
import type { AppState } from 'amo/store';
import type { ErrorHandlerType } from 'amo/types/errorHandler';
import type { DispatchFunc } from 'amo/types/redux';
import type { ReactRouterMatchType } from 'amo/types/router';
import type { I18nType } from 'amo/types/i18n';
import type { FeedbackFormValues } from 'amo/components/FeedbackForm';
import type { UserReviewType } from 'amo/actions/reviews';
import './styles.scss';
type Props = {|
match: {|
...ReactRouterMatchType,
params: {| ratingId: number |},
|},
|};
type PropsFromState = {|
review: UserReviewType | null,
reviewIsLoading: boolean,
hasSubmitted: boolean,
isSubmitting: boolean,
|};
type InternalProps = {|
...Props,
...PropsFromState,
i18n: I18nType,
dispatch: DispatchFunc,
errorHandler: ErrorHandlerType,
|};
export class RatingFeedbackBase extends React.Component<InternalProps> {
constructor(props: InternalProps) {
super(props);
const { dispatch, errorHandler, match, review, reviewIsLoading } = props;
const { params } = match;
if (errorHandler.hasError()) {
log.warn('Not loading data because of an error.');
}
if (!review && !reviewIsLoading) {
dispatch(
fetchReview({
reviewId: params.ratingId,
errorHandlerId: errorHandler.id,
}),
);
}
}
onFormSubmitted: (values: FeedbackFormValues) => void = (values) => {
const { dispatch, errorHandler, review } = this.props;
const { anonymous, email, name, text, category } = values;
invariant(review, 'review is required');
dispatch(
sendRatingAbuseReport({
// Only authenticate the API call when the report isn't submitted
// anonymously.
auth: anonymous === false,
errorHandlerId: errorHandler.id,
message: text,
ratingId: review.id,
reason: category,
reporterEmail: anonymous ? '' : email,
reporterName: anonymous ? '' : name,
}),
);
};
render(): React.Node {
const { errorHandler, i18n, review, isSubmitting, hasSubmitted } =
this.props;
if (
errorHandler.hasError() &&
errorHandler.capturedError.responseStatusCode === 404
) {
return <NotFoundPage />;
}
return (
<Page>
<div className="RatingFeedback-page">
<Helmet>
<title>
{i18n.gettext('Submit feedback or report a review to Mozilla')}
</title>
<meta name="robots" content="noindex, follow" />
</Helmet>
<FeedbackForm
errorHandler={errorHandler}
contentHeader={
<Card className="RatingFeedback-header">
<div className="RatingFeedback-header-icon">
<div className="RatingFeedback-header-icon-wrapper">
<img
className="RatingFeedback-header-icon-image"
src={review?.reviewAddon.iconUrl || fallbackIcon}
alt=""
/>
</div>
</div>
<h1 className="RatingFeedback-header-title">
{review ? review.reviewAddon.name : <LoadingText />}
</h1>
<UserReview
review={review}
// Even if a review is a (developer) reply, we do not want to
// show the special UI for it on the rating feedback form
// page.
isReply={false}
byLine={
review ? (
i18n.sprintf(
i18n.gettext('by %(userName)s, %(timestamp)s'),
{
userName: review.userName,
timestamp: i18n.moment(review.created).fromNow(),
},
)
) : (
<LoadingText />
)
}
showRating
/>
</Card>
}
abuseIsLoading={isSubmitting}
abuseSubmitted={hasSubmitted}
categoryHeader={i18n.gettext('Report this review to Mozilla')}
// This title isn't used because we didn't select any of the
// "feedback" categories.
feedbackTitle=""
reportTitle={i18n.gettext(
'Report the review because it is illegal or incompliant',
)}
categories={[
CATEGORY_HATEFUL_VIOLENT_DECEPTIVE,
CATEGORY_ILLEGAL,
CATEGORY_OTHER,
]}
showLocation={false}
onSubmit={this.onFormSubmitted}
/>
</div>
</Page>
);
}
}
function mapStateToProps(
state: AppState,
ownProps: InternalProps,
): PropsFromState {
const { ratingId } = ownProps.match.params;
const review = selectReview(state.reviews, ratingId) || null;
const view = state.reviews.view[ratingId];
const reviewIsLoading = view?.loadingReview;
const { inProgress: isSubmitting, wasFlagged: hasSubmitted } =
view?.flag || {};
return {
review,
reviewIsLoading,
isSubmitting,
hasSubmitted,
};
}
export const extractId = (ownProps: InternalProps): string => {
return String(ownProps.match.params.ratingId);
};
const RatingFeedback: React.ComponentType<Props> = compose(
translate(),
connect(mapStateToProps),
withFixedErrorHandler({ fileName: __filename, extractId }),
)(RatingFeedbackBase);
export default RatingFeedback;

Просмотреть файл

@ -0,0 +1,50 @@
@import '~amo/css/styles';
$icon-size: 16px;
.RatingFeedback-page {
@include page-padding;
}
.RatingFeedback-header > .Card-contents {
align-items: center;
display: grid;
grid-template-columns: ($icon-size + 8px) 1fr;
.RatingFeedback-header-icon {
grid-column: 1;
}
.RatingFeedback-header-icon-wrapper {
height: $icon-size;
width: $icon-size;
}
.RatingFeedback-header-icon-image {
height: auto;
width: 100%;
}
.RatingFeedback-header-title {
font-size: $font-size-default;
grid-column: 2;
margin: 0;
}
.UserReview {
grid-column: 1 / span 2;
grid-row: 2;
}
.UserReview-body {
margin-bottom: 0;
}
.Rating {
display: none;
@include respond-to(medium) {
display: inherit;
}
}
}

Просмотреть файл

@ -3,6 +3,7 @@ import { createEvent, fireEvent, waitFor } from '@testing-library/react';
import defaultUserEvent, {
PointerEventsCheckLevel,
} from '@testing-library/user-event';
import config from 'config';
import {
SAVED_RATING,
@ -42,11 +43,14 @@ import {
fakeAddon,
fakeI18n,
fakeReview,
getMockConfig,
render as defaultRender,
screen,
within,
} from 'tests/unit/helpers';
jest.mock('config');
describe(__filename, () => {
let i18n;
let store;
@ -68,6 +72,17 @@ describe(__filename, () => {
// pointer events not being available.
pointerEventsCheck: PointerEventsCheckLevel.Never,
});
const fakeConfig = getMockConfig({
enableFeatureFeedbackFormLinks: false,
});
config.get.mockImplementation((key) => {
return fakeConfig[key];
});
});
afterEach(() => {
jest.clearAllMocks().resetModules();
});
const render = ({
@ -1458,6 +1473,27 @@ describe(__filename, () => {
});
describe('Tests for FlagReviewMenu', () => {
it('changes the "inappropriate language" menu item to a link pointing to the rating feedback form when enableFeatureFeedbackFormLinks is set', async () => {
const fakeConfig = getMockConfig({
enableFeatureFeedbackFormLinks: true,
});
config.get.mockImplementation((key) => {
return fakeConfig[key];
});
const review = createReviewAndSignInAsUnrelatedUser();
render({ review });
await openFlagMenu();
expect(
// It would have to be a 'button' if `enableFeatureFeedbackFormLinks`
// was set to `false`.
screen.getByRole('link', {
name: 'This contains inappropriate language',
}),
).toBeInTheDocument();
});
it('can be configured with an openerClass', () => {
const review = createReviewAndSignInAsUnrelatedUser();
render({ review });

Просмотреть файл

@ -0,0 +1,422 @@
/* global window */
import config from 'config';
import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/react';
import {
fetchReview,
sendRatingAbuseReport,
setReview,
setReviewWasFlagged,
} from 'amo/actions/reviews';
import { CATEGORY_OTHER } from 'amo/components/FeedbackForm';
import { CLIENT_APP_FIREFOX } from 'amo/constants';
import { extractId } from 'amo/pages/RatingFeedback';
import { clearError } from 'amo/reducers/errors';
import { createApiError } from 'amo/api';
import {
createFailedErrorHandler,
createFakeErrorHandler,
createLocalizedString,
dispatchClientMetadata,
dispatchSignInActionsWithStore,
fakeReview,
getMockConfig,
renderPage as defaultRender,
screen,
} from 'tests/unit/helpers';
jest.mock('config');
describe(__filename, () => {
let fakeConfig;
beforeEach(() => {
fakeConfig = getMockConfig({ enableFeatureFeedbackForm: true });
config.get.mockImplementation((key) => {
return fakeConfig[key];
});
window.scroll = jest.fn();
});
afterEach(() => {
jest.clearAllMocks().resetModules();
});
const getErrorHandlerId = (addonId) =>
`src/amo/pages/RatingFeedback/index.js-${addonId}`;
const signInRatingWithProps = (
props = {},
store = dispatchClientMetadata().store,
) => {
const { id, ...userProps } = props;
return dispatchSignInActionsWithStore({ userId: id, userProps, store });
};
const renderWithoutLoading = ({
ratingId,
lang = 'en-US',
clientApp = CLIENT_APP_FIREFOX,
store = dispatchClientMetadata({ lang, clientApp }).store,
}) => {
const renderOptions = {
initialEntries: [`/${lang}/${clientApp}/feedback/review/${ratingId}/`],
store,
};
return defaultRender(renderOptions);
};
const render = (props = {}, store = dispatchClientMetadata().store) => {
const review = { ...fakeReview, ...props };
store.dispatch(setReview(review));
return renderWithoutLoading({ ratingId: review.id, store });
};
describe('error handling', () => {
it('renders errors', () => {
const ratingId = 1234;
const message = 'Some error message';
const { store } = dispatchClientMetadata();
createFailedErrorHandler({
id: getErrorHandlerId(ratingId),
message,
store,
});
render({ id: ratingId }, store);
expect(screen.getByText(message)).toBeInTheDocument();
// We do not call `scroll()` here because we mount the component and
// `componentDidUpdate()` is not called. It is valid because we only
// mount the component when the server processes the request OR the user
// navigates to the feedback form page and, in both cases, the scroll
// will be at the top of the page.
expect(window.scroll).not.toHaveBeenCalled();
});
it('scrolls to the top of the page when an error is rendered', async () => {
const ratingId = 1234;
const { store } = dispatchClientMetadata();
render({ id: ratingId }, store);
createFailedErrorHandler({ id: getErrorHandlerId(ratingId), store });
await waitFor(() => expect(window.scroll).toHaveBeenCalledWith(0, 0));
});
it('clears the error handler when unmounting', () => {
const ratingId = 1234;
const { store } = dispatchClientMetadata();
const dispatch = jest.spyOn(store, 'dispatch');
createFailedErrorHandler({ id: getErrorHandlerId(ratingId), store });
const { unmount } = render({ id: ratingId }, store);
unmount();
expect(dispatch).toHaveBeenCalledWith(
clearError(getErrorHandlerId(ratingId)),
);
});
describe('extractId', () => {
it('returns a unique ID based on params', () => {
const ratingId = 8;
expect(extractId({ match: { params: { ratingId } } })).toEqual('8');
});
});
});
it('renders a 404 page when enableFeatureFeedbackForm is false', () => {
fakeConfig = { ...fakeConfig, enableFeatureFeedbackForm: false };
render();
expect(
screen.getByText('Oops! We cant find that page'),
).toBeInTheDocument();
});
it('renders a 404 page when the API returned a 404', () => {
const ratingId = 1234;
const { store } = dispatchClientMetadata();
createFailedErrorHandler({
error: createApiError({
response: { status: 404 },
apiURL: 'https://some/api/endpoint',
jsonResponse: { message: 'not found' },
}),
id: getErrorHandlerId(ratingId),
store,
});
render({ id: ratingId }, store);
expect(
screen.getByText('Oops! We cant find that page'),
).toBeInTheDocument();
});
it('dispatches fetchReview when the review is not loaded yet', () => {
const ratingId = 1234;
const { store } = dispatchClientMetadata();
const dispatch = jest.spyOn(store, 'dispatch');
const errorHandler = createFakeErrorHandler({
id: getErrorHandlerId(ratingId),
});
renderWithoutLoading({ ratingId, store });
expect(dispatch).toHaveBeenCalledWith(
fetchReview({ errorHandlerId: errorHandler.id, reviewId: `${ratingId}` }),
);
});
it('renders the feedback form for a signed out user', () => {
const reviewBody = 'this is a review about an add-on';
const addonName = 'some add-on name';
const reviewAddon = {
...fakeReview.addon,
name: createLocalizedString(addonName),
};
const userName = 'some user name';
const reviewUser = { ...fakeReview.user, name: userName };
render({ body: reviewBody, addon: reviewAddon, user: reviewUser });
// Header.
expect(screen.getByText(addonName)).toBeInTheDocument();
expect(
screen.getByText(new RegExp(`^by ${userName}, `)),
).toBeInTheDocument();
expect(screen.getByText(reviewBody)).toBeInTheDocument();
expect(
screen.getByText(`Report this review to Mozilla`),
).toBeInTheDocument();
expect(
screen.getByText(
'Report the review because it is illegal or incompliant',
),
).toBeInTheDocument();
expect(screen.getByText('Submit report')).toBeInTheDocument();
expect(screen.getByLabelText('Your name(optional)')).not.toBeDisabled();
expect(screen.getByLabelText('Your name(optional)').value).toBeEmpty();
expect(
screen.getByLabelText('Your email address(optional)'),
).not.toBeDisabled();
expect(
screen.getByLabelText('Your email address(optional)').value,
).toBeEmpty();
// This should never be shown for reviews.
expect(
screen.queryByRole('combobox', {
name: 'Place of the violation (optional)',
}),
).not.toBeInTheDocument();
// We shouldn't show the confirmation message.
expect(
screen.queryByClassName('FeedbackForm-success-first-paragraph'),
).not.toBeInTheDocument();
});
it('renders the feedback form for a signed in user', () => {
const signedInRatingname = 'signed-in-username';
const signedInEmail = 'signed-in-email';
const store = signInRatingWithProps({
username: signedInRatingname,
email: signedInEmail,
});
const reviewBody = 'this is a review about an add-on';
render({ body: reviewBody }, store);
// Header.
expect(screen.getByText(reviewBody)).toBeInTheDocument();
expect(
screen.getByText(`Report this review to Mozilla`),
).toBeInTheDocument();
expect(
screen.getByText(
'Report the review because it is illegal or incompliant',
),
).toBeInTheDocument();
expect(screen.getByText('Submit report')).toBeInTheDocument();
const nameInput = screen.getByLabelText('Your name');
expect(nameInput).toBeDisabled();
expect(nameInput).toHaveValue(signedInRatingname);
const emailInput = screen.getByLabelText('Your email address');
expect(emailInput).toBeDisabled();
expect(emailInput).toHaveValue(signedInEmail);
// This should never be shown for reviews.
expect(
screen.queryByRole('combobox', {
name: 'Place of the violation (optional)',
}),
).not.toBeInTheDocument();
// SignedInRating component should be visible.
expect(
screen.getByText(`Signed in as ${signedInRatingname}`),
).toBeInTheDocument();
// We shouldn't show the confirmation message.
expect(
screen.queryByClassName('FeedbackForm-success-first-paragraph'),
).not.toBeInTheDocument();
});
it('renders the different categories for a user', () => {
render();
// A
expect(screen.queryByLabelText(/^It doesnt work/)).not.toBeInTheDocument();
expect(
screen.queryByText(/^Example: Features are slow/),
).not.toBeInTheDocument();
// B
expect(screen.queryByLabelText('Its spam')).not.toBeInTheDocument();
expect(
screen.queryByText(/^Example: The listing advertises/),
).not.toBeInTheDocument();
// C
expect(
screen.queryByLabelText('It violates Add-on Policies'),
).not.toBeInTheDocument();
expect(
screen.queryByText(/^Example: It compromised/),
).not.toBeInTheDocument();
// D
expect(screen.getByLabelText(/^It contains hateful/)).toBeInTheDocument();
expect(
screen.getByText(/^Example: It contains racist/),
).toBeInTheDocument();
// E
expect(screen.getByLabelText(/^It violates the law /)).toBeInTheDocument();
expect(screen.getByText(/^Example: Copyright/)).toBeInTheDocument();
// F
expect(screen.getByLabelText('Something else')).toBeInTheDocument();
expect(screen.getByText(/^Anything that doesnt/)).toBeInTheDocument();
});
it('dispatches sendRatingAbuseReport with all fields on submit', async () => {
const ratingId = 9999;
const { store } = dispatchClientMetadata();
const dispatch = jest.spyOn(store, 'dispatch');
render({ id: ratingId }, store);
await userEvent.click(
screen.getByRole('radio', { name: 'Something else' }),
);
await userEvent.click(
screen.getByRole('button', { name: 'Submit report' }),
);
expect(dispatch).toHaveBeenCalledWith(
sendRatingAbuseReport({
ratingId,
errorHandlerId: getErrorHandlerId(ratingId),
reporterEmail: '',
reporterName: '',
message: '',
reason: CATEGORY_OTHER,
auth: true,
}),
);
});
it('shows a certification checkbox when the chosen reason requires it', async () => {
render();
expect(
screen.queryByLabelText(/^By submitting this report I certify/),
).not.toBeInTheDocument();
await userEvent.click(
screen.getByRole('radio', {
name: 'It violates the law or contains content that violates the law',
}),
);
expect(
screen.getByLabelText(/^By submitting this report I certify/),
).toBeInTheDocument();
await userEvent.click(
screen.getByRole('radio', { name: 'Something else' }),
);
expect(
screen.queryByLabelText(/^By submitting this report I certify/),
).not.toBeInTheDocument();
});
it('disables the submit button when no reason selected', async () => {
render();
expect(
screen.getByRole('button', { name: 'Submit report' }),
).toBeDisabled();
});
it('shows success message after submission', async () => {
const ratingId = 456;
const { store } = dispatchClientMetadata();
render({ id: ratingId }, store);
store.dispatch(
setReviewWasFlagged({ reviewId: ratingId, reason: CATEGORY_OTHER }),
);
expect(
await screen.findByText(
'We have received your report. Thanks for letting us know.',
),
).toBeInTheDocument();
expect(
screen.queryByText('Report this add-on to Mozilla'),
).not.toBeInTheDocument();
expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
});
it('renders a submit button with a different text when updating', async () => {
render();
await userEvent.click(
screen.getByRole('radio', { name: 'Something else' }),
);
expect(
screen.getByRole('button', { name: 'Submit report' }),
).not.toBeDisabled();
await userEvent.click(
screen.getByRole('button', { name: 'Submit report' }),
);
expect(
screen.getByRole('button', { name: 'Submitting your report…' }),
).toBeInTheDocument();
});
});