Add reducers

Reducers as a whole are a WIP, but this implements a baseline from which
to add more.

Changes:
 * Create ext.popups.reducers to house all reducers
 * Create reducers for preview and renderer state manipulation
 * Create rootReducer by combining preview and renderer reducers
 * Add QUnit tests for reducers
 * Move action types into ext.popups.actionTypes
 * Extract rootReducer from boot.js

Change-Id: I8a2296c6846cd4b0552a485e671af1d974944195
This commit is contained in:
Jeff Hobson 2016-11-10 13:02:29 -05:00 committed by Sam Smith
parent 722bfe12a5
commit 2215560866
5 changed files with 154 additions and 17 deletions

View file

@ -63,6 +63,7 @@
"resources/ext.popups/actions.js",
"resources/ext.popups/processLinks.js",
"resources/ext.popups/gateway.js",
"resources/ext.popups/reducers.js",
"resources/ext.popups/boot.js"
],
"templates": {

View file

@ -1,13 +1,28 @@
( function ( mw, Redux ) {
var actions = {};
var actions = {},
types = {
BOOT: 'BOOT',
LINK_DWELL: 'LINK_DWELL',
LINK_ABANDON: 'LINK_ABANDON',
LINK_CLICK: 'LINK_CLICK',
FETCH_START: 'FETCH_START',
FETCH_END: 'FETCH_END',
FETCH_FAILED: 'FETCH_FAILED',
PREVIEW_ANIMATING: 'PREVIEW_ANIMATING',
PREVIEW_INTERACTIVE: 'PREVIEW_INTERACTIVE',
PREVIEW_CLICK: 'PREVIEW_CLICK',
COG_CLICK: 'COG_CLICK',
SETTINGS_DIALOG_RENDERED: 'SETTINGS_DIALOG_RENDERED',
SETTINGS_DIALOG_CLOSED: 'SETTINGS_DIALOG_CLOSED'
};
/**
* @param {Function} isUserInCondition See `mw.popups.createExperiment`
*/
actions.boot = function ( isUserInCondition ) {
return {
type: 'BOOT',
type: types.BOOT,
isUserInCondition: isUserInCondition()
};
};
@ -21,7 +36,7 @@
*/
actions.linkDwell = function ( $el ) {
return {
type: 'LINK_DWELL',
type: types.LINK_DWELL,
el: $el
};
};
@ -36,11 +51,13 @@
*/
actions.linkAbandon = function ( $el ) {
return {
type: 'LINK_ABANDON',
type: types.LINK_ABANDON,
el: $el
};
};
mw.popups.actionTypes = types;
/**
* Represents the user clicking on a link with their mouse, keyboard, or an
* assistive device.

View file

@ -9,18 +9,6 @@
'.cancelLink a'
];
/**
* A [null](https://en.wikipedia.org/wiki/Null_Object_pattern) reducer.
*
* @param {Object} state The current state
* @param {Object} action The action that was dispatched against the store
* @return {Object} The new state
*/
function rootReducer( state, action ) {
/* jshint unused: false */
return state;
}
/**
* Return whether the user is in the experiment group
*
@ -47,7 +35,7 @@
}
store = Redux.createStore(
rootReducer,
mw.popups.reducers.rootReducer,
compose( Redux.applyMiddleware(
ReduxThunk.default
) )

View file

@ -0,0 +1,71 @@
( function ( mw, $, Redux ) {
mw.popups.reducers = {};
/**
* Reducer for actions that modify the state of the preview model
*
* @param {Object} state before action
* @param {Object} action Redux action that modified state.
* Must have `type` property.
* @return {Object} state after action
*/
mw.popups.reducers.preview = function ( state, action ) {
if ( state === undefined ) {
state = {
enabled: false,
activeLink: undefined,
previousActiveLink: undefined,
interactionStarted: undefined,
isDelayingFetch: false,
isFetching: false,
linkInteractionToken: undefined
};
}
switch ( action.type ) {
case mw.popups.actionTypes.BOOT:
return $.extend( {}, state, {
enabled: action.isUserInCondition
} );
default:
return state;
}
};
/**
* Reducer for actions that modify the state of the view
*
* @param {Object} state before action
* @param {Object} action Redux action that modified state.
* Must have `type` property.
* @return {Object} state after action
*/
mw.popups.reducers.renderer = function ( state, action ) {
if ( state === undefined ) {
state = {
isAanimating: false,
isInteractive: false,
showSettings: false
};
}
switch ( action.type ) {
case mw.popups.actionTypes.PREVIEW_ANIMATING:
return $.extend( {}, state, {
isAnimating: true
} );
default:
return state;
}
};
/**
* Root reducer for all actions
*
* @param {Object} global state before action
* @param {Object} action Redux action that modified state.
* Must have `type` property.
* @return {Object} global state after action
*/
mw.popups.reducers.rootReducer = Redux.combineReducers( mw.popups.reducers );
}( mediaWiki, jQuery, Redux ) );

View file

@ -0,0 +1,60 @@
( function ( mw ) {
QUnit.module( 'ext.popups/reducers' );
QUnit.test( '#rootReducer', function ( assert ) {
var state = mw.popups.reducers.rootReducer( undefined, { type: '@@INIT' } );
assert.expect( 1 );
assert.deepEqual(
state,
{
preview: {
enabled: false,
activeLink: undefined,
previousActiveLink: undefined,
interactionStarted: undefined,
isDelayingFetch: false,
isFetching: false,
linkInteractionToken: undefined
},
renderer: {
isAanimating: false,
isInteractive: false,
showSettings: false
}
},
'It should initialize the state by default'
);
} );
QUnit.test( '#model', function ( assert ) {
var state = mw.popups.reducers.preview(
{},
{
type: 'BOOT',
isUserInCondition: true
}
);
assert.expect( 1 );
assert.ok(
state.enabled,
'It should set enabled to true when the user is in the enabled condition.'
);
} );
QUnit.test( '#renderer', function ( assert ) {
assert.expect( 1 );
assert.deepEqual(
// FIXME: There may be more to the action object when this action is implemented
mw.popups.reducers.renderer( {}, { type: 'PREVIEW_ANIMATING' } ),
{ isAnimating: true },
'It should set isAnimating to true when the preview begins rendering.'
);
} );
}( mediaWiki ) );