Add new illegal_category and illegal_subcategory fields in the feedback forms (#13070)

This commit is contained in:
William Durand 2024-06-24 20:42:57 +02:00 коммит произвёл GitHub
Родитель a174fc2ae4
Коммит b200cefaec
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
23 изменённых файлов: 1017 добавлений и 21 удалений

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

@ -334,7 +334,7 @@
"bundlewatch": [
{
"path": "./dist/static/amo-!(i18n-)*.js",
"maxSize": "361 kB"
"maxSize": "363 kB"
},
{
"path": "./dist/static/amo-i18n-*.js",

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

@ -817,6 +817,8 @@ type SendRatingAbuseReportParams = {|
reason: string | null,
reporterEmail: string | null,
reporterName: string | null,
illegalCategory: string | null,
illegalSubcategory: string | null,
|};
export type SendRatingAbuseReportAction = {|
@ -832,6 +834,8 @@ export const sendRatingAbuseReport = ({
reason,
reporterEmail,
reporterName,
illegalCategory,
illegalSubcategory,
}: SendRatingAbuseReportParams): SendRatingAbuseReportAction => {
invariant(errorHandlerId, 'errorHandlerId is required');
invariant(ratingId, 'ratingId is required');
@ -846,6 +850,8 @@ export const sendRatingAbuseReport = ({
reason,
reporterEmail,
reporterName,
illegalCategory,
illegalSubcategory,
},
};
};

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

@ -22,6 +22,9 @@ export type AbuseReporter = {|
username: string,
|} | null;
// We are using snake case in this type because it makes it easier to share
// the same set of parameters between the `mozAddonManager.sendAbuseReport()`
// method and the direct API call (`reportAddon()` below).
export type ReportAddonParams = {|
addonId: string,
api: ApiState,
@ -31,12 +34,11 @@ export type ReportAddonParams = {|
reason: string | null,
location: string | null,
addon_version: string | null,
illegal_category: string | null,
illegal_subcategory: string | null,
auth: boolean,
|};
// We are using snake case in this type because it makes it easier to share
// the same set of parameters between the `mozAddonManager.sendAbuseReport()`
// method and the direct API call (`reportAddon()` below).
export type ReportAddonResponse = {|
addon: {|
guid: string,
@ -61,6 +63,8 @@ export function reportAddon({
reason,
location,
addon_version,
illegal_category,
illegal_subcategory,
auth,
}: ReportAddonParams): Promise<ReportAddonResponse> {
return callApi({
@ -75,6 +79,8 @@ export function reportAddon({
reason,
location,
addon_version,
illegal_category,
illegal_subcategory,
lang: api.lang,
},
apiState: api,
@ -88,6 +94,8 @@ export type ReportUserParams = {|
message: string | null,
reason: string | null,
userId: UserId,
illegalCategory: string | null,
illegalSubcategory: string | null,
auth: boolean,
|};
@ -112,6 +120,8 @@ export function reportUser({
reporterEmail,
reporterName,
userId,
illegalCategory,
illegalSubcategory,
auth = true,
}: ReportUserParams): Promise<ReportUserResponse> {
if (!reason) {
@ -132,6 +142,8 @@ export function reportUser({
reporter_email: reporterEmail,
reporter_name: reporterName,
lang: api.lang,
illegal_category: illegalCategory,
illegal_subcategory: illegalSubcategory,
},
apiState: api,
});
@ -144,6 +156,8 @@ export type ReportRatingParams = {|
reason: string | null,
reporterName: string | null,
reporterEmail: string | null,
illegalCategory: string | null,
illegalSubcategory: string | null,
auth: boolean,
|};
@ -166,6 +180,8 @@ export function reportRating({
reason,
reporterName,
reporterEmail,
illegalCategory,
illegalSubcategory,
auth,
}: ReportRatingParams): Promise<ReportRatingResponse> {
if (!reason) {
@ -186,6 +202,8 @@ export function reportRating({
reporter_name: reporterName,
reporter_email: reporterEmail,
lang: api.lang,
illegal_category: illegalCategory,
illegal_subcategory: illegalSubcategory,
},
apiState: api,
});
@ -198,6 +216,8 @@ export type ReportCollectionParams = {|
reason: string | null,
reporterName: string | null,
reporterEmail: string | null,
illegalCategory: string | null,
illegalSubcategory: string | null,
auth: boolean,
|};
@ -220,6 +240,8 @@ export function reportCollection({
reason,
reporterName,
reporterEmail,
illegalCategory,
illegalSubcategory,
auth,
}: ReportCollectionParams): Promise<ReportCollectionResponse> {
if (!reason) {
@ -240,6 +262,8 @@ export function reportCollection({
reporter_name: reporterName,
reporter_email: reporterEmail,
lang: api.lang,
illegal_category: illegalCategory,
illegal_subcategory: illegalSubcategory,
},
apiState: api,
});

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

@ -70,7 +70,16 @@ export class AddonFeedbackFormBase extends React.Component<InternalProps> {
values: FeedbackFormValues,
) => {
const { addon, dispatch, errorHandler, installedAddon } = this.props;
const { anonymous, email, name, text, category, location } = values;
const {
anonymous,
email,
name,
text,
category,
location,
illegalCategory,
illegalSubcategory,
} = values;
invariant(addon, 'An add-on is required for a report.');
@ -89,6 +98,8 @@ export class AddonFeedbackFormBase extends React.Component<InternalProps> {
? 'addon'
: location,
addonVersion: installedAddon?.version || null,
illegalCategory,
illegalSubcategory,
// Only authenticate the API call when the report isn't submitted
// anonymously.
auth: anonymous === false,

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

@ -30,6 +30,8 @@ export type FeedbackFormValues = {|
text: string,
category: string | null,
location: string | null,
illegalCategory: string | null,
illegalSubcategory: string | null,
|};
type Props = {|
@ -169,6 +171,373 @@ const getLocationOptions = (
];
};
// These categories don't have any subcategories other than "other".
export const ILLEGAL_CATEGORIES_WITHOUT_SUBCATEGORIES = [
'animal_welfare',
'other',
];
export const getIllegalCategoryOptions = (
i18n: I18nType,
): Array<{| children: string, value: string |}> => {
return [
{ children: i18n.gettext('Select type'), value: '' },
{ value: 'animal_welfare', children: i18n.gettext('Animal welfare') },
{
value: 'consumer_information',
children: i18n.gettext('Consumer information infringements'),
},
{
value: 'data_protection_and_privacy_violations',
children: i18n.gettext('Data protection and privacy violations'),
},
{
value: 'illegal_or_harmful_speech',
children: i18n.gettext('Illegal or harmful speech'),
},
{
value: 'intellectual_property_infringements',
children: i18n.gettext('Intellectual property infringements'),
},
{
value: 'negative_effects_on_civic_discourse_or_elections',
children: i18n.gettext(
'Negative effects on civic discourse or elections',
),
},
{
value: 'non_consensual_behaviour',
children: i18n.gettext('Non-consensual behavior'),
},
{
value: 'pornography_or_sexualized_content',
children: i18n.gettext('Pornography or sexualized content'),
},
{
value: 'protection_of_minors',
children: i18n.gettext('Protection of minors'),
},
{
value: 'risk_for_public_security',
children: i18n.gettext('Risk for public security'),
},
{ value: 'scams_and_fraud', children: i18n.gettext('Scams or fraud') },
{ value: 'self_harm', children: i18n.gettext('Self-harm') },
{
value: 'unsafe_and_prohibited_products',
children: i18n.gettext('Unsafe, non-compliant, or prohibited products'),
},
{ value: 'violence', children: i18n.gettext('Violence') },
{ value: 'other', children: i18n.gettext('Other') },
];
};
export const getIllegalSubcategoryOptions = (
i18n: I18nType,
category: string | null,
): Array<{| children: string, value: string |}> => {
const options = [{ children: i18n.gettext('Select violation'), value: '' }];
switch (category) {
case 'consumer_information':
options.push(
...[
{
value: 'insufficient_information_on_traders',
children: i18n.gettext('Insufficient information on traders'),
},
{
value: 'noncompliance_pricing',
children: i18n.gettext('Non-compliance with pricing regulations'),
},
{
value: 'hidden_advertisement',
children: i18n.gettext(
'Hidden advertisement or commercial communication, including by influencers',
),
},
{
value: 'misleading_info_goods_services',
children: i18n.gettext(
'Misleading information about the characteristics of the goods and services',
),
},
{
value: 'misleading_info_consumer_rights',
children: i18n.gettext(
'Misleading information about the consumers rights',
),
},
],
);
break;
case 'data_protection_and_privacy_violations':
options.push(
...[
{
value: 'biometric_data_breach',
children: i18n.gettext('Biometric data breach'),
},
{
value: 'missing_processing_ground',
children: i18n.gettext('Missing processing ground for data'),
},
{
value: 'right_to_be_forgotten',
children: i18n.gettext('Right to be forgotten'),
},
{
value: 'data_falsification',
children: i18n.gettext('Data falsification'),
},
],
);
break;
case 'illegal_or_harmful_speech':
options.push(
...[
{ value: 'defamation', children: i18n.gettext('Defamation') },
{ value: 'discrimination', children: i18n.gettext('Discrimination') },
{
value: 'hate_speech',
children: i18n.gettext(
'Illegal incitement to violence and hatred based on protected characteristics (hate speech)',
),
},
],
);
break;
case 'intellectual_property_infringements':
options.push(
...[
{
value: 'design_infringement',
children: i18n.gettext('Design infringements'),
},
{
value: 'geographic_indications_infringement',
children: i18n.gettext('Geographical indications infringements'),
},
{
value: 'patent_infringement',
children: i18n.gettext('Patent infringements'),
},
{
value: 'trade_secret_infringement',
children: i18n.gettext('Trade secret infringements'),
},
],
);
break;
case 'negative_effects_on_civic_discourse_or_elections':
options.push(
...[
{
value: 'violation_eu_law',
children: i18n.gettext(
'Violation of EU law relevant to civic discourse or elections',
),
},
{
value: 'violation_national_law',
children: i18n.gettext(
'Violation of national law relevant to civic discourse or elections',
),
},
{
value: 'misinformation_disinformation_disinformation',
children: i18n.gettext(
'Misinformation, disinformation, foreign information manipulation and interference',
),
},
],
);
break;
case 'non_consensual_behaviour':
options.push(
...[
{
value: 'non_consensual_image_sharing',
children: i18n.gettext('Non-consensual image sharing'),
},
{
value: 'non_consensual_items_deepfake',
children: i18n.gettext(
"Non-consensual items containing deepfake or similar technology using a third party's features",
),
},
{
value: 'online_bullying_intimidation',
children: i18n.gettext('Online bullying/intimidation'),
},
{ value: 'stalking', children: i18n.gettext('Stalking') },
],
);
break;
case 'pornography_or_sexualized_content':
options.push(
...[
{
value: 'adult_sexual_material',
children: i18n.gettext('Adult sexual material'),
},
{
value: 'image_based_sexual_abuse',
children: i18n.gettext(
'Image-based sexual abuse (excluding content depicting minors)',
),
},
],
);
break;
case 'protection_of_minors':
options.push(
...[
{
value: 'age_specific_restrictions_minors',
children: i18n.gettext(
'age-specific restrictions concerning minors',
),
},
{
value: 'child_sexual_abuse_material',
children: i18n.gettext('Child sexual abuse material'),
},
{
value: 'grooming_sexual_enticement_minors',
children: i18n.gettext('Grooming/sexual enticement of minors'),
},
],
);
break;
case 'risk_for_public_security':
options.push(
...[
{
value: 'illegal_organizations',
children: i18n.gettext('Illegal organizations'),
},
{
value: 'risk_environmental_damage',
children: i18n.gettext('Risk for environmental damage'),
},
{
value: 'risk_public_health',
children: i18n.gettext('Risk for public health'),
},
{
value: 'terrorist_content',
children: i18n.gettext('Terrorist content'),
},
],
);
break;
case 'scams_and_fraud':
options.push(
...[
{
value: 'inauthentic_accounts',
children: i18n.gettext('Inauthentic accounts'),
},
{
value: 'inauthentic_listings',
children: i18n.gettext('Inauthentic listings'),
},
{
value: 'inauthentic_user_reviews',
children: i18n.gettext('Inauthentic user reviews'),
},
{
value: 'impersonation_account_hijacking',
children: i18n.gettext('Impersonation or account hijacking'),
},
{ value: 'phishing', children: i18n.gettext('Phishing') },
{
value: 'pyramid_schemes',
children: i18n.gettext('Pyramid schemes'),
},
],
);
break;
case 'self_harm':
options.push(
...[
{
value: 'content_promoting_eating_disorders',
children: i18n.gettext('Content promoting eating disorders'),
},
{
value: 'self_mutilation',
children: i18n.gettext('Self-mutilation'),
},
{ value: 'suicide', children: i18n.gettext('Suicide') },
],
);
break;
case 'unsafe_and_prohibited_products':
options.push(
...[
{
value: 'prohibited_products',
children: i18n.gettext('Prohibited or restricted products'),
},
{
value: 'unsafe_products',
children: i18n.gettext('Unsafe or non-compliant products'),
},
],
);
break;
case 'violence':
options.push(
...[
{
value: 'coordinated_harm',
children: i18n.gettext('Coordinated harm'),
},
{
value: 'gender_based_violence',
children: i18n.gettext('Gender-based violence'),
},
{
value: 'human_exploitation',
children: i18n.gettext('Human exploitation'),
},
{
value: 'human_trafficking',
children: i18n.gettext('Human trafficking'),
},
{
value: 'incitement_violence_hatred',
children: i18n.gettext(
'General calls or incitement to violence and/or hatred',
),
},
],
);
break;
default:
}
// Other should be listed for each category..
options.push({ value: 'other', children: i18n.gettext('Something else') });
return options;
};
export class FeedbackFormBase extends React.Component<InternalProps, State> {
static defaultProps: DefaultProps = {
_window: typeof window !== 'undefined' ? window : {},
@ -208,15 +577,58 @@ export class FeedbackFormBase extends React.Component<InternalProps, State> {
newValue = checked;
}
this.setState({ [name]: newValue });
let newState = { [name]: newValue };
// We should reset the illegal category/subcategory when the category
// (reason) is no longer "illegal".
if (name === 'category' && value !== CATEGORY_ILLEGAL) {
newState = {
...newState,
illegalCategory: null,
illegalSubcategory: null,
};
} else if (name === 'illegalCategory') {
// Some illegal categories do not have any sub-categories other than
// "Something else" (other) so we set the subcategory to 'other' by
// default here, and we'll hide the input field in the UI. Otherwise, we
// reset the subcategory to make sure that the user always see "Select
// violation" when the illegal category is updated.
newState = {
...newState,
illegalSubcategory: ILLEGAL_CATEGORIES_WITHOUT_SUBCATEGORIES.includes(
value,
)
? 'other'
: null,
};
}
this.setState(newState);
};
onSubmit: HTMLElementEventHandler = (event: ElementEvent) => {
event.preventDefault();
const { anonymous, email, name, text, category, location } = this.state;
const {
anonymous,
email,
name,
text,
category,
location,
illegalCategory,
illegalSubcategory,
} = this.state;
this.props.onSubmit({ anonymous, email, name, text, category, location });
this.props.onSubmit({
anonymous,
email,
name,
text,
category,
location,
illegalCategory,
illegalSubcategory,
});
};
getFeedbackFormValues(currentUser: UserType | null): FeedbackFormValues {
@ -227,6 +639,8 @@ export class FeedbackFormBase extends React.Component<InternalProps, State> {
text: '',
category: null,
location: null,
illegalCategory: null,
illegalSubcategory: null,
};
// `name` is either the `display_name` when set or `Firefox user XYZ`
@ -396,6 +810,59 @@ export class FeedbackFormBase extends React.Component<InternalProps, State> {
</>
)}
{this.state.category === CATEGORY_ILLEGAL && (
<>
<label
className="FeedbackForm-label"
htmlFor="feedbackIllegalCategory"
>
{i18n.gettext('Type of illegal content')}
</label>
<Select
className="FeedbackForm-illegalCategory"
id="feedbackIllegalCategory"
name="illegalCategory"
onChange={this.onFieldChange}
value={this.state.illegalCategory}
required
>
{getIllegalCategoryOptions(i18n).map((option) => {
return <option key={option.value} {...option} />;
})}
</Select>
</>
)}
{this.state.category === CATEGORY_ILLEGAL &&
this.state.illegalCategory &&
!ILLEGAL_CATEGORIES_WITHOUT_SUBCATEGORIES.includes(
this.state.illegalCategory,
) && (
<>
<label
className="FeedbackForm-label"
htmlFor="feedbackIllegalSubcategory"
>
{i18n.gettext('Specific violation')}
</label>
<Select
className="FeedbackForm-illegalSubcategory"
id="feedbackIllegalSubcategory"
name="illegalSubcategory"
onChange={this.onFieldChange}
value={this.state.illegalSubcategory}
required
>
{getIllegalSubcategoryOptions(
i18n,
this.state.illegalCategory,
).map((option) => {
return <option key={option.value} {...option} />;
})}
</Select>
</>
)}
<label className="FeedbackForm-label" htmlFor="feedbackText">
{i18n.gettext('Provide more details')}
</label>

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

@ -86,6 +86,8 @@
.FeedbackForm-name,
.FeedbackForm-email,
.FeedbackForm-location,
.FeedbackForm-illegalCategory,
.FeedbackForm-illegalSubcategory,
.FeedbackForm-text,
.FeedbackForm .SignedInUser {
margin-bottom: 16px;

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

@ -81,7 +81,15 @@ export class CollectionFeedbackBase extends React.Component<InternalProps> {
onFormSubmitted: (values: FeedbackFormValues) => void = (values) => {
const { dispatch, errorHandler, collection } = this.props;
const { anonymous, email, name, text, category } = values;
const {
anonymous,
email,
name,
text,
category,
illegalCategory,
illegalSubcategory,
} = values;
invariant(collection, 'collection is required');
@ -96,6 +104,8 @@ export class CollectionFeedbackBase extends React.Component<InternalProps> {
reason: category,
reporterEmail: anonymous ? '' : email,
reporterName: anonymous ? '' : name,
illegalCategory,
illegalSubcategory,
}),
);
};

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

@ -77,7 +77,15 @@ export class RatingFeedbackBase extends React.Component<InternalProps> {
onFormSubmitted: (values: FeedbackFormValues) => void = (values) => {
const { dispatch, errorHandler, review } = this.props;
const { anonymous, email, name, text, category } = values;
const {
anonymous,
email,
name,
text,
category,
illegalCategory,
illegalSubcategory,
} = values;
invariant(review, 'review is required');
@ -92,6 +100,8 @@ export class RatingFeedbackBase extends React.Component<InternalProps> {
reason: category,
reporterEmail: anonymous ? '' : email,
reporterName: anonymous ? '' : name,
illegalCategory,
illegalSubcategory,
}),
);
};

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

@ -77,7 +77,15 @@ export class UserFeedbackBase extends React.Component<InternalProps> {
onFormSubmitted: (values: FeedbackFormValues) => void = (values) => {
const { dispatch, errorHandler, user } = this.props;
const { anonymous, email, name, text, category } = values;
const {
anonymous,
email,
name,
text,
category,
illegalCategory,
illegalSubcategory,
} = values;
invariant(user, 'user is required');
@ -89,6 +97,8 @@ export class UserFeedbackBase extends React.Component<InternalProps> {
message: text,
reason: category,
userId: user.id,
illegalCategory,
illegalSubcategory,
// Only authenticate the API call when the report isn't submitted
// anonymously.
auth: anonymous === false,

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

@ -84,12 +84,14 @@ export function loadAddonAbuseReport({
type SendAddonAbuseReportParams = {|
addonId: string,
errorHandlerId: string,
reporterEmail?: string | null,
reporterName?: string | null,
reporterEmail: string | null,
reporterName: string | null,
message: string | null,
reason?: string | null,
location?: string | null,
addonVersion?: string | null,
reason: string | null,
location: string | null,
addonVersion: string | null,
illegalCategory: string | null,
illegalSubcategory: string | null,
auth: boolean,
|};
@ -107,6 +109,8 @@ export function sendAddonAbuseReport({
reason = null,
location = null,
addonVersion = null,
illegalCategory = null,
illegalSubcategory = null,
auth,
}: SendAddonAbuseReportParams): SendAddonAbuseReportAction {
invariant(addonId, 'addonId is required');
@ -123,6 +127,8 @@ export function sendAddonAbuseReport({
reason,
location,
addonVersion,
illegalCategory,
illegalSubcategory,
auth,
},
};

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

@ -27,6 +27,8 @@ type SendCollectionAbuseReportParams = {|
reason: string | null,
reporterEmail: string | null,
reporterName: string | null,
illegalCategory: string | null,
illegalSubcategory: string | null,
|};
export type SendCollectionAbuseReportAction = {|
@ -42,6 +44,8 @@ export const sendCollectionAbuseReport = ({
reason,
reporterEmail,
reporterName,
illegalCategory,
illegalSubcategory,
}: SendCollectionAbuseReportParams): SendCollectionAbuseReportAction => {
invariant(errorHandlerId, 'errorHandlerId is required');
invariant(collectionId, 'collectionId is required');
@ -56,6 +60,8 @@ export const sendCollectionAbuseReport = ({
reason,
reporterEmail,
reporterName,
illegalCategory,
illegalSubcategory,
},
};
};

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

@ -71,6 +71,8 @@ export type SendUserAbuseReportParams = {|
reporterEmail?: string | null,
reporterName?: string | null,
userId: UserId,
illegalCategory: string | null,
illegalSubcategory: string | null,
|};
export type SendUserAbuseReportAction = {|
@ -86,6 +88,8 @@ export function sendUserAbuseReport({
reporterEmail = null,
userId,
reporterName = null,
illegalCategory,
illegalSubcategory,
}: SendUserAbuseReportParams): SendUserAbuseReportAction {
invariant(errorHandlerId, 'errorHandlerId is required');
invariant(userId, 'userId is required');
@ -100,6 +104,8 @@ export function sendUserAbuseReport({
reporterEmail,
reporterName,
userId,
illegalCategory,
illegalSubcategory,
},
};
}

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

@ -24,6 +24,8 @@ export function* reportAddon({
reason,
location,
addonVersion,
illegalCategory,
illegalSubcategory,
auth,
},
}: SendAddonAbuseReportAction): Saga {
@ -43,6 +45,8 @@ export function* reportAddon({
reason: reason || null,
location: location || null,
addon_version: addonVersion || null,
illegal_category: illegalCategory || null,
illegal_subcategory: illegalSubcategory || null,
auth,
};
let response;

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

@ -22,6 +22,8 @@ export function* reportCollection({
reporterEmail,
reporterName,
collectionId,
illegalCategory,
illegalSubcategory,
},
}: SendCollectionAbuseReportAction): Saga {
const errorHandler = createErrorHandler(errorHandlerId);
@ -39,6 +41,8 @@ export function* reportCollection({
reporterName: reporterName || null,
reporterEmail: reporterEmail || null,
collectionId,
illegalCategory: illegalCategory || null,
illegalSubcategory: illegalSubcategory || null,
};
const response = yield call(reportCollectionApi, params);

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

@ -435,6 +435,8 @@ function* sendRatingAbuseReport({
reason,
reporterEmail,
reporterName,
illegalCategory,
illegalSubcategory,
},
}: SendRatingAbuseReportAction): Saga {
const errorHandler = createErrorHandler(errorHandlerId);
@ -452,6 +454,8 @@ function* sendRatingAbuseReport({
reason,
reporterName: reporterName || null,
reporterEmail: reporterEmail || null,
illegalCategory: illegalCategory || null,
illegalSubcategory: illegalSubcategory || null,
};
const response: ReportRatingResponse = yield call(reportRating, params);

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

@ -22,6 +22,8 @@ export function* reportUser({
reporterEmail,
reporterName,
userId,
illegalCategory,
illegalSubcategory,
},
}: SendUserAbuseReportAction): Saga {
const errorHandler = createErrorHandler(errorHandlerId);
@ -39,6 +41,8 @@ export function* reportUser({
reporterName: reporterName || null,
reporterEmail: reporterEmail || null,
userId,
illegalCategory: illegalCategory || null,
illegalSubcategory: illegalSubcategory || null,
};
const response = yield call(reportUserApi, params);

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

@ -54,6 +54,8 @@ describe(__filename, () => {
location,
addon_version,
lang: DEFAULT_LANG_IN_TESTS,
illegal_category: null,
illegal_subcategory: null,
},
apiState,
})
@ -74,6 +76,8 @@ describe(__filename, () => {
reporter_name,
location,
addon_version,
illegal_category: null,
illegal_subcategory: null,
auth: true,
});
@ -88,7 +92,7 @@ describe(__filename, () => {
});
}
it('calls the report add-on abuse API', async () => {
it('calls the report user abuse API', async () => {
const apiState = dispatchClientMetadata().store.getState().api;
const message = 'I do not like this!';
const userId = 1234;
@ -107,18 +111,26 @@ describe(__filename, () => {
reporter_email: undefined,
reporter_name: undefined,
lang: DEFAULT_LANG_IN_TESTS,
illegal_category: null,
illegal_subcategory: null,
},
apiState,
})
.once()
.returns(mockResponse({ message, user }));
await reportUser({ api: apiState, message, userId: user.id });
await reportUser({
api: apiState,
message,
userId: user.id,
illegalCategory: null,
illegalSubcategory: null,
});
mockApi.verify();
});
it('calls the report add-on abuse API with more information', async () => {
it('calls the report user abuse API with more information', async () => {
const apiState = dispatchClientMetadata().store.getState().api;
const reason = 'other';
const reporterEmail = 'some-reporter-email';
@ -139,6 +151,8 @@ describe(__filename, () => {
reporter_email: reporterEmail,
reporter_name: reporterName,
lang: DEFAULT_LANG_IN_TESTS,
illegal_category: null,
illegal_subcategory: null,
},
apiState,
})
@ -159,12 +173,14 @@ describe(__filename, () => {
reason,
reporterEmail,
reporterName,
illegalCategory: null,
illegalSubcategory: null,
});
mockApi.verify();
});
it('allows the add-on abuse report API to be called anonymously', async () => {
it('allows the user abuse report API to be called anonymously', async () => {
const apiState = dispatchClientMetadata().store.getState().api;
const message = 'I do not like this!';
const userId = 1234;
@ -183,6 +199,8 @@ describe(__filename, () => {
reporter_email: undefined,
reporter_name: undefined,
lang: DEFAULT_LANG_IN_TESTS,
illegal_category: null,
illegal_subcategory: null,
},
apiState,
})
@ -194,6 +212,8 @@ describe(__filename, () => {
auth: false,
message,
userId: user.id,
illegalCategory: null,
illegalSubcategory: null,
});
mockApi.verify();
@ -224,6 +244,8 @@ describe(__filename, () => {
},
message: '',
reason: 'illegal',
illegal_category: null,
illegal_subcategory: null,
...otherProps,
},
});
@ -249,6 +271,8 @@ describe(__filename, () => {
reporter_email: reporterEmail,
reporter_name: reporterName,
lang: DEFAULT_LANG_IN_TESTS,
illegal_category: null,
illegal_subcategory: null,
},
apiState,
})
@ -262,6 +286,8 @@ describe(__filename, () => {
reason,
reporterEmail,
reporterName,
illegalCategory: null,
illegalSubcategory: null,
});
mockApi.verify();
@ -285,13 +311,22 @@ describe(__filename, () => {
reporter_email: undefined,
reporter_name: undefined,
lang: DEFAULT_LANG_IN_TESTS,
illegal_category: null,
illegal_subcategory: null,
},
apiState,
})
.once()
.returns(mockResponse());
await reportRating({ api: apiState, auth: false, message, ratingId });
await reportRating({
api: apiState,
auth: false,
message,
ratingId,
illegalCategory: null,
illegalSubcategory: null,
});
mockApi.verify();
});
@ -320,6 +355,8 @@ describe(__filename, () => {
},
message: '',
reason: 'illegal',
illegal_category: null,
illegal_subcategory: null,
...otherProps,
},
});
@ -345,6 +382,8 @@ describe(__filename, () => {
reporter_email: reporterEmail,
reporter_name: reporterName,
lang: DEFAULT_LANG_IN_TESTS,
illegal_category: null,
illegal_subcategory: null,
},
apiState,
})
@ -358,6 +397,8 @@ describe(__filename, () => {
reason,
reporterEmail,
reporterName,
illegalCategory: null,
illegalSubcategory: null,
});
mockApi.verify();
@ -381,6 +422,8 @@ describe(__filename, () => {
reporter_email: undefined,
reporter_name: undefined,
lang: DEFAULT_LANG_IN_TESTS,
illegal_category: null,
illegal_subcategory: null,
},
apiState,
})
@ -392,6 +435,8 @@ describe(__filename, () => {
auth: false,
message,
collectionId,
illegalCategory: null,
illegalSubcategory: null,
});
mockApi.verify();

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

@ -0,0 +1,170 @@
import { fakeI18n } from 'tests/unit/helpers';
import {
getIllegalCategoryOptions,
getIllegalSubcategoryOptions,
} from 'amo/components/FeedbackForm';
describe(__filename, () => {
describe('getIllegalCategoryOptions', () => {
// See: https://mozilla.github.io/addons-server/topics/api/abuse.html#abuse-report-illegal-category-parameter
it('returns a list of illegal categories', () => {
const categories = getIllegalCategoryOptions(fakeI18n());
expect(categories.map((category) => category.value)).toEqual([
'',
'animal_welfare',
'consumer_information',
'data_protection_and_privacy_violations',
'illegal_or_harmful_speech',
'intellectual_property_infringements',
'negative_effects_on_civic_discourse_or_elections',
'non_consensual_behaviour',
'pornography_or_sexualized_content',
'protection_of_minors',
'risk_for_public_security',
'scams_and_fraud',
'self_harm',
'unsafe_and_prohibited_products',
'violence',
'other',
]);
});
});
describe('getIllegalSubcategoryOptions', () => {
// See: https://mozilla.github.io/addons-server/topics/api/abuse.html#abuse-report-illegal-subcategory-parameter
it.each([
['animal_welfare', ['', 'other']],
[
'consumer_information',
[
'',
'insufficient_information_on_traders',
'noncompliance_pricing',
'hidden_advertisement',
'misleading_info_goods_services',
'misleading_info_consumer_rights',
'other',
],
],
[
'data_protection_and_privacy_violations',
[
'',
'biometric_data_breach',
'missing_processing_ground',
'right_to_be_forgotten',
'data_falsification',
'other',
],
],
[
'illegal_or_harmful_speech',
['', 'defamation', 'discrimination', 'hate_speech', 'other'],
],
[
'intellectual_property_infringements',
[
'',
'design_infringement',
'geographic_indications_infringement',
'patent_infringement',
'trade_secret_infringement',
'other',
],
],
[
'negative_effects_on_civic_discourse_or_elections',
[
'',
'violation_eu_law',
'violation_national_law',
'misinformation_disinformation_disinformation',
'other',
],
],
[
'non_consensual_behaviour',
[
'',
'non_consensual_image_sharing',
'non_consensual_items_deepfake',
'online_bullying_intimidation',
'stalking',
'other',
],
],
[
'pornography_or_sexualized_content',
['', 'adult_sexual_material', 'image_based_sexual_abuse', 'other'],
],
[
'protection_of_minors',
[
'',
'age_specific_restrictions_minors',
'child_sexual_abuse_material',
'grooming_sexual_enticement_minors',
'other',
],
],
[
'risk_for_public_security',
[
'',
'illegal_organizations',
'risk_environmental_damage',
'risk_public_health',
'terrorist_content',
'other',
],
],
[
'scams_and_fraud',
[
'',
'inauthentic_accounts',
'inauthentic_listings',
'inauthentic_user_reviews',
'impersonation_account_hijacking',
'phishing',
'pyramid_schemes',
'other',
],
],
[
'self_harm',
[
'',
'content_promoting_eating_disorders',
'self_mutilation',
'suicide',
'other',
],
],
[
'unsafe_and_prohibited_products',
['', 'prohibited_products', 'unsafe_products', 'other'],
],
[
'violence',
[
'',
'coordinated_harm',
'gender_based_violence',
'human_exploitation',
'human_trafficking',
'incitement_violence_hatred',
'other',
],
],
['other', ['', 'other']],
])('returns a list of sub-category for %s', (category, expected) => {
const subcategories = getIllegalSubcategoryOptions(fakeI18n(), category);
expect(subcategories.map((subcategory) => subcategory.value)).toEqual(
expected,
);
});
});
});

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

@ -13,6 +13,7 @@ import {
CATEGORY_HATEFUL_VIOLENT_DECEPTIVE,
CATEGORY_ILLEGAL,
CATEGORY_SOMETHING_ELSE,
ILLEGAL_CATEGORIES_WITHOUT_SUBCATEGORIES,
} from 'amo/components/FeedbackForm';
import { extractId } from 'amo/pages/AddonFeedback';
import { loadAddonAbuseReport, sendAddonAbuseReport } from 'amo/reducers/abuse';
@ -314,6 +315,18 @@ describe(__filename, () => {
}),
defaultLocationLabel,
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Type of illegal content',
}),
'Consumer information infringements',
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Specific violation',
}),
'Non-compliance with pricing regulations',
);
await userEvent.click(
screen.getByRole('checkbox', {
name: certificationLabel,
@ -338,10 +351,131 @@ describe(__filename, () => {
reason: CATEGORY_ILLEGAL,
location: defaultLocation,
auth: false,
illegalCategory: 'consumer_information',
illegalSubcategory: 'noncompliance_pricing',
}),
);
});
it.each(ILLEGAL_CATEGORIES_WITHOUT_SUBCATEGORIES)(
'does not show the "violation" select field when illegal category is %s (the subcategory is set to "other")',
async (illegalCategory) => {
const dispatch = jest.spyOn(store, 'dispatch');
render();
await userEvent.type(
screen.getByRole('textbox', {
name: 'Provide more details',
}),
defaultMessage,
);
await userEvent.click(
screen.getByRole('radio', { name: illegalReasonLabel }),
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Place of the violation',
}),
defaultLocationLabel,
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Type of illegal content',
}),
illegalCategory,
);
await userEvent.click(
screen.getByRole('checkbox', {
name: certificationLabel,
}),
);
await userEvent.click(
screen.getByRole('checkbox', {
name: 'File report anonymously',
}),
);
await userEvent.click(
screen.getByRole('button', { name: 'Submit report' }),
);
expect(
screen.queryByLabelText('Specific violation'),
).not.toBeInTheDocument();
expect(dispatch).toHaveBeenCalledWith(
sendAddonAbuseReport({
errorHandlerId: getErrorHandlerId(defaultAddonGUID),
addonId: defaultAddonGUID,
reporterEmail: '',
reporterName: '',
message: defaultMessage,
reason: CATEGORY_ILLEGAL,
location: defaultLocation,
auth: false,
illegalCategory,
illegalSubcategory: 'other',
}),
);
},
);
it('resets the illegal subcategory when the illegal category is updated', async () => {
render();
await userEvent.click(
screen.getByRole('radio', { name: illegalReasonLabel }),
);
// select a category that has no visible subcategory
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Type of illegal content',
}),
'animal_welfare',
);
expect(
screen.queryByLabelText('Specific violation'),
).not.toBeInTheDocument();
// select a different category
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Type of illegal content',
}),
'consumer_information',
);
// we expect the field for subcategories to be visible now, and its value
// should be reset
expect(screen.getByLabelText('Specific violation')).toBeInTheDocument();
expect(
screen.getByRole('option', { name: 'Select violation' }).selected,
).toBe(true);
// select a subcategory
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Specific violation',
}),
'hidden_advertisement',
);
expect(
screen.getByRole('option', {
name: 'Hidden advertisement or commercial communication, including by influencers',
}).selected,
).toBe(true);
// select a new category
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Type of illegal content',
}),
'data_protection_and_privacy_violations',
);
// at this point, we should reset the subcategory value/field
expect(screen.getByLabelText('Specific violation')).toBeInTheDocument();
expect(
screen.getByRole('option', { name: 'Select violation' }).selected,
).toBe(true);
});
it('dispatches sendAddonAbuseReport action with all fields on submit for a signed-in user', async () => {
const name = 'some user name';
const email = 'some user email';
@ -349,6 +483,23 @@ describe(__filename, () => {
const dispatch = jest.spyOn(store, 'dispatch');
render();
// start by filing a report for an illegal matter
await userEvent.click(
screen.getByRole('radio', { name: illegalReasonLabel }),
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Type of illegal content',
}),
'Consumer information infringements',
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Specific violation',
}),
'Non-compliance with pricing regulations',
);
// then change for something else
await userEvent.click(
screen.getByRole('radio', { name: 'Something else' }),
);
@ -376,6 +527,10 @@ describe(__filename, () => {
reason: CATEGORY_SOMETHING_ELSE,
location: defaultLocation,
auth: true,
// We expect these fields to have been reset since the user changed the
// reason of the report.
illegalCategory: null,
illegalSubcategory: null,
}),
);
});
@ -394,6 +549,18 @@ describe(__filename, () => {
}),
defaultLocationLabel,
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Type of illegal content',
}),
'Consumer information infringements',
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Specific violation',
}),
'Non-compliance with pricing regulations',
);
await userEvent.type(
screen.getByRole('textbox', { name: 'Provide more details' }),
'Some details...',
@ -422,6 +589,8 @@ describe(__filename, () => {
reason: CATEGORY_ILLEGAL,
location: defaultLocation,
auth: false,
illegalCategory: 'consumer_information',
illegalSubcategory: 'noncompliance_pricing',
}),
);
});
@ -489,6 +658,8 @@ describe(__filename, () => {
reason: 'does_not_work',
location: 'addon',
auth: false,
illegalCategory: null,
illegalSubcategory: null,
}),
);
});
@ -524,6 +695,18 @@ describe(__filename, () => {
await userEvent.click(
screen.getByRole('radio', { name: illegalReasonLabel }),
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Type of illegal content',
}),
'Consumer information infringements',
);
await userEvent.selectOptions(
screen.getByRole('combobox', {
name: 'Specific violation',
}),
'Non-compliance with pricing regulations',
);
await userEvent.click(
screen.getByRole('checkbox', {
name: certificationLabel,
@ -552,6 +735,8 @@ describe(__filename, () => {
reason: CATEGORY_ILLEGAL,
location: 'addon',
auth: false,
illegalCategory: 'consumer_information',
illegalSubcategory: 'noncompliance_pricing',
}),
);
},
@ -621,6 +806,8 @@ describe(__filename, () => {
location: defaultLocation,
addonVersion: version,
auth: false,
illegalCategory: null,
illegalSubcategory: null,
}),
);
});

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

@ -313,6 +313,8 @@ describe(__filename, () => {
reporterName: signedInName,
message: 'Some details...',
reason: CATEGORY_SOMETHING_ELSE,
illegalCategory: null,
illegalSubcategory: null,
auth: true,
}),
);
@ -355,6 +357,8 @@ describe(__filename, () => {
reporterName: '',
message: 'Some details...',
reason: CATEGORY_SOMETHING_ELSE,
illegalCategory: null,
illegalSubcategory: null,
auth: false,
}),
);
@ -495,6 +499,8 @@ describe(__filename, () => {
reporterName: '',
message: 'Some details...',
reason: CATEGORY_FEEDBACK_SPAM,
illegalCategory: null,
illegalSubcategory: null,
auth: false,
}),
);

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

@ -352,6 +352,8 @@ describe(__filename, () => {
reporterName: '',
message: 'Some details...',
reason: CATEGORY_SOMETHING_ELSE,
illegalCategory: null,
illegalSubcategory: null,
auth: false,
}),
);
@ -387,6 +389,8 @@ describe(__filename, () => {
reporterName: signedInName,
message: 'Some details...',
reason: CATEGORY_SOMETHING_ELSE,
illegalCategory: null,
illegalSubcategory: null,
auth: true,
}),
);
@ -427,6 +431,8 @@ describe(__filename, () => {
reporterName: '',
message: 'Some details...',
reason: CATEGORY_SOMETHING_ELSE,
illegalCategory: null,
illegalSubcategory: null,
auth: false,
}),
);

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

@ -332,6 +332,8 @@ describe(__filename, () => {
reporterName: '',
message: 'Some details...',
reason: CATEGORY_FEEDBACK_SPAM,
illegalCategory: null,
illegalSubcategory: null,
auth: false,
}),
);
@ -367,6 +369,8 @@ describe(__filename, () => {
reporterName: signedInName,
message: 'Some details...',
reason: CATEGORY_SOMETHING_ELSE,
illegalCategory: null,
illegalSubcategory: null,
auth: true,
}),
);
@ -407,6 +411,8 @@ describe(__filename, () => {
reporterName: '',
message: 'Some details...',
reason: CATEGORY_SOMETHING_ELSE,
illegalCategory: null,
illegalSubcategory: null,
auth: false,
}),
);

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

@ -67,6 +67,8 @@ describe(__filename, () => {
reporterEmail: 'made_up@email',
location: 'both',
addonVersion: '1.2.3',
illegalCategory: 'some illegal category',
illegalSubcategory: 'some illegal subcategory',
};
});