mediawiki-extensions-Popups/tests/node-qunit/integration.test.js
Sam Smith 35bf613964 eventLogging: Add missing *Hover properties
Action changes:
* Include the namespace ID in LINK_DWELL.

Reducer changes:
* Add the createEvent helper, which adds the pageTitleHover and
  namespaceIdHover properties in the event.
* Create "dismissed", "dwelledButAbandoned", and "opened" events using
  the createEvent helper.

Additional changes:
* Store the target page's associated mw.Title using jQuery.
* Update the eventLogging reducer's test cases so that all LINK_DWELL
  representations include title and namespaceID attributes.
* Create the createStubTitle factory function, which returns a minimal
  usable mw.Title stub, and use it in the actions and integration test
  cases.

Bug: T164256
Change-Id: I8a63d82a65324680dff9176020a8ea97695428c4
2017-05-13 08:27:58 +01:00

263 lines
7.3 KiB
JavaScript

var mock = require( 'mock-require' ),
Redux = require( 'redux' ),
ReduxThunk = require( 'redux-thunk' ),
wait = require( '../../src/wait' ),
mw = mediaWiki,
stubs = require( './stubs' ),
$ = jQuery;
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', {
beforeEach: function () {
var that = this,
reducers, actions, registerChangeListener,
title;
// The worst-case implementation of mw.now.
mw.now = function () { return Date.now(); };
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, invalidating the
// require cache for modules that depend on the wait function.
actions = mock.reRequire( '../../src/actions' );
reducers = require( '../../src/reducers' );
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 );
};
title = stubs.createStubTitle( 1, 'Foo' );
this.el = $( '<a href="/wiki/Foo">' )
.data( 'page-previews-title', title );
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();
// Wait for the next tick to resolve pending callbacks. N.B. that the
// fetch action invokes wait twice.
return wait( 0 )
.then( function () {
that.waitDeferred.resolve();
return that.waitPromise;
} );
};
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(
this.el, 'event',
gateway,
constant( 'pagetoken' )
);
state = this.store.getState();
assert.equal( state.preview.activeLink, this.el, '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(),
el = this.el;
this.dwellAndShowPreview( el, 'event', 42 ).then( function () {
var state = store.getState();
assert.equal( state.preview.activeLink, el );
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(),
el = this.el;
this.dwellAndShowPreview( el, 'event', 42 ).then( function () {
return that.abandonAndWait( el );
} ).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(),
el = this.el;
this.dwellAndPreviewDwell( el, 'event', 42 )
.then( function () {
var state = that.store.getState();
assert.equal( state.preview.activeLink, el );
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(),
el = this.el;
this.dwellAndPreviewDwell( el, 'event', 42 )
.then( function () {
var abandonPreviewDeferred, dwellPromise, dwellDeferred;
// Start abandoning the preview
that.abandonPreview( el );
abandonPreviewDeferred = that.waitDeferred;
// Dwell back into the link, new event is triggered
dwellPromise = that.dwell( el, '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, el );
done();
} );
} );