Track the most recent upload time for performance events

Change-Id: I673f9487deea15dc148452a3a4d6b91563a2c417
Bug: T76035
This commit is contained in:
Gilles Dubuc 2014-12-02 11:05:12 +01:00
parent d280920121
commit 30029b8b78
11 changed files with 181 additions and 86 deletions

View file

@ -1063,7 +1063,7 @@ $wgResourceModules += array(
$wgHooks['EventLoggingRegisterSchemas'][] = function( array &$schemas ) {
$schemas += array(
'MediaViewer' => 10536413,
'MultimediaViewerNetworkPerformance' => 7917896,
'MultimediaViewerNetworkPerformance' => 10596581,
'MultimediaViewerDuration' => 10427980,
'MultimediaViewerAttribution' => 9758179,
'MultimediaViewerDimensions' => 10014238,

View file

@ -62,9 +62,10 @@
* cached by the browser, as it will consume unnecessary bandwidth for the user.
* @param {string} type the type of request to be measured
* @param {string} url URL to be measured
* @param {jQuery.Deferred.<string>} [extraStatsDeferred] A promise which resolves to the extra stats.
* @returns {jQuery.Promise} A promise that resolves when the contents of the URL have been fetched
*/
PL.record = function ( type, url ) {
PL.record = function ( type, url, extraStatsDeferred ) {
var deferred = $.Deferred(),
request,
perf = this,
@ -89,7 +90,7 @@
if ( request.readyState === 4 ) {
deferred.notify( request.response, 100 );
deferred.resolve( request.response );
perf.recordEntryDelayed( type, total, url, request );
perf.recordEntryDelayed( type, total, url, request, extraStatsDeferred );
}
};
@ -111,9 +112,11 @@
* @param {number} total the total load time tracked with a basic technique
* @param {string} url URL of that was measured
* @param {XMLHttpRequest} request HTTP request that just completed
* @param {jQuery.Deferred.<string>} [extraStatsDeferred] A promise which resolves to extra stats to be included.
*/
PL.recordEntry = function ( type, total, url, request ) {
PL.recordEntry = function ( type, total, url, request, extraStatsDeferred ) {
var matches,
logger = this,
stats = { type: type,
contentHost: window.location.host,
isHttps: window.location.protocol === 'https:',
@ -165,7 +168,11 @@
}
}
this.log( stats );
( extraStatsDeferred || $.Deferred().reject() ).done( function ( extraStats ) {
stats = $.extend( stats, extraStats );
} ).always( function () {
logger.log( stats );
} );
};
/**
@ -305,14 +312,15 @@
* @param {number} total the total load time tracked with a basic technique
* @param {string} url URL of that was measured
* @param {XMLHttpRequest} request HTTP request that just completed
* @param {jQuery.Promise.<string>} extraStatsDeferred A promise which resolves to extra stats.
*/
PL.recordEntryDelayed = function ( type, total, url, request ) {
PL.recordEntryDelayed = function ( type, total, url, request, extraStatsDeferred ) {
var perf = this;
// The timeout is necessary because if there's an entry in window.performance,
// it hasn't been added yet at this point
setTimeout( function() {
perf.recordEntry( type, total, url, request );
perf.recordEntry( type, total, url, request, extraStatsDeferred );
}, 0 );
};
@ -402,6 +410,15 @@
return new XMLHttpRequest();
};
/**
* @override
* @inheritdoc
*/
PL.log = function ( data ) {
mw.log( 'mw.mmv.logging.PerformanceLogger', data );
return mw.mmv.logging.Logger.prototype.log.call( this, data );
};
new PerformanceLogger().init();
mw.mmv.logging.PerformanceLogger = PerformanceLogger;

View file

@ -157,6 +157,8 @@
thumb.thumb,
thumb.caption
);
thumb.extraStatsDeferred = $.Deferred();
}
};
@ -249,9 +251,9 @@
imagePromise,
metadataPromise,
start,
uploadTimestamp,
viewer = this,
$initialImage = $( initialImage );
$initialImage = $( initialImage ),
extraStatsDeferred = $.Deferred();
this.currentIndex = image.index;
@ -282,7 +284,8 @@
start = $.now();
mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'show' );
imagePromise = this.fetchThumbnailForLightboxImage( image, imageWidths.real );
imagePromise = this.fetchThumbnailForLightboxImage( image, imageWidths.real, extraStatsDeferred );
this.resetBlurredThumbnailStates();
if ( imagePromise.state() === 'pending' ) {
@ -302,19 +305,12 @@
mw.mmv.durationLogger.stop( 'click-to-first-image' );
metadataPromise.done( function ( imageInfo ) {
if ( !imageInfo || !imageInfo.uploadDateTime ) {
if ( !imageInfo || !imageInfo.anonymizedUploadDateTime ) {
return;
}
uploadTimestamp = imageInfo.uploadDateTime.toString();
// Convert to "timestamp" format commonly used in EventLogging
uploadTimestamp = uploadTimestamp.replace( /[:\s]/g, '' );
// Anonymise the timestamp to avoid making the file identifiable
// We only need to know the day
uploadTimestamp = uploadTimestamp.substr( 0, uploadTimestamp.length - 6 ) + '000000';
mw.mmv.durationLogger.record( 'click-to-first-image', {
uploadTimestamp: uploadTimestamp
uploadTimestamp: imageInfo.anonymizedUploadDateTime
} );
} );
}
@ -324,6 +320,8 @@
} );
metadataPromise.done( function ( imageInfo, repoInfo, userInfo ) {
extraStatsDeferred.resolve( { uploadTimestamp: imageInfo.anonymizedUploadDateTime } );
if ( viewer.currentIndex !== image.index ) {
return;
}
@ -337,6 +335,8 @@
// File reuse steals a bunch of information from the DOM, so do it last
viewer.ui.setFileReuseData( imageInfo, repoInfo, image.caption );
} ).fail( function ( error ) {
extraStatsDeferred.reject();
if ( viewer.currentIndex !== image.index ) {
return;
}
@ -579,13 +579,15 @@
if ( this.currentIndex + i < this.thumbs.length ) {
callback(
this.currentIndex + i,
this.thumbs[ this.currentIndex + i ].image
this.thumbs[ this.currentIndex + i ].image,
this.thumbs[ this.currentIndex + i ].extraStatsDeferred
);
}
if ( i && this.currentIndex - i >= 0 ) { // skip duplicate for i==0
callback(
this.currentIndex - i,
this.thumbs[ this.currentIndex - i ].image
this.thumbs[ this.currentIndex - i ].image,
this.thumbs[ this.currentIndex - i ].extraStatsDeferred
);
}
}
@ -601,8 +603,8 @@
MMVP.pushLightboxImagesIntoQueue = function( taskFactory ) {
var queue = new mw.mmv.model.TaskQueue();
this.eachPrealoadableLightboxIndex( function( i, lightboxImage ) {
queue.push( taskFactory( lightboxImage ) );
this.eachPrealoadableLightboxIndex( function( i, lightboxImage, extraStatsDeferred ) {
queue.push( taskFactory( lightboxImage, extraStatsDeferred ) );
} );
return queue;
@ -636,9 +638,15 @@
this.cancelImageMetadataPreloading();
this.metadataPreloadQueue = this.pushLightboxImagesIntoQueue( function( lightboxImage ) {
this.metadataPreloadQueue = this.pushLightboxImagesIntoQueue( function( lightboxImage, extraStatsDeferred ) {
return function() {
return viewer.fetchSizeIndependentLightboxInfo( lightboxImage.filePageTitle );
var metadatapromise = viewer.fetchSizeIndependentLightboxInfo( lightboxImage.filePageTitle );
metadatapromise.done( function ( imageInfo ) {
extraStatsDeferred.resolve( { uploadTimestamp: imageInfo.anonymizedUploadDateTime } );
} ).fail( function () {
extraStatsDeferred.reject();
} );
return metadatapromise;
};
} );
@ -655,7 +663,7 @@
this.cancelThumbnailsPreloading();
this.thumbnailPreloadQueue = this.pushLightboxImagesIntoQueue( function( lightboxImage ) {
this.thumbnailPreloadQueue = this.pushLightboxImagesIntoQueue( function( lightboxImage, extraStatsDeferred ) {
return function() {
var imageWidths, canvasDimensions;
@ -670,7 +678,7 @@
mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'preload' );
return viewer.fetchThumbnailForLightboxImage( lightboxImage, imageWidths.real );
return viewer.fetchThumbnailForLightboxImage( lightboxImage, imageWidths.real, extraStatsDeferred );
};
} );
@ -721,15 +729,17 @@
* Loads size-dependent components of a lightbox - the thumbnail model and the image itself.
* @param {mw.mmv.LightboxImage} image
* @param {number} width the width of the requested thumbnail
* @param {jQuery.Deferred.<string>} [extraStatsDeferred] Promise that resolves to the image's upload timestamp when the metadata is loaded
* @returns {jQuery.Promise.<mw.mmv.model.Thumbnail, HTMLImageElement>}
*/
MMVP.fetchThumbnailForLightboxImage = function ( image, width ) {
MMVP.fetchThumbnailForLightboxImage = function ( image, width, extraStatsDeferred ) {
return this.fetchThumbnail(
image.filePageTitle,
width,
image.src,
image.originalWidth,
image.originalHeight
image.originalHeight,
extraStatsDeferred
);
};
@ -740,11 +750,12 @@
* @param {string} [sampleUrl] a thumbnail URL for the same file (but with different size) (might be missing)
* @param {number} [originalWidth] the width of the original, full-sized file (might be missing)
* @param {number} [originalHeight] the height of the original, full-sized file (might be missing)
* @param {jQuery.Deferred.<string>} [extraStatsDeferred] Promise that resolves to the image's upload timestamp when the metadata is loaded
* @returns {jQuery.Promise.<mw.mmv.model.Thumbnail, HTMLImageElement>} A promise resolving to
* a thumbnail model and an <img> element. It might or might not have progress events which
* return a single number.
*/
MMVP.fetchThumbnail = function ( fileTitle, width, sampleUrl, originalWidth, originalHeight ) {
MMVP.fetchThumbnail = function ( fileTitle, width, sampleUrl, originalWidth, originalHeight, extraStatsDeferred ) {
var viewer = this,
guessing = false,
thumbnailPromise,
@ -771,7 +782,7 @@
}
imagePromise = thumbnailPromise.then( function ( thumbnail ) {
return viewer.imageProvider.get( thumbnail.url );
return viewer.imageProvider.get( thumbnail.url, extraStatsDeferred );
} );
if ( guessing ) {
@ -780,7 +791,7 @@
// because thumbnailInfoProvider.get is already called above when guessedThumbnailInfoProvider.get fails.
imagePromise = imagePromise.then( null, function () {
return viewer.thumbnailInfoProvider.get( fileTitle, width ).then( function ( thumbnail ) {
return viewer.imageProvider.get( thumbnail.url );
return viewer.imageProvider.get( thumbnail.url, extraStatsDeferred );
} );
} );
}

View file

@ -54,23 +54,5 @@
this.originalHeight = undefined;
}
var LIP = LightboxImage.prototype;
/**
* The URL of the image (in the size we intend use to display the it in the lightbox)
* @type {String}
* @protected
*/
LIP.src = null;
/**
* The URL of a placeholder while the image loads. Typically a smaller version of the image, which is already
* loaded in the browser.
* @type {String}
* @return {jQuery.Promise.<mw.mmv.LightboxImage, HTMLImageElement>}
* @protected
*/
LIP.initialSrc = null;
mw.mmv.LightboxImage = LightboxImage;
}( mediaWiki, jQuery ) );

View file

@ -33,6 +33,7 @@
* @param {string} repo The repository this image belongs to
* @param {string} lastUploader The last person to upload a version of this image.
* @param {string} uploadDateTime The time and date the last upload occurred
* @param {string} anonymizedUploadDateTime Anonymized and EL-friendly version of uploadDateTime
* @param {string} creationDateTime The time and date the original upload occurred
* @param {string} description
* @param {string} source
@ -55,6 +56,7 @@
repo,
lastUploader,
uploadDateTime,
anonymizedUploadDateTime,
creationDateTime,
description,
source,
@ -98,6 +100,9 @@
/** @property {string} uploadDateTime The date and time of the last upload */
this.uploadDateTime = uploadDateTime;
/** @property {string} anonymizedUploadDateTime The anonymized date and time of the last upload */
this.anonymizedUploadDateTime = anonymizedUploadDateTime;
/** @property {string} creationDateTime The date and time that the image was created */
this.creationDateTime = creationDateTime;
@ -144,7 +149,7 @@
* @returns {mw.mmv.model.Image}
*/
Image.newFromImageInfo = function ( title, imageInfo ) {
var name, uploadDateTime, creationDateTime, imageData,
var name, uploadDateTime, anonymizedUploadDateTime, creationDateTime, imageData,
description, source, author, authorCount, license, permission,
latitude, longitude,
innerInfo = imageInfo.imageinfo[0],
@ -152,7 +157,15 @@
if ( extmeta ) {
creationDateTime = this.parseExtmeta( extmeta.DateTimeOriginal, 'plaintext' );
uploadDateTime = this.parseExtmeta( extmeta.DateTime, 'plaintext' );
uploadDateTime = this.parseExtmeta( extmeta.DateTime, 'plaintext' ).toString();
// Convert to "timestamp" format commonly used in EventLogging
anonymizedUploadDateTime = uploadDateTime.replace( /[^\d]/g, '' );
// Anonymise the timestamp to avoid making the file identifiable
// We only need to know the day
anonymizedUploadDateTime = anonymizedUploadDateTime.substr( 0, anonymizedUploadDateTime.length - 6 ) + '000000';
name = this.parseExtmeta( extmeta.ObjectName, 'plaintext' );
description = this.parseExtmeta( extmeta.ImageDescription, 'string' );
@ -172,6 +185,7 @@
name = title.getNameText();
}
imageData = new Image(
title,
name,
@ -184,6 +198,7 @@
imageInfo.imagerepository,
innerInfo.user,
uploadDateTime,
anonymizedUploadDateTime,
creationDateTime,
description,
source,

View file

@ -44,11 +44,12 @@
* Loads an image and returns it. Includes performance metrics via mw.mmv.logging.PerformanceLogger.
* When the browser supports it, the image is loaded as an AJAX request.
* @param {string} url
* @param {jQuery.Deferred.<string>} extraStatsDeferred A promise which resolves to extra statistics.
* @return {jQuery.Promise.<HTMLImageElement>} A promise which resolves to the image object.
* When loaded via AJAX, it has progress events, which return an array with the content loaded
* so far and with the progress as a floating-point number between 0 and 100.
*/
Image.prototype.get = function ( url ) {
Image.prototype.get = function ( url, extraStatsDeferred ) {
var provider = this,
cacheKey = url,
extraParam = {},
@ -65,12 +66,12 @@
if ( !this.cache[cacheKey] ) {
if ( this.imagePreloadingSupported() ) {
rawGet = $.proxy( provider.rawGet, provider, url, true );
this.cache[cacheKey] = this.performance.record( 'image', url ).then( rawGet, rawGet );
this.cache[cacheKey] = this.performance.record( 'image', url, extraStatsDeferred ).then( rawGet, rawGet );
} else {
start = $.now();
this.cache[cacheKey] = this.rawGet( url );
this.cache[cacheKey].always( function () {
provider.performance.recordEntry( 'image', $.now() - start, url );
provider.performance.recordEntry( 'image', $.now() - start, url, undefined, extraStatsDeferred );
} );
}
this.cache[cacheKey].fail( function ( error ) {

View file

@ -186,6 +186,49 @@
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].bandwidth, Math.round( bandwidth ), 'bandwidth is correct' );
} );
QUnit.test( 'recordEntry: with async extra stats', 11, function ( assert ) {
var performance = new mw.mmv.logging.PerformanceLogger(),
fakeEventLog = { logEvent: this.sandbox.stub() },
type = 'gender',
total = 100,
overriddenType = 'image',
foo = 'bar',
extraStatsPromise = $.Deferred();
this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() );
this.sandbox.stub( performance, 'isInSample' );
performance.setEventLog( fakeEventLog );
performance.isInSample.returns( true );
performance.recordEntry( type, total, 'URL1', undefined, extraStatsPromise );
assert.strictEqual( fakeEventLog.logEvent.callCount, 0, 'Stats should not be logged if the promise hasn\'t completed yet' );
extraStatsPromise.reject();
assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should be logged' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, total, 'total is correct' );
extraStatsPromise = $.Deferred();
performance.recordEntry( type, total, 'URL2', undefined, extraStatsPromise );
assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should not be logged if the promise hasn\'t been resolved yet' );
extraStatsPromise.resolve( { type: overriddenType, foo: foo } );
assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should be logged' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].type, overriddenType, 'type is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].total, total, 'total is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].foo, foo, 'extra stat is correct' );
} );
QUnit.test( 'parseVarnishXCacheHeader', 15, function ( assert ) {
var varnish1 = 'cp1061',
varnish2 = 'cp3006',

View file

@ -6,7 +6,7 @@
options.licenseInternalName, options.licenseLongName, options.licenseUrl ) : undefined,
imageInfo = new mw.mmv.model.Image( options.title, options.title.getNameText(), undefined,
undefined, undefined, undefined, options.imgUrl, options.filePageUrl, 'repo', undefined,
undefined, undefined, undefined, options.source, options.author, options.authorCount, license ),
undefined, undefined, undefined, undefined, options.source, options.author, options.authorCount, license ),
repoInfo = { displayName: options.siteName, getSiteLink:
function () { return options.siteUrl; } };

View file

@ -102,21 +102,25 @@
QUnit.test( 'Progress', 4, function ( assert ) {
var imageDeferred = $.Deferred(),
viewer = new mw.mmv.MultimediaViewer( { get : $.noop } );
viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ),
fakeImage = {
filePageTitle: new mw.Title( 'File:Stuff.jpg' ),
extraStatsDeferred: $.Deferred().reject()
};
viewer.thumbs = [];
viewer.displayPlaceholderThumbnail = $.noop;
viewer.setImage = $.noop;
viewer.scroll = $.noop;
viewer.preloadFullscreenThumbnail = $.noop;
viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve(); };
viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve( {} ); };
viewer.ui = {
setFileReuseData: $.noop,
setupForLoad: $.noop,
canvas: { set : $.noop,
canvas: { set: $.noop,
unblurWithAnimation: $.noop,
unblur: $.noop,
getCurrentImageWidths: function () { return { real : 0 }; },
getCurrentImageWidths: function () { return { real: 0 }; },
getDimensions: function () { return {}; }
},
panel: {
@ -132,10 +136,10 @@
open: $.noop };
viewer.imageProvider.get = function() { return imageDeferred.promise(); };
viewer.imageInfoProvider.get = function() { return $.Deferred().resolve(); };
viewer.imageInfoProvider.get = function() { return $.Deferred().resolve( {} ); };
viewer.thumbnailInfoProvider.get = function() { return $.Deferred().resolve( {} ); };
viewer.loadImage( { filePageTitle : new mw.Title( 'File:Stuff.jpg' ) }, new Image() );
viewer.loadImage( fakeImage, new Image() );
assert.ok( viewer.ui.panel.progressBar.jumpTo.lastCall.calledWith( 0 ),
'Percentage correctly reset by loadImage' );
@ -156,8 +160,16 @@
QUnit.test( 'Progress when switching images', 11, function ( assert ) {
var firstImageDeferred = $.Deferred(),
secondImageDeferred = $.Deferred(),
firstImage = { index: 1, filePageTitle : new mw.Title( 'File:First.jpg' ) },
secondImage = { index: 2, filePageTitle : new mw.Title( 'File:Second.jpg' ) },
firstImage = {
index: 1,
filePageTitle: new mw.Title( 'File:First.jpg' ),
extraStatsDeferred: $.Deferred().reject()
},
secondImage = {
index: 2,
filePageTitle: new mw.Title( 'File:Second.jpg' ),
extraStatsDeferred: $.Deferred().reject()
},
viewer = new mw.mmv.MultimediaViewer( { get : $.noop } );
viewer.thumbs = [];
@ -167,7 +179,7 @@
viewer.preloadFullscreenThumbnail = $.noop;
viewer.preloadImagesMetadata = $.noop;
viewer.preloadThumbnails = $.noop;
viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve(); };
viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve( {} ); };
viewer.ui = {
setFileReuseData: $.noop,
setupForLoad : $.noop,
@ -191,7 +203,7 @@
open : $.noop,
empty: $.noop };
viewer.imageInfoProvider.get = function() { return $.Deferred().resolve(); };
viewer.imageInfoProvider.get = function() { return $.Deferred().resolve( {} ); };
viewer.thumbnailInfoProvider.get = function() { return $.Deferred().resolve( {} ); };
// load some image
@ -361,7 +373,17 @@
firstImageDeferred = $.Deferred(),
secondImageDeferred = $.Deferred(),
firstLigthboxInfoDeferred = $.Deferred(),
secondLigthboxInfoDeferred = $.Deferred();
secondLigthboxInfoDeferred = $.Deferred(),
firstImage = {
filePageTitle: new mw.Title( 'File:Foo.jpg' ),
index: 0,
extraStatsDeferred: $.Deferred().reject()
},
secondImage = {
filePageTitle: new mw.Title( 'File:Bar.jpg' ),
index: 1,
extraStatsDeferred: $.Deferred().reject()
};
viewer.preloadFullscreenThumbnail = $.noop;
viewer.fetchSizeIndependentLightboxInfo = this.sandbox.stub();
@ -395,25 +417,25 @@
viewer.imageProvider.get.returns( firstImageDeferred.promise() );
viewer.fetchSizeIndependentLightboxInfo.returns( firstLigthboxInfoDeferred.promise() );
viewer.loadImage( { filePageTitle : new mw.Title( 'File:Foo.jpg' ), index : 0 }, new Image() );
viewer.loadImage( firstImage, new Image() );
assert.ok( !viewer.animateMetadataDivOnce.called, 'Metadata of the first image should not be animated' );
assert.ok( !viewer.ui.panel.setImageInfo.called, 'Metadata of the first image should not be shown' );
viewer.imageProvider.get.returns( secondImageDeferred.promise() );
viewer.fetchSizeIndependentLightboxInfo.returns( secondLigthboxInfoDeferred.promise() );
viewer.loadImage( { filePageTitle : new mw.Title( 'File:Bar.jpg' ), index : 1 }, new Image() );
viewer.loadImage( secondImage, new Image() );
viewer.ui.panel.progressBar.animateTo.reset();
firstImageDeferred.notify( undefined, 45 );
assert.ok( !viewer.ui.panel.progressBar.animateTo.reset.called, 'Progress of the first image should not be shown' );
firstImageDeferred.resolve();
firstLigthboxInfoDeferred.resolve();
firstLigthboxInfoDeferred.resolve( {} );
assert.ok( !viewer.displayRealThumbnail.called, 'The first image being done loading should have no effect');
viewer.displayRealThumbnail = this.sandbox.spy( function () { viewer.close(); } );
secondImageDeferred.resolve();
secondLigthboxInfoDeferred.resolve();
secondLigthboxInfoDeferred.resolve( {} );
assert.ok( viewer.displayRealThumbnail.called, 'The second image being done loading should result in the image being shown');
} );
@ -438,8 +460,9 @@
viewer.preloadFullscreenThumbnail = $.noop;
viewer.initWithThumbs( [] );
viewer.loadImage( { filePageTitle : new mw.Title( 'File:Stuff.jpg' ),
thumbnail : new mw.mmv.model.Thumbnail( 'foo', 10, 10 ) },
viewer.loadImage( { filePageTitle: new mw.Title( 'File:Stuff.jpg' ),
thumbnail: new mw.mmv.model.Thumbnail( 'foo', 10, 10 ),
extraStatsDeferred: $.Deferred().reject() },
new Image() );
viewer.ui.$closeButton.click();
@ -534,7 +557,7 @@
assert.ok( !guessedThumbnailInfoStub.called, 'When we lack sample URL and original dimensions, GuessedThumbnailInfoProvider is not called' );
assert.ok( thumbnailInfoStub.calledOnce, 'When we lack sample URL and original dimensions, ThumbnailInfoProvider is called once' );
assert.ok( imageStub.calledOnce, 'When we lack sample URL and original dimensions, ImageProvider is called once' );
assert.ok( imageStub.calledWithExactly( 'apiURL' ), 'When we lack sample URL and original dimensions, ImageProvider is called with the API url' );
assert.ok( imageStub.calledWithExactly( 'apiURL', undefined ), 'When we lack sample URL and original dimensions, ImageProvider is called with the API url' );
assert.strictEqual( promise.state(), 'resolved', 'When we lack sample URL and original dimensions, fetchThumbnail resolves' );
// When the guesser bails out, the classic provider should be used
@ -546,7 +569,7 @@
assert.ok( guessedThumbnailInfoStub.calledOnce, 'When the guesser bails out, GuessedThumbnailInfoProvider is called once' );
assert.ok( thumbnailInfoStub.calledOnce, 'When the guesser bails out, ThumbnailInfoProvider is called once' );
assert.ok( imageStub.calledOnce, 'When the guesser bails out, ImageProvider is called once' );
assert.ok( imageStub.calledWithExactly( 'apiURL' ), 'When the guesser bails out, ImageProvider is called with the API url' );
assert.ok( imageStub.calledWithExactly( 'apiURL', undefined ), 'When the guesser bails out, ImageProvider is called with the API url' );
assert.strictEqual( promise.state(), 'resolved', 'When the guesser bails out, fetchThumbnail resolves' );
// When the guesser returns an URL, that should be used
@ -558,7 +581,7 @@
assert.ok( guessedThumbnailInfoStub.calledOnce, 'When the guesser returns an URL, GuessedThumbnailInfoProvider is called once' );
assert.ok( !thumbnailInfoStub.called, 'When the guesser returns an URL, ThumbnailInfoProvider is not called' );
assert.ok( imageStub.calledOnce, 'When the guesser returns an URL, ImageProvider is called once' );
assert.ok( imageStub.calledWithExactly( 'guessedURL' ), 'When the guesser returns an URL, ImageProvider is called with the guessed url' );
assert.ok( imageStub.calledWithExactly( 'guessedURL', undefined ), 'When the guesser returns an URL, ImageProvider is called with the guessed url' );
assert.strictEqual( promise.state(), 'resolved', 'When the guesser returns an URL, fetchThumbnail resolves' );
// When the guesser returns an URL, but that returns 404, image loading should be retried with the classic provider
@ -571,8 +594,8 @@
assert.ok( guessedThumbnailInfoStub.calledOnce, 'When the guesser returns an URL, but that returns 404, GuessedThumbnailInfoProvider is called once' );
assert.ok( thumbnailInfoStub.calledOnce, 'When the guesser returns an URL, but that returns 404, ThumbnailInfoProvider is called once' );
assert.ok( imageStub.calledTwice, 'When the guesser returns an URL, but that returns 404, ImageProvider is called twice' );
assert.ok( imageStub.getCall( 0 ).calledWithExactly( 'guessedURL' ), 'When the guesser returns an URL, but that returns 404, ImageProvider is called first with the guessed url' );
assert.ok( imageStub.getCall( 1 ).calledWithExactly( 'apiURL' ), 'When the guesser returns an URL, but that returns 404, ImageProvider is called second with the guessed url' );
assert.ok( imageStub.getCall( 0 ).calledWithExactly( 'guessedURL', undefined ), 'When the guesser returns an URL, but that returns 404, ImageProvider is called first with the guessed url' );
assert.ok( imageStub.getCall( 1 ).calledWithExactly( 'apiURL', undefined ), 'When the guesser returns an URL, but that returns 404, ImageProvider is called second with the guessed url' );
assert.strictEqual( promise.state(), 'resolved', 'When the guesser returns an URL, but that returns 404, fetchThumbnail resolves' );
// When even the retry fails, fetchThumbnail() should reject
@ -585,8 +608,8 @@
assert.ok( guessedThumbnailInfoStub.calledOnce, 'When even the retry fails, GuessedThumbnailInfoProvider is called once' );
assert.ok( thumbnailInfoStub.calledOnce, 'When even the retry fails, ThumbnailInfoProvider is called once' );
assert.ok( imageStub.calledTwice, 'When even the retry fails, ImageProvider is called twice' );
assert.ok( imageStub.getCall( 0 ).calledWithExactly( 'guessedURL' ), 'When even the retry fails, ImageProvider is called first with the guessed url' );
assert.ok( imageStub.getCall( 1 ).calledWithExactly( 'apiURL' ), 'When even the retry fails, ImageProvider is called second with the guessed url' );
assert.ok( imageStub.getCall( 0 ).calledWithExactly( 'guessedURL', undefined ), 'When even the retry fails, ImageProvider is called first with the guessed url' );
assert.ok( imageStub.getCall( 1 ).calledWithExactly( 'apiURL', undefined ), 'When even the retry fails, ImageProvider is called second with the guessed url' );
assert.strictEqual( promise.state(), 'rejected', 'When even the retry fails, fetchThumbnail rejects' );
mw.config.get( 'wgMultimediaViewer' ).useThumbnailGuessing = false;
@ -600,7 +623,7 @@
assert.ok( !guessedThumbnailInfoStub.called, 'When guessing is disabled, GuessedThumbnailInfoProvider is not called' );
assert.ok( thumbnailInfoStub.calledOnce, 'When guessing is disabled, ThumbnailInfoProvider is called once' );
assert.ok( imageStub.calledOnce, 'When guessing is disabled, ImageProvider is called once' );
assert.ok( imageStub.calledWithExactly( 'apiURL' ), 'When guessing is disabled, ImageProvider is called with the API url' );
assert.ok( imageStub.calledWithExactly( 'apiURL', undefined ), 'When guessing is disabled, ImageProvider is called with the API url' );
assert.strictEqual( promise.state(), 'resolved', 'When guessing is disabled, fetchThumbnail resolves' );
mw.config.get( 'wgMultimediaViewer' ).useThumbnailGuessing = oldUseThumbnailGuessing;

View file

@ -18,7 +18,7 @@
( function( mw ) {
QUnit.module( 'mmv.model.Image', QUnit.newMwEnvironment() );
QUnit.test( 'Image model constructor sanity check', 21, function ( assert ) {
QUnit.test( 'Image model constructor sanity check', 22, function ( assert ) {
var
title = mw.Title.newFromText( 'File:Foobar.jpg' ),
name = 'Foo bar',
@ -31,6 +31,7 @@
repo = 'wikimediacommons',
user = 'Kaldari',
datetime = '2011-07-04T23:31:14Z',
anondatetime = '20110704000000',
origdatetime = '2010-07-04T23:31:14Z',
description = 'This is a test file.',
source = 'WMF',
@ -42,7 +43,7 @@
longitude = 100.983829,
imageData = new mw.mmv.model.Image(
title, name, size, width, height, mime, url,
descurl, repo, user, datetime, origdatetime,
descurl, repo, user, datetime, anondatetime, origdatetime,
description, source, author, authorCount, license, permission,
latitude, longitude );
@ -57,6 +58,7 @@
assert.strictEqual( imageData.repo, repo, 'Repository name is set correctly' );
assert.strictEqual( imageData.lastUploader, user, 'Name of last uploader is set correctly' );
assert.strictEqual( imageData.uploadDateTime, datetime, 'Date and time of last upload is set correctly' );
assert.strictEqual( imageData.anonymizedUploadDateTime, anondatetime, 'Anonymized date and time of last upload is set correctly' );
assert.strictEqual( imageData.creationDateTime, origdatetime, 'Date and time of original upload is set correctly' );
assert.strictEqual( imageData.description, description, 'Description is set correctly' );
assert.strictEqual( imageData.source, source, 'Source is set correctly' );
@ -74,13 +76,13 @@
firstImageData = new mw.mmv.model.Image(
mw.Title.newFromText( 'File:Foobar.pdf.jpg' ), 'Foo bar',
10, 10, 10, 'image/jpeg', 'http://example.org', 'http://example.com',
'example', 'tester', '2013-11-10', '2013-11-09', 'Blah blah blah',
'example', 'tester', '2013-11-10', '20131110', '2013-11-09', 'Blah blah blah',
'A person', 'Another person', 1, 'CC-BY-SA-3.0', 'Permitted'
),
secondImageData = new mw.mmv.model.Image(
mw.Title.newFromText( 'File:Foobar.pdf.jpg' ), 'Foo bar',
10, 10, 10, 'image/jpeg', 'http://example.org', 'http://example.com',
'example', 'tester', '2013-11-10', '2013-11-09', 'Blah blah blah',
'example', 'tester', '2013-11-10', '20131110', '2013-11-09', 'Blah blah blah',
'A person', 'Another person', 1, 'CC-BY-SA-3.0', 'Permitted',
'39.91820938', '78.09812938'
);

View file

@ -25,7 +25,7 @@
assert.ok( imageInfoProvider );
} );
QUnit.asyncTest( 'ImageInfo get test', 26, function ( assert ) {
QUnit.asyncTest( 'ImageInfo get test', 27, function ( assert ) {
var apiCallCount = 0,
api = { get: function() {
apiCallCount++;
@ -142,6 +142,7 @@
assert.strictEqual( image.repo, 'shared', 'repo is set correctly' );
assert.strictEqual( image.lastUploader, 'Dylanbot11', 'lastUploader is set correctly' );
assert.strictEqual( image.uploadDateTime, '2013-08-25T14:41:02Z', 'uploadDateTime is set correctly' );
assert.strictEqual( image.anonymizedUploadDateTime, '20130825000000', 'anonymizedUploadDateTime is set correctly' );
assert.strictEqual( image.creationDateTime, '18 February 2009\u00a0(according to EXIF data)', 'creationDateTime is set correctly' );
assert.strictEqual( image.description, 'Wikis stuff', 'description is set correctly' );
assert.strictEqual( image.source, 'Wikipedia', 'source is set correctly' );