mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Popups
synced 2024-11-28 01:10:04 +00:00
Add reading depth
Use schema revision 16163887. Add the 'checkin' action, which is accompanied by the 'checkin' property. The action is logged at the following seconds (Fibonacci numbers) after the page loads: 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765. The `checkin` property contains the values listed above. Bug: T147314 Change-Id: Ib9ec7bd0e60aa34a04e32222b025347f6ee31794
This commit is contained in:
parent
397d3e8bc2
commit
4afa1958a0
|
@ -38,7 +38,7 @@
|
|||
]
|
||||
},
|
||||
"EventLoggingSchemas": {
|
||||
"Popups": 16112163
|
||||
"Popups": 16163887
|
||||
},
|
||||
"callback": "PopupsHooks::onExtensionRegistration",
|
||||
"config": {
|
||||
|
@ -85,9 +85,9 @@
|
|||
"resources/ext.popups/changeListeners/eventLogging.js",
|
||||
"resources/ext.popups/changeListeners/syncUserSettings.js",
|
||||
"resources/ext.popups/changeListeners/settings.js",
|
||||
|
||||
"resources/ext.popups/settingsDialog.js",
|
||||
|
||||
"resources/ext.popups/pageVisibility.js",
|
||||
"resources/ext.popups/checkin.js",
|
||||
"resources/ext.popups/boot.js"
|
||||
],
|
||||
"templates": {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
var actions = {},
|
||||
types = {
|
||||
BOOT: 'BOOT',
|
||||
CHECKIN: 'CHECKIN',
|
||||
LINK_DWELL: 'LINK_DWELL',
|
||||
LINK_ABANDON_START: 'LINK_ABANDON_START',
|
||||
LINK_ABANDON_END: 'LINK_ABANDON_END',
|
||||
|
@ -88,6 +89,18 @@
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* How long has the user been actively reading the page?
|
||||
* @param {number} time The number of seconds the user has seen the page
|
||||
* @returns {{type: string, time: number}}
|
||||
*/
|
||||
actions.checkin = function ( time ) {
|
||||
return {
|
||||
type: types.CHECKIN,
|
||||
time: time
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents Page Previews fetching data via the [gateway](./gateway.js).
|
||||
*
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
* 2. Binding the actions to such store
|
||||
* 3. Trigger the boot action to bootstrap the system
|
||||
* 4. When the page content is ready:
|
||||
* - Setup `checkin` actions
|
||||
* - Process the eligible links for page previews
|
||||
* - Initialize the renderer
|
||||
* - Bind hover and click events to the eligible links to trigger actions
|
||||
|
@ -127,6 +128,8 @@
|
|||
mw.config
|
||||
);
|
||||
|
||||
mw.popups.checkin.setupActions( actions.checkin );
|
||||
|
||||
mw.popups.renderer.init();
|
||||
|
||||
previewLinks
|
||||
|
|
130
resources/ext.popups/checkin.js
Normal file
130
resources/ext.popups/checkin.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
( function ( mw, $ ) {
|
||||
var pageVisibility = mw.popups.pageVisibility,
|
||||
checkin = {
|
||||
/**
|
||||
* Checkin times - Fibonacci numbers
|
||||
*
|
||||
* Exposed for testing only.
|
||||
*
|
||||
* @type {number[]}
|
||||
* @private
|
||||
*/
|
||||
CHECKIN_TIMES: [ 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,
|
||||
377, 610, 987, 1597, 2584, 4181, 6765 ],
|
||||
/**
|
||||
* Have checkin actions been setup already?
|
||||
*
|
||||
* Exposed for testing only.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
haveCheckinActionsBeenSetup: false
|
||||
};
|
||||
|
||||
/**
|
||||
* A customized `setTimeout` function that takes page visibility into account
|
||||
*
|
||||
* If the document is not visible to the user, e.g. browser window is minimized,
|
||||
* then pause the time. Otherwise execute `callback` after `delay` milliseconds.
|
||||
* The callback won't be executed if the browser does not suppor the page visibility
|
||||
* API.
|
||||
*
|
||||
* Exposed for testing only.
|
||||
*
|
||||
* @see https://www.w3.org/TR/page-visibility/#dom-document-hidden
|
||||
* @private
|
||||
* @param {Function} callback Function to call when the time is up
|
||||
* @param {number} delay The number of milliseconds to wait before executing the callback
|
||||
*/
|
||||
checkin.setVisibleTimeout = function ( callback, delay ) {
|
||||
var hiddenPropertyName = pageVisibility.getDocumentHiddenPropertyName( document ),
|
||||
visibilityChangeEventName = pageVisibility.getDocumentVisibilitychangeEventName( document ),
|
||||
timeoutId,
|
||||
lastStartedAt;
|
||||
|
||||
if ( !hiddenPropertyName || !visibilityChangeEventName ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the callback and turn off listening to the visibilitychange event
|
||||
*/
|
||||
function done() {
|
||||
callback();
|
||||
$( document ).off( visibilityChangeEventName, visibilityChangeHandler );
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause or resume the timer depending on the page visibility state
|
||||
*/
|
||||
function visibilityChangeHandler() {
|
||||
var millisecondsPassed;
|
||||
|
||||
// Pause the timer if the page is hidden ...
|
||||
if ( pageVisibility.isDocumentHidden( document ) ) {
|
||||
// ... and only if the timer has started.
|
||||
// Timer may not have been started if the document opened in a
|
||||
// hidden tab for example. The timer will be started when the
|
||||
// document is visible to the user.
|
||||
if ( lastStartedAt ) {
|
||||
millisecondsPassed = new Date().getTime() - lastStartedAt;
|
||||
delay = Math.max( 0, delay - millisecondsPassed );
|
||||
clearTimeout( timeoutId );
|
||||
}
|
||||
} else {
|
||||
lastStartedAt = new Date().getTime();
|
||||
timeoutId = setTimeout( done, delay );
|
||||
}
|
||||
}
|
||||
|
||||
visibilityChangeHandler();
|
||||
|
||||
$( document ).on( visibilityChangeEventName, visibilityChangeHandler );
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform the passed `checkin` action at the predefined times
|
||||
*
|
||||
* Actions are setup only once no matter how many times this function is
|
||||
* called. Ideally this function should be called once.
|
||||
*
|
||||
* @see checkin.CHECKIN_TIMES
|
||||
* @param {Function} checkinAction
|
||||
*/
|
||||
checkin.setupActions = function( checkinAction ) {
|
||||
var timeIndex = 0,
|
||||
timesLength = checkin.CHECKIN_TIMES.length,
|
||||
time, // current checkin time
|
||||
nextTime; // the checkin time that will be logged next
|
||||
|
||||
if ( checkin.haveCheckinActionsBeenSetup ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the checkin action with the current checkin time
|
||||
*
|
||||
* If more checkin times are left, then setup a timer to log the next one.
|
||||
*/
|
||||
function setup() {
|
||||
time = checkin.CHECKIN_TIMES[ timeIndex ];
|
||||
checkinAction( time );
|
||||
|
||||
timeIndex += 1;
|
||||
if ( timeIndex < timesLength ) {
|
||||
nextTime = checkin.CHECKIN_TIMES[ timeIndex ];
|
||||
// Execute the callback after the number of seconds left till the
|
||||
// next checkin time.
|
||||
checkin.setVisibleTimeout( setup, ( nextTime - time ) * 1000 );
|
||||
}
|
||||
}
|
||||
|
||||
checkin.setVisibleTimeout( setup, checkin.CHECKIN_TIMES[ timeIndex ] * 1000 );
|
||||
|
||||
checkin.haveCheckinActionsBeenSetup = true;
|
||||
};
|
||||
|
||||
mw.popups.checkin = checkin;
|
||||
|
||||
}( mediaWiki, jQuery ) );
|
111
resources/ext.popups/pageVisibility.js
Normal file
111
resources/ext.popups/pageVisibility.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
( function ( mw ) {
|
||||
var pageVisibility = {
|
||||
/**
|
||||
* Cached value of the browser specific name of the `document.hidden` property
|
||||
*
|
||||
* Exposed for testing only.
|
||||
*
|
||||
* @type {null|undefined|string}
|
||||
* @private
|
||||
*/
|
||||
documentHiddenPropertyName: null,
|
||||
/**
|
||||
* Cached value of the browser specific name of the `document.visibilitychange` event
|
||||
*
|
||||
* Exposed for testing only.
|
||||
*
|
||||
* @type {null|undefined|string}
|
||||
* @private
|
||||
*/
|
||||
documentVisibilityChangeEventName: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the browser specific name of the `document.hidden` property
|
||||
*
|
||||
* Exposed for testing only.
|
||||
*
|
||||
* @see https://www.w3.org/TR/page-visibility/#dom-document-hidden
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
|
||||
* @private
|
||||
* @param {Object} doc window.document object
|
||||
* @return {string|undefined}
|
||||
*/
|
||||
pageVisibility.getDocumentHiddenPropertyName = function ( doc ) {
|
||||
var property;
|
||||
|
||||
if ( pageVisibility.documentHiddenPropertyName === null ) {
|
||||
if ( doc.hidden !== undefined ) {
|
||||
property = 'hidden';
|
||||
} else if ( doc.mozHidden !== undefined ) {
|
||||
property = 'mozHidden';
|
||||
} else if ( doc.msHidden !== undefined ) {
|
||||
property = 'msHidden';
|
||||
} else if ( doc.webkitHidden !== undefined ) {
|
||||
property = 'webkitHidden';
|
||||
} else {
|
||||
// let's be explicit about returning `undefined`
|
||||
property = undefined;
|
||||
}
|
||||
// cache
|
||||
pageVisibility.documentHiddenPropertyName = property;
|
||||
}
|
||||
|
||||
return pageVisibility.documentHiddenPropertyName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the browser specific name of the `document.visibilitychange` event
|
||||
*
|
||||
* Exposed for testing only.
|
||||
*
|
||||
* @see https://www.w3.org/TR/page-visibility/#sec-visibilitychange-event
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
|
||||
* @private
|
||||
* @param {Object} doc window.document object
|
||||
* @return {string|undefined}
|
||||
*/
|
||||
pageVisibility.getDocumentVisibilitychangeEventName = function ( doc ) {
|
||||
var eventName;
|
||||
|
||||
if ( pageVisibility.documentVisibilityChangeEventName === null ) {
|
||||
if ( doc.hidden !== undefined ) {
|
||||
eventName = 'visibilitychange';
|
||||
} else if ( doc.mozHidden !== undefined ) {
|
||||
eventName = 'mozvisibilitychange';
|
||||
} else if ( doc.msHidden !== undefined ) {
|
||||
eventName = 'msvisibilitychange';
|
||||
} else if ( doc.webkitHidden !== undefined ) {
|
||||
eventName = 'webkitvisibilitychange';
|
||||
} else {
|
||||
// let's be explicit about returning `undefined`
|
||||
eventName = undefined;
|
||||
}
|
||||
// cache
|
||||
pageVisibility.documentVisibilityChangeEventName = eventName;
|
||||
}
|
||||
|
||||
return pageVisibility.documentVisibilityChangeEventName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether `window.document` is visible
|
||||
*
|
||||
* `undefined` is returned if the browser does not support the Visibility API.
|
||||
*
|
||||
* Exposed for testing only.
|
||||
*
|
||||
* @see https://www.w3.org/TR/page-visibility/#dom-document-hidden
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
|
||||
* @private
|
||||
* @param {Object} doc window.document object
|
||||
* @returns {boolean|undefined}
|
||||
*/
|
||||
pageVisibility.isDocumentHidden = function ( doc ) {
|
||||
var property = pageVisibility.getDocumentHiddenPropertyName( doc );
|
||||
|
||||
return property !== undefined ? doc[ property ] : undefined;
|
||||
};
|
||||
|
||||
mw.popups.pageVisibility = pageVisibility;
|
||||
}( mediaWiki ) );
|
|
@ -52,6 +52,14 @@
|
|||
}
|
||||
} );
|
||||
|
||||
case popups.actionTypes.CHECKIN:
|
||||
return nextState( state, {
|
||||
event: {
|
||||
action: 'checkin',
|
||||
checkin: action.time
|
||||
}
|
||||
} );
|
||||
|
||||
case popups.actionTypes.EVENT_LOGGED:
|
||||
return nextState( state, {
|
||||
event: undefined
|
||||
|
|
108
tests/qunit/ext.popups/checkin.test.js
Normal file
108
tests/qunit/ext.popups/checkin.test.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
( function ( mw, $ ) {
|
||||
var pageVisibility = mw.popups.pageVisibility,
|
||||
checkin = mw.popups.checkin,
|
||||
// remember the original chechkin times
|
||||
CHECKIN_TIMES = checkin.CHECKIN_TIMES;
|
||||
|
||||
QUnit.module( 'ext.popups/checkin', {
|
||||
setup: function () {
|
||||
checkin.CHECKIN_TIMES = CHECKIN_TIMES;
|
||||
checkin.haveCheckinActionsBeenSetup = false;
|
||||
this.CHECKIN_TIMES = [ 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,
|
||||
377, 610, 987, 1597, 2584, 4181, 6765 ];
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( 'checkin times are correct', function ( assert ) {
|
||||
assert.expect( 1 );
|
||||
assert.deepEqual( checkin.CHECKIN_TIMES, this.CHECKIN_TIMES );
|
||||
} );
|
||||
|
||||
QUnit.test( 'visible timeout will not fire the callback if the' +
|
||||
' browser does not support the visibility API', function ( assert ) {
|
||||
var delay = 1000,
|
||||
spy = this.sandbox.spy(),
|
||||
done = assert.async();
|
||||
|
||||
assert.expect( 1 );
|
||||
|
||||
this.sandbox.stub( pageVisibility, 'getDocumentHiddenPropertyName' ).returns( undefined );
|
||||
|
||||
checkin.setVisibleTimeout( spy, delay );
|
||||
setTimeout( function () {
|
||||
assert.ok( spy.notCalled );
|
||||
done();
|
||||
}, 2 * delay ); // wait a little more in case the the event loop is busy
|
||||
|
||||
} );
|
||||
|
||||
QUnit.test( 'visible timeout pause works correctly', function ( assert ) {
|
||||
var delay = 5000,
|
||||
pause = 2000,
|
||||
// error margin in milliseconds
|
||||
delta = 50,
|
||||
spy = this.sandbox.spy(),
|
||||
done = assert.async();
|
||||
|
||||
assert.expect( 2 );
|
||||
|
||||
this.sandbox.stub( pageVisibility, 'getDocumentHiddenPropertyName' ).returns( 'customHidden' );
|
||||
this.sandbox.stub( pageVisibility, 'getDocumentVisibilitychangeEventName' ).returns( 'customvisibilitychange' );
|
||||
|
||||
checkin.setVisibleTimeout( spy, delay );
|
||||
|
||||
// pause immediately, after making sure the document is hidden
|
||||
this.sandbox.stub( pageVisibility, 'isDocumentHidden' ).returns( true );
|
||||
$( document ).trigger( 'customvisibilitychange' );
|
||||
|
||||
// resume after `pause` milliseconds
|
||||
pageVisibility.isDocumentHidden.restore();
|
||||
this.sandbox.stub( pageVisibility, 'isDocumentHidden' ).returns( false );
|
||||
setTimeout( function () {
|
||||
$( document ).trigger( 'customvisibilitychange' );
|
||||
pageVisibility.isDocumentHidden.restore();
|
||||
}, pause );
|
||||
|
||||
setTimeout( function () {
|
||||
// make sure the spy is not called after `delay` as we've paused
|
||||
assert.ok( spy.notCalled );
|
||||
|
||||
setTimeout( function () {
|
||||
// make sure the spy is called after `delay` + `pause` as we've resumed
|
||||
assert.ok( spy.called );
|
||||
done();
|
||||
}, pause + delta );
|
||||
}, delay + delta );
|
||||
|
||||
} );
|
||||
|
||||
QUnit.test( 'checkin actions will not be set up if they already have been', function ( assert ) {
|
||||
var spy = this.sandbox.spy( checkin, 'setVisibleTimeout' ),
|
||||
actionSpy = this.sandbox.spy();
|
||||
|
||||
assert.expect( 1 );
|
||||
|
||||
checkin.haveCheckinActionsBeenSetup = true;
|
||||
checkin.setupActions( actionSpy );
|
||||
assert.ok( spy.notCalled, 'setVisibleTimeout has not been called.' );
|
||||
} );
|
||||
|
||||
QUnit.test( 'checkin actions are setup correctly', function ( assert ) {
|
||||
var actionSpy = this.sandbox.spy(),
|
||||
done = assert.async();
|
||||
|
||||
checkin.CHECKIN_TIMES = [ 1, 2, 3 ];
|
||||
|
||||
assert.expect( checkin.CHECKIN_TIMES.length );
|
||||
|
||||
checkin.setupActions( actionSpy );
|
||||
setTimeout( function () {
|
||||
$.each( checkin.CHECKIN_TIMES, function ( i, time ) {
|
||||
assert.ok( actionSpy.calledWith( time ),
|
||||
'`action` has been called the correct checkin time: ' + time + '.' );
|
||||
} );
|
||||
done();
|
||||
// give two more seconds to catch up
|
||||
}, ( checkin.CHECKIN_TIMES[ checkin.CHECKIN_TIMES.length - 1 ] + 2 ) * 1000 );
|
||||
} );
|
||||
}( mediaWiki, jQuery ) );
|
75
tests/qunit/ext.popups/pageVisibility.test.js
Normal file
75
tests/qunit/ext.popups/pageVisibility.test.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
( function ( mw, $ ) {
|
||||
var pageVisibility = mw.popups.pageVisibility;
|
||||
|
||||
QUnit.module( 'ext.popups/pageVisibility', {
|
||||
setup: function () {
|
||||
pageVisibility.documentHiddenPropertyName = null;
|
||||
pageVisibility.documentVisibilityChangeEventName = null;
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( 'browser specific `document.hidden` property name is correctly detected', function ( assert ) {
|
||||
var testCases = [
|
||||
[ { hidden: false }, 'hidden' ],
|
||||
[ { mozHidden: false }, 'mozHidden' ],
|
||||
[ { msHidden: false }, 'msHidden' ],
|
||||
[ { webkitHidden: false }, 'webkitHidden' ],
|
||||
[ { unsupportedHidden: false }, undefined ],
|
||||
[ {}, undefined ]
|
||||
];
|
||||
|
||||
assert.expect( testCases.length );
|
||||
|
||||
$.each( testCases, function ( i, testCase ) {
|
||||
// clear the cache so we get a fresh result
|
||||
pageVisibility.documentHiddenPropertyName = null;
|
||||
assert.equal(
|
||||
pageVisibility.getDocumentHiddenPropertyName( testCase[ 0 ] ), testCase[ 1 ] );
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'browser specific `document.visibilitychange` event name is correctly detected', function ( assert ) {
|
||||
var testCases = [
|
||||
[ { hidden: false }, 'visibilitychange' ],
|
||||
[ { mozHidden: false }, 'mozvisibilitychange' ],
|
||||
[ { msHidden: false }, 'msvisibilitychange' ],
|
||||
[ { webkitHidden: false }, 'webkitvisibilitychange' ],
|
||||
[ { unsupportedHidden: false }, undefined ],
|
||||
[ {}, undefined ]
|
||||
];
|
||||
|
||||
assert.expect( testCases.length );
|
||||
|
||||
$.each( testCases, function ( i, testCase ) {
|
||||
// clear the cache so we get a fresh result
|
||||
pageVisibility.documentVisibilityChangeEventName = null;
|
||||
assert.equal(
|
||||
pageVisibility.getDocumentVisibilitychangeEventName( testCase[ 0 ] ), testCase[ 1 ] );
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'document visibility is correctly detected', function ( assert ) {
|
||||
var testCases = [
|
||||
[ { hidden: false }, false ],
|
||||
[ { hidden: true }, true ],
|
||||
[ { mozHidden: false }, false ],
|
||||
[ { mozHidden: true }, true ],
|
||||
[ { msHidden: false }, false ],
|
||||
[ { msHidden: true }, true ],
|
||||
[ { webkitHidden: false }, false ],
|
||||
[ { webkitHidden: true }, true ],
|
||||
[ { unsupportedHidden: false }, undefined ],
|
||||
[ {}, undefined ]
|
||||
];
|
||||
|
||||
assert.expect( testCases.length );
|
||||
|
||||
$.each( testCases, function ( i, testCase ) {
|
||||
// clear the cache so we get a fresh result
|
||||
pageVisibility.documentHiddenPropertyName = null;
|
||||
assert.equal(
|
||||
pageVisibility.isDocumentHidden( testCase[ 0 ] ), testCase[ 1 ] );
|
||||
} );
|
||||
} );
|
||||
|
||||
}( mediaWiki, jQuery ) );
|
Loading…
Reference in a new issue