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:
Bob Silverberg 2019-08-26 10:41:41 -04:00 коммит произвёл GitHub
Родитель 4f4a5f0de4
Коммит 872f2cd993
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 447 добавлений и 76 удалений

16
src/amo/api/hero.js Normal file
Просмотреть файл

@ -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({