Merge "Show referencePreviews on click"

This commit is contained in:
jenkins-bot 2019-05-08 16:00:02 +00:00 committed by Gerrit Code Review
commit fe6c707da1
10 changed files with 223 additions and 10 deletions

View file

@ -53,7 +53,7 @@
"bundlesize": [
{
"path": "resources/dist/index.js",
"maxSize": "13.5KB"
"maxSize": "13.6KB"
}
]
}

Binary file not shown.

Binary file not shown.

View file

@ -8,6 +8,7 @@ export default {
ABANDON_START: 'ABANDON_START',
ABANDON_END: 'ABANDON_END',
LINK_CLICK: 'LINK_CLICK',
REFERENCE_CLICK: 'REFERENCE_CLICK',
/** Precedes a fetch. */
FETCH_START: 'FETCH_START',
/** Follows a successful fetch. */

View file

@ -308,6 +308,59 @@ export function linkClick( el ) {
} );
}
/**
* Represents the user clicking on a reference preview link with their mouse, keyboard, or an
* assistive device.
*
* @param {mw.Title} title
* @param {Element} el
* @param {Gateway} gateway
* @param {Function} generateToken
* @return {Redux.Thunk}
*/
export function referenceClick( title, el, gateway, generateToken ) {
return ( dispatch, getState ) => {
const {
activeLink,
activeToken: dwellToken,
promise: dwellPromise,
wasClicked
} = getState().preview;
if ( wasClicked ) {
return $.Deferred().resolve().promise();
}
const xhr = gateway.fetchPreviewForTitle( title, el );
function clickFollowsDwellEvent() {
return activeLink === el && dwellToken !== '';
}
let token = dwellToken;
if ( !clickFollowsDwellEvent() ) {
token = generateToken();
} else {
dwellPromise.abort();
}
dispatch( timedAction( {
type: types.REFERENCE_CLICK,
el,
token
} ) );
return xhr.then( ( result ) => {
dispatch( {
type: types.FETCH_COMPLETE,
el,
result,
token
} );
} );
};
}
/**
* Represents the user dwelling on a preview with their mouse.
*

View file

@ -263,11 +263,25 @@ function registerChangeListeners(
boundActions.abandon();
}
} )
.on( 'click', validLinkSelector, function () {
.on( 'click', validLinkSelector, function ( event ) {
const mwTitle = titleFromElement( this, mw.config );
if ( mwTitle ) {
boundActions.linkClick( this );
const type = getPreviewType( this, mw.config, mwTitle );
switch ( type ) {
case previewTypes.TYPE_PAGE:
boundActions.linkClick( this );
break;
case previewTypes.TYPE_REFERENCE:
event.preventDefault();
boundActions.referenceClick(
mwTitle,
this,
referenceGateway,
generateToken
);
break;
}
}
} );
}() );

View file

@ -17,7 +17,8 @@ export default function preview( state, action ) {
activeEvent: undefined,
activeToken: '',
shouldShow: false,
isUserDwelling: false
isUserDwelling: false,
wasClicked: false
};
}
@ -56,6 +57,14 @@ export default function preview( state, action ) {
isUserDwelling: true
} );
case actionTypes.REFERENCE_CLICK:
return nextState( state, {
activeLink: action.el,
activeToken: action.token,
isUserDwelling: true,
wasClicked: true
} );
case actionTypes.ABANDON_END:
if ( action.token === state.activeToken && !state.isUserDwelling ) {
return nextState( state, {
@ -75,7 +84,8 @@ export default function preview( state, action ) {
case actionTypes.ABANDON_START:
return nextState( state, {
isUserDwelling: false
isUserDwelling: false,
wasClicked: false
} );
case actionTypes.FETCH_START:

View file

@ -613,3 +613,116 @@ QUnit.test( 'PREVIEW_SEEN action not called if preview type not page', function
);
} );
} );
QUnit.module( 'ext.popups/actions#referenceClick @integration', {
beforeEach() {
this.state = {
preview: {}
};
this.getState = () => this.state;
this.gatewayDeferred = $.Deferred();
this.gateway = {
fetchPreviewForTitle: this.sandbox.stub().returns(
this.gatewayDeferred.resolve( {} ).promise()
)
};
// lets just set this to always return 0
mw.now = () => 0;
setupEl( this );
}
} );
QUnit.test( '#referenceClick', function ( assert ) {
const dispatch = this.sandbox.spy();
this.sandbox.stub( mw, 'now' ).returns( new Date() );
const referenceClicked = actions.referenceClick(
this.title, this.el, this.gateway, generateToken
)(
dispatch,
this.getState
);
assert.propEqual(
dispatch.getCall( 0 ).args[ 0 ], {
type: actionTypes.REFERENCE_CLICK,
el: this.el,
token: 'ABC',
timestamp: mw.now()
},
'The dispatcher was called with the correct REFERENCE_CLICK arguments.'
);
return referenceClicked.then( () => {
assert.propEqual(
dispatch.getCall( 1 ).args[ 0 ], {
type: actionTypes.FETCH_COMPLETE,
el: this.el,
result: {},
token: 'ABC'
},
'The dispatcher was called with the correct FETCH_COMPLETE arguments.'
);
} );
} );
QUnit.test( '#referenceClick doesn\'t continue when clicked several times', function ( assert ) {
const dispatch = this.sandbox.spy();
this.state.preview = {
wasClicked: true
};
const referenceClicked = actions.referenceClick(
this.title, this.el, this.gateway, generateToken
)(
dispatch,
this.getState
);
return referenceClicked.then( () => {
assert.ok(
dispatch.notCalled,
'The dispatcher was never called.'
);
} );
} );
QUnit.test( '#referenceClick re-uses the linkDwell token if present', function ( assert ) {
const dispatch = this.sandbox.spy(),
oldDeferred = $.Deferred().promise( { abort() {} } );
this.state.preview = {
activeLink: this.el,
activeToken: 'OLD_TOKEN',
promise: oldDeferred
};
const referenceClicked = actions.referenceClick(
this.title, this.el, this.gateway, generateToken
)(
dispatch,
this.getState
);
assert.propEqual(
dispatch.getCall( 0 ).args[ 0 ], {
type: actionTypes.REFERENCE_CLICK,
el: this.el,
token: 'OLD_TOKEN',
timestamp: mw.now()
},
'The dispatcher was called with the correct REFERENCE_CLICK arguments.'
);
return referenceClicked.then( () => {
assert.ok(
dispatch.calledTwice,
'The dispatcher was called twice.'
);
} );
} );

View file

@ -21,7 +21,8 @@ QUnit.test( '@@INIT', ( assert ) => {
activeEvent: undefined,
activeToken: '',
shouldShow: false,
isUserDwelling: false
isUserDwelling: false,
wasClicked: false
},
'The initial state is correct.'
);
@ -196,6 +197,7 @@ QUnit.test( 'FETCH_COMPLETE', ( assert ) => {
{
activeToken: token,
isUserDwelling: false, // Set when ABANDON_START is reduced.
wasClicked: false,
fetchResponse: action.result,
shouldShow: false
@ -286,8 +288,28 @@ QUnit.test( 'ABANDON_START', ( assert ) => {
assert.deepEqual(
preview( {}, action ),
{
isUserDwelling: false
isUserDwelling: false,
wasClicked: false
},
'ABANDON_START should mark the preview having been abandoned.'
);
} );
QUnit.test( 'REFERENCE_CLICK updates the state for a click', function ( assert ) {
const action = {
type: actionTypes.REFERENCE_CLICK,
el: this.el,
token: '1234567890'
};
assert.deepEqual(
preview( {}, action ),
{
activeLink: action.el,
activeToken: action.token,
isUserDwelling: true,
wasClicked: true
},
'It should set active link and token as well as dwelling and click status.'
);
} );

View file

@ -110,8 +110,8 @@ module.exports = ( env, argv ) => ( {
// Minified uncompressed size limits for chunks / assets and entrypoints. Keep these numbers
// up-to-date and rounded to the nearest 10th of a kibibyte so that code sizing costs are
// well understood. Related to bundlesize minified, gzipped compressed file size tests.
maxAssetSize: 41 * 1024,
maxEntrypointSize: 41 * 1024,
maxAssetSize: 41.5 * 1024,
maxEntrypointSize: 41.5 * 1024,
// The default filter excludes map files but we rename ours.
assetFilter: ( filename ) => !filename.endsWith( srcMapExt )