mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Popups
synced 2024-09-23 18:29:35 +00:00
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:
parent
7a54c13ec4
commit
82e315b124
BIN
resources/dist/index.js
vendored
BIN
resources/dist/index.js
vendored
Binary file not shown.
BIN
resources/dist/index.js.map
vendored
BIN
resources/dist/index.js.map
vendored
Binary file not shown.
|
@ -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,
|
||||
|
|
424
tests/node-qunit/actions.test.js
Normal file
424
tests/node-qunit/actions.test.js
Normal 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' );
|
||||
} );
|
234
tests/node-qunit/integration.test.js
Normal file
234
tests/node-qunit/integration.test.js
Normal 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();
|
||||
} );
|
||||
} );
|
|
@ -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 ) );
|
|
@ -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 ) );
|
Loading…
Reference in a new issue