2017-07-28 17:32:46 +00:00
|
|
|
import { createStubUser, createStubTitle } from './stubs';
|
|
|
|
import * as actions from '../../src/actions';
|
|
|
|
import * as WaitModule from '../../src/wait';
|
2018-07-13 15:12:49 +00:00
|
|
|
import actionTypes from '../../src/actionTypes';
|
2019-03-11 13:14:17 +00:00
|
|
|
import { previewTypes } from '../../src/preview/model';
|
2017-07-28 17:32:46 +00:00
|
|
|
|
2019-10-17 08:51:49 +00:00
|
|
|
const REFERRER = 'https://en.wikipedia.org/wiki/Kitten',
|
2019-04-26 10:53:46 +00:00
|
|
|
TEST_TITLE = createStubTitle( 0, 'Foo' );
|
2017-02-27 16:10:50 +00:00
|
|
|
|
|
|
|
function generateToken() {
|
2018-08-21 23:43:33 +00:00
|
|
|
return 'ABC';
|
2017-02-27 16:10:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QUnit.module( 'ext.popups/actions' );
|
|
|
|
|
2018-03-14 23:50:09 +00:00
|
|
|
QUnit.test( '#boot', ( assert ) => {
|
2019-09-17 11:47:25 +00:00
|
|
|
const config = new Map(),
|
2018-03-19 19:39:41 +00:00
|
|
|
stubUser = createStubUser( /* isAnon = */ true );
|
2017-02-27 16:10:50 +00:00
|
|
|
|
|
|
|
config.set( 'wgTitle', 'Foo' );
|
2019-04-26 10:53:46 +00:00
|
|
|
config.set( 'wgNamespaceNumber', 0 );
|
2017-02-27 16:10:50 +00:00
|
|
|
config.set( 'wgArticleId', 2 );
|
|
|
|
config.set( 'wgUserEditCount', 3 );
|
|
|
|
config.set( 'wgPopupsConflictsWithNavPopupGadget', true );
|
|
|
|
|
2018-03-19 19:39:41 +00:00
|
|
|
const stubUserSettings = {
|
2018-03-14 22:04:59 +00:00
|
|
|
getPreviewCount() {
|
2017-02-27 16:10:50 +00:00
|
|
|
return 22;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-03-19 19:39:41 +00:00
|
|
|
const action = actions.boot(
|
2021-04-07 10:23:05 +00:00
|
|
|
{ page: false },
|
2017-02-27 16:10:50 +00:00
|
|
|
stubUser,
|
|
|
|
stubUserSettings,
|
2018-02-15 20:15:23 +00:00
|
|
|
config,
|
|
|
|
REFERRER
|
2017-02-27 16:10:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
assert.deepEqual(
|
|
|
|
action,
|
|
|
|
{
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.BOOT,
|
2021-04-07 10:23:05 +00:00
|
|
|
initiallyEnabled: { page: false },
|
2017-02-27 16:10:50 +00:00
|
|
|
isNavPopupsEnabled: true,
|
|
|
|
sessionToken: '0123456789',
|
|
|
|
pageToken: '9876543210',
|
|
|
|
page: {
|
2018-02-15 20:15:23 +00:00
|
|
|
url: REFERRER,
|
2017-02-27 16:10:50 +00:00
|
|
|
title: 'Foo',
|
2019-04-26 10:53:46 +00:00
|
|
|
namespaceId: 0,
|
2017-02-27 16:10:50 +00:00
|
|
|
id: 2
|
|
|
|
},
|
|
|
|
user: {
|
|
|
|
isAnon: true,
|
2021-03-02 17:08:45 +00:00
|
|
|
editCount: 3
|
2017-02-27 16:10:50 +00:00
|
|
|
}
|
2018-02-15 20:15:23 +00:00
|
|
|
},
|
|
|
|
'boots with the initial state'
|
2017-02-27 16:10:50 +00:00
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
/**
|
2018-07-09 16:56:45 +00:00
|
|
|
* Stubs `wait.js` and adds the deferred and its promise as properties
|
|
|
|
* of the module.
|
|
|
|
*
|
|
|
|
* @param {Object} module
|
|
|
|
* @return {void}
|
|
|
|
*/
|
2017-02-27 16:10:50 +00:00
|
|
|
function setupWait( module ) {
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
module.waitPromise = $.Deferred().resolve().promise( { abort() {} } );
|
2018-03-14 23:50:09 +00:00
|
|
|
module.wait = module.sandbox.stub( WaitModule, 'default' ).callsFake(
|
|
|
|
() => module.waitPromise
|
|
|
|
);
|
2017-02-27 16:10:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-07-09 16:56:45 +00:00
|
|
|
* Sets up a link/mw.Title stub pair that can be passed to the linkDwell action
|
|
|
|
* creator.
|
|
|
|
*
|
|
|
|
* @param {Object} module
|
|
|
|
* @return {void}
|
|
|
|
*/
|
2017-02-27 16:10:50 +00:00
|
|
|
function setupEl( module ) {
|
2019-01-17 12:51:13 +00:00
|
|
|
module.title = TEST_TITLE;
|
2020-04-25 21:32:53 +00:00
|
|
|
module.el = $( '<a>' ).get( 0 );
|
2017-02-27 16:10:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QUnit.module( 'ext.popups/actions#linkDwell @integration', {
|
2018-03-14 22:04:59 +00:00
|
|
|
beforeEach() {
|
2017-02-27 16:10:50 +00:00
|
|
|
this.state = {
|
|
|
|
preview: {}
|
|
|
|
};
|
2018-02-16 12:52:56 +00:00
|
|
|
this.getState = () => this.state;
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2017-03-26 21:05:32 +00:00
|
|
|
// The worst-case implementation of mw.now.
|
2018-03-14 23:50:09 +00:00
|
|
|
mw.now = () => Date.now();
|
2017-02-27 16:10:50 +00:00
|
|
|
|
|
|
|
setupEl( this );
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
QUnit.test( '#linkDwell', function ( assert ) {
|
2020-11-24 14:33:42 +00:00
|
|
|
const measures = {},
|
2017-02-27 16:10:50 +00:00
|
|
|
dispatch = this.sandbox.spy();
|
|
|
|
|
|
|
|
this.sandbox.stub( mw, 'now' ).returns( new Date() );
|
|
|
|
this.sandbox.stub( actions, 'fetch' );
|
|
|
|
|
2017-03-26 21:05:32 +00:00
|
|
|
// Stub the state tree being updated by the LINK_DWELL action.
|
2017-02-27 16:10:50 +00:00
|
|
|
this.state.preview = {
|
|
|
|
activeToken: generateToken()
|
|
|
|
};
|
|
|
|
|
2018-03-19 19:39:41 +00:00
|
|
|
const linkDwelled = actions.linkDwell(
|
2020-11-24 14:33:42 +00:00
|
|
|
this.title, this.el, measures, null, generateToken, previewTypes.TYPE_PAGE
|
2018-01-18 18:48:16 +00:00
|
|
|
)(
|
2017-02-27 16:10:50 +00:00
|
|
|
dispatch,
|
|
|
|
this.getState
|
|
|
|
);
|
|
|
|
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
assert.propEqual(
|
2018-05-08 19:48:17 +00:00
|
|
|
dispatch.getCall( 0 ).args[ 0 ], {
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.LINK_DWELL,
|
2018-05-08 19:48:17 +00:00
|
|
|
el: this.el,
|
2021-04-07 10:23:05 +00:00
|
|
|
previewType: 'page',
|
2020-11-24 14:33:42 +00:00
|
|
|
measures,
|
2018-08-21 23:43:33 +00:00
|
|
|
token: 'ABC',
|
2018-05-08 19:48:17 +00:00
|
|
|
timestamp: mw.now(),
|
|
|
|
title: 'Foo',
|
2019-04-26 10:53:46 +00:00
|
|
|
namespaceId: 0,
|
Fix action reducer forgetting *all* duplicate dwell actions
I was able to track the issue down to the changes made in this patch:
https://gerrit.wikimedia.org/r/331563
This patch is mostly a simplification of ce8a2d4
(I9a73b3086fc8fb0edd897a347b5497d5362e20ef):
- Don't make wait#wait() abortable. This adds complexity and isn't
needed since the token is rechecked in the .then() of
actions#linkDwell(). The request is permitted to continue and fetch if
that token still matches and is never issued otherwise.
Once a request has been issued, that request is still abortable.
However, note that calling XHR.abort() is just a request to abort and
may not be granted. Whether or not XHR.catch() is invoked is what
dispatches the FETCH_ABORTED action (or doesn't if the request to
abort was denied).
- Remove the abort tests for wait#wait() since the functionality is no
longer provided.
- Pass the preview token in the FETCH_ABORTED action and reduce
FETCH_ABORTED the same way as ABANDON_COMPLETE in reducers/preview.
The follow-up patch, I3597b8025f7a12db0cf5d83cce5a77abace9bae3, adds
integration tests for the specific bug fix. Note that these Selenium
tests are incompatible with the content proxy, so it is probably best to
simply unset $wgPopupsRestGatewayEndpoint and $wgPopupsGateway and allow
the defaults to be used. Also note that the tests are incompatible with
recent versions of Node.js (so use NVM) and emit many deprecation
warnings (so set deprecationWarnings to false in
tests/selenium/wdio.conf.js) An example run of the tests looks something
like:
chromedriver --url-base=wd/hub --port=4444 &
# If any changes are made locally, also run `npm -s start &`.
MW_SERVER=http://localhost:8181 \
MEDIAWIKI_USER=foo \
MEDIAWIKI_PASSWORD=bar \
DISPLAY= \
npm -s run selenium-test
Live testing may be performed as well. Remember that RESTBase requests
are incompatible with MobileFrontend's content proxy hack so ensure to
comment it out if $wgPopupsGateway is configured for RESTBase (see
T218159).
1. Open the DevTools network tab.
2. Disable the browser cache. Chromium, at least, won't abort requests
coming form the cache.
3. Hover back and forth quickly over a preview. In Chromium, canceled
requests are labeled and appear red. This is a good scenario to test.
With the patch, a preview should always be shown when ultimately
resting on a link. Without the patch, it is possible to rest upon the
link with no preview showing. This may require several attempts.
Bug: T219434
Change-Id: I9da84b0296dd14e9ce69cb35f1ca475272fb249a
2019-02-05 17:38:22 +00:00
|
|
|
promise: $.Deferred().promise()
|
2018-05-08 19:48:17 +00:00
|
|
|
},
|
|
|
|
'The dispatcher was called with the correct arguments.'
|
|
|
|
);
|
2017-02-27 16:10:50 +00:00
|
|
|
|
|
|
|
// Stub the state tree being updated.
|
|
|
|
this.state.preview = {
|
2021-04-07 10:23:05 +00:00
|
|
|
enabled: { page: true },
|
2017-02-27 16:10:50 +00:00
|
|
|
activeLink: this.el,
|
|
|
|
activeToken: generateToken()
|
|
|
|
};
|
|
|
|
|
|
|
|
// ---
|
|
|
|
|
2018-03-14 23:50:09 +00:00
|
|
|
return linkDwelled.then( () => {
|
2017-02-27 16:10:50 +00:00
|
|
|
assert.strictEqual(
|
|
|
|
dispatch.callCount,
|
|
|
|
2,
|
2017-03-26 21:05:32 +00:00
|
|
|
'The fetch action is dispatched after FETCH_COMPLETE milliseconds.'
|
2017-02-27 16:10:50 +00:00
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
QUnit.test( '#linkDwell doesn\'t continue when previews are disabled', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const event = {},
|
2017-02-27 16:10:50 +00:00
|
|
|
dispatch = this.sandbox.spy();
|
|
|
|
|
2017-03-26 21:05:32 +00:00
|
|
|
// Stub the state tree being updated by the LINK_DWELL action.
|
|
|
|
this.state.preview = {
|
2021-04-07 10:23:05 +00:00
|
|
|
enabled: { page: false },
|
2017-03-26 21:05:32 +00:00
|
|
|
activeLink: this.el,
|
|
|
|
activeToken: generateToken()
|
|
|
|
};
|
|
|
|
|
2018-03-19 19:39:41 +00:00
|
|
|
const linkDwelled = actions.linkDwell(
|
2019-03-11 13:14:17 +00:00
|
|
|
this.title, this.el, event, /* gateway = */ null, generateToken, previewTypes.TYPE_PAGE
|
2018-01-18 18:48:16 +00:00
|
|
|
)(
|
2017-02-27 16:10:50 +00:00
|
|
|
dispatch,
|
|
|
|
this.getState
|
|
|
|
);
|
|
|
|
|
2018-05-08 19:48:17 +00:00
|
|
|
assert.strictEqual(
|
|
|
|
dispatch.callCount,
|
|
|
|
1,
|
|
|
|
'The dispatcher was called once.'
|
|
|
|
);
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2018-03-14 23:50:09 +00:00
|
|
|
return linkDwelled.then( () => {
|
2018-05-08 19:48:17 +00:00
|
|
|
assert.strictEqual(
|
|
|
|
dispatch.callCount,
|
|
|
|
1,
|
|
|
|
'The dispatcher was not called again.'
|
|
|
|
);
|
2017-02-27 16:10:50 +00:00
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
QUnit.test( '#linkDwell doesn\'t continue if the token has changed', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const event = {},
|
2017-02-27 16:10:50 +00:00
|
|
|
dispatch = this.sandbox.spy();
|
|
|
|
|
2017-03-26 21:05:32 +00:00
|
|
|
// Stub the state tree being updated by a LINK_DWELL action.
|
|
|
|
this.state.preview = {
|
2021-04-07 10:23:05 +00:00
|
|
|
enabled: { page: true },
|
2017-03-26 21:05:32 +00:00
|
|
|
activeLink: this.el,
|
|
|
|
activeToken: generateToken()
|
|
|
|
};
|
|
|
|
|
2018-03-19 19:39:41 +00:00
|
|
|
const linkDwelled = actions.linkDwell(
|
2019-03-11 13:14:17 +00:00
|
|
|
this.title, this.el, event, /* gateway = */ null, generateToken, previewTypes.TYPE_PAGE
|
2018-01-18 18:48:16 +00:00
|
|
|
)(
|
2017-02-27 16:10:50 +00:00
|
|
|
dispatch,
|
|
|
|
this.getState
|
|
|
|
);
|
|
|
|
|
2017-03-26 21:05:32 +00:00
|
|
|
// Stub the state tree being updated by another LINK_DWELL action.
|
2017-02-27 16:10:50 +00:00
|
|
|
this.state.preview = {
|
2021-04-07 10:23:05 +00:00
|
|
|
enabled: { page: true },
|
2017-02-27 16:10:50 +00:00
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
2017-03-26 21:05:32 +00:00
|
|
|
activeToken: 'banana'
|
2017-02-27 16:10:50 +00:00
|
|
|
};
|
|
|
|
|
2018-03-14 23:50:09 +00:00
|
|
|
return linkDwelled.then( () => {
|
2018-05-08 19:48:17 +00:00
|
|
|
assert.strictEqual(
|
|
|
|
dispatch.callCount,
|
|
|
|
1,
|
|
|
|
'The dispatcher was called once.'
|
|
|
|
);
|
2017-02-27 16:10:50 +00:00
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
2017-03-26 21:05:32 +00:00
|
|
|
QUnit.test( '#linkDwell dispatches the fetch action', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const event = {},
|
2017-02-27 16:10:50 +00:00
|
|
|
dispatch = this.sandbox.spy();
|
|
|
|
|
|
|
|
this.state.preview = {
|
2021-04-07 10:23:05 +00:00
|
|
|
enabled: { page: true },
|
2017-03-26 21:05:32 +00:00
|
|
|
activeToken: generateToken()
|
2017-02-27 16:10:50 +00:00
|
|
|
};
|
|
|
|
|
2018-02-16 12:52:56 +00:00
|
|
|
return actions.linkDwell(
|
2019-03-11 13:14:17 +00:00
|
|
|
this.title, this.el, event, /* gateway = */ null, generateToken, previewTypes.TYPE_PAGE
|
2018-01-18 18:48:16 +00:00
|
|
|
)(
|
2017-02-27 16:10:50 +00:00
|
|
|
dispatch,
|
|
|
|
this.getState
|
2018-03-14 23:50:09 +00:00
|
|
|
).then( () => {
|
2018-05-08 19:48:17 +00:00
|
|
|
assert.strictEqual(
|
|
|
|
dispatch.callCount,
|
|
|
|
2,
|
|
|
|
'The dispatcher was called twice.'
|
|
|
|
);
|
2017-02-27 16:10:50 +00:00
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
QUnit.module( 'ext.popups/actions#fetch', {
|
2018-03-14 22:04:59 +00:00
|
|
|
beforeEach() {
|
2017-04-07 11:34:21 +00:00
|
|
|
this.now = 0;
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2018-02-16 12:52:56 +00:00
|
|
|
this.sandbox.stub( mw, 'now' ).callsFake( () => this.now );
|
2017-02-27 16:10:50 +00:00
|
|
|
|
|
|
|
setupWait( this );
|
|
|
|
setupEl( this );
|
|
|
|
|
2017-05-03 09:19:19 +00:00
|
|
|
this.gatewayDeferred = $.Deferred();
|
2017-02-27 16:10:50 +00:00
|
|
|
this.gateway = {
|
2019-01-23 16:50:19 +00:00
|
|
|
fetchPreviewForTitle: this.sandbox.stub().returns(
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
this.gatewayDeferred.promise( { abort() {} } )
|
2018-02-16 12:52:56 +00:00
|
|
|
)
|
2017-02-27 16:10:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
this.dispatch = this.sandbox.spy();
|
|
|
|
|
2017-04-07 11:34:21 +00:00
|
|
|
this.token = '1234567890';
|
|
|
|
|
2017-02-27 16:10:50 +00:00
|
|
|
// Sugar.
|
2018-02-16 12:52:56 +00:00
|
|
|
this.fetch = () => {
|
2018-01-18 18:48:16 +00:00
|
|
|
return actions.fetch(
|
2019-03-11 13:14:17 +00:00
|
|
|
this.gateway, this.title, this.el, this.token, previewTypes.TYPE_PAGE
|
2018-02-16 12:52:56 +00:00
|
|
|
)( this.dispatch );
|
2017-02-27 16:10:50 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
QUnit.test( 'it should fetch data from the gateway immediately', function ( assert ) {
|
|
|
|
this.fetch();
|
|
|
|
|
2018-05-08 19:48:17 +00:00
|
|
|
assert.ok(
|
2019-01-23 16:50:19 +00:00
|
|
|
this.gateway.fetchPreviewForTitle.calledWith( TEST_TITLE ),
|
2018-05-08 19:48:17 +00:00
|
|
|
'The gateway was called with the correct arguments.'
|
|
|
|
);
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2018-04-24 22:03:32 +00:00
|
|
|
assert.strictEqual( this.dispatch.callCount, 1 );
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
assert.propEqual(
|
2017-05-09 18:41:23 +00:00
|
|
|
this.dispatch.getCall( 0 ).args[ 0 ],
|
|
|
|
{
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.FETCH_START,
|
2017-02-27 16:10:50 +00:00
|
|
|
el: this.el,
|
2017-03-07 00:27:38 +00:00
|
|
|
title: 'Foo',
|
2019-04-26 10:53:46 +00:00
|
|
|
namespaceId: 0,
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
timestamp: this.now,
|
|
|
|
promise: $.Deferred().promise( { abort() {} } )
|
2017-05-09 18:41:23 +00:00
|
|
|
},
|
2017-02-27 16:10:50 +00:00
|
|
|
'It dispatches the FETCH_START action immediately.'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
2017-03-26 23:18:29 +00:00
|
|
|
QUnit.test( 'it should dispatch the FETCH_END action when the API request ends', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const fetched = this.fetch();
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2017-03-26 23:18:29 +00:00
|
|
|
this.now += 115;
|
|
|
|
this.gatewayDeferred.resolve( {} );
|
|
|
|
|
2018-02-16 12:52:56 +00:00
|
|
|
return fetched.then( () => {
|
2017-03-26 21:05:32 +00:00
|
|
|
assert.deepEqual(
|
2018-02-16 12:52:56 +00:00
|
|
|
this.dispatch.getCall( 1 ).args[ 0 ],
|
2017-03-26 21:05:32 +00:00
|
|
|
{
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.FETCH_END,
|
2018-02-16 12:52:56 +00:00
|
|
|
el: this.el,
|
2017-03-26 23:18:29 +00:00
|
|
|
timestamp: 115
|
2018-05-08 19:48:17 +00:00
|
|
|
},
|
|
|
|
'The dispatcher was called with the correct arguments.'
|
2017-02-27 16:10:50 +00:00
|
|
|
);
|
|
|
|
} );
|
2017-03-26 23:18:29 +00:00
|
|
|
} );
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2018-01-18 17:33:58 +00:00
|
|
|
QUnit.test( 'it should delay dispatching the FETCH_COMPLETE action', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const result = {},
|
2018-02-16 12:52:56 +00:00
|
|
|
fetched = this.fetch();
|
2017-04-25 12:12:36 +00:00
|
|
|
|
2017-03-26 23:18:29 +00:00
|
|
|
assert.strictEqual(
|
|
|
|
this.wait.getCall( 0 ).args[ 0 ],
|
2017-04-06 20:12:19 +00:00
|
|
|
350,
|
|
|
|
'It waits for FETCH_COMPLETE_TARGET_DELAY - FETCH_START_DELAY milliseconds.'
|
2017-03-26 23:18:29 +00:00
|
|
|
);
|
2018-02-16 12:52:56 +00:00
|
|
|
this.gatewayDeferred.resolve( result );
|
2017-03-26 23:18:29 +00:00
|
|
|
|
2018-02-16 12:52:56 +00:00
|
|
|
return fetched.then( () => {
|
|
|
|
assert.deepEqual(
|
|
|
|
this.dispatch.getCall( 2 ).args[ 0 ],
|
|
|
|
{
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.FETCH_COMPLETE,
|
2018-02-16 12:52:56 +00:00
|
|
|
el: this.el,
|
2018-03-14 19:44:22 +00:00
|
|
|
result,
|
2018-02-16 12:52:56 +00:00
|
|
|
token: this.token
|
2018-05-08 19:48:17 +00:00
|
|
|
},
|
|
|
|
'The dispatcher was called with the correct arguments.'
|
2018-02-16 12:52:56 +00:00
|
|
|
);
|
2017-03-26 23:18:29 +00:00
|
|
|
} );
|
|
|
|
} );
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2017-08-23 17:36:05 +00:00
|
|
|
QUnit.test( 'it should dispatch the FETCH_FAILED action when the request fails', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const fetched = this.fetch();
|
2017-08-23 17:36:05 +00:00
|
|
|
|
|
|
|
this.gatewayDeferred.reject( new Error( 'API req failed' ) );
|
|
|
|
|
2018-02-16 12:52:56 +00:00
|
|
|
this.now += 115;
|
|
|
|
|
|
|
|
return fetched.then( () => {
|
2018-05-20 12:32:51 +00:00
|
|
|
assert.strictEqual(
|
2018-02-16 12:52:56 +00:00
|
|
|
this.dispatch.callCount, 3,
|
2018-01-18 18:48:16 +00:00
|
|
|
'dispatch called thrice, START, FAILED, and COMPLETE'
|
|
|
|
);
|
2017-08-23 17:36:05 +00:00
|
|
|
assert.deepEqual(
|
2018-02-16 12:52:56 +00:00
|
|
|
this.dispatch.getCall( 1 ).args[ 0 ],
|
2017-08-23 17:36:05 +00:00
|
|
|
{
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.FETCH_FAILED,
|
Fix action reducer forgetting *all* duplicate dwell actions
I was able to track the issue down to the changes made in this patch:
https://gerrit.wikimedia.org/r/331563
This patch is mostly a simplification of ce8a2d4
(I9a73b3086fc8fb0edd897a347b5497d5362e20ef):
- Don't make wait#wait() abortable. This adds complexity and isn't
needed since the token is rechecked in the .then() of
actions#linkDwell(). The request is permitted to continue and fetch if
that token still matches and is never issued otherwise.
Once a request has been issued, that request is still abortable.
However, note that calling XHR.abort() is just a request to abort and
may not be granted. Whether or not XHR.catch() is invoked is what
dispatches the FETCH_ABORTED action (or doesn't if the request to
abort was denied).
- Remove the abort tests for wait#wait() since the functionality is no
longer provided.
- Pass the preview token in the FETCH_ABORTED action and reduce
FETCH_ABORTED the same way as ABANDON_COMPLETE in reducers/preview.
The follow-up patch, I3597b8025f7a12db0cf5d83cce5a77abace9bae3, adds
integration tests for the specific bug fix. Note that these Selenium
tests are incompatible with the content proxy, so it is probably best to
simply unset $wgPopupsRestGatewayEndpoint and $wgPopupsGateway and allow
the defaults to be used. Also note that the tests are incompatible with
recent versions of Node.js (so use NVM) and emit many deprecation
warnings (so set deprecationWarnings to false in
tests/selenium/wdio.conf.js) An example run of the tests looks something
like:
chromedriver --url-base=wd/hub --port=4444 &
# If any changes are made locally, also run `npm -s start &`.
MW_SERVER=http://localhost:8181 \
MEDIAWIKI_USER=foo \
MEDIAWIKI_PASSWORD=bar \
DISPLAY= \
npm -s run selenium-test
Live testing may be performed as well. Remember that RESTBase requests
are incompatible with MobileFrontend's content proxy hack so ensure to
comment it out if $wgPopupsGateway is configured for RESTBase (see
T218159).
1. Open the DevTools network tab.
2. Disable the browser cache. Chromium, at least, won't abort requests
coming form the cache.
3. Hover back and forth quickly over a preview. In Chromium, canceled
requests are labeled and appear red. This is a good scenario to test.
With the patch, a preview should always be shown when ultimately
resting on a link. Without the patch, it is possible to rest upon the
link with no preview showing. This may require several attempts.
Bug: T219434
Change-Id: I9da84b0296dd14e9ce69cb35f1ca475272fb249a
2019-02-05 17:38:22 +00:00
|
|
|
el: this.el,
|
|
|
|
token: this.token
|
2018-05-08 19:48:17 +00:00
|
|
|
},
|
|
|
|
'The dispatcher was called with the correct arguments.'
|
2017-08-23 17:36:05 +00:00
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
QUnit.test( 'it should dispatch the FETCH_FAILED action when the request fails even after the wait timeout', function ( assert ) {
|
2018-02-16 12:52:56 +00:00
|
|
|
// After the wait interval happens, resolve the gateway request
|
|
|
|
return this.waitPromise.then( () => {
|
|
|
|
this.gatewayDeferred.reject( new Error( 'API req failed' ) );
|
2018-05-03 21:15:43 +00:00
|
|
|
return this.fetch();
|
2018-02-16 12:52:56 +00:00
|
|
|
} ).then( () => {
|
2018-05-20 12:32:51 +00:00
|
|
|
assert.strictEqual(
|
2018-02-16 12:52:56 +00:00
|
|
|
this.dispatch.callCount, 3,
|
2018-01-18 18:48:16 +00:00
|
|
|
'dispatch called thrice, START, FAILED, and COMPLETE'
|
|
|
|
);
|
2017-08-23 17:36:05 +00:00
|
|
|
assert.deepEqual(
|
2018-02-16 12:52:56 +00:00
|
|
|
this.dispatch.getCall( 1 ).args[ 0 ],
|
2017-08-23 17:36:05 +00:00
|
|
|
{
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.FETCH_FAILED,
|
Fix action reducer forgetting *all* duplicate dwell actions
I was able to track the issue down to the changes made in this patch:
https://gerrit.wikimedia.org/r/331563
This patch is mostly a simplification of ce8a2d4
(I9a73b3086fc8fb0edd897a347b5497d5362e20ef):
- Don't make wait#wait() abortable. This adds complexity and isn't
needed since the token is rechecked in the .then() of
actions#linkDwell(). The request is permitted to continue and fetch if
that token still matches and is never issued otherwise.
Once a request has been issued, that request is still abortable.
However, note that calling XHR.abort() is just a request to abort and
may not be granted. Whether or not XHR.catch() is invoked is what
dispatches the FETCH_ABORTED action (or doesn't if the request to
abort was denied).
- Remove the abort tests for wait#wait() since the functionality is no
longer provided.
- Pass the preview token in the FETCH_ABORTED action and reduce
FETCH_ABORTED the same way as ABANDON_COMPLETE in reducers/preview.
The follow-up patch, I3597b8025f7a12db0cf5d83cce5a77abace9bae3, adds
integration tests for the specific bug fix. Note that these Selenium
tests are incompatible with the content proxy, so it is probably best to
simply unset $wgPopupsRestGatewayEndpoint and $wgPopupsGateway and allow
the defaults to be used. Also note that the tests are incompatible with
recent versions of Node.js (so use NVM) and emit many deprecation
warnings (so set deprecationWarnings to false in
tests/selenium/wdio.conf.js) An example run of the tests looks something
like:
chromedriver --url-base=wd/hub --port=4444 &
# If any changes are made locally, also run `npm -s start &`.
MW_SERVER=http://localhost:8181 \
MEDIAWIKI_USER=foo \
MEDIAWIKI_PASSWORD=bar \
DISPLAY= \
npm -s run selenium-test
Live testing may be performed as well. Remember that RESTBase requests
are incompatible with MobileFrontend's content proxy hack so ensure to
comment it out if $wgPopupsGateway is configured for RESTBase (see
T218159).
1. Open the DevTools network tab.
2. Disable the browser cache. Chromium, at least, won't abort requests
coming form the cache.
3. Hover back and forth quickly over a preview. In Chromium, canceled
requests are labeled and appear red. This is a good scenario to test.
With the patch, a preview should always be shown when ultimately
resting on a link. Without the patch, it is possible to rest upon the
link with no preview showing. This may require several attempts.
Bug: T219434
Change-Id: I9da84b0296dd14e9ce69cb35f1ca475272fb249a
2019-02-05 17:38:22 +00:00
|
|
|
el: this.el,
|
|
|
|
token: this.token
|
2018-05-08 19:48:17 +00:00
|
|
|
},
|
|
|
|
'The dispatcher was called with the correct arguments.'
|
2017-08-23 17:36:05 +00:00
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
2018-07-12 22:11:54 +00:00
|
|
|
QUnit.test( 'it should dispatch the FETCH_ABORTED action when the request is aborted', function ( assert ) {
|
|
|
|
const fetched = this.fetch();
|
|
|
|
this.now += 115;
|
|
|
|
this.gatewayDeferred.reject( 'http', {
|
|
|
|
textStatus: 'abort',
|
|
|
|
exception: 'abort',
|
|
|
|
xhr: {
|
|
|
|
readyState: 0
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
return fetched.then( () => {
|
|
|
|
assert.strictEqual(
|
|
|
|
this.dispatch.callCount, 2,
|
|
|
|
'dispatch called twice with START and ABORT'
|
|
|
|
);
|
|
|
|
assert.deepEqual(
|
|
|
|
this.dispatch.getCall( 1 ).args[ 0 ],
|
|
|
|
{
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.FETCH_ABORTED,
|
Fix action reducer forgetting *all* duplicate dwell actions
I was able to track the issue down to the changes made in this patch:
https://gerrit.wikimedia.org/r/331563
This patch is mostly a simplification of ce8a2d4
(I9a73b3086fc8fb0edd897a347b5497d5362e20ef):
- Don't make wait#wait() abortable. This adds complexity and isn't
needed since the token is rechecked in the .then() of
actions#linkDwell(). The request is permitted to continue and fetch if
that token still matches and is never issued otherwise.
Once a request has been issued, that request is still abortable.
However, note that calling XHR.abort() is just a request to abort and
may not be granted. Whether or not XHR.catch() is invoked is what
dispatches the FETCH_ABORTED action (or doesn't if the request to
abort was denied).
- Remove the abort tests for wait#wait() since the functionality is no
longer provided.
- Pass the preview token in the FETCH_ABORTED action and reduce
FETCH_ABORTED the same way as ABANDON_COMPLETE in reducers/preview.
The follow-up patch, I3597b8025f7a12db0cf5d83cce5a77abace9bae3, adds
integration tests for the specific bug fix. Note that these Selenium
tests are incompatible with the content proxy, so it is probably best to
simply unset $wgPopupsRestGatewayEndpoint and $wgPopupsGateway and allow
the defaults to be used. Also note that the tests are incompatible with
recent versions of Node.js (so use NVM) and emit many deprecation
warnings (so set deprecationWarnings to false in
tests/selenium/wdio.conf.js) An example run of the tests looks something
like:
chromedriver --url-base=wd/hub --port=4444 &
# If any changes are made locally, also run `npm -s start &`.
MW_SERVER=http://localhost:8181 \
MEDIAWIKI_USER=foo \
MEDIAWIKI_PASSWORD=bar \
DISPLAY= \
npm -s run selenium-test
Live testing may be performed as well. Remember that RESTBase requests
are incompatible with MobileFrontend's content proxy hack so ensure to
comment it out if $wgPopupsGateway is configured for RESTBase (see
T218159).
1. Open the DevTools network tab.
2. Disable the browser cache. Chromium, at least, won't abort requests
coming form the cache.
3. Hover back and forth quickly over a preview. In Chromium, canceled
requests are labeled and appear red. This is a good scenario to test.
With the patch, a preview should always be shown when ultimately
resting on a link. Without the patch, it is possible to rest upon the
link with no preview showing. This may require several attempts.
Bug: T219434
Change-Id: I9da84b0296dd14e9ce69cb35f1ca475272fb249a
2019-02-05 17:38:22 +00:00
|
|
|
el: this.el,
|
|
|
|
token: this.token
|
2018-07-12 22:11:54 +00:00
|
|
|
},
|
|
|
|
'The dispatcher was called with the correct arguments.'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
2017-02-27 16:10:50 +00:00
|
|
|
QUnit.module( 'ext.popups/actions#abandon', {
|
2018-03-14 22:04:59 +00:00
|
|
|
beforeEach() {
|
2018-02-16 12:52:56 +00:00
|
|
|
setupWait( this );
|
2018-02-13 11:57:31 +00:00
|
|
|
setupEl( this );
|
2017-02-27 16:10:50 +00:00
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
QUnit.test( 'it should dispatch start and end actions', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const dispatch = this.sandbox.spy(),
|
2017-02-27 16:10:50 +00:00
|
|
|
token = '0123456789',
|
2018-03-14 23:50:09 +00:00
|
|
|
getState = () =>
|
|
|
|
( {
|
2017-02-27 16:10:50 +00:00
|
|
|
preview: {
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
activeToken: token,
|
|
|
|
promise: $.Deferred().promise( { abort() {} } )
|
2017-02-27 16:10:50 +00:00
|
|
|
}
|
2018-03-19 19:39:41 +00:00
|
|
|
} );
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2018-02-16 12:52:56 +00:00
|
|
|
this.sandbox.stub( mw, 'now' ).returns( new Date() );
|
2018-02-13 11:57:31 +00:00
|
|
|
|
2018-05-08 19:48:17 +00:00
|
|
|
const abandoned = actions.abandon()( dispatch, getState );
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2018-05-08 19:48:17 +00:00
|
|
|
assert.ok(
|
|
|
|
dispatch.calledWith( {
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.ABANDON_START,
|
2018-05-08 19:48:17 +00:00
|
|
|
timestamp: mw.now(),
|
|
|
|
token
|
|
|
|
} ),
|
|
|
|
'The dispatcher was called with the correct arguments.'
|
|
|
|
);
|
2017-02-27 16:10:50 +00:00
|
|
|
|
|
|
|
// ---
|
|
|
|
|
|
|
|
assert.ok(
|
|
|
|
this.wait.calledWith( 300 ),
|
|
|
|
'Have you spoken with #Design about changing this value?'
|
|
|
|
);
|
|
|
|
|
2018-03-14 23:50:09 +00:00
|
|
|
return abandoned.then( () => {
|
2017-02-27 16:10:50 +00:00
|
|
|
assert.ok(
|
|
|
|
dispatch.calledWith( {
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.ABANDON_END,
|
2018-03-14 19:44:22 +00:00
|
|
|
token
|
2017-02-27 16:10:50 +00:00
|
|
|
} ),
|
|
|
|
'ABANDON_* share the same token.'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
2017-04-18 18:43:16 +00:00
|
|
|
QUnit.test( 'it shouldn\'t dispatch under certain conditions', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const dispatch = this.sandbox.spy(),
|
2018-03-14 23:50:09 +00:00
|
|
|
getState = () =>
|
|
|
|
( {
|
2017-04-18 18:43:16 +00:00
|
|
|
preview: {
|
|
|
|
activeToken: undefined
|
|
|
|
}
|
2018-03-14 23:50:09 +00:00
|
|
|
} );
|
2017-04-18 18:43:16 +00:00
|
|
|
|
2018-05-08 19:48:17 +00:00
|
|
|
return actions.abandon()( dispatch, getState )
|
2018-03-14 23:50:09 +00:00
|
|
|
.then( () => {
|
2018-05-08 19:48:17 +00:00
|
|
|
assert.strictEqual(
|
|
|
|
dispatch.callCount,
|
|
|
|
0,
|
|
|
|
'The dispatcher was not called.'
|
|
|
|
);
|
2018-02-13 11:57:31 +00:00
|
|
|
} );
|
2017-04-18 18:43:16 +00:00
|
|
|
} );
|
|
|
|
|
2017-02-27 16:10:50 +00:00
|
|
|
QUnit.module( 'ext.popups/actions#saveSettings' );
|
|
|
|
|
|
|
|
QUnit.test( 'it should dispatch an action with previous and current enabled state', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const dispatch = this.sandbox.spy(),
|
2017-02-27 16:10:50 +00:00
|
|
|
getState = this.sandbox.stub().returns( {
|
|
|
|
preview: {
|
2021-04-07 10:23:05 +00:00
|
|
|
enabled: { page: false }
|
2017-02-27 16:10:50 +00:00
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
2021-04-15 07:20:00 +00:00
|
|
|
actions.saveSettings( { page: true } )( dispatch, getState );
|
2017-02-27 16:10:50 +00:00
|
|
|
|
2018-01-18 18:48:16 +00:00
|
|
|
assert.ok(
|
|
|
|
getState.calledOnce,
|
|
|
|
'it should query the global state for the current state'
|
|
|
|
);
|
|
|
|
assert.ok(
|
|
|
|
dispatch.calledWith( {
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.SETTINGS_CHANGE,
|
2021-04-15 07:20:00 +00:00
|
|
|
oldValue: { page: false },
|
|
|
|
newValue: { page: true }
|
2018-01-18 18:48:16 +00:00
|
|
|
} ),
|
|
|
|
'it should dispatch the action with the previous and next enabled state'
|
|
|
|
);
|
2017-02-27 16:10:50 +00:00
|
|
|
} );
|
2017-04-10 17:28:24 +00:00
|
|
|
|
2018-02-08 22:11:44 +00:00
|
|
|
QUnit.module( 'ext.popups/actions#previewShow', {
|
2018-03-14 22:04:59 +00:00
|
|
|
beforeEach() {
|
2018-02-08 22:11:44 +00:00
|
|
|
setupWait( this );
|
|
|
|
}
|
|
|
|
} );
|
2017-04-10 17:28:24 +00:00
|
|
|
|
2018-02-08 22:11:44 +00:00
|
|
|
QUnit.test( 'it should dispatch the PREVIEW_SHOW action and log a pageview', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const token = '1234567890',
|
2018-02-08 22:11:44 +00:00
|
|
|
dispatch = this.sandbox.spy(),
|
|
|
|
getState = this.sandbox.stub().returns( {
|
|
|
|
preview: {
|
|
|
|
activeToken: token,
|
|
|
|
fetchResponse: {
|
|
|
|
title: 'A',
|
2018-02-15 20:15:23 +00:00
|
|
|
pageId: 42,
|
2018-02-08 22:11:44 +00:00
|
|
|
type: 'page'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} );
|
2017-04-10 17:28:24 +00:00
|
|
|
|
|
|
|
this.sandbox.stub( mw, 'now' ).returns( new Date() );
|
2018-03-19 19:39:41 +00:00
|
|
|
const previewShow = actions
|
2018-02-08 22:11:44 +00:00
|
|
|
.previewShow( token )( dispatch, getState );
|
2017-04-10 17:28:24 +00:00
|
|
|
|
2018-02-08 22:11:44 +00:00
|
|
|
assert.ok(
|
|
|
|
dispatch.calledWith( {
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.PREVIEW_SHOW,
|
2018-03-14 19:44:22 +00:00
|
|
|
token,
|
2017-04-10 17:28:24 +00:00
|
|
|
timestamp: mw.now()
|
2018-02-08 22:11:44 +00:00
|
|
|
} ),
|
|
|
|
'dispatches the preview show event'
|
|
|
|
);
|
|
|
|
|
|
|
|
assert.strictEqual(
|
|
|
|
this.wait.getCall( 0 ).args[ 0 ],
|
|
|
|
1000,
|
2018-02-21 18:28:56 +00:00
|
|
|
'It waits for PAGEVIEW_VISIBILITY_DURATION milliseconds before trigging a pageview.'
|
2017-04-10 17:28:24 +00:00
|
|
|
);
|
2018-03-14 23:50:09 +00:00
|
|
|
return previewShow.then( () => {
|
2018-02-08 22:11:44 +00:00
|
|
|
assert.ok(
|
|
|
|
dispatch.calledTwice,
|
|
|
|
'Dispatch was called twice - once for PREVIEW_SHOW then for PREVIEW_SEEN'
|
|
|
|
);
|
|
|
|
assert.ok(
|
|
|
|
dispatch.calledWith( {
|
2018-07-13 15:12:49 +00:00
|
|
|
type: actionTypes.PREVIEW_SEEN,
|
2018-02-08 22:11:44 +00:00
|
|
|
namespace: 0,
|
2018-02-15 20:15:23 +00:00
|
|
|
pageId: 42,
|
2018-02-08 22:11:44 +00:00
|
|
|
title: 'A'
|
|
|
|
} ),
|
2018-02-21 18:28:56 +00:00
|
|
|
'Dispatches virtual pageview'
|
2018-02-08 22:11:44 +00:00
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
QUnit.test( 'PREVIEW_SEEN action not called if activeToken changes', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const token = '1234567890',
|
2018-02-08 22:11:44 +00:00
|
|
|
dispatch = this.sandbox.spy(),
|
|
|
|
getState = this.sandbox.stub().returns( {
|
|
|
|
preview: {
|
|
|
|
activeToken: '911',
|
|
|
|
fetchResponse: {
|
|
|
|
title: 'A',
|
|
|
|
type: 'page'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
// dispatch event
|
2018-03-19 19:39:41 +00:00
|
|
|
const previewShow = actions
|
2018-02-08 22:11:44 +00:00
|
|
|
.previewShow( token )( dispatch, getState );
|
|
|
|
|
2018-03-14 23:50:09 +00:00
|
|
|
return previewShow.then( () => {
|
2018-02-08 22:11:44 +00:00
|
|
|
assert.ok(
|
|
|
|
dispatch.calledOnce,
|
|
|
|
'Dispatch was only called for PREVIEW_SHOW'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
QUnit.test( 'PREVIEW_SEEN action not called if preview type not page', function ( assert ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
const token = '1234567890',
|
2018-02-08 22:11:44 +00:00
|
|
|
dispatch = this.sandbox.spy(),
|
|
|
|
getState = this.sandbox.stub().returns( {
|
|
|
|
preview: {
|
|
|
|
activeToken: token,
|
|
|
|
fetchResponse: {
|
|
|
|
title: 'A',
|
|
|
|
type: 'empty'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
// dispatch event
|
2018-03-19 19:39:41 +00:00
|
|
|
const previewShow = actions
|
2018-02-08 22:11:44 +00:00
|
|
|
.previewShow( token )( dispatch, getState );
|
|
|
|
|
2018-03-14 23:50:09 +00:00
|
|
|
return previewShow.then( () => {
|
2018-02-08 22:11:44 +00:00
|
|
|
assert.ok(
|
|
|
|
dispatch.calledOnce,
|
|
|
|
'Dispatch was only called for PREVIEW_SHOW'
|
|
|
|
);
|
|
|
|
} );
|
2017-04-10 17:28:24 +00:00
|
|
|
} );
|