Implement API, saga and reducer code to support Hero Shelves (#8485)
* Implement API, saga and reducer code to support Hero Shelves * Address review comments
This commit is contained in:
Родитель
4f4a5f0de4
Коммит
872f2cd993
|
@ -0,0 +1,16 @@
|
|||
/* @flow */
|
||||
import { callApi } from 'core/api';
|
||||
import type { ApiState } from 'core/reducers/api';
|
||||
import type { ExternalHeroShelvesType } from 'amo/reducers/home';
|
||||
|
||||
export type GetHeroShelvesParams = {| api: ApiState |};
|
||||
|
||||
export const getHeroShelves = ({
|
||||
api,
|
||||
}: GetHeroShelvesParams): Promise<ExternalHeroShelvesType> => {
|
||||
return callApi({
|
||||
apiState: api,
|
||||
auth: true,
|
||||
endpoint: 'hero',
|
||||
});
|
||||
};
|
|
@ -14,7 +14,7 @@ import HeadMetaTags from 'amo/components/HeadMetaTags';
|
|||
import HeroRecommendation from 'amo/components/HeroRecommendation';
|
||||
import LandingAddonsCard from 'amo/components/LandingAddonsCard';
|
||||
import Link from 'amo/components/Link';
|
||||
import { fetchHomeAddons } from 'amo/reducers/home';
|
||||
import { fetchHomeData } from 'amo/reducers/home';
|
||||
import { makeQueryStringWithUTM } from 'amo/utils';
|
||||
import {
|
||||
ADDON_TYPE_EXTENSION,
|
||||
|
@ -119,7 +119,7 @@ export class HomeBase extends React.Component {
|
|||
|
||||
if (!resultsLoaded) {
|
||||
dispatch(
|
||||
fetchHomeAddons({
|
||||
fetchHomeData({
|
||||
collectionsToFetch: FEATURED_COLLECTIONS,
|
||||
enableFeatureRecommendedBadges: _config.get(
|
||||
'enableFeatureRecommendedBadges',
|
||||
|
|
|
@ -8,25 +8,91 @@ import {
|
|||
import { SET_CLIENT_APP } from 'core/constants';
|
||||
import { createInternalAddon } from 'core/reducers/addons';
|
||||
import { isTheme } from 'core/utils';
|
||||
import type { AddonType, ExternalAddonType } from 'core/types/addons';
|
||||
import type { SetClientAppAction } from 'core/actions';
|
||||
import type {
|
||||
AddonType,
|
||||
ExternalAddonType,
|
||||
PartialExternalAddonType,
|
||||
} from 'core/types/addons';
|
||||
|
||||
export const FETCH_HOME_ADDONS: 'FETCH_HOME_ADDONS' = 'FETCH_HOME_ADDONS';
|
||||
export const LOAD_HOME_ADDONS: 'LOAD_HOME_ADDONS' = 'LOAD_HOME_ADDONS';
|
||||
export const FETCH_HOME_DATA: 'FETCH_HOME_DATA' = 'FETCH_HOME_DATA';
|
||||
export const LOAD_HOME_DATA: 'LOAD_HOME_DATA' = 'LOAD_HOME_DATA';
|
||||
|
||||
export type PrimaryHeroShelfExternalType = {|
|
||||
id: string,
|
||||
guid: string,
|
||||
homepage: string,
|
||||
name: string,
|
||||
type: string,
|
||||
|};
|
||||
|
||||
type HeroGradientType = {|
|
||||
start: string,
|
||||
end: string,
|
||||
|};
|
||||
|
||||
export type ExternalPrimaryHeroShelfType = {|
|
||||
gradient: HeroGradientType,
|
||||
featured_image: string,
|
||||
description: string | null,
|
||||
addon?: PartialExternalAddonType,
|
||||
external?: PrimaryHeroShelfExternalType,
|
||||
|};
|
||||
|
||||
export type PrimaryHeroShelfType = {|
|
||||
gradient: {|
|
||||
start: string,
|
||||
end: string,
|
||||
|},
|
||||
featuredImage: string,
|
||||
description: string | null,
|
||||
addon: AddonType | void,
|
||||
external: PrimaryHeroShelfExternalType | void,
|
||||
|};
|
||||
|
||||
export type HeroCallToActionType = {|
|
||||
url: string,
|
||||
text: string,
|
||||
|};
|
||||
|
||||
export type SecondaryHeroModuleType = {|
|
||||
icon: string,
|
||||
description: string,
|
||||
cta: HeroCallToActionType | null,
|
||||
|};
|
||||
|
||||
export type SecondaryHeroShelfType = {|
|
||||
headline: string,
|
||||
description: string,
|
||||
cta: HeroCallToActionType | null,
|
||||
modules: Array<SecondaryHeroModuleType>,
|
||||
|};
|
||||
|
||||
export type ExternalHeroShelvesType = {|
|
||||
primary: ExternalPrimaryHeroShelfType,
|
||||
secondary: SecondaryHeroShelfType,
|
||||
|};
|
||||
|
||||
export type HeroShelvesType = {|
|
||||
primary: PrimaryHeroShelfType,
|
||||
secondary: SecondaryHeroShelfType,
|
||||
|};
|
||||
|
||||
export type HomeState = {
|
||||
collections: Array<Object | null>,
|
||||
heroShelves: HeroShelvesType | null,
|
||||
resultsLoaded: boolean,
|
||||
shelves: { [shelfName: string]: Array<AddonType> | null },
|
||||
};
|
||||
|
||||
export const initialState: HomeState = {
|
||||
collections: [],
|
||||
heroShelves: null,
|
||||
resultsLoaded: false,
|
||||
shelves: {},
|
||||
};
|
||||
|
||||
type FetchHomeAddonsParams = {|
|
||||
type FetchHomeDataParams = {|
|
||||
collectionsToFetch: Array<Object>,
|
||||
enableFeatureRecommendedBadges: boolean,
|
||||
errorHandlerId: string,
|
||||
|
@ -34,23 +100,23 @@ type FetchHomeAddonsParams = {|
|
|||
includeTrendingExtensions: boolean,
|
||||
|};
|
||||
|
||||
export type FetchHomeAddonsAction = {|
|
||||
type: typeof FETCH_HOME_ADDONS,
|
||||
payload: FetchHomeAddonsParams,
|
||||
export type FetchHomeDataAction = {|
|
||||
type: typeof FETCH_HOME_DATA,
|
||||
payload: FetchHomeDataParams,
|
||||
|};
|
||||
|
||||
export const fetchHomeAddons = ({
|
||||
export const fetchHomeData = ({
|
||||
collectionsToFetch,
|
||||
enableFeatureRecommendedBadges,
|
||||
errorHandlerId,
|
||||
includeRecommendedThemes,
|
||||
includeTrendingExtensions,
|
||||
}: FetchHomeAddonsParams): FetchHomeAddonsAction => {
|
||||
}: FetchHomeDataParams): FetchHomeDataAction => {
|
||||
invariant(errorHandlerId, 'errorHandlerId is required');
|
||||
invariant(collectionsToFetch, 'collectionsToFetch is required');
|
||||
|
||||
return {
|
||||
type: FETCH_HOME_ADDONS,
|
||||
type: FETCH_HOME_DATA,
|
||||
payload: {
|
||||
collectionsToFetch,
|
||||
enableFeatureRecommendedBadges,
|
||||
|
@ -66,33 +132,36 @@ type ApiAddonsResponse = {|
|
|||
results: Array<ExternalAddonType>,
|
||||
|};
|
||||
|
||||
type LoadHomeAddonsParams = {|
|
||||
type LoadHomeDataParams = {|
|
||||
collections: Array<Object | null>,
|
||||
heroShelves: ExternalHeroShelvesType,
|
||||
shelves: { [shelfName: string]: ApiAddonsResponse },
|
||||
|};
|
||||
|
||||
type LoadHomeAddonsAction = {|
|
||||
type: typeof LOAD_HOME_ADDONS,
|
||||
payload: LoadHomeAddonsParams,
|
||||
type LoadHomeDataAction = {|
|
||||
type: typeof LOAD_HOME_DATA,
|
||||
payload: LoadHomeDataParams,
|
||||
|};
|
||||
|
||||
export const loadHomeAddons = ({
|
||||
export const loadHomeData = ({
|
||||
collections,
|
||||
heroShelves,
|
||||
shelves,
|
||||
}: LoadHomeAddonsParams): LoadHomeAddonsAction => {
|
||||
}: LoadHomeDataParams): LoadHomeDataAction => {
|
||||
invariant(collections, 'collections is required');
|
||||
invariant(shelves, 'shelves is required');
|
||||
|
||||
return {
|
||||
type: LOAD_HOME_ADDONS,
|
||||
type: LOAD_HOME_DATA,
|
||||
payload: {
|
||||
collections,
|
||||
heroShelves,
|
||||
shelves,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type Action = FetchHomeAddonsAction | LoadHomeAddonsAction | SetClientAppAction;
|
||||
type Action = FetchHomeDataAction | LoadHomeDataAction | SetClientAppAction;
|
||||
|
||||
const createInternalAddons = (
|
||||
response: ApiAddonsResponse,
|
||||
|
@ -100,6 +169,23 @@ const createInternalAddons = (
|
|||
return response.results.map((addon) => createInternalAddon(addon));
|
||||
};
|
||||
|
||||
export const createInternalHeroShelves = (
|
||||
heroShelves: ExternalHeroShelvesType,
|
||||
): HeroShelvesType => {
|
||||
const { primary, secondary } = heroShelves;
|
||||
|
||||
return {
|
||||
primary: {
|
||||
gradient: primary.gradient,
|
||||
featuredImage: primary.featured_image,
|
||||
description: primary.description,
|
||||
addon: primary.addon ? createInternalAddon(primary.addon) : undefined,
|
||||
external: primary.external || undefined,
|
||||
},
|
||||
secondary,
|
||||
};
|
||||
};
|
||||
|
||||
const reducer = (
|
||||
state: HomeState = initialState,
|
||||
action: Action,
|
||||
|
@ -108,14 +194,14 @@ const reducer = (
|
|||
case SET_CLIENT_APP:
|
||||
return initialState;
|
||||
|
||||
case FETCH_HOME_ADDONS:
|
||||
case FETCH_HOME_DATA:
|
||||
return {
|
||||
...state,
|
||||
resultsLoaded: false,
|
||||
};
|
||||
|
||||
case LOAD_HOME_ADDONS: {
|
||||
const { collections, shelves } = action.payload;
|
||||
case LOAD_HOME_DATA: {
|
||||
const { collections, heroShelves, shelves } = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
@ -130,6 +216,7 @@ const reducer = (
|
|||
}
|
||||
return null;
|
||||
}),
|
||||
heroShelves: createInternalHeroShelves(heroShelves),
|
||||
resultsLoaded: true,
|
||||
shelves: Object.keys(shelves).reduce((shelvesToLoad, shelfName) => {
|
||||
const response = shelves[shelfName];
|
||||
|
|
|
@ -3,11 +3,12 @@ import { oneLine } from 'common-tags';
|
|||
import { all, call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
|
||||
import { getCollectionAddons } from 'amo/api/collections';
|
||||
import { getHeroShelves } from 'amo/api/hero';
|
||||
import {
|
||||
LANDING_PAGE_EXTENSION_COUNT,
|
||||
LANDING_PAGE_THEME_COUNT,
|
||||
} from 'amo/constants';
|
||||
import { FETCH_HOME_ADDONS, loadHomeAddons } from 'amo/reducers/home';
|
||||
import { FETCH_HOME_DATA, loadHomeData } from 'amo/reducers/home';
|
||||
import {
|
||||
ADDON_TYPE_EXTENSION,
|
||||
ADDON_TYPE_THEME,
|
||||
|
@ -20,11 +21,11 @@ import { getAddonTypeFilter } from 'core/utils';
|
|||
import log from 'core/logger';
|
||||
import { createErrorHandler, getState } from 'core/sagas/utils';
|
||||
import type { GetCollectionAddonsParams } from 'amo/api/collections';
|
||||
import type { FetchHomeAddonsAction } from 'amo/reducers/home';
|
||||
import type { FetchHomeDataAction } from 'amo/reducers/home';
|
||||
import type { SearchParams } from 'core/api/search';
|
||||
import type { Saga } from 'core/types/sagas';
|
||||
|
||||
export function* fetchHomeAddons({
|
||||
export function* fetchHomeData({
|
||||
payload: {
|
||||
collectionsToFetch,
|
||||
enableFeatureRecommendedBadges,
|
||||
|
@ -32,7 +33,7 @@ export function* fetchHomeAddons({
|
|||
includeRecommendedThemes,
|
||||
includeTrendingExtensions,
|
||||
},
|
||||
}: FetchHomeAddonsAction): Saga {
|
||||
}: FetchHomeDataAction): Saga {
|
||||
const errorHandler = createErrorHandler(errorHandlerId);
|
||||
|
||||
yield put(errorHandler.createClearingAction());
|
||||
|
@ -40,6 +41,14 @@ export function* fetchHomeAddons({
|
|||
try {
|
||||
const state = yield select(getState);
|
||||
|
||||
let heroShelves = null;
|
||||
try {
|
||||
heroShelves = yield call(getHeroShelves, { api: state.api });
|
||||
} catch (error) {
|
||||
log.warn(`Home hero shelves failed to load: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const collections = [];
|
||||
for (const collection of collectionsToFetch) {
|
||||
try {
|
||||
|
@ -131,8 +140,9 @@ export function* fetchHomeAddons({
|
|||
}
|
||||
|
||||
yield put(
|
||||
loadHomeAddons({
|
||||
loadHomeData({
|
||||
collections,
|
||||
heroShelves,
|
||||
shelves,
|
||||
}),
|
||||
);
|
||||
|
@ -142,5 +152,5 @@ export function* fetchHomeAddons({
|
|||
}
|
||||
|
||||
export default function* homeSaga(): Saga {
|
||||
yield takeLatest(FETCH_HOME_ADDONS, fetchHomeAddons);
|
||||
yield takeLatest(FETCH_HOME_DATA, fetchHomeData);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
LOAD_CURRENT_COLLECTION,
|
||||
LOAD_CURRENT_COLLECTION_PAGE,
|
||||
} from 'amo/reducers/collections';
|
||||
import { LOAD_HOME_ADDONS } from 'amo/reducers/home';
|
||||
import { LOAD_HOME_DATA } from 'amo/reducers/home';
|
||||
import { LOAD_LANDING } from 'amo/reducers/landing';
|
||||
import { LOAD_RECOMMENDATIONS } from 'amo/reducers/recommendations';
|
||||
import {
|
||||
|
@ -474,7 +474,7 @@ const reducer = (
|
|||
};
|
||||
}
|
||||
|
||||
case LOAD_HOME_ADDONS: {
|
||||
case LOAD_HOME_DATA: {
|
||||
const { collections, shelves } = action.payload;
|
||||
|
||||
const newVersions = {};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import * as api from 'core/api';
|
||||
import { getHeroShelves } from 'amo/api/hero';
|
||||
import { createApiResponse, dispatchClientMetadata } from 'tests/unit/helpers';
|
||||
|
||||
describe(__filename, () => {
|
||||
it('calls the hero API', async () => {
|
||||
const mockApi = sinon.mock(api);
|
||||
const apiState = dispatchClientMetadata().store.getState().api;
|
||||
|
||||
mockApi
|
||||
.expects('callApi')
|
||||
.withArgs({
|
||||
auth: true,
|
||||
endpoint: 'hero',
|
||||
apiState,
|
||||
})
|
||||
.once()
|
||||
.returns(createApiResponse());
|
||||
|
||||
await getHeroShelves({ api: apiState });
|
||||
mockApi.verify();
|
||||
});
|
||||
});
|
|
@ -15,7 +15,7 @@ import HeadLinks from 'amo/components/HeadLinks';
|
|||
import HeadMetaTags from 'amo/components/HeadMetaTags';
|
||||
import HeroRecommendation from 'amo/components/HeroRecommendation';
|
||||
import LandingAddonsCard from 'amo/components/LandingAddonsCard';
|
||||
import { fetchHomeAddons, loadHomeAddons } from 'amo/reducers/home';
|
||||
import { fetchHomeData, loadHomeData } from 'amo/reducers/home';
|
||||
import { createInternalCollection } from 'amo/reducers/collections';
|
||||
import { createApiError } from 'core/api/index';
|
||||
import {
|
||||
|
@ -36,6 +36,7 @@ import {
|
|||
createFakeCollectionAddons,
|
||||
createFakeCollectionAddonsListResponse,
|
||||
createFakeCollectionDetail,
|
||||
createHeroShelves,
|
||||
createStubErrorHandler,
|
||||
dispatchClientMetadata,
|
||||
fakeAddon,
|
||||
|
@ -245,7 +246,7 @@ describe(__filename, () => {
|
|||
sinon.assert.calledWith(fakeDispatch, setViewContext(VIEW_CONTEXT_HOME));
|
||||
sinon.assert.calledWith(
|
||||
fakeDispatch,
|
||||
fetchHomeAddons({
|
||||
fetchHomeData({
|
||||
enableFeatureRecommendedBadges,
|
||||
errorHandlerId: errorHandler.id,
|
||||
collectionsToFetch: FEATURED_COLLECTIONS,
|
||||
|
@ -279,7 +280,7 @@ describe(__filename, () => {
|
|||
sinon.assert.calledWith(fakeDispatch, setViewContext(VIEW_CONTEXT_HOME));
|
||||
sinon.assert.calledWith(
|
||||
fakeDispatch,
|
||||
fetchHomeAddons({
|
||||
fetchHomeData({
|
||||
enableFeatureRecommendedBadges,
|
||||
errorHandlerId: errorHandler.id,
|
||||
collectionsToFetch: FEATURED_COLLECTIONS,
|
||||
|
@ -301,8 +302,9 @@ describe(__filename, () => {
|
|||
const recommendedExtensions = createAddonsApiResult(addons);
|
||||
|
||||
store.dispatch(
|
||||
loadHomeAddons({
|
||||
loadHomeData({
|
||||
collections,
|
||||
heroShelves: createHeroShelves(),
|
||||
shelves: { recommendedExtensions },
|
||||
}),
|
||||
);
|
||||
|
@ -352,7 +354,7 @@ describe(__filename, () => {
|
|||
sinon.assert.calledWith(fakeDispatch, setViewContext(VIEW_CONTEXT_HOME));
|
||||
sinon.assert.calledWith(
|
||||
fakeDispatch,
|
||||
fetchHomeAddons({
|
||||
fetchHomeData({
|
||||
enableFeatureRecommendedBadges,
|
||||
errorHandlerId: root.instance().props.errorHandler.id,
|
||||
collectionsToFetch: FEATURED_COLLECTIONS,
|
||||
|
@ -371,8 +373,9 @@ describe(__filename, () => {
|
|||
const recommendedExtensions = createAddonsApiResult(addons);
|
||||
|
||||
store.dispatch(
|
||||
loadHomeAddons({
|
||||
loadHomeData({
|
||||
collections,
|
||||
heroShelves: createHeroShelves(),
|
||||
shelves: { recommendedExtensions },
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -3,9 +3,10 @@ import {
|
|||
LANDING_PAGE_THEME_COUNT,
|
||||
} from 'amo/constants';
|
||||
import homeReducer, {
|
||||
fetchHomeAddons,
|
||||
createInternalHeroShelves,
|
||||
fetchHomeData,
|
||||
initialState,
|
||||
loadHomeAddons,
|
||||
loadHomeData,
|
||||
} from 'amo/reducers/home';
|
||||
import { createInternalAddon } from 'core/reducers/addons';
|
||||
import { ADDON_TYPE_THEME, CLIENT_APP_FIREFOX } from 'core/constants';
|
||||
|
@ -14,16 +15,26 @@ import {
|
|||
createAddonsApiResult,
|
||||
createFakeCollectionAddon,
|
||||
createFakeCollectionAddonsListResponse,
|
||||
createPrimaryHeroShelf,
|
||||
createSecondaryHeroShelf,
|
||||
dispatchClientMetadata,
|
||||
fakeAddon,
|
||||
fakePrimaryHeroShelfExternal,
|
||||
createHeroShelves,
|
||||
} from 'tests/unit/helpers';
|
||||
|
||||
describe(__filename, () => {
|
||||
describe('reducer', () => {
|
||||
const _loadHomeAddons = ({ store, collections = [], shelves = {} }) => {
|
||||
const _loadHomeData = ({
|
||||
store,
|
||||
collections = [],
|
||||
heroShelves = createHeroShelves(),
|
||||
shelves = {},
|
||||
}) => {
|
||||
store.dispatch(
|
||||
loadHomeAddons({
|
||||
loadHomeData({
|
||||
collections,
|
||||
heroShelves,
|
||||
shelves,
|
||||
}),
|
||||
);
|
||||
|
@ -42,7 +53,7 @@ describe(__filename, () => {
|
|||
it('loads collections', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
|
||||
_loadHomeAddons({
|
||||
_loadHomeData({
|
||||
store,
|
||||
collections: [
|
||||
createFakeCollectionAddonsListResponse({
|
||||
|
@ -72,7 +83,7 @@ describe(__filename, () => {
|
|||
const addon1 = { ...fakeAddon, slug: 'addon1' };
|
||||
const addon2 = { ...fakeAddon, slug: 'addon2' };
|
||||
|
||||
_loadHomeAddons({
|
||||
_loadHomeData({
|
||||
store,
|
||||
shelves: {
|
||||
[shelfName1]: createAddonsApiResult([addon1]),
|
||||
|
@ -90,13 +101,29 @@ describe(__filename, () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('loads hero shelves', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
|
||||
const heroShelves = createHeroShelves();
|
||||
_loadHomeData({
|
||||
store,
|
||||
heroShelves,
|
||||
});
|
||||
|
||||
const homeState = store.getState().home;
|
||||
|
||||
expect(homeState.heroShelves).toEqual(
|
||||
createInternalHeroShelves(heroShelves),
|
||||
);
|
||||
});
|
||||
|
||||
it('sets null when a shelf has no response', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
const shelfName1 = 'someShelfName1';
|
||||
const shelfName2 = 'someShelfName2';
|
||||
const addon1 = { ...fakeAddon, slug: 'addon1' };
|
||||
|
||||
_loadHomeAddons({
|
||||
_loadHomeData({
|
||||
store,
|
||||
shelves: {
|
||||
[shelfName1]: createAddonsApiResult([addon1]),
|
||||
|
@ -115,7 +142,7 @@ describe(__filename, () => {
|
|||
it('loads the the correct amount of theme add-ons in a collection to display on homepage', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
|
||||
_loadHomeAddons({
|
||||
_loadHomeData({
|
||||
store,
|
||||
collections: [
|
||||
createFakeCollectionAddonsListResponse({
|
||||
|
@ -149,7 +176,7 @@ describe(__filename, () => {
|
|||
it('loads a null for a missing collection', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
|
||||
_loadHomeAddons({
|
||||
_loadHomeData({
|
||||
store,
|
||||
collections: [null],
|
||||
});
|
||||
|
@ -163,7 +190,7 @@ describe(__filename, () => {
|
|||
it('returns null for an empty collection', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
|
||||
_loadHomeAddons({
|
||||
_loadHomeData({
|
||||
store,
|
||||
collections: [
|
||||
createFakeCollectionAddonsListResponse({
|
||||
|
@ -181,7 +208,7 @@ describe(__filename, () => {
|
|||
|
||||
const state = homeReducer(
|
||||
loadedState,
|
||||
fetchHomeAddons({
|
||||
fetchHomeData({
|
||||
collectionsToFetch: [],
|
||||
errorHandlerId: 'some-error-handler-id',
|
||||
includeFeaturedThemes: true,
|
||||
|
@ -194,7 +221,7 @@ describe(__filename, () => {
|
|||
it('resets the state when clientApp changes', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
|
||||
_loadHomeAddons({
|
||||
_loadHomeData({
|
||||
store,
|
||||
collections: [
|
||||
createFakeCollectionAddonsListResponse({
|
||||
|
@ -210,4 +237,101 @@ describe(__filename, () => {
|
|||
expect(state).toEqual(initialState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createInternalHeroShelves', () => {
|
||||
it('creates an internal representation of hero shelves', () => {
|
||||
const addon = fakeAddon;
|
||||
const heroShelves = createHeroShelves({
|
||||
primaryProps: { addon, external: undefined },
|
||||
});
|
||||
|
||||
expect(createInternalHeroShelves(heroShelves)).toEqual({
|
||||
primary: {
|
||||
addon: createInternalAddon(addon),
|
||||
description: heroShelves.primary.description,
|
||||
external: undefined,
|
||||
featuredImage: heroShelves.primary.featured_image,
|
||||
gradient: {
|
||||
end: heroShelves.primary.gradient.end,
|
||||
start: heroShelves.primary.gradient.start,
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
cta: heroShelves.secondary.cta,
|
||||
description: heroShelves.secondary.description,
|
||||
headline: heroShelves.secondary.headline,
|
||||
modules: heroShelves.secondary.modules,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('works when an addon is not defined', () => {
|
||||
const external = fakePrimaryHeroShelfExternal;
|
||||
const heroShelves = createHeroShelves({
|
||||
primaryProps: {
|
||||
addon: undefined,
|
||||
external,
|
||||
},
|
||||
});
|
||||
|
||||
expect(createInternalHeroShelves(heroShelves).primary).toMatchObject({
|
||||
addon: undefined,
|
||||
external,
|
||||
});
|
||||
});
|
||||
|
||||
it('works when external is not defined', () => {
|
||||
const addon = fakeAddon;
|
||||
const heroShelves = createHeroShelves({
|
||||
primaryProps: {
|
||||
addon,
|
||||
external: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(createInternalHeroShelves(heroShelves).primary).toMatchObject({
|
||||
addon: createInternalAddon(addon),
|
||||
external: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('works when primary description is null', () => {
|
||||
const addon = fakeAddon;
|
||||
const heroShelves = createHeroShelves({
|
||||
primaryProps: {
|
||||
addon,
|
||||
description: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(createInternalHeroShelves(heroShelves).primary).toMatchObject({
|
||||
addon: createInternalAddon(addon),
|
||||
description: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('works when secondary cta is null', () => {
|
||||
const heroShelves = createHeroShelves({
|
||||
primaryProps: { addon: fakeAddon },
|
||||
secondaryProps: { cta: null },
|
||||
});
|
||||
|
||||
expect(createInternalHeroShelves(heroShelves).secondary).toMatchObject({
|
||||
cta: null,
|
||||
description: heroShelves.secondary.description,
|
||||
});
|
||||
});
|
||||
|
||||
it(`works when a secondary module's cta is null`, () => {
|
||||
const primaryShelf = createPrimaryHeroShelf({ addon: fakeAddon });
|
||||
const secondaryShelf = createSecondaryHeroShelf();
|
||||
// Replace the default cta in module 1 with null.
|
||||
secondaryShelf.modules[0].cta = null;
|
||||
const heroShelves = { primary: primaryShelf, secondary: secondaryShelf };
|
||||
|
||||
expect(createInternalHeroShelves(heroShelves).secondary).toMatchObject({
|
||||
modules: heroShelves.secondary.modules,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import SagaTester from 'redux-saga-tester';
|
||||
|
||||
import * as collectionsApi from 'amo/api/collections';
|
||||
import * as heroApi from 'amo/api/hero';
|
||||
import {
|
||||
LANDING_PAGE_EXTENSION_COUNT,
|
||||
LANDING_PAGE_THEME_COUNT,
|
||||
} from 'amo/constants';
|
||||
import homeReducer, {
|
||||
fetchHomeAddons,
|
||||
loadHomeAddons,
|
||||
} from 'amo/reducers/home';
|
||||
import homeReducer, { fetchHomeData, loadHomeData } from 'amo/reducers/home';
|
||||
import homeSaga from 'amo/sagas/home';
|
||||
import { createApiError } from 'core/api';
|
||||
import * as searchApi from 'core/api/search';
|
||||
|
@ -23,6 +21,7 @@ import apiReducer from 'core/reducers/api';
|
|||
import {
|
||||
createAddonsApiResult,
|
||||
createFakeCollectionAddonsListResponse,
|
||||
createHeroShelves,
|
||||
createStubErrorHandler,
|
||||
dispatchClientMetadata,
|
||||
fakeAddon,
|
||||
|
@ -33,12 +32,14 @@ import { getAddonTypeFilter } from 'core/utils';
|
|||
describe(__filename, () => {
|
||||
let errorHandler;
|
||||
let mockCollectionsApi;
|
||||
let mockHeroApi;
|
||||
let mockSearchApi;
|
||||
let sagaTester;
|
||||
|
||||
beforeEach(() => {
|
||||
errorHandler = createStubErrorHandler();
|
||||
mockCollectionsApi = sinon.mock(collectionsApi);
|
||||
mockHeroApi = sinon.mock(heroApi);
|
||||
mockSearchApi = sinon.mock(searchApi);
|
||||
sagaTester = new SagaTester({
|
||||
initialState: dispatchClientMetadata().state,
|
||||
|
@ -50,10 +51,10 @@ describe(__filename, () => {
|
|||
sagaTester.start(homeSaga);
|
||||
});
|
||||
|
||||
describe('fetchHomeAddons', () => {
|
||||
function _fetchHomeAddons(params) {
|
||||
describe('fetchHomeData', () => {
|
||||
function _fetchHomeData(params) {
|
||||
sagaTester.dispatch(
|
||||
fetchHomeAddons({
|
||||
fetchHomeData({
|
||||
collectionsToFetch: [{ slug: 'some-slug', user: 'some-user' }],
|
||||
enableFeatureRecommendedBadges: true,
|
||||
errorHandlerId: errorHandler.id,
|
||||
|
@ -65,7 +66,7 @@ describe(__filename, () => {
|
|||
}
|
||||
|
||||
it.each([true, false])(
|
||||
'calls the API to fetch the add-ons to display on home, enableFeatureRecommendedBadges: %s',
|
||||
'calls the API to fetch the data to display on home, enableFeatureRecommendedBadges: %s',
|
||||
async (enableFeatureRecommendedBadges) => {
|
||||
const state = sagaTester.getState();
|
||||
|
||||
|
@ -167,7 +168,13 @@ describe(__filename, () => {
|
|||
})
|
||||
.returns(Promise.resolve(recommendedExtensions));
|
||||
|
||||
_fetchHomeAddons({
|
||||
const heroShelves = createHeroShelves();
|
||||
mockHeroApi
|
||||
.expects('getHeroShelves')
|
||||
.withArgs(baseArgs)
|
||||
.returns(Promise.resolve(heroShelves));
|
||||
|
||||
_fetchHomeData({
|
||||
collectionsToFetch: [
|
||||
{ slug: firstCollectionSlug, userId: firstCollectionUserId },
|
||||
{ slug: secondCollectionSlug, userId: secondCollectionUserId },
|
||||
|
@ -176,8 +183,9 @@ describe(__filename, () => {
|
|||
includeRecommendedThemes: true,
|
||||
});
|
||||
|
||||
const loadAction = loadHomeAddons({
|
||||
const loadAction = loadHomeData({
|
||||
collections,
|
||||
heroShelves,
|
||||
shelves: {
|
||||
recommendedExtensions,
|
||||
recommendedThemes,
|
||||
|
@ -216,13 +224,19 @@ describe(__filename, () => {
|
|||
const popularThemes = createAddonsApiResult([fakeAddon]);
|
||||
mockSearchApi.expects('search').returns(Promise.resolve(popularThemes));
|
||||
|
||||
_fetchHomeAddons({
|
||||
const heroShelves = createHeroShelves();
|
||||
mockHeroApi
|
||||
.expects('getHeroShelves')
|
||||
.returns(Promise.resolve(heroShelves));
|
||||
|
||||
_fetchHomeData({
|
||||
collectionsToFetch: [],
|
||||
includeTrendingExtensions: false,
|
||||
});
|
||||
|
||||
const loadAction = loadHomeAddons({
|
||||
const loadAction = loadHomeData({
|
||||
collections,
|
||||
heroShelves,
|
||||
shelves: {
|
||||
recommendedExtensions,
|
||||
recommendedThemes,
|
||||
|
@ -258,13 +272,19 @@ describe(__filename, () => {
|
|||
.expects('search')
|
||||
.returns(Promise.resolve(trendingExtensions));
|
||||
|
||||
_fetchHomeAddons({
|
||||
const heroShelves = createHeroShelves();
|
||||
mockHeroApi
|
||||
.expects('getHeroShelves')
|
||||
.returns(Promise.resolve(heroShelves));
|
||||
|
||||
_fetchHomeData({
|
||||
collectionsToFetch: [],
|
||||
includeRecommendedThemes: false,
|
||||
});
|
||||
|
||||
const loadAction = loadHomeAddons({
|
||||
const loadAction = loadHomeData({
|
||||
collections,
|
||||
heroShelves,
|
||||
shelves: {
|
||||
recommendedExtensions,
|
||||
recommendedThemes: null,
|
||||
|
@ -311,15 +331,21 @@ describe(__filename, () => {
|
|||
.expects('search')
|
||||
.returns(Promise.resolve(trendingExtensions));
|
||||
|
||||
_fetchHomeAddons({
|
||||
const heroShelves = createHeroShelves();
|
||||
mockHeroApi
|
||||
.expects('getHeroShelves')
|
||||
.returns(Promise.resolve(heroShelves));
|
||||
|
||||
_fetchHomeData({
|
||||
collectionsToFetch: [
|
||||
{ slug: firstCollectionSlug, userId: firstCollectionUserId },
|
||||
],
|
||||
includeRecommendedThemes: false,
|
||||
});
|
||||
|
||||
const loadAction = loadHomeAddons({
|
||||
const loadAction = loadHomeData({
|
||||
collections,
|
||||
heroShelves,
|
||||
shelves: {
|
||||
recommendedExtensions,
|
||||
recommendedThemes: null,
|
||||
|
@ -335,7 +361,7 @@ describe(__filename, () => {
|
|||
);
|
||||
|
||||
it('clears the error handler', async () => {
|
||||
_fetchHomeAddons();
|
||||
_fetchHomeData();
|
||||
|
||||
const errorAction = errorHandler.createClearingAction();
|
||||
|
||||
|
@ -346,11 +372,15 @@ describe(__filename, () => {
|
|||
it('dispatches an error for a failed collection fetch', async () => {
|
||||
const error = createApiError({ response: { status: 500 } });
|
||||
|
||||
mockHeroApi
|
||||
.expects('getHeroShelves')
|
||||
.returns(Promise.resolve(createHeroShelves()));
|
||||
|
||||
mockCollectionsApi
|
||||
.expects('getCollectionAddons')
|
||||
.returns(Promise.reject(error));
|
||||
|
||||
_fetchHomeAddons();
|
||||
_fetchHomeData();
|
||||
|
||||
const errorAction = errorHandler.createErrorAction(error);
|
||||
const expectedAction = await sagaTester.waitFor(errorAction.type);
|
||||
|
@ -363,13 +393,15 @@ describe(__filename, () => {
|
|||
const slug = 'collection-slug';
|
||||
const userId = 123;
|
||||
|
||||
const baseArgs = { api: state.api };
|
||||
mockHeroApi
|
||||
.expects('getHeroShelves')
|
||||
.returns(Promise.resolve(createHeroShelves()));
|
||||
|
||||
const firstCollection = createFakeCollectionAddonsListResponse();
|
||||
mockCollectionsApi
|
||||
.expects('getCollectionAddons')
|
||||
.withArgs({
|
||||
...baseArgs,
|
||||
api: state.api,
|
||||
slug,
|
||||
userId,
|
||||
})
|
||||
|
@ -382,7 +414,19 @@ describe(__filename, () => {
|
|||
.exactly(5)
|
||||
.returns(Promise.reject(error));
|
||||
|
||||
_fetchHomeAddons({ collectionsToFetch: [{ slug, userId }] });
|
||||
_fetchHomeData({ collectionsToFetch: [{ slug, userId }] });
|
||||
|
||||
const errorAction = errorHandler.createErrorAction(error);
|
||||
const expectedAction = await sagaTester.waitFor(errorAction.type);
|
||||
expect(expectedAction).toEqual(errorAction);
|
||||
});
|
||||
|
||||
it('dispatches an error for a failed hero fetch', async () => {
|
||||
const error = createApiError({ response: { status: 500 } });
|
||||
|
||||
mockHeroApi.expects('getHeroShelves').returns(Promise.reject(error));
|
||||
|
||||
_fetchHomeData();
|
||||
|
||||
const errorAction = errorHandler.createErrorAction(error);
|
||||
const expectedAction = await sagaTester.waitFor(errorAction.type);
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
loadCurrentCollectionPage,
|
||||
loadCurrentCollection,
|
||||
} from 'amo/reducers/collections';
|
||||
import { loadHomeAddons } from 'amo/reducers/home';
|
||||
import { loadHomeData } from 'amo/reducers/home';
|
||||
import { loadLanding } from 'amo/reducers/landing';
|
||||
import {
|
||||
OUTCOME_RECOMMENDED,
|
||||
|
@ -524,11 +524,11 @@ describe(__filename, () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('LOAD_HOME_ADDONS', () => {
|
||||
describe('LOAD_HOME_DATA', () => {
|
||||
it('loads versions from shelves', () => {
|
||||
const state = versionsReducer(
|
||||
undefined,
|
||||
loadHomeAddons({
|
||||
loadHomeData({
|
||||
collections: [],
|
||||
shelves: {
|
||||
recommendedExtensions: createAddonsApiResult([
|
||||
|
@ -566,7 +566,7 @@ describe(__filename, () => {
|
|||
|
||||
state = versionsReducer(
|
||||
state,
|
||||
loadHomeAddons({
|
||||
loadHomeData({
|
||||
collections: [],
|
||||
shelves: {
|
||||
recommendedExtensions: searchResult,
|
||||
|
@ -585,7 +585,7 @@ describe(__filename, () => {
|
|||
it('handles invalid shelves', () => {
|
||||
const state = versionsReducer(
|
||||
undefined,
|
||||
loadHomeAddons({
|
||||
loadHomeData({
|
||||
collections: [],
|
||||
shelves: {
|
||||
recommendedExtensions: null,
|
||||
|
@ -608,7 +608,7 @@ describe(__filename, () => {
|
|||
|
||||
const state = versionsReducer(
|
||||
undefined,
|
||||
loadHomeAddons({
|
||||
loadHomeData({
|
||||
collections: [
|
||||
{ results: [fakeCollectionAddon1] },
|
||||
{ results: [fakeCollectionAddon2] },
|
||||
|
|
|
@ -274,6 +274,70 @@ export const fakeAddonInfo = {
|
|||
privacy_policy: ' some privacy policy text',
|
||||
};
|
||||
|
||||
export const fakePrimaryHeroShelfExternal = Object.freeze({
|
||||
id: 1,
|
||||
guid: 'some-guid',
|
||||
homepage: 'https://mozilla.org',
|
||||
name: 'some external name',
|
||||
type: ADDON_TYPE_EXTENSION,
|
||||
});
|
||||
|
||||
export const createPrimaryHeroShelf = ({
|
||||
addon = undefined,
|
||||
description = 'Primary shelf description',
|
||||
external = undefined,
|
||||
featuredImage = 'https://addons-dev-cdn.allizom.org/static/img/hero/featured/teamaddons.jpg',
|
||||
gradient = { start: '000000', end: 'FFFFFF' },
|
||||
} = {}) => {
|
||||
return {
|
||||
addon,
|
||||
description,
|
||||
external,
|
||||
featured_image: featuredImage,
|
||||
gradient,
|
||||
};
|
||||
};
|
||||
|
||||
export const createSecondaryHeroShelf = ({
|
||||
cta = { url: 'https://mozilla.org', text: 'Some cta text' },
|
||||
description = 'Secondary shelf description',
|
||||
headline = 'Secondary shelf headline',
|
||||
modules = [
|
||||
{
|
||||
icon: 'icon1',
|
||||
description: 'module 1 description',
|
||||
cta: { url: 'https://mozilla.org/1', text: 'module 1 cta text' },
|
||||
},
|
||||
{
|
||||
icon: 'icon2',
|
||||
description: 'module 2 description',
|
||||
cta: { url: 'https://mozilla.org/2', text: 'module 2 cta text' },
|
||||
},
|
||||
{
|
||||
icon: 'icon3',
|
||||
description: 'module 3 description',
|
||||
cta: { url: 'https://mozilla.org/3', text: 'module 3 cta text' },
|
||||
},
|
||||
],
|
||||
} = {}) => {
|
||||
return {
|
||||
cta,
|
||||
description,
|
||||
headline,
|
||||
modules,
|
||||
};
|
||||
};
|
||||
|
||||
export const createHeroShelves = ({
|
||||
primaryProps = {},
|
||||
secondaryProps = {},
|
||||
} = {}) => {
|
||||
return {
|
||||
primary: createPrimaryHeroShelf(primaryProps),
|
||||
secondary: createSecondaryHeroShelf(secondaryProps),
|
||||
};
|
||||
};
|
||||
|
||||
export const onLocationChanged = ({ pathname, search = '', ...others }) => {
|
||||
const history = addQueryParamsToHistory({
|
||||
history: createMemoryHistory({
|
||||
|
|
Загрузка…
Ссылка в новой задаче