From 82e315b12425fce1e9df71fedf09c71726ba6d1d Mon Sep 17 00:00:00 2001 From: joakin Date: Mon, 27 Feb 2017 17:10:50 +0100 Subject: [PATCH] Tests: Migrate {integration,actions}.test.js to node qunit Because of the globals mw.popups.wait usage and mocking in both actions and integration, they need to be migrated in a single step, fixing them both to require wait.js and mock using mock-require instead of the global variable. Additional changes: * Fix FIXMEs about actions.js using the global mw.popups.wait instead of the require one. * Fix the unit tests to use require mocking for wait.js instead of global variable mocking in both integration and actions tests * Change tests that use deferreds and promises to be async qunit tests (Deferreds are asynchronous with jQuery in node, apparently they weren't in the browser) * Change integration.test.js to use require on Redux and ReduxThunk Change-Id: I8e3e87b158bd11c9620e77d0a73e611cf9e82183 --- resources/dist/index.js | Bin 122944 -> 122213 bytes resources/dist/index.js.map | Bin 154852 -> 154029 bytes src/actions.js | 19 +- tests/node-qunit/actions.test.js | 424 +++++++++++++++++++++ tests/node-qunit/integration.test.js | 234 ++++++++++++ tests/qunit/ext.popups/actions.test.js | 392 ------------------- tests/qunit/ext.popups/integration.test.js | 216 ----------- 7 files changed, 662 insertions(+), 623 deletions(-) create mode 100644 tests/node-qunit/actions.test.js create mode 100644 tests/node-qunit/integration.test.js delete mode 100644 tests/qunit/ext.popups/actions.test.js delete mode 100644 tests/qunit/ext.popups/integration.test.js diff --git a/resources/dist/index.js b/resources/dist/index.js index 86713a3094f53d3dc9bc0d91a12dff2a2b43d2c5..b3f1e27de26531124ab539f3a2e9154c74d0d1e3 100644 GIT binary patch delta 40 ycmV+@0N4M(zz5~J2Y|Eze0`S?3jrjze0>2G47Zkw0nr1uWRC&t0=G?*0rN~xPY@vh delta 821 zcmd^*ze~h06vwF!MG>4F+}@^x^mezZJruNp7IZt(_VpTRliH*f!Q;fuL2yx5{|Kev zbpL|8oA`g2^se>CGA$j?{_x*gln4^(-`D9!Ss>gw`v@q;+pu2Ot=d6PlQvnPy z6d*a|7-5bqKp@ahxK9%BF(8FNkg?h*qyUoR{G48jB4pwFl=a*CH4q#E!d9gEsd%Ra z%`pTb#{iz`<50s=o+6W0i#m#t{p_Mj&eUHuSHRPhGwVb^i6CNLBq2nUDK3OGs%Mr^ zlJgeEqp#7Vj#v^Z$FFH=nHkTDOjy$KYtpOilj!tr4t`ON`= z{Y;a0sd7#J8~kqa2Vu6!6Fm4QPl)56+#aGb$ya3Z241Pj4l;6+1LFB7_eHTy?hEFc zEFPk@`9tV`My9IZ%^$ delta 979 zcmZ3xne)j`&It}8mL`d5CaIQYsiujk76!&fDW(QSlMiypZ7kX!FgaRRWby|-fz45Y zT}+c7=!;I?t;#w1UGTff1p*?IF9?ZF4lrSztPm|Z`9iShBwvxq8+fHACj={lmB!0( zZmtitWMV1^nY_?Cc(Q>!`(}}dy@FV6Y<`-#{b?rSkB;qsJ~7VcnZ7oZk*hu=BePf` zFEur#SfM0ep(r&iwJ0?&IaMK1Aw4HQDKSSODK$B' ) + .data( 'page-previews-title', 'Foo' ) + .eq( 0 ); +} + +QUnit.module( 'ext.popups/actions#linkDwell @integration', { + setup: function () { + var that = this; + + this.state = { + preview: {} + }; + this.getState = function () { + return that.state; + }; + + // Fake implementation of mw.now + mw.now = function () { return Date.now(); }; + + setupWait( this ); + setupEl( this ); + }, + teardown: function () { + teardownWait(); + } +} ); + +QUnit.test( '#linkDwell', function ( assert ) { + var done = assert.async(), + event = {}, + dispatch = this.sandbox.spy(); + + this.sandbox.stub( mw, 'now' ).returns( new Date() ); + this.sandbox.stub( actions, 'fetch' ); + + // Set the state for when dispatch is called. New token is accepted + this.state.preview = { + activeToken: generateToken() + }; + + actions.linkDwell( this.el, event, /* gateway = */ null, generateToken )( + dispatch, + this.getState + ); + + assert.deepEqual( dispatch.getCall( 0 ).args[ 0 ], { + type: 'LINK_DWELL', + el: this.el, + event: event, + token: '9876543210', + timestamp: mw.now() + } ); + + // Stub the state tree being updated. + this.state.preview = { + enabled: true, + activeLink: this.el, + activeToken: generateToken() + }; + + // --- + + this.waitPromise.then( function () { + assert.strictEqual( + dispatch.callCount, + 2, + 'The fetch action is dispatched after 50 ms' + ); + + done(); + } ); + + // After 50 ms... + this.waitDeferred.resolve(); +} ); + +QUnit.test( '#linkDwell doesn\'t continue when previews are disabled', function ( assert ) { + var done = assert.async(), + event = {}, + dispatch = this.sandbox.spy(); + + actions.linkDwell( this.el, event, /* gateway = */ null, generateToken )( + dispatch, + this.getState + ); + + this.state.preview = { + enabled: false + }; + + this.waitPromise.then( function () { + assert.strictEqual( dispatch.callCount, 1 ); + + done(); + } ); + + // After 500 ms... + this.waitDeferred.resolve(); +} ); + +QUnit.test( '#linkDwell doesn\'t continue if the token has changed', function ( assert ) { + var done = assert.async(), + event = {}, + dispatch = this.sandbox.spy(); + + actions.linkDwell( this.el, event, /* gateway = */ null, generateToken )( + dispatch, + this.getState + ); + + this.state.preview = { + enabled: true, + + // Consider the user tabbing back and forth between two links in the time + // it takes to start fetching data via the gateway: the active link hasn't + // changed, but the active token has. + activeLink: this.el, + + activeToken: '0123456789' + }; + + this.waitPromise.then( function () { + assert.strictEqual( dispatch.callCount, 1 ); + + done(); + } ); + + // After 500 ms... + this.waitDeferred.resolve(); +} ); + +QUnit.test( '#linkDwell doesn\'t continue if the interaction is the same one', function ( assert ) { + var done = assert.async(), + event = {}, + dispatch = this.sandbox.spy(); + + this.state.preview = { + activeToken: '0123456789' + }; + + actions.linkDwell( this.el, event, /* gateway = */ null, generateToken )( + dispatch, + this.getState + ); + + this.waitPromise.then( function () { + assert.strictEqual( dispatch.callCount, 1 ); + + done(); + } ); + + // After 500 ms... + this.waitDeferred.resolve(); +} ); + +QUnit.module( 'ext.popups/actions#fetch', { + setup: function () { + var that = this; + + // Setup the mw.now stub before actions is re-required in setupWait + that.now = 0; + + this.sandbox.stub( mw, 'now', function () { + return that.now; + } ); + + setupWait( this ); + setupEl( this ); + + this.gatewayDeferred = $.Deferred(), + this.gatewayPromise = this.gatewayDeferred.promise(); + this.gateway = { + getPageSummary: this.sandbox.stub().returns( this.gatewayPromise ) + }; + + this.dispatch = this.sandbox.spy(); + + // Sugar. + this.fetch = function () { + actions.fetch( that.gateway, that.el, that.now )( that.dispatch ); + }; + }, + teardown: function () { + teardownWait(); + } +} ); + +QUnit.test( 'it should fetch data from the gateway immediately', function ( assert ) { + this.fetch(); + + assert.ok( this.gateway.getPageSummary.calledWith( 'Foo' ) ); + + assert.ok( + this.dispatch.calledWith( { + type: 'FETCH_START', + el: this.el, + title: 'Foo' + } ), + 'It dispatches the FETCH_START action immediately.' + ); +} ); + +QUnit.test( 'it should delay dispatching the FETCH_END action', function ( assert ) { + var that = this, + result = {}, + done = assert.async( 2 ); + + assert.expect( 3 ); + + this.fetch(); + + this.gatewayPromise.then( function () { + assert.ok( + that.wait.calledWith( 250 ), + 'FETCH_END is delayed by 250 (500 - 250) ms. ' + + 'If you\'ve changed FETCH_END_TARGET_DELAY, then have you spoken with #Design about changing this value?' + ); + + that.waitDeferred.resolve(); + + done(); + } ); + + this.waitPromise.then( function () { + // Let the wait.then execute to run the dispatch before asserting + setTimeout( function () { + assert.equal( that.dispatch.callCount, 2, 'dispatch called for start and end' ); + assert.deepEqual( that.dispatch.getCall( 1 ).args[ 0 ], { + type: 'FETCH_END', + el: that.el, + result: result + } ); + + done(); + } ); + } ); + + // The API request took 250 ms. + this.now += 250; + this.gatewayDeferred.resolve( result ); +} ); + +QUnit.test( + 'it shouldn\'t delay dispatching the FETCH_END action if the API request is over the target', + function ( assert ) { + var that = this, + done = assert.async(); + + this.fetch(); + + this.gatewayPromise.then( function () { + assert.ok( + that.wait.calledWith( 0 ), + 'FETCH_END isn\'t delayed.' + ); + done(); + } ); + + // The API request took 301 ms. + this.now += 501; + this.gatewayDeferred.resolve(); + } +); + +QUnit.module( 'ext.popups/actions#abandon', { + setup: function () { + setupWait( this ); + }, + teardown: function () { + teardownWait(); + } +} ); + +QUnit.test( 'it should dispatch start and end actions', function ( assert ) { + var dispatch = this.sandbox.spy(), + token = '0123456789', + getState = function () { + return { + preview: { + activeToken: token + } + }; + }, + done = assert.async(); + + this.sandbox.stub( mw, 'now' ).returns( new Date() ); + + actions.abandon( this.el )( dispatch, getState ); + + assert.ok( dispatch.calledWith( { + type: 'ABANDON_START', + timestamp: mw.now(), + token: token + } ) ); + + // --- + + assert.ok( + this.wait.calledWith( 300 ), + 'Have you spoken with #Design about changing this value?' + ); + + this.waitPromise.then( function () { + assert.ok( + dispatch.calledWith( { + type: 'ABANDON_END', + token: token + } ), + 'ABANDON_* share the same token.' + ); + + done(); + } ); + + // After 300 ms... + this.waitDeferred.resolve(); +} ); + +QUnit.module( 'ext.popups/actions#saveSettings' ); + +QUnit.test( 'it should dispatch an action with previous and current enabled state', function ( assert ) { + var dispatch = this.sandbox.spy(), + getState = this.sandbox.stub().returns( { + preview: { + enabled: false + } + } ); + + actions.saveSettings( /* enabled = */ true )( dispatch, getState ); + + assert.ok( getState.calledOnce, 'it should query the global state for the current state' ); + assert.ok( dispatch.calledWith( { + type: 'SETTINGS_CHANGE', + wasEnabled: false, + enabled: true + } ), 'it should dispatch the action with the previous and next enabled state' ); +} ); diff --git a/tests/node-qunit/integration.test.js b/tests/node-qunit/integration.test.js new file mode 100644 index 000000000..1bbc84cad --- /dev/null +++ b/tests/node-qunit/integration.test.js @@ -0,0 +1,234 @@ +var mock = require( 'mock-require' ), + Redux = require( 'redux' ), + ReduxThunk = require( 'redux-thunk' ), + wait = require( '../../src/wait' ); + +function identity( x ) { return x; } +function constant( x ) { return function () { return x; }; } + +/* + * Integration tests for actions and state of the preview part of the system. + * Follows a diagram of the interactions considered valid, which will be + * used as a basis for the following tests: + * + ++--------+ +|INACTIVE+-----------------------+ ++---+----+ | + ^ | + | link or preview | + | abandon end | + | link_dwell| + | | + | | + | | ++---|----------------------------|---+ +| | ACTIVE | | ++---|----------------------------|---+ +| + v | +| OFF_LINK ON_LINK | Inside ACTIVE, or out +| + ^ link or preview | ^ | of it, only actions with +| | | abandon start | | | that same active link are +| | +----------------------+ | | valid. Others are ignored. +| | | | +| +----------------------------+ | +| preview or same link dwell | +| | +| NO_DATA +-------> DATA | +| fetch end | ++------------------------------------+ + + */ + +QUnit.module( 'ext.popups preview @integration', { + setup: function () { + var that = this, + reducers, actions, registerChangeListener; + + this.resetWait = function () { + that.waitDeferred = $.Deferred(); + that.waitPromise = that.waitDeferred.promise(); + that.wait.returns( that.waitPromise ); + }; + + this.wait = this.sandbox.stub(); + this.resetWait(); + + mock( '../../src/wait', this.wait ); + + // Require modules after the setting require mocks + reducers = require( '../../src/reducers' ); + actions = require( '../../src/actions' ); + registerChangeListener = require( '../../src/changeListener' ); + + this.store = Redux.createStore( + Redux.combineReducers( reducers ), + Redux.compose( Redux.applyMiddleware( ReduxThunk.default ) ) + ); + + this.actions = Redux.bindActionCreators( + actions, + this.store.dispatch + ); + + this.registerChangeListener = function ( fn ) { + return registerChangeListener( that.store, fn ); + }; + + this.actions.boot( + /* isEnabled: */ + constant( true ), + /* user */ + { sessionId: constant( 'sessiontoken' ), + isAnon: constant( true ) }, + /* userSettings: */ + { getPreviewCount: constant( 1 ) }, + /* generateToken: */ + constant( 'pagetoken' ), + /* config: */ + { get: identity } + ); + + this.dwell = function ( el, ev, fetchResponse ) { + that.resetWait(); + that.actions.linkDwell( el, ev, { + getPageSummary: function () { + return $.Deferred().resolve( fetchResponse ).promise(); + } + }, function () { return 'pagetoken'; } ); + return that.waitPromise; + }; + + this.dwellAndShowPreview = function ( el, ev, fetchResponse ) { + that.dwell( el, ev, fetchResponse ); + that.waitDeferred.resolve(); + return wait( 0 ); // Wait for next tick to resolve pending callbacks + }; + + this.abandon = function () { + that.resetWait(); + that.actions.abandon(); + return that.waitPromise; + }; + + this.abandonAndWait = function () { + that.abandon(); + that.waitDeferred.resolve(); + return wait( 0 ); // Wait for next tick to resolve pending callbacks + }; + + this.dwellAndPreviewDwell = function ( el, ev, res ) { + return that.dwellAndShowPreview( el, ev, res ).then( function () { + + // Get out of the link, and before the delay ends... + var abandonPromise = that.abandon( el ), + abandonDeferred = that.waitDeferred; + + // Dwell over the preview + that.actions.previewDwell( el ); + + // Then the abandon delay finishes + abandonDeferred.resolve(); + + return abandonPromise; + } ); + }; + + this.abandonPreview = function () { + that.resetWait(); + that.actions.abandon(); + + return that.waitPromise; + }; + } +} ); + +QUnit.test( 'it boots in INACTIVE state', function ( assert ) { + var state = this.store.getState(); + + assert.equal( state.preview.activeLink, undefined ); + assert.equal( state.preview.linkInteractionToken, undefined ); +} ); + +QUnit.test( 'in INACTIVE state, a link dwell switches it to ACTIVE', function ( assert ) { + var state, + gateway = { + getPageSummary: function () { + $.Deferred().promise(); + } + }; + + this.actions.linkDwell( + 'element', 'event', + gateway, + constant( 'pagetoken' ) + ); + state = this.store.getState(); + assert.equal( state.preview.activeLink, 'element', 'It has an active link' ); + assert.equal( state.preview.shouldShow, false, 'Initializes with NO_DATA' ); +} ); + +QUnit.test( 'in ACTIVE state, fetch end switches it to DATA', function ( assert ) { + var store = this.store, + done = assert.async(); + this.dwellAndShowPreview( 'element', 'event', 42 ).then( function () { + var state = store.getState(); + assert.equal( state.preview.activeLink, 'element' ); + assert.equal( state.preview.shouldShow, true, 'Should show when data has been fetched' ); + done(); + } ); +} ); + +QUnit.test( 'in ACTIVE state, abandon start, and then end, switch it to INACTIVE', function ( assert ) { + var that = this, + done = assert.async(); + this.dwellAndShowPreview( 'element', 'event', 42 ).then( function () { + return that.abandonAndWait( 'element' ); + } ).then( function () { + var state = that.store.getState(); + assert.equal( state.preview.activeLink, undefined, 'After abandoning, preview is back to INACTIVE' ); + done(); + } ); +} ); + +QUnit.test( 'in ACTIVE state, abandon link, and then dwell preview, should keep it active after all delays', function ( assert ) { + var that = this, + done = assert.async(); + this.dwellAndPreviewDwell( 'element', 'event', 42 ) + .then( function () { + var state = that.store.getState(); + assert.equal( state.preview.activeLink, 'element' ); + done(); + } ); +} ); + +QUnit.test( 'in ACTIVE state, abandon link, hover preview, back to link, should keep it active after all delays', function ( assert ) { + var that = this, + done = assert.async(); + + this.dwellAndPreviewDwell( 'element', 'event', 42 ) + .then( function () { + var abandonPreviewDeferred, dwellPromise, dwellDeferred; + + // Start abandoning the preview + that.abandonPreview( 'element' ); + + abandonPreviewDeferred = that.waitDeferred; + // Dwell back into the link, new event is triggered + dwellPromise = that.dwell( 'element', 'event2', 42 ); + dwellDeferred = that.waitDeferred; + + // Preview abandon happens next, before the fetch + abandonPreviewDeferred.resolve(); + + // Then fetch happens + dwellDeferred.resolve(); + + return dwellPromise; + } ) + .then( function () { + var state = that.store.getState(); + assert.equal( state.preview.activeLink, 'element' ); + done(); + } ); +} ); diff --git a/tests/qunit/ext.popups/actions.test.js b/tests/qunit/ext.popups/actions.test.js deleted file mode 100644 index a9722c504..000000000 --- a/tests/qunit/ext.popups/actions.test.js +++ /dev/null @@ -1,392 +0,0 @@ -( function ( mw, $ ) { - - function generateToken() { - return '9876543210'; - } - - QUnit.module( 'ext.popups/actions' ); - - QUnit.test( '#boot', function ( assert ) { - var config = new mw.Map(), - stubUser = mw.popups.tests.stubs.createStubUser( /* isAnon = */ true ), - stubUserSettings, - action; - - config.set( { - wgTitle: 'Foo', - wgNamespaceNumber: 1, - wgArticleId: 2, - wgUserEditCount: 3, - wgPopupsConflictsWithNavPopupGadget: true - } ); - - stubUserSettings = { - getPreviewCount: function () { - return 22; - } - }; - - assert.expect( 1 ); - - action = mw.popups.actions.boot( - false, - stubUser, - stubUserSettings, - generateToken, - config - ); - - assert.deepEqual( - action, - { - type: 'BOOT', - isEnabled: false, - isNavPopupsEnabled: true, - sessionToken: '0123456789', - pageToken: '9876543210', - page: { - title: 'Foo', - namespaceID: 1, - id: 2 - }, - user: { - isAnon: true, - editCount: 3, - previewCount: 22 - } - } - ); - } ); - - /** - * Stubs `mw.popups.wait` and adds the deferred and its promise as properties - * of the module. - * - * @param {Object} module - */ - function setupWait( module ) { - module.waitDeferred = $.Deferred(); - module.waitPromise = module.waitDeferred.promise(); - - module.sandbox.stub( mw.popups, 'wait', function () { - return module.waitPromise; - } ); - } - - /** - * Sets up an `a` element that can be passed to action creators. - * - * @param {Object} module - */ - function setupEl( module ) { - module.el = $( '' ) - .data( 'page-previews-title', 'Foo' ) - .eq( 0 ); - } - - QUnit.module( 'ext.popups/actions#linkDwell @integration', { - setup: function () { - var that = this; - - this.state = { - preview: {} - }; - this.getState = function () { - return that.state; - }; - - setupWait( this ); - setupEl( this ); - } - } ); - - QUnit.test( '#linkDwell', function ( assert ) { - var done = assert.async(), - event = {}, - dispatch = this.sandbox.spy(); - - this.sandbox.stub( mw, 'now' ).returns( new Date() ); - this.sandbox.stub( mw.popups.actions, 'fetch' ); - - // Set the state for when dispatch is called. New token is accepted - this.state.preview = { - activeToken: generateToken() - }; - - mw.popups.actions.linkDwell( this.el, event, /* gateway = */ null, generateToken )( - dispatch, - this.getState - ); - - assert.deepEqual( dispatch.getCall( 0 ).args[ 0 ], { - type: 'LINK_DWELL', - el: this.el, - event: event, - token: '9876543210', - timestamp: mw.now() - } ); - - // Stub the state tree being updated. - this.state.preview = { - enabled: true, - activeLink: this.el, - activeToken: generateToken() - }; - - // --- - - this.waitPromise.then( function () { - assert.strictEqual( - dispatch.callCount, - 2, - 'The fetch action is dispatched after 50 ms' - ); - - done(); - } ); - - // After 50 ms... - this.waitDeferred.resolve(); - } ); - - QUnit.test( '#linkDwell doesn\'t continue when previews are disabled', function ( assert ) { - var done = assert.async(), - event = {}, - dispatch = this.sandbox.spy(); - - mw.popups.actions.linkDwell( this.el, event, /* gateway = */ null, generateToken )( - dispatch, - this.getState - ); - - this.state.preview = { - enabled: false - }; - - this.waitPromise.then( function () { - assert.strictEqual( dispatch.callCount, 1 ); - - done(); - } ); - - // After 500 ms... - this.waitDeferred.resolve(); - } ); - - QUnit.test( '#linkDwell doesn\'t continue if the token has changed', function ( assert ) { - var done = assert.async(), - event = {}, - dispatch = this.sandbox.spy(); - - mw.popups.actions.linkDwell( this.el, event, /* gateway = */ null, generateToken )( - dispatch, - this.getState - ); - - this.state.preview = { - enabled: true, - - // Consider the user tabbing back and forth between two links in the time - // it takes to start fetching data via the gateway: the active link hasn't - // changed, but the active token has. - activeLink: this.el, - - activeToken: '0123456789' - }; - - this.waitPromise.then( function () { - assert.strictEqual( dispatch.callCount, 1 ); - - done(); - } ); - - // After 500 ms... - this.waitDeferred.resolve(); - } ); - - QUnit.test( '#linkDwell doesn\'t continue if the interaction is the same one', function ( assert ) { - var done = assert.async(), - event = {}, - dispatch = this.sandbox.spy(); - - this.state.preview = { - activeToken: '0123456789' - }; - - mw.popups.actions.linkDwell( this.el, event, /* gateway = */ null, generateToken )( - dispatch, - this.getState - ); - - this.waitPromise.then( function () { - assert.strictEqual( dispatch.callCount, 1 ); - - done(); - } ); - - // After 500 ms... - this.waitDeferred.resolve(); - } ); - - QUnit.module( 'ext.popups/actions#fetch', { - setup: function () { - var that = this; - - this.gatewayDeferred = $.Deferred(), - this.gatewayPromise = this.gatewayDeferred.promise(); - this.gateway = { - getPageSummary: this.sandbox.stub().returns( this.gatewayPromise ) - }; - - // Setup the mw.now stub. - that.now = 0; - - this.sandbox.stub( mw, 'now', function () { - return that.now; - } ); - - setupWait( this ); - setupEl( this ); - - this.dispatch = this.sandbox.spy(); - - // Sugar. - this.fetch = function () { - mw.popups.actions.fetch( that.gateway, that.el, that.now )( that.dispatch ); - }; - } - } ); - - QUnit.test( 'it should fetch data from the gateway immediately', function ( assert ) { - this.fetch(); - - assert.ok( this.gateway.getPageSummary.calledWith( 'Foo' ) ); - - assert.ok( - this.dispatch.calledWith( { - type: 'FETCH_START', - el: this.el, - title: 'Foo' - } ), - 'It dispatches the FETCH_START action immediately.' - ); - } ); - - QUnit.test( 'it should delay dispatching the FETCH_END action', function ( assert ) { - var that = this, - result = {}; - - this.fetch(); - - this.gatewayPromise.then( function () { - assert.ok( - mw.popups.wait.calledWith( 250 ), - 'FETCH_END is delayed by 250 (500 - 250) ms. ' + - 'If you\'ve changed FETCH_END_TARGET_DELAY, then have you spoken with #Design about changing this value?' - ); - - that.waitDeferred.resolve(); - - assert.ok( that.dispatch.calledWith( { - type: 'FETCH_END', - el: that.el, - result: result - } ) ); - } ); - - // The API request took 250 ms. - this.now += 250; - this.gatewayDeferred.resolve( result ); - } ); - - QUnit.test( - 'it shouldn\'t delay dispatching the FETCH_END action if the API request is over the target', - function ( assert ) { - - this.fetch(); - - this.gatewayPromise.then( function () { - assert.ok( - mw.popups.wait.calledWith( 0 ), - 'FETCH_END isn\'t delayed.' - ); - } ); - - // The API request took 301 ms. - this.now += 501; - this.gatewayDeferred.resolve(); - } - ); - - QUnit.module( 'ext.popups/actions#abandon', { - setup: function () { - setupWait( this ); - } - } ); - - QUnit.test( 'it should dispatch start and end actions', function ( assert ) { - var that = this, - dispatch = that.sandbox.spy(), - token = '0123456789', - getState = function () { - return { - preview: { - activeToken: token - } - }; - }, - done = assert.async(); - - this.sandbox.stub( mw, 'now' ).returns( new Date() ); - - mw.popups.actions.abandon( that.el )( dispatch, getState ); - - assert.ok( dispatch.calledWith( { - type: 'ABANDON_START', - timestamp: mw.now(), - token: token - } ) ); - - // --- - - assert.ok( - mw.popups.wait.calledWith( 300 ), - 'Have you spoken with #Design about changing this value?' - ); - - that.waitPromise.then( function () { - assert.ok( - dispatch.calledWith( { - type: 'ABANDON_END', - token: token - } ), - 'ABANDON_* share the same token.' - ); - - done(); - } ); - - // After 300 ms... - that.waitDeferred.resolve(); - } ); - - QUnit.module( 'ext.popups/actions#saveSettings' ); - - QUnit.test( 'it should dispatch an action with previous and current enabled state', function ( assert ) { - var dispatch = this.sandbox.spy(), - getState = this.sandbox.stub().returns( { - preview: { - enabled: false - } - } ); - - mw.popups.actions.saveSettings( /* enabled = */ true )( dispatch, getState ); - - assert.ok( getState.calledOnce, 'it should query the global state for the current state' ); - assert.ok( dispatch.calledWith( { - type: 'SETTINGS_CHANGE', - wasEnabled: false, - enabled: true - } ), 'it should dispatch the action with the previous and next enabled state' ); - } ); -}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/ext.popups/integration.test.js b/tests/qunit/ext.popups/integration.test.js deleted file mode 100644 index f0fc1a71c..000000000 --- a/tests/qunit/ext.popups/integration.test.js +++ /dev/null @@ -1,216 +0,0 @@ -( function ( mw, $ ) { - - function identity( x ) { return x; } - function constant( x ) { return function () { return x; }; } - - /* - * Integration tests for actions and state of the preview part of the system. - * Follows a diagram of the interactions considered valid, which will be - * used as a basis for the following tests: - * - - +--------+ - |INACTIVE+-----------------------+ - +---+----+ | - ^ | - | link or preview | - | abandon end | - | link_dwell| - | | - | | - | | - +---|----------------------------|---+ - | | ACTIVE | | - +---|----------------------------|---+ - | + v | - | OFF_LINK ON_LINK | Inside ACTIVE, or out - | + ^ link or preview | ^ | of it, only actions with - | | | abandon start | | | that same active link are - | | +----------------------+ | | valid. Others are ignored. - | | | | - | +----------------------------+ | - | preview or same link dwell | - | | - | NO_DATA +-------> DATA | - | fetch end | - +------------------------------------+ - - */ - - QUnit.module( 'ext.popups preview @integration', { - setup: function () { - var that = this; - - this.resetWait = function () { - that.waitDeferred = $.Deferred(); - that.waitPromise = that.waitDeferred.promise(); - }; - this.resetWait(); - this.sandbox.stub( mw.popups, 'wait', function () { - return that.waitPromise; - } ); - - this.store = Redux.createStore( - Redux.combineReducers( mw.popups.reducers ), - Redux.compose( Redux.applyMiddleware( ReduxThunk.default ) ) - ); - - this.actions = Redux.bindActionCreators( - mw.popups.actions, - this.store.dispatch - ); - - this.registerChangeListener = function ( fn ) { - return mw.popups.registerChangeListener( that.store, fn ); - }; - - this.actions.boot( - /* isEnabled: */ - constant( true ), - /* user */ - { sessionId: constant( 'sessiontoken' ), - isAnon: constant( true ) }, - /* userSettings: */ - { getPreviewCount: constant( 1 ) }, - /* generateToken: */ - constant( 'pagetoken' ), - /* config: */ - { get: identity } - ); - - this.dwell = function ( el, ev, fetchResponse ) { - that.resetWait(); - that.actions.linkDwell( el, ev, { - getPageSummary: function () { - return $.Deferred().resolve( fetchResponse ).promise(); - } - }, function () { return 'pagetoken'; } ); - return that.waitPromise; - }; - - this.dwellAndShowPreview = function ( el, ev, fetchResponse ) { - var p = that.dwell( el, ev, fetchResponse ); - that.waitDeferred.resolve(); - return p; - }; - - this.abandon = function () { - that.resetWait(); - that.actions.abandon(); - return that.waitPromise; - }; - - this.abandonAndWait = function () { - var p = that.abandon(); - that.waitDeferred.resolve(); - return p; - }; - - this.dwellAndPreviewDwell = function ( el, ev, res ) { - return that.dwellAndShowPreview( el, ev, res ).then( function () { - - // Get out of the link, and before the delay ends... - var abandonPromise = that.abandon( el ), - abandonDeferred = that.waitDeferred; - - // Dwell over the preview - that.actions.previewDwell( el ); - - // Then the abandon delay finishes - abandonDeferred.resolve(); - - return abandonPromise; - } ); - }; - - this.abandonPreview = function () { - that.resetWait(); - that.actions.abandon(); - - return that.waitPromise; - }; - } - } ); - - QUnit.test( 'it boots in INACTIVE state', function ( assert ) { - var state = this.store.getState(); - - assert.equal( state.preview.activeLink, undefined ); - assert.equal( state.preview.linkInteractionToken, undefined ); - } ); - - QUnit.test( 'in INACTIVE state, a link dwell switches it to ACTIVE', function ( assert ) { - var state, - gateway = { - getPageSummary: function () { - $.Deferred().promise(); - } - }; - - this.actions.linkDwell( - 'element', 'event', - gateway, - constant( 'pagetoken' ) - ); - state = this.store.getState(); - assert.equal( state.preview.activeLink, 'element', 'It has an active link' ); - assert.equal( state.preview.shouldShow, false, 'Initializes with NO_DATA' ); - } ); - - QUnit.test( 'in ACTIVE state, fetch end switches it to DATA', function ( assert ) { - var store = this.store; - this.dwellAndShowPreview( 'element', 'event', 42 ).then( function () { - var state = store.getState(); - assert.equal( state.preview.activeLink, 'element' ); - assert.equal( state.preview.shouldShow, true, 'Should show when data has been fetched' ); - } ); - } ); - - QUnit.test( 'in ACTIVE state, abandon start, and then end, switch it to INACTIVE', function ( assert ) { - var that = this; - this.dwellAndShowPreview( 'element', 'event', 42 ).then( function () { - return that.abandonAndWait( 'element' ); - } ).then( function () { - var state = that.store.getState(); - assert.equal( state.preview.activeLink, undefined, 'After abandoning, preview is back to INACTIVE' ); - } ); - } ); - - QUnit.test( 'in ACTIVE state, abandon link, and then dwell preview, should keep it active after all delays', function ( assert ) { - var that = this; - this.dwellAndPreviewDwell( 'element', 'event', 42 ) - .then( function () { - var state = that.store.getState(); - assert.equal( state.preview.activeLink, 'element' ); - } ); - } ); - - QUnit.test( 'in ACTIVE state, abandon link, hover preview, back to link, should keep it active after all delays', function ( assert ) { - var that = this; - this.dwellAndPreviewDwell( 'element', 'event', 42 ) - .then( function () { - var abandonPreviewDeferred, dwellPromise, dwellDeferred; - - // Start abandoning the preview - that.abandonPreview( 'element' ); - - abandonPreviewDeferred = that.waitDeferred; - // Dwell back into the link, new event is triggered - dwellPromise = that.dwell( 'element', 'event2', 42 ); - dwellDeferred = that.waitDeferred; - - // Preview abandon happens next, before the fetch - abandonPreviewDeferred.resolve(); - - // Then fetch happens - dwellDeferred.resolve(); - - return dwellPromise; - } ) - .then( function () { - var state = that.store.getState(); - assert.equal( state.preview.activeLink, 'element' ); - } ); - } ); - -}( mediaWiki, jQuery ) );