Merge "Refactor progressbar & blur handling"

This commit is contained in:
jenkins-bot 2014-05-01 23:12:22 +00:00 committed by Gerrit Code Review
commit 9e0df0dd4c
7 changed files with 315 additions and 160 deletions

View file

@ -232,11 +232,11 @@
* Loads and sets the specified image. It also updates the controls. * Loads and sets the specified image. It also updates the controls.
* @param {mw.mmv.LightboxInterface} ui image container * @param {mw.mmv.LightboxInterface} ui image container
* @param {mw.mmv.model.Thumbnail} thumbnail thumbnail information * @param {mw.mmv.model.Thumbnail} thumbnail thumbnail information
* @param {HTMLImageElement} image * @param {HTMLImageElement} imageElement
* @param {mw.mmv.model.ThumbnailWidth} imageWidths * @param {mw.mmv.model.ThumbnailWidth} imageWidths
*/ */
MMVP.setImage = function ( ui, thumbnail, image, imageWidths ) { MMVP.setImage = function ( ui, thumbnail, imageElement, imageWidths ) {
ui.canvas.setImageAndMaxDimensions( thumbnail, image, imageWidths ); ui.canvas.setImageAndMaxDimensions( thumbnail, imageElement, imageWidths );
this.updateControls(); this.updateControls();
}; };
@ -255,7 +255,6 @@
this.currentIndex = image.index; this.currentIndex = image.index;
this.currentImageFilename = image.filePageTitle.getPrefixedText();
this.currentImageFileTitle = image.filePageTitle; this.currentImageFileTitle = image.filePageTitle;
if ( !this.isOpen ) { if ( !this.isOpen ) {
@ -279,15 +278,17 @@
imageWidths = this.ui.canvas.getCurrentImageWidths(); imageWidths = this.ui.canvas.getCurrentImageWidths();
this.resetBlurredThumbnailStates();
start = $.now(); start = $.now();
imagePromise = this.fetchThumbnailForLightboxImage( image, imageWidths.real ); imagePromise = this.fetchThumbnailForLightboxImage( image, imageWidths.real );
viewer.displayPlaceholderThumbnail( image, $initialImage, imageWidths ); this.resetBlurredThumbnailStates();
if ( imagePromise.state() === 'pending' ) {
this.displayPlaceholderThumbnail( image, $initialImage, imageWidths );
}
this.setupProgressBar( image, imagePromise ); this.setupProgressBar( image, imagePromise, imageWidths.real );
imagePromise.done( function ( thumbnail, imageElement ) { imagePromise.done( function ( thumbnail, imageElement ) {
if ( viewer.currentIndex !== image.index ) { if ( viewer.currentIndex !== image.index ) {
@ -356,29 +357,52 @@
}; };
/** /**
* Resets the cross-request states needed to handle the blurred thumbnail logic * @private
* Image loading progress. Keyed by image (database) name + '|' + thumbnail width in pixels,
* value is undefined, 'blurred' or 'real' (meaning respectively that no thumbnail is shown
* yet / the thumbnail that existed on the page is shown, enlarged and blurred / the real,
* correct-size thumbnail is shown).
* @property {Object.<string, string>}
*/
MMVP.thumbnailStateCache = {};
/**
* Resets the cross-request states needed to handle the blurred thumbnail logic.
*/ */
MMVP.resetBlurredThumbnailStates = function () { MMVP.resetBlurredThumbnailStates = function () {
/**
* Stores whether the real image was loaded and displayed already.
* This is reset when paging, so it is not necessarily accurate.
* @property {boolean}
*/
this.realThumbnailShown = false; this.realThumbnailShown = false;
/**
* Stores whether the a blurred placeholder is being displayed in place of the real image.
* When a placeholder is displayed, but it is not blurred, this is false.
* This is reset when paging, so it is not necessarily accurate.
* @property {boolean}
*/
this.blurredThumbnailShown = false; this.blurredThumbnailShown = false;
}; };
/** /**
* Display the real, full-resolution, thumbnail that was fetched with fetchThumbnail * Display the real, full-resolution, thumbnail that was fetched with fetchThumbnail
* @param {mw.mmv.model.Thumbnail} thumbnail * @param {mw.mmv.model.Thumbnail} thumbnail
* @param {HTMLImageElement} image * @param {HTMLImageElement} imageElement
* @param {mw.mmv.model.ThumbnailWidth} imageWidths * @param {mw.mmv.model.ThumbnailWidth} imageWidths
* @param {number} loadTime Time it took to load the thumbnail * @param {number} loadTime Time it took to load the thumbnail
*/ */
MMVP.displayRealThumbnail = function ( thumbnail, image, imageWidths, loadTime ) { MMVP.displayRealThumbnail = function ( thumbnail, imageElement, imageWidths, loadTime ) {
this.realThumbnailShown = true; this.realThumbnailShown = true;
this.setImage( this.ui, thumbnail, image, imageWidths ); this.setImage( this.ui, thumbnail, imageElement, imageWidths );
// We only animate unblur if the image wasn't loaded from the cache // We only animate unblurWithAnimation if the image wasn't loaded from the cache
// A load in < 10ms is considered to be a browser cache hit // A load in < 10ms is considered to be a browser cache hit
// And of course we only unblur if there was a blur to begin with
if ( this.blurredThumbnailShown && loadTime > 10 ) { if ( this.blurredThumbnailShown && loadTime > 10 ) {
this.ui.canvas.unblurWithAnimation();
} else {
this.ui.canvas.unblur(); this.ui.canvas.unblur();
} }
@ -390,13 +414,15 @@
* @param {mw.mmv.LightboxImage} image * @param {mw.mmv.LightboxImage} image
* @param {jQuery} $initialImage The thumbnail from the page * @param {jQuery} $initialImage The thumbnail from the page
* @param {mw.mmv.model.ThumbnailWidth} imageWidths * @param {mw.mmv.model.ThumbnailWidth} imageWidths
* @param {boolean} [recursion=false] for internal use, never set this * @param {boolean} [recursion=false] for internal use, never set this when calling from outside
*/ */
MMVP.displayPlaceholderThumbnail = function ( image, $initialImage, imageWidths, recursion ) { MMVP.displayPlaceholderThumbnail = function ( image, $initialImage, imageWidths, recursion ) {
var viewer = this, var viewer = this,
size = { width : image.originalWidth, height : image.originalHeight }; size = { width : image.originalWidth, height : image.originalHeight };
// If the actual image has already been displayed, there's no point showing the blurry one // If the actual image has already been displayed, there's no point showing the blurry one.
// This can happen if the API request to get the original image size needed to show the
// placeholder thumbnail takes longer then loading the actual thumbnail.
if ( this.realThumbnailShown ) { if ( this.realThumbnailShown ) {
return; return;
} }
@ -425,58 +451,71 @@
} }
}; };
/**
* @private
* Image loading progress. Keyed by image (database) name + '|' + thumbnail width in pixels,
* value is a number between 0-100.
* @property {Object.<string, number>}
*/
MMVP.progressCache = {};
/** /**
* Displays a progress bar for the image loading, if necessary, and sets up handling of * Displays a progress bar for the image loading, if necessary, and sets up handling of
* all the related callbacks. * all the related callbacks.
* FIXME would be nice to pass a simple promise which only returns a single number
* and does not fire when the image is not visible
* @param {mw.mmv.LightboxImage} image * @param {mw.mmv.LightboxImage} image
* @param {jQuery.Promise.<mw.mmv.model.Thumbnail, HTMLImageElement>} imagePromise * @param {jQuery.Promise.<mw.mmv.model.Thumbnail, HTMLImageElement>} imagePromise
* @param {number} imageWidth needed for caching progress (FIXME)
*/ */
MMVP.setupProgressBar = function ( image, imagePromise ) { MMVP.setupProgressBar = function ( image, imagePromise, imageWidth ) {
var viewer = this; var viewer = this,
progressBar = viewer.ui.panel.progressBar,
// Reset the progress bar, it could be at any state if we're calling loadImage key = image.filePageTitle.getPrefixedDb() + '|' + imageWidth;
// while another image is already loading
// FIXME we should probably jump to the current progress instead
viewer.ui.panel.progressBar.percent( 0 );
if ( imagePromise.state() !== 'pending' ) { if ( imagePromise.state() !== 'pending' ) {
// image has already loaded (or failed to load) - nothing to do // image has already loaded (or failed to load) - do not show the progress bar
progressBar.hide();
return; return;
} }
// FIXME this is all wrong, we might be navigating back to a half-loaded image if ( !this.progressCache[key] ) {
// Animate progress bar to 5 to give a sense that something is happening, and make sure
// the progress bar is noticeable, even if we're sitting at 0% stuck waiting for
// server-side processing, such as thumbnail (re)generation
progressBar.jumpTo( 0 );
progressBar.animateTo( 5 );
viewer.progressCache[key] = 5;
} else {
progressBar.jumpTo( this.progressCache[key] );
}
// Animate progress bar to 5 to give a sense to something is happening, even if we're // FIXME would be nice to have a "filtered" promise which does not fire when the image is not visible
// stuck waiting for server-side processing, such as thumbnail (re)generation imagePromise.progress( function ( progress ) {
viewer.ui.panel.progressBar.percent( 5 ); // We pretend progress is always at least 5%, so progress events below 5% should be ignored
// 100 will be handled by the done handler, do not mix two animations
imagePromise.progress( function ( thumbnailInfoResponse, imageResponse ) { if ( progress < 5 || progress === 100 ) {
// FIXME this should be explained in a comment
var progress = imageResponse[1];
if ( viewer.currentIndex !== image.index ) {
return; return;
} }
// We started from 5, don't move backwards viewer.progressCache[key] = progress;
if ( progress > 5 ) {
viewer.ui.panel.progressBar.percent( progress ); // Touch the UI only if the user is looking at this image
if ( viewer.currentIndex === image.index ) {
progressBar.animateTo( progress );
} }
} ).done( function () { } ).done( function () {
if ( viewer.currentIndex !== image.index ) { viewer.progressCache[key] = 100;
return;
}
// Fallback in case the browser doesn't have fancy progress updates if ( viewer.currentIndex === image.index ) {
viewer.ui.panel.progressBar.percent( 100 ); // Fallback in case the browser doesn't have fancy progress updates
} ).fail( function () { progressBar.animateTo( 100 );
if ( viewer.currentIndex !== image.index ) { }
return; } ).fail( function () {
viewer.progressCache[key] = 100;
if ( viewer.currentIndex === image.index ) {
// Hide progress bar on error
progressBar.hide();
} }
// Hide progress bar on error
viewer.ui.panel.progressBar.percent( 0 );
} ); } );
}; };
@ -671,7 +710,9 @@
* @param {string} [sampleUrl] a thumbnail URL for the same file (but with different size) (might be missing) * @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} [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 {number} [originalHeight] the height of the original, full-sized file (might be missing)
* @returns {jQuery.Promise.<mw.mmv.model.Thumbnail, HTMLImageElement>} * @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 ) {
var viewer = this, var viewer = this,
@ -714,7 +755,10 @@
} ); } );
} }
return $.when( thumbnailPromise, imagePromise ); return $.when( thumbnailPromise, imagePromise ).then( null, null, function ( thumbnailProgress, imageProgress ) {
// Make progress events have a nice format.
return imageProgress[1];
} );
}; };
/** /**

View file

@ -37,10 +37,12 @@
} }
/** /**
* Loads an image and returns it. * Loads an image and returns it. Includes performance metrics via mw.mmv.Performance.
* Includes performance metrics. * When the browser supports it, the image is loaded as an AJAX request.
* @param {string} url * @param {string} url
* @return {jQuery.Promise.<HTMLImageElement>} a promise which resolves to the image object * @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 ) {
var provider = this, var provider = this,
@ -59,11 +61,12 @@
provider.performance.recordEntry( 'image', $.now() - start, url ); provider.performance.recordEntry( 'image', $.now() - start, url );
} ); } );
} }
this.cache[cacheKey].fail( function ( error ) {
mw.log( provider.constructor.name + ' provider failed to load: ', error );
} );
} }
return this.cache[cacheKey].fail( function ( error ) { return this.cache[cacheKey];
mw.log( provider.constructor.name + ' provider failed to load: ', error );
} );
}; };
/** /**

View file

@ -113,11 +113,11 @@
* Sets contained image and also the max dimensions. Called while resizing the viewer. * Sets contained image and also the max dimensions. Called while resizing the viewer.
* Assumes set function called before. * Assumes set function called before.
* @param {mw.mmv.model.Thumbnail} thumbnail thumbnail information * @param {mw.mmv.model.Thumbnail} thumbnail thumbnail information
* @param {HTMLImageElement} imageEle * @param {HTMLImageElement} imageElement
* @param {mw.mmv.model.ThumbnailWidth} imageWidths * @param {mw.mmv.model.ThumbnailWidth} imageWidths
*/ */
C.setImageAndMaxDimensions = function( thumbnail, imageEle, imageWidths ) { C.setImageAndMaxDimensions = function( thumbnail, imageElement, imageWidths ) {
var $image = $( imageEle ); var $image = $( imageElement );
function makeMaxMatchParent ( $image ) { function makeMaxMatchParent ( $image ) {
$image.css( { $image.css( {
@ -128,10 +128,10 @@
// we downscale larger images but do not scale up smaller ones, that would look ugly // we downscale larger images but do not scale up smaller ones, that would look ugly
if ( thumbnail.width > imageWidths.cssWidth ) { if ( thumbnail.width > imageWidths.cssWidth ) {
imageEle.width = imageWidths.cssWidth; imageElement.width = imageWidths.cssWidth;
} }
if ( this.$image.is( imageEle ) ) { // http://bugs.jquery.com/ticket/4087 if ( this.$image.is( imageElement ) ) { // http://bugs.jquery.com/ticket/4087
// We may be changing the width of the image when we resize, we should also // We may be changing the width of the image when we resize, we should also
// update the max dimensions otherwise the image is not scaled properly // update the max dimensions otherwise the image is not scaled properly
makeMaxMatchParent( this.$image ); makeMaxMatchParent( this.$image );
@ -233,7 +233,7 @@
/** /**
* Animates the image into focus * Animates the image into focus
*/ */
C.unblur = function() { C.unblurWithAnimation = function() {
var self = this, var self = this,
animationLength = 300; animationLength = 300;
@ -251,15 +251,19 @@
'filter' : 'blur(' + step + 'px)' } ); 'filter' : 'blur(' + step + 'px)' } );
}, },
complete: function () { complete: function () {
// When the animation is complete, the blur value is 0 // When the animation is complete, the blur value is 0, clean things up
// We apply empty CSS values to remove the inline styles applied by jQuery self.unblur();
// so that they don't get in the way of styles defined in CSS
self.$image.css( { '-webkit-filter' : '', 'opacity' : '' } )
.removeClass( 'blurred' );
} }
} ); } );
}; };
C.unblur = function() {
// We apply empty CSS values to remove the inline styles applied by jQuery
// so that they don't get in the way of styles defined in CSS
this.$image.css( { '-webkit-filter' : '', 'opacity' : '' } )
.removeClass( 'blurred' );
};
/** /**
* Displays a message and error icon when loading the image fails. * Displays a message and error icon when loading the image fails.
* @param {string} error error message * @param {string} error error message

View file

@ -45,37 +45,46 @@
}; };
PBP.empty = function () { PBP.empty = function () {
this.hide();
};
/**
* Hides the bar, resets it to 0 and stops any animation in progress.
*/
PBP.hide = function () {
this.$progress.addClass( 'empty' ); this.$progress.addClass( 'empty' );
this.$percent.stop().css( { width : 0 } );
}; };
/** /**
* Handles the progress display when a percentage of progress is received * Handles the progress display when a percentage of progress is received
* @param {number} percent * @param {number} percent a number between 0 and 100
*/ */
PBP.percent = function ( percent ) { PBP.animateTo = function ( percent ) {
var panel = this; var panel = this;
if ( percent === 0 ) { this.$progress.removeClass( 'empty' );
// When a 0% update comes in, we jump without animation to 0 and we hide the bar this.$percent.stop();
this.$progress.addClass( 'empty' );
this.$percent.stop().css( { width : 0 } ); if ( percent === 100 ) {
} else if ( percent === 100 ) {
// When a 100% update comes in, we make sure that the bar is visible, we animate // When a 100% update comes in, we make sure that the bar is visible, we animate
// fast to 100 and we hide the bar when the animation is done // fast to 100 and we hide the bar when the animation is done
this.$progress.removeClass( 'empty' ); this.$percent.animate( { width : percent + '%' }, 50, 'swing', $.proxy( panel.hide, panel ) );
this.$percent.stop().animate( { width : percent + '%' }, 50, 'swing',
function () {
// Reset the position for good measure
panel.$percent.stop().css( { width : 0 } );
panel.$progress.addClass( 'empty' );
} );
} else { } else {
// When any other % update comes in, we make sure the bar is visible // When any other % update comes in, we make sure the bar is visible
// and we animate to the right position // and we animate to the right position
this.$progress.removeClass( 'empty' ); this.$percent.animate( { width : percent + '%' } );
this.$percent.stop().animate( { width : percent + '%' } );
} }
}; };
/**
* Goes to the given percent without animation
* @param {number} percent a number between 0 and 100
*/
PBP.jumpTo = function ( percent ) {
this.$progress.removeClass( 'empty' );
this.$percent.stop().css( { width : percent + '%' } );
};
mw.mmv.ui.ProgressBar = ProgressBar; mw.mmv.ui.ProgressBar = ProgressBar;
}( mediaWiki, jQuery, OO ) ); }( mediaWiki, jQuery, OO ) );

View file

@ -102,8 +102,7 @@
QUnit.test( 'Progress', 4, function ( assert ) { QUnit.test( 'Progress', 4, function ( assert ) {
var imageDeferred = $.Deferred(), var imageDeferred = $.Deferred(),
viewer = new mw.mmv.MultimediaViewer(), viewer = new mw.mmv.MultimediaViewer();
i = 0;
viewer.thumbs = []; viewer.thumbs = [];
viewer.displayPlaceholderThumbnail = $.noop; viewer.displayPlaceholderThumbnail = $.noop;
@ -114,28 +113,15 @@
viewer.ui = { viewer.ui = {
setupForLoad : $.noop, setupForLoad : $.noop,
canvas : { set : $.noop, canvas : { set : $.noop,
unblurWithAnimation: $.noop,
unblur: $.noop,
getCurrentImageWidths : function () { return { real : 0 }; } }, getCurrentImageWidths : function () { return { real : 0 }; } },
panel : { panel : {
setImageInfo : $.noop, setImageInfo : $.noop,
animateMetadataOnce : $.noop, animateMetadataOnce : $.noop,
progressBar: { progressBar: {
percent : function ( percent ) { animateTo: this.sandbox.stub(),
if ( i === 0 ) { jumpTo: this.sandbox.stub()
assert.strictEqual( percent, 0,
'Percentage correctly reset by loadImage' );
} else if ( i === 1 ) {
assert.strictEqual( percent, 5,
'Percentage correctly animated to 5 by loadImage' );
} else if ( i === 2 ) {
assert.strictEqual( percent, 45,
'Percentage correctly funneled to panel UI' );
} else {
assert.strictEqual( percent, 100,
'Percentage correctly funneled to panel UI' );
}
i++;
}
} }
}, },
open : $.noop }; open : $.noop };
@ -145,9 +131,116 @@
viewer.thumbnailInfoProvider.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( { filePageTitle : new mw.Title( 'File:Stuff.jpg' ) }, new Image() );
assert.ok( viewer.ui.panel.progressBar.jumpTo.lastCall.calledWith( 0 ),
'Percentage correctly reset by loadImage' );
assert.ok( viewer.ui.panel.progressBar.animateTo.lastCall.calledWith( 5 ),
'Percentage correctly animated to 5 by loadImage' );
imageDeferred.notify( 'response', 45 ); imageDeferred.notify( 'response', 45 );
assert.ok( viewer.ui.panel.progressBar.animateTo.lastCall.calledWith( 45 ),
'Percentage correctly funneled to panel UI' );
imageDeferred.resolve(); imageDeferred.resolve();
assert.ok( viewer.ui.panel.progressBar.animateTo.lastCall.calledWith( 100 ),
'Percentage correctly funneled to panel UI' );
viewer.close();
} );
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' ) },
viewer = new mw.mmv.MultimediaViewer();
viewer.thumbs = [];
viewer.displayPlaceholderThumbnail = $.noop;
viewer.setImage = $.noop;
viewer.scroll = $.noop;
viewer.preloadFullscreenThumbnail = $.noop;
viewer.preloadImagesMetadata = $.noop;
viewer.preloadThumbnails = $.noop;
viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve(); };
viewer.ui = {
setupForLoad : $.noop,
canvas : { set : $.noop,
unblurWithAnimation: $.noop,
unblur: $.noop,
getCurrentImageWidths : function () { return { real : 0 }; } },
panel : {
setImageInfo : $.noop,
animateMetadataOnce : $.noop,
progressBar: {
hide: this.sandbox.stub(),
animateTo: this.sandbox.stub(),
jumpTo: this.sandbox.stub()
}
},
open : $.noop,
empty: $.noop };
viewer.imageInfoProvider.get = function() { return $.Deferred().resolve(); };
viewer.thumbnailInfoProvider.get = function() { return $.Deferred().resolve( {} ); };
// load some image
viewer.imageProvider.get = this.sandbox.stub().returns( firstImageDeferred );
viewer.loadImage( firstImage, new Image() );
assert.ok( viewer.ui.panel.progressBar.jumpTo.lastCall.calledWith( 0 ),
'Percentage correctly reset for new first image' );
assert.ok( viewer.ui.panel.progressBar.animateTo.lastCall.calledWith( 5 ),
'Percentage correctly animated to 5 for first new image' );
firstImageDeferred.notify( 'response', 20 );
assert.ok( viewer.ui.panel.progressBar.animateTo.lastCall.calledWith( 20 ),
'Percentage correctly animated when active image is loading' );
// change to another image
viewer.imageProvider.get = this.sandbox.stub().returns( secondImageDeferred );
viewer.loadImage( secondImage, new Image() );
assert.ok( viewer.ui.panel.progressBar.jumpTo.lastCall.calledWith( 0 ),
'Percentage correctly reset for second new image' );
assert.ok( viewer.ui.panel.progressBar.animateTo.lastCall.calledWith( 5 ),
'Percentage correctly animated to 5 for second new image' );
secondImageDeferred.notify( 'response', 30 );
assert.ok( viewer.ui.panel.progressBar.animateTo.lastCall.calledWith( 30 ),
'Percentage correctly animated when active image is loading' );
// this is the most convenient way of checking for new calls - just reset() and check called
viewer.ui.panel.progressBar.animateTo.reset();
viewer.ui.panel.progressBar.jumpTo.reset();
firstImageDeferred.notify( 'response', 40 );
assert.ok( !viewer.ui.panel.progressBar.animateTo.called,
'Percentage not animated when inactive image is loading' );
assert.ok( !viewer.ui.panel.progressBar.jumpTo.called,
'Percentage not changed when inactive image is loading' );
secondImageDeferred.notify( 'response', 50 );
// change back to first image
viewer.loadImage( firstImage, new Image() );
assert.ok( viewer.ui.panel.progressBar.jumpTo.lastCall.calledWith( 40 ),
'Percentage jumps to right value when changing images' );
secondImageDeferred.resolve();
assert.ok( !viewer.ui.panel.progressBar.hide.called,
'Progress bar not hidden when something finishes in the background' );
// change to second image which has finished loading
viewer.imageProvider.get = this.sandbox.stub().returns( secondImageDeferred );
viewer.loadImage( secondImage, new Image() );
assert.ok( viewer.ui.panel.progressBar.hide.called,
'Progress bar not hidden when switching to finished image' );
viewer.close(); viewer.close();
} ); } );
@ -172,6 +265,8 @@
viewer.setImage = $.noop; viewer.setImage = $.noop;
viewer.ui = { canvas : { viewer.ui = { canvas : {
unblurWithAnimation: $.noop,
unblur: $.noop,
maybeDisplayPlaceholder : function() { return true; } maybeDisplayPlaceholder : function() { return true; }
} }; } };
viewer.imageInfoProvider.get = this.sandbox.stub(); viewer.imageInfoProvider.get = this.sandbox.stub();
@ -193,6 +288,8 @@
viewer.currentIndex = 1; viewer.currentIndex = 1;
viewer.setImage = $.noop; viewer.setImage = $.noop;
viewer.ui = { canvas : { viewer.ui = { canvas : {
unblurWithAnimation: $.noop,
unblur: $.noop,
maybeDisplayPlaceholder : function() { return true; } maybeDisplayPlaceholder : function() { return true; }
} }; } };
viewer.imageInfoProvider.get = this.sandbox.stub().returns( $.Deferred().resolve( {width: 100, height: 100 } ) ); viewer.imageInfoProvider.get = this.sandbox.stub().returns( $.Deferred().resolve( {width: 100, height: 100 } ) );
@ -213,8 +310,11 @@
viewer.setImage = $.noop; viewer.setImage = $.noop;
viewer.ui = { viewer.ui = {
showImage : $.noop showImage : $.noop,
}; canvas : {
unblurWithAnimation: $.noop,
unblur: $.noop
} };
viewer.displayRealThumbnail(); viewer.displayRealThumbnail();
@ -227,94 +327,78 @@
assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' );
} ); } );
QUnit.test( 'displayRealThumbnail', 1, function ( assert ) { QUnit.test( 'displayRealThumbnail', 2, function ( assert ) {
var viewer = new mw.mmv.MultimediaViewer(); var viewer = new mw.mmv.MultimediaViewer();
viewer.setImage = $.noop; viewer.setImage = $.noop;
viewer.ui = { canvas : { viewer.ui = { canvas : {
unblur : function () { assert.ok( false, 'Image should not be unblurred yet' ); } unblurWithAnimation : this.sandbox.stub(),
unblur: $.noop
} }; } };
viewer.blurredThumbnailShown = true; viewer.blurredThumbnailShown = true;
// Should not result in an unblur (image cache from cache) // Should not result in an unblurWithAnimation animation (image cache from cache)
viewer.displayRealThumbnail( undefined, undefined, undefined, 5 ); viewer.displayRealThumbnail( undefined, undefined, undefined, 5 );
assert.ok( !viewer.ui.canvas.unblurWithAnimation.called, 'There should not be an unblurWithAnimation animation' );
viewer.ui.canvas.unblur = function () { // Should result in an unblurWithAnimation (image didn't come from cache)
assert.ok( true, 'Image needs to be unblurred' );
};
// Should result in an unblur (image didn't come from cache)
viewer.displayRealThumbnail( undefined, undefined, undefined, 1000 ); viewer.displayRealThumbnail( undefined, undefined, undefined, 1000 );
assert.ok( viewer.ui.canvas.unblurWithAnimation.called, 'There should be an unblurWithAnimation animation' );
} ); } );
QUnit.test( 'New image loaded while another one is loading', 1, function ( assert ) { QUnit.test( 'New image loaded while another one is loading', 5, function ( assert ) {
var imageDeferred, var viewer = new mw.mmv.MultimediaViewer(),
ligthboxInfoDeferred,
viewer = new mw.mmv.MultimediaViewer(),
firstImageDeferred = $.Deferred(), firstImageDeferred = $.Deferred(),
secondImageDeferred = $.Deferred(), secondImageDeferred = $.Deferred(),
firstLigthboxInfoDeferred = $.Deferred(), firstLigthboxInfoDeferred = $.Deferred(),
secondLigthboxInfoDeferred = $.Deferred(); secondLigthboxInfoDeferred = $.Deferred();
viewer.preloadFullscreenThumbnail = $.noop; viewer.preloadFullscreenThumbnail = $.noop;
viewer.fetchSizeIndependentLightboxInfo = function () { return ligthboxInfoDeferred.promise(); }; viewer.fetchSizeIndependentLightboxInfo = this.sandbox.stub();
viewer.ui = { viewer.ui = {
setupForLoad : $.noop, setupForLoad : $.noop,
canvas : { set : $.noop, canvas : { set : $.noop,
getCurrentImageWidths : function () { return { real : 0 }; } }, getCurrentImageWidths : function () { return { real : 0 }; } },
panel : { setImageInfo : function () { panel : {
assert.ok( false, 'Metadata of the first image should not be shown' ); setImageInfo : this.sandbox.stub(),
},
progressBar: { progressBar: {
percent : function ( response, percent ) { animateTo : this.sandbox.stub(),
if ( percent === 45 ) { jumpTo : this.sandbox.stub()
assert.ok( false, 'Progress of the first image should not be shown' );
}
}
}, },
empty: $.noop, empty: $.noop,
animateMetadataOnce: $.noop animateMetadataOnce: $.noop
}, },
open : $.noop, open : $.noop,
empty: $.noop }; empty: $.noop };
viewer.displayRealThumbnail = this.sandbox.stub();
viewer.eachPrealoadableLightboxIndex = $.noop; viewer.eachPrealoadableLightboxIndex = $.noop;
viewer.animateMetadataDivOnce = function () { viewer.animateMetadataDivOnce = this.sandbox.stub().returns( $.Deferred().reject() );
assert.ok( false, 'Metadata of the first image should not be animated' ); viewer.imageProvider.get = this.sandbox.stub();
return $.Deferred().reject();
};
viewer.imageProvider.get = function() { return imageDeferred.promise(); };
viewer.imageInfoProvider.get = function() { return $.Deferred().reject(); }; viewer.imageInfoProvider.get = function() { return $.Deferred().reject(); };
viewer.thumbnailInfoProvider.get = function() { return $.Deferred().resolve( {} ); }; viewer.thumbnailInfoProvider.get = function() { return $.Deferred().resolve( {} ); };
imageDeferred = firstImageDeferred; viewer.imageProvider.get.returns( firstImageDeferred.promise() );
ligthboxInfoDeferred = firstLigthboxInfoDeferred; viewer.fetchSizeIndependentLightboxInfo.returns( firstLigthboxInfoDeferred.promise() );
viewer.loadImage( { filePageTitle : new mw.Title( 'File:Foo.jpg' ), index : 0 }, new Image() ); viewer.loadImage( { filePageTitle : new mw.Title( 'File:Foo.jpg' ), index : 0 }, 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' );
imageDeferred = secondImageDeferred; viewer.imageProvider.get.returns( secondImageDeferred.promise() );
ligthboxInfoDeferred = secondLigthboxInfoDeferred; viewer.fetchSizeIndependentLightboxInfo.returns( secondLigthboxInfoDeferred.promise() );
viewer.loadImage( { filePageTitle : new mw.Title( 'File:Bar.jpg' ), index : 1 }, new Image() ); viewer.loadImage( { filePageTitle : new mw.Title( 'File:Bar.jpg' ), index : 1 }, new Image() );
viewer.displayRealThumbnail = function () { viewer.ui.panel.progressBar.animateTo.reset();
assert.ok( false, 'The first image being done loading should have no effect');
};
firstImageDeferred.notify( undefined, 45 ); firstImageDeferred.notify( undefined, 45 );
assert.ok( !viewer.ui.panel.progressBar.animateTo.reset.called, 'Progress of the first image should not be shown' );
firstImageDeferred.resolve(); firstImageDeferred.resolve();
firstLigthboxInfoDeferred.resolve(); firstLigthboxInfoDeferred.resolve();
assert.ok( !viewer.displayRealThumbnail.called, 'The first image being done loading should have no effect');
viewer.ui.panel.setImageInfo = $.noop; viewer.displayRealThumbnail = this.sandbox.spy( function () { viewer.close(); } );
viewer.animateMetadataDivOnce = function() { return $.Deferred().reject(); };
viewer.displayRealThumbnail = function () {
assert.ok( true, 'The second image being done loading should result in the image being shown');
QUnit.start();
viewer.close();
};
QUnit.stop();
secondImageDeferred.resolve(); secondImageDeferred.resolve();
secondLigthboxInfoDeferred.resolve(); secondLigthboxInfoDeferred.resolve();
assert.ok( viewer.displayRealThumbnail.called, 'The second image being done loading should result in the image being shown');
} ); } );
QUnit.test( 'Events are not trapped after the viewer is closed', 0, function( assert ) { QUnit.test( 'Events are not trapped after the viewer is closed', 0, function( assert ) {

View file

@ -287,7 +287,7 @@
canvas.$image = $( '<img>' ); canvas.$image = $( '<img>' );
canvas.unblur(); canvas.unblurWithAnimation();
assert.ok( ! canvas.$image.css( '-webkit-filter' ) || !canvas.$image.css( '-webkit-filter' ).length, assert.ok( ! canvas.$image.css( '-webkit-filter' ) || !canvas.$image.css( '-webkit-filter' ).length,
'Image has no filter left' ); 'Image has no filter left' );

View file

@ -24,7 +24,7 @@
assert.ok( progressBar.$progress.hasClass( 'empty' ), 'ProgressBar starts empty' ); assert.ok( progressBar.$progress.hasClass( 'empty' ), 'ProgressBar starts empty' );
} ); } );
QUnit.test( 'Progress bar', 11, function ( assert ) { QUnit.test( 'animateTo()', 8, function ( assert ) {
var $qf = $( '#qunit-fixture' ), var $qf = $( '#qunit-fixture' ),
progress = new mw.mmv.ui.ProgressBar( $qf ); progress = new mw.mmv.ui.ProgressBar( $qf );
@ -35,17 +35,10 @@
$( this ).css( target ); $( this ).css( target );
assert.strictEqual( target.width, '50%', 'Animation should go to 50%' ); assert.strictEqual( target.width, '50%', 'Animation should go to 50%' );
} ); } );
progress.percent( 50 ); progress.animateTo( 50 );
assert.ok( !progress.$progress.hasClass( 'empty' ), 'Progress bar is visible' ); assert.ok( !progress.$progress.hasClass( 'empty' ), 'Progress bar is visible' );
assert.strictEqual( progress.$percent.width(), $qf.width() / 2, 'Progress bar\'s indicator is at half' ); assert.strictEqual( progress.$percent.width(), $qf.width() / 2, 'Progress bar\'s indicator is at half' );
$.fn.animate.restore();
this.sandbox.stub( $.fn, 'animate' );
progress.percent( 0 );
assert.ok( !$.fn.animate.called, 'Going to 0% should not animate' );
assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' );
assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' );
$.fn.animate.restore(); $.fn.animate.restore();
this.sandbox.stub( $.fn, 'animate', function ( target, duration, transition, callback ) { this.sandbox.stub( $.fn, 'animate', function ( target, duration, transition, callback ) {
$( this ).css( target ); $( this ).css( target );
@ -56,7 +49,25 @@
callback(); callback();
} }
} ); } );
progress.percent( 100 ); progress.animateTo( 100 );
assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' );
assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' );
} );
QUnit.test( 'jumpTo()/hide()', 6, function ( assert ) {
var $qf = $( '#qunit-fixture' ),
progress = new mw.mmv.ui.ProgressBar( $qf );
assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' );
assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' );
progress.jumpTo( 50 );
assert.ok( !progress.$progress.hasClass( 'empty' ), 'Progress bar is visible' );
assert.strictEqual( progress.$percent.width(), $qf.width() / 2, 'Progress bar\'s indicator is at half' );
progress.hide();
assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' ); assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' );
assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' ); assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' );
} ); } );