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
This commit is contained in:
joakin 2017-02-27 17:10:50 +01:00
parent 7a54c13ec4
commit 82e315b124
7 changed files with 662 additions and 623 deletions

Binary file not shown.

Binary file not shown.

View file

@ -2,6 +2,7 @@ var $ = jQuery,
mw = window.mediaWiki,
actions = {},
types = require( './actionTypes' ),
wait = require( './wait' ),
FETCH_START_DELAY = 50, // ms.
// The delay after which a FETCH_END action should be dispatched.
@ -113,11 +114,7 @@ actions.fetch = function ( gateway, el, started ) {
0
);
// FIXME: This needs to reference a global because the tests are
// stubbing a global, so can't be required at the top at the moment.
// When the tests are moved to common.js we should find a different way
// of stubbing this wait
mw.popups.wait( delay )
wait( delay )
.then( function () {
dispatch( {
type: types.FETCH_END,
@ -161,11 +158,7 @@ actions.linkDwell = function ( el, event, gateway, generateToken ) {
return;
}
// FIXME: This needs to reference a global because the tests are stubbing
// a global, so can't be required at the top at the moment. When the tests
// are moved to common.js we should find a different way of stubbing this
// wait
mw.popups.wait( FETCH_START_DELAY )
wait( FETCH_START_DELAY )
.then( function () {
var previewState = getState().preview;
@ -193,11 +186,7 @@ actions.abandon = function () {
token: token
} ) );
// FIXME: This needs to reference a global because the tests are stubbing
// a global, so can't be required at the top at the moment. When the tests
// are moved to common.js we should find a different way of stubbing this
// wait
mw.popups.wait( ABANDON_END_DELAY )
wait( ABANDON_END_DELAY )
.then( function () {
dispatch( {
type: types.ABANDON_END,

View file

@ -0,0 +1,424 @@
var mock = require( 'mock-require' ),
createStubUser = require( './stubs' ).createStubUser,
actions = require( '../../src/actions' ),
mw = mediaWiki;
function generateToken() {
return '9876543210';
}
QUnit.module( 'ext.popups/actions' );
QUnit.test( '#boot', function ( assert ) {
var config = new Map(), /* global Map */
stubUser = createStubUser( /* isAnon = */ true ),
stubUserSettings,
action;
config.set( 'wgTitle', 'Foo' );
config.set( 'wgNamespaceNumber', 1 );
config.set( 'wgArticleId', 2 );
config.set( 'wgUserEditCount', 3 );
config.set( 'wgPopupsConflictsWithNavPopupGadget', true );
stubUserSettings = {
getPreviewCount: function () {
return 22;
}
};
assert.expect( 1 );
action = 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 `wait.js` 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.wait = module.sandbox.stub().returns( module.waitPromise );
mock( '../../src/wait', module.wait );
// Re-require actions so that it uses the mocked wait module
actions = mock.reRequire( '../../src/actions' );
}
function teardownWait() {
mock.stop( '../../src/wait' );
}
/**
* Sets up an `a` element that can be passed to action creators.
*
* @param {Object} module
*/
function setupEl( module ) {
module.el = $( '<a>' )
.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' );
} );

View file

@ -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();
} );
} );

View file

@ -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 = $( '<a>' )
.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 ) );

View file

@ -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 ) );