Introduce new page for rating feedback (#12581)
This commit is contained in:
Родитель
dfdfa1b49e
Коммит
940f7ce1eb
|
@ -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 can’t 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 can’t 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 doesn’t work/)).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/^Example: Features are slow/),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// B
|
||||
expect(screen.queryByLabelText('It’s 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 doesn’t/)).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();
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче