diff --git a/client/lib/plans/index.js b/client/lib/plans/index.js index ef70ea06a0b55c..254bafb9bf0aa4 100644 --- a/client/lib/plans/index.js +++ b/client/lib/plans/index.js @@ -59,7 +59,7 @@ export function isInGracePeriod( plan ) { }; export function shouldFetchSitePlans( sitePlans, selectedSite ) { - return ! sitePlans.hasLoadedFromServer && ! sitePlans.isFetching && selectedSite; + return ! sitePlans.hasLoadedFromServer && ! sitePlans.isRequesting && selectedSite; }; export function filterPlansBySiteAndProps( plans, site, hideFreePlan ) { diff --git a/client/my-sites/plans/main.jsx b/client/my-sites/plans/main.jsx index d916fe08f8a8ce..b1c35548340c7f 100644 --- a/client/my-sites/plans/main.jsx +++ b/client/my-sites/plans/main.jsx @@ -81,7 +81,7 @@ var Plans = React.createClass( { }, redirectToDefault() { - page.redirect( paths.plans( this.props.getSelectedSite().slug ) ); + page.redirect( paths.plans( this.props.sites.getSelectedSite().slug ) ); }, renderNotice() { @@ -148,8 +148,7 @@ var Plans = React.createClass( { cart={ this.props.cart } destinationType={ this.props.context.params.destinationType } plan={ currentPlan } - selectedSite={ selectedSite } - store={ this.props.context.store } /> + selectedSite={ selectedSite } /> ); } diff --git a/client/my-sites/plans/plan-overview/index.jsx b/client/my-sites/plans/plan-overview/index.jsx index c24a284e9e7e0f..6fe12a6cd7f779 100644 --- a/client/my-sites/plans/plan-overview/index.jsx +++ b/client/my-sites/plans/plan-overview/index.jsx @@ -25,8 +25,7 @@ const PlanOverview = React.createClass( { selectedSite: React.PropTypes.oneOfType( [ React.PropTypes.object, React.PropTypes.bool - ] ).isRequired, - store: React.PropTypes.object.isRequired + ] ).isRequired }, redirectToDefault() { @@ -70,8 +69,7 @@ const PlanOverview = React.createClass( { + selectedSite={ this.props.selectedSite } /> ); diff --git a/client/my-sites/plans/plan-overview/plan-remove/index.jsx b/client/my-sites/plans/plan-overview/plan-remove/index.jsx index 4418ace733b0dd..0de305e31c9460 100644 --- a/client/my-sites/plans/plan-overview/plan-remove/index.jsx +++ b/client/my-sites/plans/plan-overview/plan-remove/index.jsx @@ -8,54 +8,48 @@ import React from 'react'; /** * Internal dependencies */ +import { cancelSitePlanTrial } from 'state/sites/plans/actions'; import CompactCard from 'components/card/compact'; +import { connect } from 'react-redux'; import Dialog from 'components/dialog'; +import { getPlansBySite } from 'state/sites/plans/selectors'; import { isInGracePeriod } from 'lib/plans'; import notices from 'notices'; import paths from '../../paths'; -import { fetchSitePlansCompleted } from 'state/sites/plans/actions'; -import wpcom from 'lib/wp'; const PlanRemove = React.createClass( { propTypes: { + cancelSitePlanTrial: React.PropTypes.func.isRequired, + isRequesting: React.PropTypes.bool.isRequired, plan: React.PropTypes.object.isRequired, selectedSite: React.PropTypes.oneOfType( [ React.PropTypes.object, React.PropTypes.bool - ] ).isRequired, - store: React.PropTypes.object.isRequired + ] ).isRequired }, getInitialState() { return { - isCanceling: false, showDialog: false }; }, removePlan( closeDialog ) { - this.setState( { isCanceling: true } ); + this.props.cancelSitePlanTrial( this.props.selectedSite.ID, this.props.plan.id ).then( () => { + Dispatcher.handleViewAction( { + type: 'FETCH_SITES' + } ); - wpcom.undocumented().cancelPlanTrial( this.props.plan.id, ( error, data ) => { - if ( data && data.success ) { - this.props.store.dispatch( fetchSitePlansCompleted( this.props.selectedSite.ID, data.plans ) ); + page( paths.plansDestination( this.props.selectedSite.slug, 'free-trial-canceled' ) ); + } ).catch( ( error ) => { + closeDialog(); - Dispatcher.handleViewAction( { - type: 'FETCH_SITES' - } ); - - page( paths.plansDestination( this.props.selectedSite.slug, 'free-trial-canceled' ) ); - } else { - closeDialog(); - - notices.error( error.message || this.translate( 'There was a problem removing the plan. Please try again later or contact support.' ) ); - } + notices.error( error ); } ); }, closeDialog() { - this.setState( { - isCanceling: false, + this.setState( { showDialog: false } ); }, @@ -84,12 +78,12 @@ const PlanRemove = React.createClass( { const buttons = [ { action: 'cancel', - disabled: this.state.isCanceling, + disabled: this.props.isRequesting, label: this.translate( 'Cancel' ) }, { action: 'remove', - disabled: this.state.isCanceling, + disabled: this.props.isRequesting, isPrimary: true, label: this.translate( 'Remove Now' ), onClick: this.removePlan @@ -135,4 +129,19 @@ const PlanRemove = React.createClass( { } } ); -export default PlanRemove; +export default connect( + ( state, props ) => { + const plans = getPlansBySite( state, props.selectedSite ); + + return { + isRequesting: plans.isRequesting + }; + }, + ( dispatch ) => { + return { + cancelSitePlanTrial: ( siteId, planId ) => { + return dispatch( cancelSitePlanTrial( siteId, planId ) ); + } + }; + } +)( PlanRemove ); diff --git a/client/state/action-types.js b/client/state/action-types.js index 46ccab5f33ee64..b8d860d0983934 100644 --- a/client/state/action-types.js +++ b/client/state/action-types.js @@ -22,8 +22,6 @@ export const EXPORT_ADVANCED_SETTINGS_FETCH = 'EXPORT_ADVANCED_SETTINGS_FETCH'; export const EXPORT_ADVANCED_SETTINGS_FETCH_FAIL = 'EXPORT_ADVANCED_SETTINGS_FETCH_FAIL'; export const EXPORT_ADVANCED_SETTINGS_RECEIVE = 'EXPORT_ADVANCED_SETTINGS_RECEIVE'; export const FAIL_EXPORT = 'FAIL_EXPORT'; -export const FETCH_SITE_PLANS = 'FETCH_SITE_PLANS'; -export const FETCH_SITE_PLANS_COMPLETED = 'FETCH_SITE_PLANS_COMPLETED'; export const FETCH_WPORG_PLUGIN_DATA = 'FETCH_WPORG_PLUGIN_DATA'; export const NEW_NOTICE = 'NEW_NOTICE'; export const POST_REQUEST = 'POST_REQUEST'; @@ -39,7 +37,6 @@ export const PUBLICIZE_CONNECTIONS_REQUEST_FAILURE = 'PUBLICIZE_CONNECTIONS_REQU export const READER_SIDEBAR_LISTS_TOGGLE = 'READER_SIDEBAR_LISTS_TOGGLE'; export const READER_SIDEBAR_TAGS_TOGGLE = 'READER_SIDEBAR_TAGS_TOGGLE'; export const REMOVE_NOTICE = 'REMOVE_NOTICE'; -export const REMOVE_SITE_PLANS = 'REMOVE_SITE_PLANS'; export const REPLY_START_EXPORT = 'REPLY_START_EXPORT'; export const REQUEST_START_EXPORT = 'REQUEST_START_EXPORT'; export const SELECTED_SITE_SET = 'SELECTED_SITE_SET'; @@ -47,6 +44,13 @@ export const SERIALIZE = 'SERIALIZE'; export const SET_EXPORT_POST_TYPE = 'SET_EXPORT_POST_TYPE'; export const SET_ROUTE = 'SET_ROUTE'; export const SET_SECTION = 'SET_SECTION'; +export const SITE_PLANS_FETCH = 'SITE_PLANS_FETCH'; +export const SITE_PLANS_FETCH_COMPLETED = 'SITE_PLANS_FETCH_COMPLETED'; +export const SITE_PLANS_FETCH_FAILED = 'SITE_PLANS_FETCH_FAILED'; +export const SITE_PLANS_REMOVE = 'SITE_PLANS_REMOVE'; +export const SITE_PLANS_TRIAL_CANCEL = 'SITE_PLANS_TRIAL_CANCEL'; +export const SITE_PLANS_TRIAL_CANCEL_COMPLETED = 'SITE_PLANS_TRIAL_CANCEL_COMPLETED'; +export const SITE_PLANS_TRIAL_CANCEL_FAILED = 'SITE_PLANS_TRIAL_CANCEL_FAILED'; export const SITE_RECEIVE = 'SITE_RECEIVE'; export const SUPPORT_USER_TOKEN_FETCH = 'SUPPORT_USER_TOKEN_FETCH'; export const SUPPORT_USER_TOKEN_SET = 'SUPPORT_USER_TOKEN_SET'; diff --git a/client/state/sites/plans/README.md b/client/state/sites/plans/README.md index bb377fde453594..331f2021da36a6 100644 --- a/client/state/sites/plans/README.md +++ b/client/state/sites/plans/README.md @@ -11,9 +11,9 @@ Used in combination with the Redux store instance `dispatch` function, actions c Fetches plans for the site with the given site ID. -### `fetchSitePlansCompleted( siteId: Number, plans: Object )` +### `fetchSitePlansCompleted( siteId: Number, data: Object )` -Adds the plans to the set of plans for the given site ID. +Adds the plans fetched from the API to the set of plans for the given site ID. ```js import { fetchSitePlans, fetchSitePlansCompleted } from 'state/sites/plans/actions'; diff --git a/client/state/sites/plans/action-types.js b/client/state/sites/plans/action-types.js deleted file mode 100644 index e863d0d88b511b..00000000000000 --- a/client/state/sites/plans/action-types.js +++ /dev/null @@ -1,3 +0,0 @@ -export const FETCH_SITE_PLANS = 'FETCH_SITE_PLANS'; -export const FETCH_SITE_PLANS_COMPLETED = 'FETCH_SITE_PLANS_COMPLETED'; -export const REMOVE_SITE_PLANS = 'REMOVE_SITE_PLANS'; diff --git a/client/state/sites/plans/actions.js b/client/state/sites/plans/actions.js index 74fe1eb4bb21d3..27a6cd8c81dc6f 100644 --- a/client/state/sites/plans/actions.js +++ b/client/state/sites/plans/actions.js @@ -11,28 +11,73 @@ const debug = debugFactory( 'calypso:site-plans:actions' ); * Internal dependencies */ import { createSitePlanObject } from './assembler'; -import wpcom from 'lib/wp'; +import i18n from 'lib/mixins/i18n'; import { - FETCH_SITE_PLANS, - FETCH_SITE_PLANS_COMPLETED, - REMOVE_SITE_PLANS -} from './action-types'; + SITE_PLANS_FETCH, + SITE_PLANS_FETCH_COMPLETED, + SITE_PLANS_FETCH_FAILED, + SITE_PLANS_REMOVE, + SITE_PLANS_TRIAL_CANCEL, + SITE_PLANS_TRIAL_CANCEL_COMPLETED, + SITE_PLANS_TRIAL_CANCEL_FAILED +} from 'state/action-types'; +import wpcom from 'lib/wp'; /** - * Clears plans for the given site. + * Cancels the specified plan trial for the given site. * * @param {Number} siteId identifier of the site - * @returns {Function} the corresponding action thunk + * @param {Number} planId identifier of the plan + * @returns {Function} a promise that will resolve once updating is completed */ -export function clearSitePlans( siteId ) { +export function cancelSitePlanTrial( siteId, planId ) { return ( dispatch ) => { dispatch( { - type: REMOVE_SITE_PLANS, + type: SITE_PLANS_TRIAL_CANCEL, siteId } ); + + return new Promise( ( resolve, reject ) => { + wpcom.undocumented().cancelPlanTrial( planId, ( error, data ) => { + if ( data && data.success ) { + dispatch( { + type: SITE_PLANS_TRIAL_CANCEL_COMPLETED, + siteId, + plans: map( data.plans, createSitePlanObject ) + } ); + + resolve(); + } else { + debug( 'Canceling site plan trial failed: ', error ); + + const errorMessage = error.message || i18n.translate( 'There was a problem canceling the plan trial. Please try again later or contact support.' ); + + dispatch( { + type: SITE_PLANS_TRIAL_CANCEL_FAILED, + siteId, + error: errorMessage + } ); + + reject( errorMessage ); + } + } ); + } ); } } +/** + * Returns an action object to be used in signalling that plans for the given site has been cleared. + * + * @param {Number} siteId identifier of the site + * @returns {Object} the corresponding action object + */ +export function clearSitePlans( siteId ) { + return { + type: SITE_PLANS_REMOVE, + siteId + }; +} + /** * Fetches plans for the given site. * @@ -42,7 +87,7 @@ export function clearSitePlans( siteId ) { export function fetchSitePlans( siteId ) { return ( dispatch ) => { dispatch( { - type: FETCH_SITE_PLANS, + type: SITE_PLANS_FETCH, siteId } ); @@ -50,6 +95,14 @@ export function fetchSitePlans( siteId ) { wpcom.undocumented().getSitePlans( siteId, ( error, data ) => { if ( error ) { debug( 'Fetching site plans failed: ', error ); + + const errorMessage = error.message || i18n.translate( 'There was a problem fetching site plans. Please try again later or contact support.' ); + + dispatch( { + type: SITE_PLANS_FETCH_FAILED, + siteId, + error: errorMessage + } ); } else { dispatch( fetchSitePlansCompleted( siteId, data ) ); } @@ -65,16 +118,16 @@ export function fetchSitePlans( siteId ) { * the plans for a given site have been received. * * @param {Number} siteId identifier of the site - * @param {Object} plans list of plans received from the API + * @param {Object} data list of plans received from the API * @returns {Object} the corresponding action object */ -export function fetchSitePlansCompleted( siteId, plans ) { - plans = reject( plans, '_headers' ); +export function fetchSitePlansCompleted( siteId, data ) { + data = reject( data, '_headers' ); return { - type: FETCH_SITE_PLANS_COMPLETED, + type: SITE_PLANS_FETCH_COMPLETED, siteId, - plans: map( plans, createSitePlanObject ) + plans: map( data, createSitePlanObject ) }; } @@ -86,11 +139,7 @@ export function fetchSitePlansCompleted( siteId, plans ) { */ export function refreshSitePlans( siteId ) { return ( dispatch ) => { - dispatch( { - type: REMOVE_SITE_PLANS, - siteId - } ); - + dispatch( clearSitePlans( siteId ) ); dispatch( fetchSitePlans( siteId ) ); } } diff --git a/client/state/sites/plans/reducer.js b/client/state/sites/plans/reducer.js index 354e2eb727c224..e851191fe0e501 100644 --- a/client/state/sites/plans/reducer.js +++ b/client/state/sites/plans/reducer.js @@ -1,46 +1,95 @@ +/** + * External dependencies + */ +import omit from 'lodash/object/omit'; + /** * Internal dependencies */ import { - FETCH_SITE_PLANS, - FETCH_SITE_PLANS_COMPLETED, - REMOVE_SITE_PLANS -} from './action-types'; -import { SERIALIZE, DESERIALIZE } from 'state/action-types'; -import omit from 'lodash/object/omit'; + SITE_PLANS_FETCH, + SITE_PLANS_FETCH_COMPLETED, + SITE_PLANS_FETCH_FAILED, + SITE_PLANS_REMOVE, + SITE_PLANS_TRIAL_CANCEL, + SITE_PLANS_TRIAL_CANCEL_COMPLETED, + SITE_PLANS_TRIAL_CANCEL_FAILED, + SERIALIZE, + DESERIALIZE +} from 'state/action-types'; export const initialSiteState = { + data: null, error: null, hasLoadedFromServer: false, - isFetching: false, - data: null + isRequesting: false }; +/** + * Returns a new state with the given attributes updated for the specified site. + * + * @param {Object} state current state + * @param {Number} siteId identifier of the site + * @param {Object} attributes list of attributes and their values + * @returns {Object} the new state + */ +function updateSiteState( state, siteId, attributes ) { + return Object.assign( {}, state, { + [ siteId ]: Object.assign( {}, initialSiteState, state[ siteId ], attributes ) + } ); +} + export function plans( state = {}, action ) { switch ( action.type ) { - case FETCH_SITE_PLANS: - return Object.assign( {}, state, { - [ action.siteId ]: Object.assign( {}, initialSiteState, state[ action.siteId ], { - isFetching: true - } ) + case SITE_PLANS_FETCH: + return updateSiteState( state, action.siteId, { + error: null, + isRequesting: true } ); - case FETCH_SITE_PLANS_COMPLETED: - return Object.assign( {}, state, { - [ action.siteId ]: Object.assign( {}, state[ action.siteId ], { - error: null, - hasLoadedFromServer: true, - isFetching: false, - data: action.plans - } ) + + case SITE_PLANS_FETCH_COMPLETED: + return updateSiteState( state, action.siteId, { + error: null, + hasLoadedFromServer: true, + isRequesting: false, + data: action.plans + } ); + + case SITE_PLANS_FETCH_FAILED: + return updateSiteState( state, action.siteId, { + error: action.error, + isRequesting: false } ); - case REMOVE_SITE_PLANS: + + case SITE_PLANS_REMOVE: return omit( state, action.siteId ); + + case SITE_PLANS_TRIAL_CANCEL: + return updateSiteState( state, action.siteId, { + isRequesting: true + } ); + + case SITE_PLANS_TRIAL_CANCEL_COMPLETED: + return updateSiteState( state, action.siteId, { + error: null, + hasLoadedFromServer: true, + isRequesting: false, + data: action.plans + } ); + + case SITE_PLANS_TRIAL_CANCEL_FAILED: + return updateSiteState( state, action.siteId, { + error: action.error, + isRequesting: false + } ); + case SERIALIZE: //TODO: we have full instances of moment.js on sites.plans[siteID].data return {}; + case DESERIALIZE: return {}; } return state; -} +}; diff --git a/client/state/sites/plans/test/actions.js b/client/state/sites/plans/test/actions.js index b24b4aab8270df..190ab3b9ab0891 100644 --- a/client/state/sites/plans/test/actions.js +++ b/client/state/sites/plans/test/actions.js @@ -6,7 +6,7 @@ import { expect } from 'chai'; /** * Internal dependencies */ -import { FETCH_SITE_PLANS_COMPLETED } from 'state/action-types'; +import { SITE_PLANS_FETCH_COMPLETED } from 'state/action-types'; import { fetchSitePlansCompleted } from '../actions'; describe( 'actions', () => { @@ -16,7 +16,7 @@ describe( 'actions', () => { action = fetchSitePlansCompleted( siteId ); expect( action ).to.eql( { - type: FETCH_SITE_PLANS_COMPLETED, + type: SITE_PLANS_FETCH_COMPLETED, siteId, plans: [] } ); diff --git a/client/state/sites/plans/test/reducer.js b/client/state/sites/plans/test/reducer.js index 5e75f2c5149c67..f5bf7c1b62deca 100644 --- a/client/state/sites/plans/test/reducer.js +++ b/client/state/sites/plans/test/reducer.js @@ -7,92 +7,328 @@ import { expect } from 'chai'; * Internal dependencies */ import { - FETCH_SITE_PLANS, - REMOVE_SITE_PLANS, + SITE_PLANS_FETCH, + SITE_PLANS_FETCH_COMPLETED, + SITE_PLANS_FETCH_FAILED, + SITE_PLANS_TRIAL_CANCEL, + SITE_PLANS_TRIAL_CANCEL_FAILED, + SITE_PLANS_TRIAL_CANCEL_COMPLETED, + SITE_PLANS_REMOVE, SERIALIZE, DESERIALIZE } from 'state/action-types'; -import { initialSiteState, plans } from '../reducer'; +import { plans } from '../reducer'; describe( 'reducer', () => { describe( '#plans()', () => { - it( 'should default to an empty object', () => { + it( 'should return an empty state when original state is undefined and action is empty', () => { const state = plans( undefined, {} ); expect( state ).to.eql( {} ); } ); - it( 'should index plans by site ID', () => { - const siteId = 11111111, - state = plans( undefined, { - type: FETCH_SITE_PLANS, - siteId: siteId + it( 'should return an empty state when original state and action are empty', () => { + const original = Object.freeze( {} ), + state = plans( original, {} ); + + expect( state ).to.eql( original ); + } ); + + it( 'should return an empty state when original state is undefined and action is unknown', () => { + const state = plans( undefined, { + type: 'SAY_HELLO', + siteId: 11111111 + } ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should return the original state when action is unknown', () => { + const original = Object.freeze( { + 11111111: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: false + } + } ), + state = plans( original, { + type: 'MAKE_COFFEE', + siteId: 11111111 } ); + expect( state ).to.eql( original ); + } ); + + it( 'should return the initial state with requesting enabled when fetching is triggered', () => { + const state = plans( undefined, { + type: SITE_PLANS_FETCH, + siteId: 11111111 + } ); + expect( state ).to.eql( { - [ siteId ]: Object.assign( {}, initialSiteState, { isFetching: true } ) + 11111111: { + data: null, + error: null, + hasLoadedFromServer: false, + isRequesting: true + } + } ); + } ); + + it( 'should return the original state with an error and requesting disabled when fetching failed', () => { + const original = Object.freeze( { + 11111111: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: true + } + } ), + state = plans( original, { + type: SITE_PLANS_FETCH_FAILED, + siteId: 11111111, + error: 'Unable to fetch site plans' + } ); + + expect( state ).to.eql( { + 11111111: { + data: [], + error: 'Unable to fetch site plans', + hasLoadedFromServer: true, + isRequesting: false + } + } ); + } ); + + it( 'should return a list of plans with loaded from server enabled and requesting disabled when fetching completed', () => { + const state = plans( undefined, { + type: SITE_PLANS_FETCH_COMPLETED, + siteId: 11111111, + plans: [] + } ); + + expect( state ).to.eql( { + 11111111: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: false + } } ); } ); it( 'should accumulate plans for different sites', () => { const original = Object.freeze( { - 11111111: initialSiteState + 11111111: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: false + } } ), state = plans( original, { - type: FETCH_SITE_PLANS, + type: SITE_PLANS_FETCH, siteId: 55555555 } ); expect( state ).to.eql( { - 11111111: initialSiteState, - 55555555: Object.assign( {}, initialSiteState, { isFetching: true } ) + 11111111: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: false + }, + 55555555: { + data: null, + error: null, + hasLoadedFromServer: false, + isRequesting: true + } } ); } ); - it( 'should override previous plans of same site ID', () => { + it( 'should override previous plans of the same site', () => { const original = Object.freeze( { - 11111111: initialSiteState + 11111111: { + data: null, + error: 'Unable to fetch site plans', + hasLoadedFromServer: false, + isRequesting: false + } } ), state = plans( original, { - type: FETCH_SITE_PLANS, + type: SITE_PLANS_FETCH, siteId: 11111111 } ); expect( state ).to.eql( { - 11111111: Object.assign( {}, initialSiteState, { isFetching: true } ) + 11111111: { + data: null, + error: null, + hasLoadedFromServer: false, + isRequesting: true + } } ); } ); - it( 'should remove plans for a given site ID', () => { + it( 'should return the original state with updating enabled when trial cancelation is triggered', () => { const original = Object.freeze( { - 11111111: initialSiteState, - 22222222: initialSiteState + 11111111: { + data: [], + error: null, + hasLoadedFromServer: false, + isRequesting: false + } } ), state = plans( original, { - type: REMOVE_SITE_PLANS, + type: SITE_PLANS_TRIAL_CANCEL, siteId: 11111111 } ); expect( state ).to.eql( { - 22222222: initialSiteState + 11111111: { + data: [], + error: null, + hasLoadedFromServer: false, + isRequesting: true + } } ); } ); - it( 'never persists state because this is not implemented', () => { + it( 'should return the original state with an error and requesting disabled when trial cancelation failed', () => { + const original = Object.freeze( { + 11111111: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: true + } + } ), + state = plans( original, { + type: SITE_PLANS_TRIAL_CANCEL_FAILED, + siteId: 11111111, + error: 'Unable to cancel plan trial' + } ); + + expect( state ).to.eql( { + 11111111: { + data: [], + error: 'Unable to cancel plan trial', + hasLoadedFromServer: true, + isRequesting: false + } + } ); + } ); + + it( 'should return a list of plans with loaded from server enabled and requesting disabled when trial cancelation completed', () => { + const state = plans( undefined, { + type: SITE_PLANS_TRIAL_CANCEL_COMPLETED, + siteId: 11111111, + plans: [] + } ); + + expect( state ).to.eql( { + 11111111: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: false + } + } ); + } ); + + it( 'should return an empty state when original state is undefined and removal is triggered', () => { + const state = plans( undefined, { + type: SITE_PLANS_REMOVE, + siteId: 11111111 + } ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should return the original state when removal is triggered for an unknown site', () => { + const original = Object.freeze( { + 11111111: { + data: null, + error: 'Unable to fetch site plans', + hasLoadedFromServer: false, + isRequesting: false + } + } ), + state = plans( original, { + type: SITE_PLANS_REMOVE, + siteId: 22222222 + } ); + + expect( state ).to.eql( original ); + } ); + + it( 'should remove plans for a given site when removal is triggered', () => { const original = Object.freeze( { - 11111111: initialSiteState, - 22222222: initialSiteState + 11111111: { + data: null, + error: 'Unable to fetch site plans', + hasLoadedFromServer: false, + isRequesting: false + }, + 22222222: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: false + } + } ), + state = plans( original, { + type: SITE_PLANS_REMOVE, + siteId: 11111111 + } ); + + expect( state ).to.eql( { + 22222222: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: false + } } ); - const state = plans( original, { type: SERIALIZE } ); + } ); + + it( 'never persists state because this is not implemented', () => { + const original = Object.freeze( { + 11111111: { + data: null, + error: 'Unable to fetch site plans', + hasLoadedFromServer: false, + isRequesting: false + } + } ), + state = plans( original, { + type: SERIALIZE + } ); + expect( state ).to.eql( {} ); } ); it( 'never loads persisted state because this is not implemented', () => { const original = Object.freeze( { - 11111111: initialSiteState, - 22222222: initialSiteState - } ); - const state = plans( original, { type: DESERIALIZE } ); + 11111111: { + data: null, + error: null, + hasLoadedFromServer: false, + isRequesting: false + }, + 22222222: { + data: [], + error: null, + hasLoadedFromServer: true, + isRequesting: false + } + } ), + state = plans( original, { + type: DESERIALIZE + } ); + expect( state ).to.eql( {} ); } ); } );