mediawiki-extensions-Popups/tests/node-qunit/integration.test.js
joakin 1fff0d2ea7 Improve & fix action and integration tests
This change fixes some issues with assertions not running, removes
unnecessary promise dances, and improves legibility and some code
patterns in the action and integration node tests mainly.

Detailed changes:

* actions.js
  * Fully migrate out of jQuery 1 promises (no done/fail)
  * Fix linkDwell action not returning the fetch action promise

* actions.test.js
  * Simplify setupWait for the tests
    * It always autoresolves immediately the wait call to ensure speedy tests
    * No waitDeferreds or waitPromises array coordination, rely on action
      returned promises instead
  * Get rid of that = this in favor of arrow functions
  * Rename generic "p" promises to meaningful names
  * Add assert.expect for more solid tests (so that we don't skip assertions in
    the future if we change them)
  * Fix some assertions that weren't being run because of the incorrect promise
    being returned (p.then, and then just returning p)
  * Get rid of $.when stubbing in favor of waiting for the promise returned from
    the action
    * Get rid of hacky setTimeout(..., 0) to run assertions after the promises

* integration.test.js
  * Get rid of wait(0) calls to hack around asynchronous actions
    * Use the action returned promises instead of the waitPromises/Deferreds
  * Remove unused "el" parameter being passed to this.abandon in several tests
  * Remove unnecessary test helper this.abandonPreview (it was the same as
    this.abandon)
  * Clarify a bit the last and more complex test with some comments and variable
    name changes
  * Get rid of that=this in favor of arrow functions

* container.test.js
  * Get rid of that=this in favor of arrow functions

* previewBehavior.test.js
  * Get rid of that=this in favor of arrow functions
  * Get rid of $.each in favor of .forEach

Bug: T170807
Change-Id: I06fafd92a1712f143018e2ddff24fadf1a6882b3
2018-02-16 13:52:56 +01:00

257 lines
7.4 KiB
JavaScript

import * as Redux from 'redux';
import * as ReduxThunk from 'redux-thunk';
import * as WaitModule from '../../src/wait';
import * as stubs from './stubs';
import * as actions from '../../src/actions';
import reducers from '../../src/reducers';
import registerChangeListener from '../../src/changeListener';
var mw = mediaWiki,
$ = jQuery,
/**
* Whether Gateway#getPageSummary is resolved or rejected.
* @enum {number}
*/
FETCH_RESOLUTION = { RESOLVE: 0, REJECT: 1 };
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 () {
// The worst-case implementation of mw.now.
mw.now = function () { return Date.now(); };
this.resetWait = () => {
this.waitDeferred = $.Deferred();
this.waitPromise = this.waitDeferred.promise();
this.wait.returns( this.waitPromise );
};
this.wait = this.sandbox.stub( WaitModule, 'default' );
this.resetWait();
this.store = Redux.createStore(
Redux.combineReducers( reducers ),
Redux.compose( Redux.applyMiddleware( ReduxThunk.default ) )
);
this.actions = Redux.bindActionCreators(
actions,
this.store.dispatch
);
this.registerChangeListener = ( fn ) => {
return registerChangeListener( this.store, fn );
};
this.title = stubs.createStubTitle( 1, 'Foo' );
this.el = $( '<a href="/wiki/Foo">' );
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 = (
title, el, ev, fetchResponse, resolution = FETCH_RESOLUTION.RESOLVE
) => {
this.resetWait();
return this.actions.linkDwell( title, el, ev, {
getPageSummary: function () {
var method = resolution === FETCH_RESOLUTION.RESOLVE ?
'resolve' : 'reject';
return $.Deferred()[ method ]( fetchResponse ).promise();
}
}, function () { return 'pagetoken'; } );
};
this.dwellAndShowPreview = (
title, el, ev, fetchResponse, reject = FETCH_RESOLUTION.RESOLVE
) => {
var dwelled = this.dwell( title, el, ev, fetchResponse, reject );
// Resolve the wait timeout for the linkDwell and the fetch action
this.waitDeferred.resolve();
return dwelled;
};
this.abandon = () => {
this.resetWait();
return this.actions.abandon();
};
this.abandonAndWait = () => {
var abandoned = this.abandon();
this.waitDeferred.resolve();
return abandoned;
};
this.dwellAndPreviewDwell = ( title, el, ev, res ) => {
return this.dwellAndShowPreview( title, el, ev, res ).then( () => {
// Get out of the link, and before the delay ends...
var abandonPromise = this.abandon(),
abandonWaitDeferred = this.waitDeferred;
// Dwell over the preview
this.actions.previewDwell( el );
// Then the abandon delay finishes
abandonWaitDeferred.resolve();
return abandonPromise;
} );
};
}
} );
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.title, 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,
el = this.el;
return this.dwellAndShowPreview( this.title, 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' );
} );
} );
QUnit.test( 'in ACTIVE state, fetch fail switches it to DATA', function ( assert ) {
var store = this.store,
el = this.el;
return this.dwellAndShowPreview(
this.title, el, 'event', 42, FETCH_RESOLUTION.REJECT
).then( function () {
var state = store.getState();
assert.equal( state.preview.activeLink, el );
assert.equal( state.preview.shouldShow, true,
'Should show when data couldn\'t be fetched' );
} );
} );
QUnit.test( 'in ACTIVE state, abandon start, and then end, switch it to INACTIVE', function ( assert ) {
var el = this.el;
return this.dwellAndShowPreview( this.title, el, 'event', 42 )
.then( () => {
return this.abandonAndWait( el );
} ).then( () => {
var state = this.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 el = this.el;
return this.dwellAndPreviewDwell( this.title, el, 'event', 42 )
.then( () => {
var state = this.store.getState();
assert.equal( state.preview.activeLink, el );
} );
} );
QUnit.test( 'in ACTIVE state, abandon link, hover preview, back to link, should keep it active after all delays', function ( assert ) {
var el = this.el;
// Dwell link, abandon it & hover preview
return this.dwellAndPreviewDwell( this.title, el, 'event', 42 )
.then( () => {
var abandonedPreview, abandonWaitDeferred, dwelled, dwellWaitDeferred;
// Start abandoning the preview
abandonedPreview = this.abandon();
abandonWaitDeferred = this.waitDeferred;
// Dwell back into the link, new event ('event2') is triggered
dwelled = this.dwell( this.title, el, 'event2', 42 );
dwellWaitDeferred = this.waitDeferred;
// Preview abandon happens next, before the fetch
abandonWaitDeferred.resolve();
// Then dwell wait & fetch happens
dwellWaitDeferred.resolve();
return $.when( abandonedPreview, dwelled );
} )
.then( () => {
var state = this.store.getState();
assert.equal( state.preview.activeLink, el );
} );
} );