Refactor progressbar & blur handling

This tries to fix a number of related issues:
* the blurred thumbnail was visible for a split-second sometimes
  when switching back to an already-loaded image. (Presumably when
  JS was sluggish enough to take more than 10 ms to execute.) We
  now check whether the promise is pending before showing a placeholder.
  (More generally, a lot of unnecessary logic was executed when paging
  through already loaded images, like displaying the placeholder, so
  this might make the UI a bit more responsive.)
* the blur could get stuck sometimes - I have seen this a few times,
  but have never been able to reproduce it, so I'm only guessing, but
  maybe the timing was really unfortunate, and we switched back less
  than 10 ms before loading finished. We now remove the blur on every
  branch, just to be sure.
* adding a progress handler to a promise might not have any immediate
  effect, so when switching to an image which was loading, the progress
  bar reacted too late. We now store the progress state per thumbnail
  so it is always available immediately.
* the progress would animate from 0 to its actual state whenever we
  navigated to the image. The change on paging is now instant; the
  progress bar only animates when we are looking at it.
* switching quickly back and forthe between a loaded and a loading
  image resulted in the loading image becoming unblurred. This seems
  fixed now, I'm not sure why. Maybe the "skip on non-pending promise"
  logic affects it somehow.

Also removes some unused things / renames some things which were
confusing, and makes an unrelated fix in the image provider, which kept
amassing fail handlers.

Change-Id: I580becff246f197ec1bc65e82acd422620e35578
Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/489
This commit is contained in:
Gergő Tisza 2014-04-25 22:26:34 +00:00
parent 7ee0cf3fcd
commit fec24e02f7
7 changed files with 315 additions and 160 deletions

View file

@ -220,11 +220,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();
}; };
@ -243,7 +243,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 ) {
@ -267,15 +266,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 ) {
@ -338,29 +339,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();
} }
@ -372,13 +396,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;
} }
@ -407,58 +433,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 );
} ); } );
}; };
@ -653,7 +692,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,
@ -696,7 +737,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' );
} ); } );