Frontend Compatibility for Multiple Promoted Addon Groups (#13343)
This commit is contained in:
Родитель
262e83a746
Коммит
c55f660ab0
|
@ -327,9 +327,10 @@ export const RECOMMENDED = 'recommended';
|
|||
export const SPOTLIGHT = 'spotlight';
|
||||
export const STRATEGIC = 'strategic';
|
||||
|
||||
// This array is sorted by "importance".
|
||||
export const ALL_PROMOTED_CATEGORIES = [
|
||||
LINE,
|
||||
RECOMMENDED,
|
||||
LINE,
|
||||
SPOTLIGHT,
|
||||
STRATEGIC,
|
||||
];
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
UpdateRatingCountsAction,
|
||||
} from 'amo/actions/reviews';
|
||||
import {
|
||||
makeInternalPromoted,
|
||||
selectLocalizedContent,
|
||||
selectCategoryObject,
|
||||
} from 'amo/reducers/utils';
|
||||
|
@ -236,7 +237,7 @@ export function createInternalAddon(
|
|||
previews: apiAddon.previews
|
||||
? createInternalPreviews(apiAddon.previews, lang)
|
||||
: undefined,
|
||||
promoted: apiAddon.promoted,
|
||||
promoted: makeInternalPromoted(apiAddon.promoted),
|
||||
ratings: apiAddon.ratings,
|
||||
requires_payment: apiAddon.requires_payment,
|
||||
review_url: apiAddon.review_url,
|
||||
|
|
|
@ -3,7 +3,10 @@ import invariant from 'invariant';
|
|||
|
||||
import { getAddonIconUrl } from 'amo/imageUtils';
|
||||
import { SET_LANG } from 'amo/reducers/api';
|
||||
import { selectLocalizedContent } from 'amo/reducers/utils';
|
||||
import {
|
||||
makeInternalPromoted,
|
||||
selectLocalizedContent,
|
||||
} from 'amo/reducers/utils';
|
||||
import type { PromotedType } from 'amo/types/addons';
|
||||
import type { LocalizedString } from 'amo/types/api';
|
||||
|
||||
|
@ -18,7 +21,7 @@ export type ExternalSuggestion = {|
|
|||
icon_url: string,
|
||||
id: number,
|
||||
name: LocalizedString,
|
||||
promoted: PromotedType | null,
|
||||
promoted: Array<PromotedType> | PromotedType | null,
|
||||
type: string,
|
||||
url: string,
|
||||
|};
|
||||
|
@ -27,7 +30,7 @@ export type SuggestionType = {|
|
|||
addonId: number,
|
||||
iconUrl: string,
|
||||
name: string,
|
||||
promoted: PromotedType | null,
|
||||
promoted: Array<PromotedType>,
|
||||
type: string,
|
||||
url: string,
|
||||
|};
|
||||
|
@ -108,7 +111,7 @@ export const createInternalSuggestion = (
|
|||
addonId: externalSuggestion.id,
|
||||
iconUrl: getAddonIconUrl(externalSuggestion),
|
||||
name: selectLocalizedContent(externalSuggestion.name, lang),
|
||||
promoted: externalSuggestion.promoted,
|
||||
promoted: makeInternalPromoted(externalSuggestion.promoted),
|
||||
type: externalSuggestion.type,
|
||||
url: externalSuggestion.url,
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import invariant from 'invariant';
|
|||
import type { LocalizedString } from 'amo/types/api';
|
||||
|
||||
import type { CategoryEntry } from './categories';
|
||||
import type { ExternalAddonType } from '../types/addons';
|
||||
import type { ExternalAddonType, PromotedType } from '../types/addons';
|
||||
|
||||
export const selectLocalizedContent = (
|
||||
field: LocalizedString,
|
||||
|
@ -26,3 +26,12 @@ export const selectCategoryObject = (
|
|||
): CategoryEntry => {
|
||||
return apiAddon.categories;
|
||||
};
|
||||
|
||||
export const makeInternalPromoted = (
|
||||
promoted: Array<PromotedType> | PromotedType | null,
|
||||
): Array<PromotedType> => {
|
||||
if (!promoted) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(promoted) ? promoted : [promoted];
|
||||
};
|
||||
|
|
|
@ -133,7 +133,7 @@ export type ExternalAddonType = {|
|
|||
locale_disambiguation?: string,
|
||||
name: LocalizedString,
|
||||
previews?: Array<ExternalPreviewType>,
|
||||
promoted: PromotedType | null,
|
||||
promoted: Array<PromotedType> | PromotedType | null,
|
||||
ratings: {|
|
||||
average: number,
|
||||
bayesian_average: number,
|
||||
|
@ -174,6 +174,8 @@ export type AddonType = {|
|
|||
summary: string | null,
|
||||
support_email: string | null,
|
||||
support_url: UrlWithOutgoing | null,
|
||||
// normalized promoted categories,
|
||||
promoted: Array<PromotedType>,
|
||||
// Here are some custom properties for our internal representation.
|
||||
currentVersionId: VersionIdType | null,
|
||||
isMozillaSignedExtension: boolean,
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
FATAL_INSTALL_ERROR,
|
||||
FATAL_UNINSTALL_ERROR,
|
||||
INSTALL_FAILED,
|
||||
ALL_PROMOTED_CATEGORIES,
|
||||
} from 'amo/constants';
|
||||
import log from 'amo/logger';
|
||||
import { getPreviewImage } from 'amo/imageUtils';
|
||||
|
@ -121,15 +122,25 @@ export const getPromotedCategory = ({
|
|||
clientApp: string,
|
||||
forBadging?: boolean,
|
||||
|}): PromotedCategoryType | null => {
|
||||
let category = null;
|
||||
if (addon && addon.promoted && addon.promoted.apps.includes(clientApp)) {
|
||||
category = addon.promoted.category;
|
||||
if (!addon?.promoted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Special logic if we're using the category for badging.
|
||||
if (forBadging && !BADGE_CATEGORIES.includes(category)) {
|
||||
category = null;
|
||||
}
|
||||
const categories: Array<PromotedCategoryType> = addon.promoted
|
||||
.filter((promoted) => {
|
||||
if (!promoted.apps.includes(clientApp)) {
|
||||
return false;
|
||||
}
|
||||
// Special logic if we're using the category for badging.
|
||||
// We shouldn't add badges that are in BADGE_CATEGORIES.
|
||||
return forBadging ? BADGE_CATEGORIES.includes(promoted.category) : true;
|
||||
})
|
||||
.map((promoted) => promoted.category)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
ALL_PROMOTED_CATEGORIES.indexOf(a) - ALL_PROMOTED_CATEGORIES.indexOf(b),
|
||||
);
|
||||
|
||||
return category;
|
||||
// Return only the 'most important' badge.
|
||||
return categories.shift() || null;
|
||||
};
|
||||
|
|
|
@ -27,6 +27,9 @@ import {
|
|||
INCOMPATIBLE_UNSUPPORTED_PLATFORM,
|
||||
INSTALLING,
|
||||
RECOMMENDED,
|
||||
LINE,
|
||||
SPOTLIGHT,
|
||||
STRATEGIC,
|
||||
REVIEWER_TOOLS_VIEW,
|
||||
SET_VIEW_CONTEXT,
|
||||
STATIC_THEMES_REVIEW,
|
||||
|
@ -2965,6 +2968,18 @@ describe(__filename, () => {
|
|||
},
|
||||
);
|
||||
|
||||
it('does not render the strategic or spotlight badges and correctly renders only the most important badge (RECOMMENDED)', () => {
|
||||
const categories = [LINE, RECOMMENDED, STRATEGIC, SPOTLIGHT];
|
||||
addon.promoted = categories.map((category) => ({
|
||||
category,
|
||||
apps: [clientApp],
|
||||
}));
|
||||
renderWithAddon();
|
||||
const badges = screen.getAllByClassName('PromotedBadge');
|
||||
expect(badges).toHaveLength(1);
|
||||
expect(badges[0]).toHaveClass(`PromotedBadge--recommended`);
|
||||
});
|
||||
|
||||
// See https://github.com/mozilla/addons-frontend/issues/8285.
|
||||
it('does not pass an alt property to IconPromotedBadge', () => {
|
||||
renderWithPromotedCategory();
|
||||
|
|
|
@ -87,7 +87,7 @@ describe(__filename, () => {
|
|||
addonId: result.id,
|
||||
iconUrl: result.icon_url,
|
||||
name,
|
||||
promoted,
|
||||
promoted: [promoted],
|
||||
url: result.url,
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { selectLocalizedContent } from 'amo/reducers/utils';
|
||||
import {
|
||||
selectLocalizedContent,
|
||||
makeInternalPromoted,
|
||||
} from 'amo/reducers/utils';
|
||||
import { CLIENT_APP_FIREFOX, RECOMMENDED } from 'amo/constants';
|
||||
|
||||
describe(__filename, () => {
|
||||
describe('selectLocalizedContent', () => {
|
||||
|
@ -33,4 +37,24 @@ describe(__filename, () => {
|
|||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeInternalPromoted', () => {
|
||||
it('returns the empty list if promoted is null', () => {
|
||||
expect(makeInternalPromoted(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns the empty list if promoted is empty', () => {
|
||||
expect(makeInternalPromoted([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns promoted if promoted is a list', () => {
|
||||
const promoted = [{ category: RECOMMENDED, apps: [CLIENT_APP_FIREFOX] }];
|
||||
expect(makeInternalPromoted(promoted)).toEqual(promoted);
|
||||
});
|
||||
|
||||
it('returns promoted in a list if promoted is an object', () => {
|
||||
const promoted = { category: RECOMMENDED, apps: [CLIENT_APP_FIREFOX] };
|
||||
expect(makeInternalPromoted(promoted)).toEqual([promoted]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -249,6 +249,88 @@ describe(__filename, () => {
|
|||
).toEqual(category);
|
||||
});
|
||||
|
||||
it('returns only the most important category if the addon is promoted in multiple categories for the specified app', () => {
|
||||
const categories = [SPOTLIGHT, STRATEGIC, RECOMMENDED];
|
||||
const promoted = categories.map((category) => ({
|
||||
category,
|
||||
apps: [CLIENT_APP_ANDROID],
|
||||
}));
|
||||
|
||||
const addon = createInternalAddonWithLang({
|
||||
...fakeAddon,
|
||||
promoted,
|
||||
});
|
||||
const suggestion = createInternalSuggestionWithLang(
|
||||
createFakeAutocompleteResult({
|
||||
promoted,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
getPromotedCategory({
|
||||
addon,
|
||||
clientApp: CLIENT_APP_ANDROID,
|
||||
}),
|
||||
).toEqual(RECOMMENDED);
|
||||
expect(
|
||||
getPromotedCategory({
|
||||
addon: suggestion,
|
||||
clientApp: CLIENT_APP_ANDROID,
|
||||
}),
|
||||
).toEqual(RECOMMENDED);
|
||||
});
|
||||
|
||||
it('returns null if the addon is promoted in multiple categories, but not for the specified app', () => {
|
||||
const categories = [RECOMMENDED, SPOTLIGHT, STRATEGIC];
|
||||
const promoted = categories.map((category) => ({
|
||||
category,
|
||||
apps: [CLIENT_APP_FIREFOX],
|
||||
}));
|
||||
|
||||
const addon = createInternalAddonWithLang({
|
||||
...fakeAddon,
|
||||
promoted,
|
||||
});
|
||||
const suggestion = createInternalSuggestionWithLang(
|
||||
createFakeAutocompleteResult({
|
||||
promoted,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
getPromotedCategory({
|
||||
addon,
|
||||
clientApp: CLIENT_APP_ANDROID,
|
||||
}),
|
||||
).toEqual(null);
|
||||
expect(
|
||||
getPromotedCategory({
|
||||
addon: suggestion,
|
||||
clientApp: CLIENT_APP_ANDROID,
|
||||
}),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('returns null if the addon is not promoted via empty list', () => {
|
||||
const addon = createInternalAddonWithLang({
|
||||
...fakeAddon,
|
||||
promoted: [],
|
||||
});
|
||||
const suggestion = createInternalSuggestionWithLang(
|
||||
createFakeAutocompleteResult({ promoted: [] }),
|
||||
);
|
||||
|
||||
expect(
|
||||
getPromotedCategory({ addon, clientApp: CLIENT_APP_ANDROID }),
|
||||
).toEqual(null);
|
||||
expect(
|
||||
getPromotedCategory({
|
||||
addon: suggestion,
|
||||
clientApp: CLIENT_APP_ANDROID,
|
||||
}),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
describe('forBadging === true', () => {
|
||||
it.each([SPOTLIGHT, STRATEGIC])(
|
||||
'returns null if the category is not one for badges, category: %s',
|
||||
|
|
Загрузка…
Ссылка в новой задаче