chore: Refactor otherAddonsByAuthors to just addonsByAuthors

Allow fetchAddonsByAuthors to search for add-ons without specifying an
`exclude_addon` param.

Fixes #4556.
This commit is contained in:
Matthew Riley MacPherson (tofumatt) 2018-03-13 21:07:58 +00:00
Родитель 148e3177e3
Коммит 36f3f3a288
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 244CF08D60266A0C
8 изменённых файлов: 385 добавлений и 228 удалений

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

@ -158,6 +158,7 @@
"classnames": "2.2.5",
"common-tags": "1.7.2",
"config": "1.29.2",
"deepcopy": "0.6.3",
"deep-eql": "3.0.1",
"dompurify": "1.0.2",
"es6-error": "4.1.0",
@ -239,7 +240,6 @@
"content-security-policy-parser": "^0.1.0",
"cookie": "^0.3.1",
"css-loader": "^0.28.3",
"deepcopy": "^0.6.3",
"enzyme": "^3.2.0",
"enzyme-adapter-react-16": "^1.1.0",
"eslint": "^4.15.0",

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

@ -20,7 +20,10 @@ import PermissionsCard from 'amo/components/PermissionsCard';
import DefaultRatingManager from 'amo/components/RatingManager';
import ScreenShots from 'amo/components/ScreenShots';
import Link from 'amo/components/Link';
import { fetchOtherAddonsByAuthors } from 'amo/reducers/addonsByAuthors';
import {
fetchAddonsByAuthors,
getAddonsForSlug,
} from 'amo/reducers/addonsByAuthors';
import {
fetchAddon,
getAddonByID,
@ -119,7 +122,7 @@ export class AddonBase extends React.Component {
}
dispatch(setViewContext(addon.type));
this.dispatchFetchOtherAddonsByAuthors({ addon });
this.dispatchFetchAddonsByAuthors({ addon });
} else {
dispatch(fetchAddon({ slug: params.slug, errorHandler }));
}
@ -139,7 +142,7 @@ export class AddonBase extends React.Component {
}
if (newAddon && oldAddon !== newAddon) {
this.dispatchFetchOtherAddonsByAuthors({ addon: newAddon });
this.dispatchFetchAddonsByAuthors({ addon: newAddon });
}
}
@ -159,14 +162,14 @@ export class AddonBase extends React.Component {
this.props.toggleThemePreview(event.currentTarget);
}
dispatchFetchOtherAddonsByAuthors({ addon }) {
dispatchFetchAddonsByAuthors({ addon }) {
const { dispatch, errorHandler } = this.props;
dispatch(fetchOtherAddonsByAuthors({
dispatch(fetchAddonsByAuthors({
addonType: addon.type,
authors: addon.authors.map((author) => author.username),
errorHandlerId: errorHandler.id,
slug: addon.slug,
forAddonSlug: addon.slug,
}));
}
@ -629,7 +632,7 @@ export function mapStateToProps(state, ownProps) {
let installedAddon = {};
if (addon) {
addonsByAuthors = state.addonsByAuthors.byAddonSlug[addon.slug];
addonsByAuthors = getAddonsForSlug(state.addonsByAuthors, addon.slug);
installedAddon = state.installations[addon.guid] || {};
}

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

@ -1,125 +1,165 @@
/* @flow */
import type { AddonType, ExternalAddonType } from 'core/types/addons';
import deepcopy from 'deepcopy';
import invariant from 'invariant';
import { createInternalAddon } from 'core/reducers/addons';
import type { AddonType, ExternalAddonType } from 'core/types/addons';
type State = {
byAddonSlug: { [string]: AddonType },
// TODO: It might be nice to eventually stop storing add-ons in this
// reducer at all and rely on the add-ons in the `addons` reducer.
// That said, these are partial add-ons returned from the search
// results and fetching all add-on data for each add-on might be too
// expensive.
byAddonId: { [number]: Array<AddonType> },
byAddonSlug: { [string]: Array<number> },
byUserId: { [number]: Array<number> },
byUsername: { [string]: Array<number> },
};
export const initialState: State = {
byAddonId: {},
byAddonSlug: {},
byUserId: {},
byUsername: {},
};
export const OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE = 6;
export const ADDONS_BY_AUTHORS_PAGE_SIZE = 6;
// For further information about this notation, see:
// https://github.com/mozilla/addons-frontend/pull/3027#discussion_r137661289
export const FETCH_OTHER_ADDONS_BY_AUTHORS: 'FETCH_OTHER_ADDONS_BY_AUTHORS'
= 'FETCH_OTHER_ADDONS_BY_AUTHORS';
export const LOAD_OTHER_ADDONS_BY_AUTHORS: 'LOAD_OTHER_ADDONS_BY_AUTHORS'
= 'LOAD_OTHER_ADDONS_BY_AUTHORS';
export const FETCH_ADDONS_BY_AUTHORS: 'FETCH_ADDONS_BY_AUTHORS'
= 'FETCH_ADDONS_BY_AUTHORS';
export const LOAD_ADDONS_BY_AUTHORS: 'LOAD_ADDONS_BY_AUTHORS'
= 'LOAD_ADDONS_BY_AUTHORS';
type FetchOtherAddonsByAuthorsParams = {|
type FetchAddonsByAuthorsParams = {|
addonType: string,
authors: Array<string>,
errorHandlerId: string,
slug: string,
forAddonSlug?: string,
|};
type FetchOtherAddonsByAuthorsAction = {|
type: typeof FETCH_OTHER_ADDONS_BY_AUTHORS,
payload: FetchOtherAddonsByAuthorsParams,
type FetchAddonsByAuthorsAction = {|
type: typeof FETCH_ADDONS_BY_AUTHORS,
payload: FetchAddonsByAuthorsParams,
|};
export const fetchOtherAddonsByAuthors = (
{ addonType, authors, errorHandlerId, slug }: FetchOtherAddonsByAuthorsParams
): FetchOtherAddonsByAuthorsAction => {
if (!errorHandlerId) {
throw new Error('An errorHandlerId is required');
}
if (!slug) {
throw new Error('An add-on slug is required.');
}
if (!addonType) {
throw new Error('An add-on type is required.');
}
if (!authors) {
throw new Error('Authors are required.');
}
if (!Array.isArray(authors)) {
throw new Error('The authors parameter must be an array.');
}
export const fetchAddonsByAuthors = (
{ addonType, authors, errorHandlerId, forAddonSlug }: FetchAddonsByAuthorsParams
): FetchAddonsByAuthorsAction => {
invariant(errorHandlerId, 'An errorHandlerId is required');
invariant(addonType, 'An add-on type is required.');
invariant(authors, 'Authors are required.');
invariant(Array.isArray(authors), 'The authors parameter must be an array.');
return {
type: FETCH_OTHER_ADDONS_BY_AUTHORS,
type: FETCH_ADDONS_BY_AUTHORS,
payload: {
addonType,
authors,
errorHandlerId,
slug,
forAddonSlug,
},
};
};
type LoadOtherAddonsByAuthorsParams = {|
slug: string,
type LoadAddonsByAuthorsParams = {|
addons: Array<ExternalAddonType>,
forAddonSlug?: string,
|};
type LoadOtherAddonsByAuthorsAction = {|
type: typeof LOAD_OTHER_ADDONS_BY_AUTHORS,
payload: LoadOtherAddonsByAuthorsParams,
type LoadAddonsByAuthorsAction = {|
type: typeof LOAD_ADDONS_BY_AUTHORS,
payload: LoadAddonsByAuthorsParams,
|};
export const loadOtherAddonsByAuthors = (
{ addons, slug }: LoadOtherAddonsByAuthorsParams
): LoadOtherAddonsByAuthorsAction => {
if (!slug) {
throw new Error('An add-on slug is required.');
}
if (!addons) {
throw new Error('A set of add-ons is required.');
}
export const loadAddonsByAuthors = (
{ addons, forAddonSlug }: LoadAddonsByAuthorsParams
): LoadAddonsByAuthorsAction => {
invariant(addons, 'A set of add-ons is required.');
return {
type: LOAD_OTHER_ADDONS_BY_AUTHORS,
payload: { slug, addons },
type: LOAD_ADDONS_BY_AUTHORS,
payload: { addons, forAddonSlug },
};
};
export const getAddonsForSlug = (state: State, slug: string) => {
const ids = state.byAddonSlug[slug];
return ids ? ids.map((id) => {
return state.byAddonId[id];
}) : null;
};
type Action =
| FetchOtherAddonsByAuthorsAction
| LoadOtherAddonsByAuthorsAction;
| FetchAddonsByAuthorsAction
| LoadAddonsByAuthorsAction;
const reducer = (
state: State = initialState,
action: Action
): State => {
switch (action.type) {
case FETCH_OTHER_ADDONS_BY_AUTHORS:
return {
...state,
byAddonSlug: {
...state.byAddonSlug,
[action.payload.slug]: undefined,
},
};
case LOAD_OTHER_ADDONS_BY_AUTHORS:
return {
...state,
byAddonSlug: {
...state.byAddonSlug,
[action.payload.slug]: action.payload.addons
.slice(0, OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE)
.map((addon) => createInternalAddon(addon)),
},
};
case FETCH_ADDONS_BY_AUTHORS: {
const newState = deepcopy(state);
if (action.payload.forAddonSlug) {
newState.byAddonSlug = {
...newState.byAddonSlug,
[action.payload.forAddonSlug]: undefined,
};
}
// Reset the data for each author requested.
for (const authorUsername of action.payload.authors) {
// TODO: Reset the userId here too.
newState.byUsername[authorUsername] = undefined;
}
return newState;
}
case LOAD_ADDONS_BY_AUTHORS: {
const newState = deepcopy(state);
if (action.payload.forAddonSlug) {
newState.byAddonSlug = {
[action.payload.forAddonSlug]: action.payload.addons
.slice(0, ADDONS_BY_AUTHORS_PAGE_SIZE)
.map((addon) => addon.id),
};
}
const addons = action.payload.addons
.map((addon) => createInternalAddon(addon));
for (const addon of addons) {
newState.byAddonId[addon.id] = addon;
if (addon.authors) {
for (const author of addon.authors) {
if (!newState.byUserId[author.id]) {
newState.byUserId[author.id] = [];
}
if (!newState.byUsername[author.username]) {
newState.byUsername[author.username] = [];
}
if (!newState.byUserId[author.id].includes(addon.id)) {
newState.byUserId[author.id].push(addon.id);
}
if (!newState.byUsername[author.username].includes(addon.id)) {
newState.byUsername[author.username].push(addon.id);
}
}
}
}
return newState;
}
default:
return state;
}

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

@ -1,17 +1,17 @@
import { call, put, select, takeLatest } from 'redux-saga/effects';
import { SEARCH_SORT_TRENDING } from 'core/constants';
import {
FETCH_OTHER_ADDONS_BY_AUTHORS,
OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE,
loadOtherAddonsByAuthors,
FETCH_ADDONS_BY_AUTHORS,
ADDONS_BY_AUTHORS_PAGE_SIZE,
loadAddonsByAuthors,
} from 'amo/reducers/addonsByAuthors';
import { search as searchApi } from 'core/api/search';
import log from 'core/logger';
import { createErrorHandler, getState } from 'core/sagas/utils';
export function* fetchOtherAddonsByAuthors({ payload }) {
const { errorHandlerId, authors, slug, addonType } = payload;
export function* fetchAddonsByAuthors({ payload }) {
const { errorHandlerId, authors, addonType, forAddonSlug } = payload;
const errorHandler = createErrorHandler(errorHandlerId);
yield put(errorHandler.createClearingAction());
@ -24,8 +24,8 @@ export function* fetchOtherAddonsByAuthors({ payload }) {
filters: {
addonType,
author: authors.join(','),
exclude_addons: slug,
page_size: OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE,
exclude_addons: forAddonSlug,
page_size: ADDONS_BY_AUTHORS_PAGE_SIZE,
sort: SEARCH_SORT_TRENDING,
},
});
@ -34,7 +34,7 @@ export function* fetchOtherAddonsByAuthors({ payload }) {
// https://github.com/mozilla/addons-frontend/issues/2917 is done.
const addons = Object.values(response.entities.addons || {});
yield put(loadOtherAddonsByAuthors({ addons, slug }));
yield put(loadAddonsByAuthors({ addons, forAddonSlug }));
} catch (error) {
log.warn(`Search for addons by authors results failed to load: ${error}`);
yield put(errorHandler.createErrorAction(error));
@ -42,5 +42,5 @@ export function* fetchOtherAddonsByAuthors({ payload }) {
}
export default function* addonsByAuthorsSaga() {
yield takeLatest(FETCH_OTHER_ADDONS_BY_AUTHORS, fetchOtherAddonsByAuthors);
yield takeLatest(FETCH_ADDONS_BY_AUTHORS, fetchAddonsByAuthors);
}

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

@ -31,8 +31,8 @@ import {
createInternalAddon, fetchAddon as fetchAddonAction, loadAddons,
} from 'core/reducers/addons';
import {
fetchOtherAddonsByAuthors,
loadOtherAddonsByAuthors,
fetchAddonsByAuthors,
loadAddonsByAuthors,
} from 'amo/reducers/addonsByAuthors';
import { setError } from 'core/actions/errors';
import { setInstallState } from 'core/actions/installations';
@ -140,10 +140,10 @@ describe(__filename, () => {
return loadAddons(createFetchAddonResult(addon).entities);
};
const _loadOtherAddonsByAuthors = ({ addon, addonsByAuthors }) => {
return loadOtherAddonsByAuthors({
slug: addon.slug,
const _loadAddonsByAuthors = ({ addon, addonsByAuthors }) => {
return loadAddonsByAuthors({
addons: addonsByAuthors,
forAddonSlug: addon.slug,
});
};
@ -1064,7 +1064,7 @@ describe(__filename, () => {
store.dispatch(_loadAddons({ addon }));
if (addonsByAuthors) {
store.dispatch(_loadOtherAddonsByAuthors({ addon, addonsByAuthors }));
store.dispatch(_loadAddonsByAuthors({ addon, addonsByAuthors }));
}
return { store };
@ -1084,11 +1084,11 @@ describe(__filename, () => {
renderComponent({ params: { slug: addon.slug }, store });
sinon.assert.calledWith(fakeDispatch, fetchOtherAddonsByAuthors({
sinon.assert.calledWith(fakeDispatch, fetchAddonsByAuthors({
addonType: addon.type,
authors: addon.authors.map((author) => author.username),
errorHandlerId: createStubErrorHandler().id,
slug: addon.slug,
forAddonSlug: addon.slug,
}));
});
@ -1128,11 +1128,11 @@ describe(__filename, () => {
).addon;
root.setProps({ addon: addonFromState });
sinon.assert.calledWith(fakeDispatch, fetchOtherAddonsByAuthors({
sinon.assert.calledWith(fakeDispatch, fetchAddonsByAuthors({
addonType: newAddon.type,
authors: newAddon.authors.map((author) => author.username),
errorHandlerId: createStubErrorHandler().id,
slug: newAddon.slug,
forAddonSlug: newAddon.slug,
}));
});
@ -1140,7 +1140,7 @@ describe(__filename, () => {
const addon = fakeTheme;
const { store } = dispatchAddonData({
addon,
addonsByAuthors: [{ ...fakeTheme, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
const root = renderComponent({ params: { slug: addon.slug }, store });
@ -1153,7 +1153,7 @@ describe(__filename, () => {
const addon = fakeAddon;
const { store } = dispatchAddonData({
addon,
addonsByAuthors: [{ ...fakeAddon, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
const root = renderComponent({ params: { slug: addon.slug }, store });
@ -1185,7 +1185,7 @@ describe(__filename, () => {
it('displays the developer name when add-on is an extension', () => {
const root = renderMoreAddons({
addon: fakeAddon,
addonsByAuthors: [{ ...fakeAddon, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
expect(root).toHaveProp('header', 'More extensions by Krupa');
});
@ -1193,7 +1193,7 @@ describe(__filename, () => {
it('displays the translator name when add-on is a dictionary', () => {
const root = renderMoreAddons({
addon: { ...fakeAddon, type: ADDON_TYPE_DICT },
addonsByAuthors: [{ ...fakeAddon, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
expect(root).toHaveProp('header', 'More dictionaries by Krupa');
});
@ -1201,7 +1201,7 @@ describe(__filename, () => {
it('displays the translator name when add-on is a language pack', () => {
const root = renderMoreAddons({
addon: { ...fakeAddon, type: ADDON_TYPE_LANG },
addonsByAuthors: [{ ...fakeAddon, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
expect(root).toHaveProp('header', 'More language packs by Krupa');
});
@ -1209,7 +1209,7 @@ describe(__filename, () => {
it('displays the artist name when add-on is a theme', () => {
const root = renderMoreAddons({
addon: fakeTheme,
addonsByAuthors: [{ ...fakeTheme, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeTheme, forAddonSlug: 'another-slug' }],
});
expect(root).toHaveProp('header', 'More themes by MaDonna');
});
@ -1217,7 +1217,7 @@ describe(__filename, () => {
it('displays the author name in any other cases', () => {
const root = renderMoreAddons({
addon: { ...fakeAddon, type: ADDON_TYPE_OPENSEARCH },
addonsByAuthors: [{ ...fakeAddon, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
expect(root).toHaveProp('header', 'More add-ons by Krupa');
});
@ -1228,7 +1228,7 @@ describe(__filename, () => {
...fakeAddon,
authors: Array(2).fill(fakeAddon.authors[0]),
},
addonsByAuthors: [{ ...fakeAddon, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
expect(root).toHaveProp('header', 'More extensions by these developers');
});
@ -1239,7 +1239,7 @@ describe(__filename, () => {
...fakeTheme,
authors: Array(2).fill(fakeTheme.authors[0]),
},
addonsByAuthors: [{ ...fakeTheme, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeTheme, forAddonSlug: 'another-slug' }],
});
expect(root).toHaveProp('header', 'More themes by these artists');
});
@ -1251,7 +1251,7 @@ describe(__filename, () => {
authors: Array(2).fill(fakeAddon.authors[0]),
type: ADDON_TYPE_LANG,
},
addonsByAuthors: [{ ...fakeAddon, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
expect(root)
.toHaveProp('header', 'More language packs by these translators');
@ -1264,7 +1264,7 @@ describe(__filename, () => {
authors: Array(2).fill(fakeAddon.authors[0]),
type: ADDON_TYPE_DICT,
},
addonsByAuthors: [{ ...fakeAddon, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
expect(root)
.toHaveProp('header', 'More dictionaries by these translators');
@ -1277,7 +1277,7 @@ describe(__filename, () => {
authors: Array(2).fill(fakeAddon.authors[0]),
type: ADDON_TYPE_OPENSEARCH,
},
addonsByAuthors: [{ ...fakeAddon, slug: 'another-slug' }],
addonsByAuthors: [{ ...fakeAddon, forAddonSlug: 'another-slug' }],
});
expect(root).toHaveProp('header', 'More add-ons by these developers');
});
@ -1285,9 +1285,9 @@ describe(__filename, () => {
it('displays more add-ons by authors', () => {
const addon = fakeAddon;
const addonsByAuthors = [
{ ...fakeAddon, slug: 'addon-1' },
{ ...fakeAddon, slug: 'addon-2' },
{ ...fakeAddon, slug: 'addon-3' },
{ ...fakeAddon, slug: 'addon-1', id: 1 },
{ ...fakeAddon, slug: 'addon-2', id: 2 },
{ ...fakeAddon, slug: 'addon-3', id: 3 },
];
const root = renderMoreAddons({ addon, addonsByAuthors });
@ -1301,9 +1301,9 @@ describe(__filename, () => {
it('indicates when other add-ons are themes', () => {
const addon = fakeTheme;
const addonsByAuthors = [
{ ...fakeTheme, slug: 'addon-1' },
{ ...fakeTheme, slug: 'addon-2' },
{ ...fakeTheme, slug: 'addon-3' },
{ ...fakeTheme },
{ ...fakeTheme },
{ ...fakeTheme },
];
const root = renderMoreAddons({ addon, addonsByAuthors });

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

@ -46,14 +46,16 @@ export const fakePlatformFile = Object.freeze({
url: 'https://a.m.o/files/321/addon.xpi',
});
export const fakeAuthor = Object.freeze({
id: 98811255,
name: 'Krupa',
picture_url: 'https://addons.cdn.mozilla.net/static/img/anon_user.png',
url: 'http://olympia.test/en-US/firefox/user/krupa/',
username: 'krupa',
});
export const fakeAddon = Object.freeze({
authors: [{
id: 98811255,
name: 'Krupa',
picture_url: 'https://addons.cdn.mozilla.net/static/img/anon_user.png',
url: 'http://olympia.test/en-US/firefox/user/krupa/',
username: 'krupa',
}],
authors: [fakeAuthor],
average_daily_users: 100,
categories: { firefox: ['other'] },
current_beta_version: null,

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

@ -1,15 +1,35 @@
import { ADDON_TYPE_THEME } from 'core/constants';
import reducer, {
OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE,
fetchOtherAddonsByAuthors,
ADDONS_BY_AUTHORS_PAGE_SIZE,
fetchAddonsByAuthors,
getAddonsForSlug,
initialState,
loadOtherAddonsByAuthors,
loadAddonsByAuthors,
} from 'amo/reducers/addonsByAuthors';
import { createInternalAddon } from 'core/reducers/addons';
import { fakeAddon } from 'tests/unit/amo/helpers';
import { fakeAddon, fakeAuthor } from 'tests/unit/amo/helpers';
describe(__filename, () => {
function fakeAddons() {
const firstAddon = {
...fakeAddon,
id: 6,
authors: [
{ username: 'test', id: 51 },
{ username: 'test2', id: 61 },
],
};
const secondAddon = {
...fakeAddon, id: 7, authors: [{ username: 'test2', id: 61 }],
};
const thirdAddon = {
...fakeAddon, id: 8, authors: [{ username: 'test3', id: 71 }],
};
return { firstAddon, secondAddon, thirdAddon };
}
describe('reducer', () => {
it('initializes properly', () => {
const state = reducer(undefined, {});
@ -19,8 +39,8 @@ describe(__filename, () => {
it('ignores unrelated actions', () => {
// Load some initial state to be sure that an unrelated action does not
// change it.
const state = reducer(undefined, loadOtherAddonsByAuthors({
slug: fakeAddon.slug,
const state = reducer(undefined, loadAddonsByAuthors({
forAddonSlug: fakeAddon.slug,
addons: [fakeAddon],
}));
const newState = reducer(state, { type: 'UNRELATED' });
@ -28,8 +48,8 @@ describe(__filename, () => {
});
it('allows an empty list of add-ons', () => {
const state = reducer(undefined, loadOtherAddonsByAuthors({
slug: 'addon-slug',
const state = reducer(undefined, loadAddonsByAuthors({
forAddonSlug: 'addon-slug',
addons: [],
}));
expect(state.byAddonSlug).toEqual({
@ -38,120 +58,174 @@ describe(__filename, () => {
});
it('adds related add-ons by slug', () => {
const state = reducer(undefined, loadOtherAddonsByAuthors({
slug: 'addon-slug',
const state = reducer(undefined, loadAddonsByAuthors({
forAddonSlug: 'addon-slug',
addons: [fakeAddon],
}));
expect(state.byAddonSlug).toEqual({
'addon-slug': [createInternalAddon(fakeAddon)],
'addon-slug': [fakeAddon.id],
});
});
it('always ensures the page size is consistent', () => {
const slug = 'addon-slug';
const state = reducer(undefined, loadOtherAddonsByAuthors({
slug,
const forAddonSlug = 'addon-slug';
const state = reducer(undefined, loadAddonsByAuthors({
forAddonSlug,
// This is the case where there are more add-ons loaded than needed.
addons: Array(OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE + 2).fill(fakeAddon),
addons: Array(ADDONS_BY_AUTHORS_PAGE_SIZE + 2).fill(fakeAddon),
}));
expect(state.byAddonSlug[slug])
.toHaveLength(OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE);
expect(state.byAddonSlug[forAddonSlug])
.toHaveLength(ADDONS_BY_AUTHORS_PAGE_SIZE);
});
it('returns state if no excluded slug is specified', () => {
const forAddonSlug = 'addon-slug';
const previousState = reducer(undefined, loadAddonsByAuthors({
addons: [fakeAddon],
forAddonSlug,
}));
expect(previousState.byAddonSlug)
.toEqual({ 'addon-slug': [fakeAddon.id] });
const state = reducer(previousState, fetchAddonsByAuthors({
authors: ['author2'],
addonType: ADDON_TYPE_THEME,
errorHandlerId: 'error-handler-id',
}));
expect(state.byAddonSlug).toEqual({ 'addon-slug': [fakeAddon.id] });
});
it('resets the loaded add-ons', () => {
const slug = 'addon-slug';
const forAddonSlug = 'addon-slug';
const previousState = reducer(undefined, loadOtherAddonsByAuthors({
const previousState = reducer(undefined, loadAddonsByAuthors({
addons: [fakeAddon],
slug,
forAddonSlug,
}));
expect(previousState.byAddonSlug)
.toEqual({ 'addon-slug': [createInternalAddon(fakeAddon)] });
.toEqual({ 'addon-slug': [fakeAddon.id] });
const state = reducer(previousState, fetchOtherAddonsByAuthors({
const state = reducer(previousState, fetchAddonsByAuthors({
authors: ['author1'],
addonType: ADDON_TYPE_THEME,
errorHandlerId: 'error-handler-id',
slug,
forAddonSlug,
}));
expect(state.byAddonSlug)
.toEqual({ 'addon-slug': undefined });
expect(state.byAddonSlug).toMatchObject({ 'addon-slug': undefined });
expect(state.byUsername).toMatchObject({ author1: undefined });
});
});
describe('fetchOtherAddonsByAuthors()', () => {
const getParams = () => {
describe('loadAddonsByAuthors()', () => {
const getParams = (extra = {}) => {
return {
authors: ['user1', 'user2'],
addonType: ADDON_TYPE_THEME,
errorHandlerId: 'error-handler-id',
slug: 'addon-slug',
addons: [],
forAddonSlug: fakeAddon.slug,
...extra,
};
};
it('requires an error id', () => {
const params = getParams();
delete params.errorHandlerId;
expect(() => {
fetchOtherAddonsByAuthors(params);
}).toThrow(/An errorHandlerId is required/);
it('adds each add-on to each author array', () => {
const firstAuthor = { ...fakeAuthor, id: 50 };
const secondAuthor = { ...fakeAuthor, id: 60 };
const multiAuthorAddon = {
...fakeAddon,
authors: [firstAuthor, secondAuthor],
};
const params = getParams({ addons: [multiAuthorAddon] });
const newState = reducer(undefined, loadAddonsByAuthors(params));
expect(newState.byUserId).toEqual({
[firstAuthor.id]: [multiAuthorAddon.id],
[secondAuthor.id]: [multiAuthorAddon.id],
});
});
it('requires a slug', () => {
const params = getParams();
delete params.slug;
expect(() => {
fetchOtherAddonsByAuthors(params);
}).toThrow(/An add-on slug is required/);
it('adds each different add-on to the byAddonId dictionary', () => {
const addons = fakeAddons();
const params = getParams({
addons: Object.values(addons),
forAddonSlug: undefined,
});
const newState = reducer(undefined, loadAddonsByAuthors(params));
expect(newState.byAddonId).toEqual({
[addons.firstAddon.id]: createInternalAddon(addons.firstAddon),
[addons.secondAddon.id]: createInternalAddon(addons.secondAddon),
[addons.thirdAddon.id]: createInternalAddon(addons.thirdAddon),
});
});
it('requires an add-on type', () => {
const params = getParams();
delete params.addonType;
expect(() => {
fetchOtherAddonsByAuthors(params);
}).toThrow(/An add-on type is required/);
it('adds each different add-on to each author array', () => {
// See fakeAddons() output, above.
const firstAuthorId = 51;
const secondAuthorId = 61;
const thirdAuthorId = 71;
const addons = fakeAddons();
const params = getParams({
addons: Object.values(addons),
forAddonSlug: undefined,
});
const newState = reducer(undefined, loadAddonsByAuthors(params));
expect(newState.byUserId).toEqual({
[firstAuthorId]: [addons.firstAddon.id],
[secondAuthorId]: [addons.firstAddon.id, addons.secondAddon.id],
[thirdAuthorId]: [addons.thirdAddon.id],
});
});
it('requires some authors', () => {
it('does not modify byAddonSlug if forAddonSlug is not set', () => {
const params = getParams();
delete params.authors;
expect(() => {
fetchOtherAddonsByAuthors(params);
}).toThrow(/Authors are required/);
delete params.forAddonSlug;
const newState = reducer(undefined, loadAddonsByAuthors(params));
expect(newState.byAddonSlug).toEqual(initialState.byAddonSlug);
});
it('requires an array of authors', () => {
const params = getParams();
params.authors = 'invalid-type';
expect(() => {
fetchOtherAddonsByAuthors(params);
}).toThrow(/The authors parameter must be an array/);
});
});
describe('loadOtherAddonsByAuthors()', () => {
const getParams = () => {
return {
it('modifies byAddonSlug if forAddonSlug is set', () => {
const params = getParams({
addons: [fakeAddon],
slug: 'addon-slug',
};
};
forAddonSlug: fakeAddon.slug,
});
it('requires an add-on slug', () => {
const params = getParams();
delete params.slug;
expect(() => {
loadOtherAddonsByAuthors(params);
}).toThrow(/An add-on slug is required/);
const newState = reducer(undefined, loadAddonsByAuthors(params));
expect(newState.byAddonSlug)
.toEqual({ [fakeAddon.slug]: [fakeAddon.id] });
});
});
describe('getAddonsForSlug', () => {
it('returns addons', () => {
const addons = fakeAddons();
const state = reducer(undefined, loadAddonsByAuthors({
addons: Object.values(addons),
forAddonSlug: 'test',
}));
expect(getAddonsForSlug(state, 'test')).toEqual([
createInternalAddon(addons.firstAddon),
createInternalAddon(addons.secondAddon),
createInternalAddon(addons.thirdAddon),
]);
});
it('requires an array of add-ons', () => {
const params = getParams();
delete params.addons;
expect(() => {
loadOtherAddonsByAuthors(params);
}).toThrow(/A set of add-ons is required/);
it('returns nothing if no add-ons are found', () => {
const addons = fakeAddons();
const state = reducer(undefined, loadAddonsByAuthors({
addons: Object.values(addons),
forAddonSlug: 'test',
}));
expect(getAddonsForSlug(state, 'not-a-slug')).toBeNull();
});
});
});

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

@ -1,9 +1,9 @@
import SagaTester from 'redux-saga-tester';
import addonsByAuthorsReducer, {
OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE,
fetchOtherAddonsByAuthors,
loadOtherAddonsByAuthors,
ADDONS_BY_AUTHORS_PAGE_SIZE,
fetchAddonsByAuthors,
loadAddonsByAuthors,
} from 'amo/reducers/addonsByAuthors';
import addonsByAuthorsSaga from 'amo/sagas/addonsByAuthors';
import {
@ -38,8 +38,8 @@ describe(__filename, () => {
sagaTester.start(addonsByAuthorsSaga);
});
function _fetchOtherAddonsByAuthors(params) {
sagaTester.dispatch(fetchOtherAddonsByAuthors({
function _fetchAddonsByAuthors(params) {
sagaTester.dispatch(fetchAddonsByAuthors({
errorHandlerId: errorHandler.id,
addonType: ADDON_TYPE_THEME,
...params,
@ -47,6 +47,39 @@ describe(__filename, () => {
}
it('calls the API to retrieve other add-ons', async () => {
const addons = [fakeAddon];
const authors = ['mozilla', 'johnedoe'];
const state = sagaTester.getState();
mockApi
.expects('search')
.withArgs({
api: state.api,
filters: {
addonType: ADDON_TYPE_THEME,
author: authors.join(','),
exclude_addons: undefined, // `callApi` will internally unset this
page_size: ADDONS_BY_AUTHORS_PAGE_SIZE,
sort: SEARCH_SORT_TRENDING,
},
})
.once()
.returns(Promise.resolve(createAddonsApiResult(addons)));
_fetchAddonsByAuthors({ authors });
const expectedLoadAction = loadAddonsByAuthors({
addons,
forAddonSlug: undefined,
});
const loadAction = await sagaTester.waitFor(expectedLoadAction.type);
mockApi.verify();
expect(loadAction).toEqual(expectedLoadAction);
});
it('sends `exclude_addons` param if `forAddonSlug` is set', async () => {
const addons = [fakeAddon];
const authors = ['mozilla', 'johnedoe'];
const { slug } = fakeAddon;
@ -60,32 +93,34 @@ describe(__filename, () => {
addonType: ADDON_TYPE_THEME,
author: authors.join(','),
exclude_addons: slug,
page_size: OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE,
page_size: ADDONS_BY_AUTHORS_PAGE_SIZE,
sort: SEARCH_SORT_TRENDING,
},
})
.once()
.returns(Promise.resolve(createAddonsApiResult(addons)));
_fetchOtherAddonsByAuthors({ authors, slug });
_fetchAddonsByAuthors({ authors, forAddonSlug: slug });
const expectedLoadAction = loadOtherAddonsByAuthors({ addons, slug });
const expectedLoadAction = loadAddonsByAuthors({
addons,
forAddonSlug: slug,
});
await sagaTester.waitFor(expectedLoadAction.type);
const loadAction = await sagaTester.waitFor(expectedLoadAction.type);
mockApi.verify();
const loadAction = sagaTester.getCalledActions()[2];
expect(loadAction).toEqual(expectedLoadAction);
});
it('clears the error handler', async () => {
_fetchOtherAddonsByAuthors({ authors: [], slug: fakeAddon.slug });
_fetchAddonsByAuthors({ authors: [], forAddonSlug: fakeAddon.slug });
const expectedAction = errorHandler.createClearingAction();
await sagaTester.waitFor(expectedAction.type);
expect(sagaTester.getCalledActions()[1])
.toEqual(errorHandler.createClearingAction());
const errorAction = await sagaTester.waitFor(expectedAction.type);
expect(errorAction).toEqual(errorHandler.createClearingAction());
});
it('dispatches an error', async () => {
@ -95,11 +130,12 @@ describe(__filename, () => {
.once()
.returns(Promise.reject(error));
_fetchOtherAddonsByAuthors({ authors: [], slug: fakeAddon.slug });
_fetchAddonsByAuthors({ authors: [], forAddonSlug: fakeAddon.slug });
const errorAction = errorHandler.createErrorAction(error);
await sagaTester.waitFor(errorAction.type);
expect(sagaTester.getCalledActions()[2]).toEqual(errorAction);
const calledErrorAction = await sagaTester.waitFor(errorAction.type);
expect(calledErrorAction).toEqual(errorAction);
});
it('handles no API results', async () => {
@ -116,21 +152,23 @@ describe(__filename, () => {
addonType: ADDON_TYPE_THEME,
author: authors.join(','),
exclude_addons: slug,
page_size: OTHER_ADDONS_BY_AUTHORS_PAGE_SIZE,
page_size: ADDONS_BY_AUTHORS_PAGE_SIZE,
sort: SEARCH_SORT_TRENDING,
},
})
.once()
.returns(Promise.resolve(createAddonsApiResult(addons)));
_fetchOtherAddonsByAuthors({ authors, slug });
_fetchAddonsByAuthors({ authors, forAddonSlug: slug });
const expectedLoadAction = loadOtherAddonsByAuthors({ addons, slug });
const expectedLoadAction = loadAddonsByAuthors({
addons,
forAddonSlug: slug,
});
await sagaTester.waitFor(expectedLoadAction.type);
const loadAction = await sagaTester.waitFor(expectedLoadAction.type);
mockApi.verify();
const loadAction = sagaTester.getCalledActions()[2];
expect(loadAction).toEqual(expectedLoadAction);
});
});