diff --git a/extension.json b/extension.json index 4596e99eb..a7330c936 100644 --- a/extension.json +++ b/extension.json @@ -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": { diff --git a/resources/ext.popups/actions.js b/resources/ext.popups/actions.js index 793d07c9c..1eeb8767c 100644 --- a/resources/ext.popups/actions.js +++ b/resources/ext.popups/actions.js @@ -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. diff --git a/resources/ext.popups/boot.js b/resources/ext.popups/boot.js index 1692beaaf..c8206b375 100644 --- a/resources/ext.popups/boot.js +++ b/resources/ext.popups/boot.js @@ -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 ) ) diff --git a/resources/ext.popups/reducers.js b/resources/ext.popups/reducers.js new file mode 100644 index 000000000..ae115999e --- /dev/null +++ b/resources/ext.popups/reducers.js @@ -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 ) ); diff --git a/tests/qunit/ext.popups/reducers.test.js b/tests/qunit/ext.popups/reducers.test.js new file mode 100644 index 000000000..2fd223fb2 --- /dev/null +++ b/tests/qunit/ext.popups/reducers.test.js @@ -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 ) ); +