diff --git a/client/state/themes/reducer.js b/client/state/themes/reducer.js index 4d55ee53473f64..7b76b277f7cbf9 100644 --- a/client/state/themes/reducer.js +++ b/client/state/themes/reducer.js @@ -2,7 +2,7 @@ * External dependencies */ import { combineReducers } from 'redux'; -import { mapValues } from 'lodash'; +import { mapValues, omit } from 'lodash'; /** * Internal dependencies @@ -162,6 +162,29 @@ export function themeRequests( state = {}, action ) { return state; } +/** + * Returns the updated site theme requests error state after an action has been + * dispatched. The state reflects a mapping of site ID, theme ID pairing to a + * object describing request error. If there is no error null is storred. + * + * @param {Object} state Current state + * @param {Object} action Action payload + * @return {Object} Updated state + */ +export const themeRequestErrors = createReducer( {}, { + [ THEME_REQUEST_FAILURE ]: ( state, { siteId, themeId, error } ) => ( { + ...state, + [ siteId ]: { + ...state[ siteId ], + [ themeId ]: error + } + } ), + [ THEME_REQUEST_SUCCESS ]: ( state, { siteId, themeId } ) => ( { + ...state, + [ siteId ]: omit( state[ siteId ], themeId ), + } ) +} ); + /** * Returns the updated theme query requesting state after an action has been * dispatched. The state reflects a mapping of serialized query to whether a @@ -270,6 +293,7 @@ export default combineReducers( { // queryRequests, // lastQuery themeRequests, + // themeRequestErrors activeThemes, activeThemeRequests, activationRequests, diff --git a/client/state/themes/selectors.js b/client/state/themes/selectors.js index 160093d9157672..96f1f8a3fc7195 100644 --- a/client/state/themes/selectors.js +++ b/client/state/themes/selectors.js @@ -59,6 +59,18 @@ export const getTheme = createSelector( ( state ) => state.themes.queries ); +/** + * Returns theme request error object + * + * @param {Object} state Global state tree + * @param {String} themeId Theme ID + * @param {Number} siteId Site ID + * @return {Object} error object if present or null otherwise + */ +export function getThemeRequestErrors( state, themeId, siteId ) { + return get( state.themes.themeRequestErrors, [ siteId, themeId ], null ); +} + /** * Returns an array of normalized themes for the themes query, or null if no * themes have been received. diff --git a/client/state/themes/test/reducer.js b/client/state/themes/test/reducer.js index 6fc8af32127c72..bac0268f47bb9e 100644 --- a/client/state/themes/test/reducer.js +++ b/client/state/themes/test/reducer.js @@ -30,6 +30,7 @@ import reducer, { queries, lastQuery, themeRequests, + themeRequestErrors, activeThemes, activationRequests, activeThemeRequests, @@ -464,6 +465,103 @@ describe( 'reducer', () => { } ); } ); + describe( '#themeRequestErrors()', () => { + it( 'should default to an empty object', () => { + const state = themeRequestErrors( undefined, {} ); + + expect( state ).to.deep.equal( {} ); + } ); + + it( 'should create empyt mapping on success if previous state was empty', () => { + const state = themeRequestErrors( deepFreeze( {} ), { + type: THEME_REQUEST_SUCCESS, + siteId: 2916284, + themeId: 'twentysixteen' + } ); + + expect( state ).to.deep.equal( { + 2916284: {} + } ); + } ); + + it( 'should map site ID, theme ID to error if request finishes with failure', () => { + const state = themeRequestErrors( deepFreeze( {} ), { + type: THEME_REQUEST_FAILURE, + siteId: 2916284, + themeId: 'vivaro', + error: 'Request error' + } ); + + expect( state ).to.deep.equal( { + 2916284: { + vivaro: 'Request error' + } + } ); + } ); + + it( 'should switch from error to no mapping after successful request after a failure', () => { + const state = themeRequestErrors( deepFreeze( { + 2916284: { + pinboard: { + error: 'Request Error' + } + } + } ), { + type: THEME_REQUEST_SUCCESS, + siteId: 2916284, + themeId: 'pinboard' + } ); + + expect( state ).to.deep.equal( { + 2916284: {} + } ); + } ); + + it( 'should accumulate mappings', () => { + const state = themeRequestErrors( deepFreeze( { + 2916284: { + twentysixteennnnn: 'No such theme!' + } + } ), { + type: THEME_REQUEST_FAILURE, + siteId: 2916284, + themeId: 'twentysixteen', + error: 'System error' + } ); + + expect( state ).to.deep.equal( { + 2916284: { + twentysixteennnnn: 'No such theme!', + twentysixteen: 'System error' + } + } ); + } ); + + it( 'never persists state', () => { + const state = themeRequestErrors( deepFreeze( { + 2916284: { + twentysixteen: null + } + } ), { + type: SERIALIZE + } ); + + expect( state ).to.deep.equal( {} ); + } ); + + it( 'never loads persisted state', () => { + const state = themeRequestErrors( deepFreeze( { + 2916284: { + twentysixteen: null + } + } ), { + type: DESERIALIZE + } ); + + expect( state ).to.deep.equal( {} ); + } ); + } ); + describe( '#activeThemes()', () => { it( 'should default to an empty object', () => { const state = activeThemes( undefined, {} ); diff --git a/client/state/themes/test/selectors.js b/client/state/themes/test/selectors.js index 9a0896f328bddf..8e982e792407bc 100644 --- a/client/state/themes/test/selectors.js +++ b/client/state/themes/test/selectors.js @@ -10,6 +10,7 @@ import { values } from 'lodash'; import { getThemes, getTheme, + getThemeRequestErrors, isRequestingTheme, getThemesForQuery, getLastThemeQuery, @@ -130,6 +131,32 @@ describe( 'themes selectors', () => { } ); } ); + describe( '#getThemesRequestError()', () => { + it( 'should return null if thre is not request error storred for that theme on site', () => { + const error = getThemeRequestErrors( { + themes: { + themeRequestErrors: {} + } + }, 'twentysixteen', 413 ); + + expect( error ).to.be.null; + } ); + + it( 'should return the error object for the site ID, theme ID pair', () => { + const error = getThemeRequestErrors( { + themes: { + themeRequestErrors: { + 2916284: { + twentysixteen: 'Request error' + } + } + } + }, 'twentysixteen', 2916284, ); + + expect( error ).to.equal( 'Request error' ); + } ); + } ); + describe( '#isRequestingTheme()', () => { it( 'should return false if there are no active requests for site', () => { const isRequesting = isRequestingTheme( {