diff --git a/resources/mmv/img/blur.svg b/resources/mmv/img/blur.svg new file mode 100644 index 000000000..f422ea932 --- /dev/null +++ b/resources/mmv/img/blur.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/resources/mmv/mmv.js b/resources/mmv/mmv.js index 5dc9cff9e..ac40e61e1 100755 --- a/resources/mmv/mmv.js +++ b/resources/mmv/mmv.js @@ -188,7 +188,7 @@ */ MMVP.setImage = function ( ui, thumbnail, image, imageWidths ) { // we downscale larger images but do not scale up smaller ones, that would look ugly - if ( thumbnail.width > imageWidths.screen ) { + if ( thumbnail.width > imageWidths.css ) { image.width = imageWidths.css; } @@ -205,7 +205,9 @@ var imageWidths, viewer = this, imagePromise, - metadataPromise; + metadataPromise, + start, + $initialImage = $( initialImage ); // FIXME dependency injection happens in completely random order and location, needs cleanup this.ui = this.lightbox.iface; @@ -223,7 +225,13 @@ this.lightbox.iface.empty(); } this.lightbox.iface.setupForLoad(); - this.lightbox.iface.showImage( image, initialImage ); + + // At this point we can't show the thumbnail because we don't + // know what size it should be. We still assign it to allow for + // size calculations in getCurrentImageWidths, which needs to know + // the aspect ratio + $initialImage.hide(); + this.lightbox.iface.showImage( image, $initialImage ); this.preloadImagesMetadata(); this.preloadThumbnails(); @@ -232,11 +240,23 @@ $( document.body ).addClass( 'mw-mlb-lightbox-open' ); imageWidths = this.ui.getCurrentImageWidths(); + + this.resetBlurredThumbnailStates(); + + start = $.now(); + imagePromise = this.fetchThumbnail( image.filePageTitle, imageWidths.real - ).done( function( thumbnail, image ) { - viewer.setImage( viewer.lightbox.iface, thumbnail, image, imageWidths ); - viewer.lightbox.iface.$imageDiv.removeClass( 'empty' ); + ).progress( function ( thumbnailInfoResponse, imageResponse ) { + if ( viewer.ui && viewer.ui.panel ) { + viewer.ui.panel.percent( imageResponse[ 1 ] ); + } + } ).done( function ( thumbnail, image ) { + viewer.displayRealThumbnail( thumbnail, image, imageWidths, $.now() - start ); + } ); + + this.imageInfoProvider.get( image.filePageTitle ).done( function ( imageInfo ) { + viewer.displayPlaceholderThumbnail( imageInfo, image, $initialImage, imageWidths ); } ); metadataPromise = this.fetchSizeIndependentLightboxInfo( @@ -278,6 +298,81 @@ } ); }; + /** + * Resets the cross-request states needed to handle the blurred thumbnail logic + */ + MMVP.resetBlurredThumbnailStates = function () { + this.realThumbnailShown = false; + this.blurredThumbnailShown = false; + }; + + /** + * Display the real, full-resolution, thumbnail that was fetched with fetchThumbnail + * @param {mw.mmv.model.Thumbnail} thumbnail + * @param {HTMLImageElement} image + * @param {mw.mmv.model.ThumbnailWidth} imageWidths + * @param {number} loadTime Time it took to load the thumbnail + */ + MMVP.displayRealThumbnail = function ( thumbnail, image, imageWidths, loadTime ) { + this.realThumbnailShown = true; + + this.setImage( this.lightbox.iface, thumbnail, image, imageWidths ); + + // We only animate unblur if the image wasn't loaded from the cache + // 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 ) { + this.lightbox.iface.unblur(); + } + }; + + /** + * Display the blurred thumbnail from the page + * @param {mw.mmv.model.Image} imageInfo + * @param {HTMLImageElement} image + * @param {jQuery} $initialImage The thumbnail from the page + * @param {mw.mmv.model.ThumbnailWidth} imageWidths + */ + MMVP.displayPlaceholderThumbnail = function ( imageInfo, image, $initialImage, imageWidths ) { + var ratio, + targetWidth, + targetHeight, + blowupFactor; + + // If the actual image has already been displayed, there's no point showing the blurry one + if ( this.realThumbnailShown ) { + return; + } + + // If the image is smaller than the screen we need to adjust the placeholder's size + if ( imageInfo.width < imageWidths.css ) { + targetWidth = imageInfo.width; + targetHeight = imageInfo.height; + } else { + ratio = $initialImage.height() / $initialImage.width(); + targetWidth = imageWidths.css; + targetHeight = Math.round( imageWidths.css * ratio ); + } + + blowupFactor = targetWidth / $initialImage.width(); + + // If the placeholder is too blown up, it's not worth showing it + if ( blowupFactor > 11 ) { + return; + } + + $initialImage.width( targetWidth ); + $initialImage.height( targetHeight ); + + // Only blur the placeholder if it's blown up significantly + if ( blowupFactor > 2 ) { + $initialImage.addClass( 'blurred' ); + this.blurredThumbnailShown = true; + } + + this.lightbox.iface.showImage( image, $initialImage.show() ); + }; + /** * Preload this many prev/next images to speed up navigation. * (E.g. preloadDistance = 3 means that the previous 3 and the next 3 images will be loaded.) @@ -502,6 +597,7 @@ imagePromise; thumbnailPromise = this.thumbnailInfoProvider.get( fileTitle, width ); + imagePromise = thumbnailPromise.then( function( thumbnail ) { return viewer.imageProvider.get( thumbnail.url ); } ); diff --git a/resources/mmv/mmv.less b/resources/mmv/mmv.less index 173565c45..2cb3c184e 100644 --- a/resources/mmv/mmv.less +++ b/resources/mmv/mmv.less @@ -21,6 +21,7 @@ @drag-height: 18px; @bottom-height: (@title-height + @drag-height); @metadata-background: rgb(251, 251, 251); +@progress-height: 3px; .mlb-image-wrapper, .mw-mlb-controls { @@ -307,6 +308,17 @@ body.mobile .mw-mlb-controls, display: none; } +.mlb-image img.blurred { + /** + * SVG is for Firefox + * Cannot be embedded because of the hash needed to access the filter inside the SVG + */ + filter: url(img/blur.svg#gaussian-blur); + filter: blur(3px); + -webkit-filter: blur(3px); + opacity: 0.8; +} + body.mw-mlb-lightbox-open { overflow-y: auto; } @@ -351,3 +363,20 @@ body.mw-mlb-lightbox-open #content { .mlb-post-image:hover .mw-mlb-drag-icon { opacity: 1; } + +.mw-mlb-progress { + width: 100%; + height: @progress-height; + background-color: rgb( 204, 204, 204 ); + margin-top: -@progress-height; +} + +.mw-mlb-progress.empty { + display: none; +} + +.mw-mlb-progress-percent { + width: 0; + height: @progress-height; + background-color: rgb( 0, 113, 188 ); +} diff --git a/resources/mmv/mmv.lightboxinterface.js b/resources/mmv/mmv.lightboxinterface.js index 430b3724c..40cc45270 100644 --- a/resources/mmv/mmv.lightboxinterface.js +++ b/resources/mmv/mmv.lightboxinterface.js @@ -107,15 +107,6 @@ this.panel = new mw.mmv.ui.MetadataPanel( this.$postDiv, this.$controlBar ); this.buttons = new mw.mmv.ui.Buttons( this.$imageWrapper, this.$closeButton, this.$fullscreenButton ); - this.initializeImage(); - }; - - /** - * Initialize the image element. - */ - LIP.initializeImage = function () { - this.$imageDiv - .addClass( 'empty' ); }; /** @@ -126,7 +117,6 @@ this.panel.empty(); - this.$imageDiv.addClass( 'empty' ); this.$imageDiv.empty(); if ( this.resizeListener ) { @@ -266,15 +256,17 @@ * Displays an already loaded image. * This is an alternative to load() when we have an image element with the image already loaded. * @param {mw.mmv.LightboxImage} image - * @param {HTMLImageElement } imageElement + * @param {jQuery} $imageElement */ - LIP.showImage = function( image, imageElement ) { + LIP.showImage = function( image, $imageElement ) { var iface = this; this.currentImage = image; - image.globalMaxWidth = imageElement.width; - image.globalMaxHeight = imageElement.height; - this.$image = $( imageElement ); + + image.globalMaxWidth = $imageElement.width(); + image.globalMaxHeight = $imageElement.height(); + + this.$image = $imageElement; this.autoResizeImage(); @@ -299,7 +291,7 @@ this.currentImage = image; image.getImageElement().done( function( image, ele ) { - iface.showImage( image, ele ); + iface.showImage( image, $( ele ) ); } ); }; @@ -348,9 +340,8 @@ var $image = $( imageEle ); - this.currentImage.src = imageEle.src; - this.$image.replaceWith( $image ); + this.currentImage.src = imageEle.src; this.$image = $image; this.$image.css( { @@ -449,6 +440,36 @@ } }; + /** + * Animates the image into focus + */ + LIP.unblur = function () { + var self = this, + animationLength = 300; + + // The blurred class has an opacity < 1. This animated the image to become fully opaque + this.$image + .addClass( 'blurred' ) + .animate( { opacity: 1.0 }, animationLength ); + + // During the same amount of time (animationLength) we animate a blur value from 3.0 to 0.0 + // We pass that value to an inline CSS Gaussian blur effect + $( { blur: 3.0 } ).animate( { blur: 0.0 }, { + duration: animationLength, + step: function ( step ) { + self.$image.css( { '-webkit-filter' : 'blur(' + step + 'px)', + 'filter' : 'blur(' + step + 'px)' } ); + }, + complete: function () { + // When the animation is complete, the blur value is 0 + // 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 + self.$image.css( { '-webkit-filter' : '', 'opacity' : '' } ) + .removeClass( 'blurred' ); + } + } ); + }; + /** * Handle a fullscreen change event. * @param {jQuery.Event} e The fullscreen change event. diff --git a/resources/mmv/mmv.performance.js b/resources/mmv/mmv.performance.js index a6b6403b5..6b67630e8 100755 --- a/resources/mmv/mmv.performance.js +++ b/resources/mmv/mmv.performance.js @@ -57,10 +57,22 @@ try { request = this.newXHR(); + + request.onprogress = function ( e ) { + var percent; + + if ( e.lengthComputable ) { + percent = ( e.loaded / e.total ) * 100; + } + + deferred.notify( request.response, percent ); + }; + request.onreadystatechange = function () { var total = $.now() - start; if ( request.readyState === 4 ) { + deferred.notify( request.response, 100 ); deferred.resolve( request.response ); perf.recordEntryDelayed( type, total, url, request ); } diff --git a/resources/mmv/ui/mmv.ui.metadataPanel.js b/resources/mmv/ui/mmv.ui.metadataPanel.js index cdcab52c2..d8b0cd8d1 100644 --- a/resources/mmv/ui/mmv.ui.metadataPanel.js +++ b/resources/mmv/ui/mmv.ui.metadataPanel.js @@ -77,6 +77,8 @@ this.$dragIcon.removeClass( 'pointing-down' ); + this.$progress.addClass( 'empty' ); + this.fileReuse.empty(); }; @@ -90,6 +92,8 @@ MPP.initializeHeader = function () { var panel = this; + this.initializeProgress(); + this.$dragBar = $( '
' ) .addClass( 'mw-mlb-drag-affordance' ) .appendTo( this.$controlBar ) @@ -322,6 +326,19 @@ .appendTo( this.$imageMetadata ); }; + /** + * Initializes the progress display at the top of the panel. + */ + MPP.initializeProgress = function () { + this.$progress = $( '
' ) + .addClass( 'mw-mlb-progress empty' ) + .appendTo( this.$controlBar ); + + this.$percent = $( '
' ) + .addClass( 'mw-mlb-progress-percent' ) + .appendTo( this.$progress ); + }; + // ********************************* // ******** Setting methods ******** // ********************************* @@ -716,5 +733,19 @@ } }; + /** + * Handles the progress display when a percentage of progress is received + * @param {number} percent + */ + MPP.percent = function ( percent ) { + if ( percent < 100 ) { + this.$percent.animate( { width : percent + '%' } ); + this.$progress.removeClass( 'empty' ); + } else { + this.$percent.css( { width : 0 } ); + this.$progress.addClass( 'empty' ); + } + }; + mw.mmv.ui.MetadataPanel = MetadataPanel; }( mediaWiki, jQuery, OO, moment ) ); diff --git a/tests/qunit/mmv/mmv.lightboxinterface.test.js b/tests/qunit/mmv/mmv.lightboxinterface.test.js index b5c70f7a8..a71cd9e6e 100644 --- a/tests/qunit/mmv/mmv.lightboxinterface.test.js +++ b/tests/qunit/mmv/mmv.lightboxinterface.test.js @@ -62,9 +62,9 @@ lightbox.originalShowImage = lightbox.showImage; // Mock showImage - lightbox.showImage = function ( image, ele ) { + lightbox.showImage = function ( image, $ele ) { // Call original showImage - this.originalShowImage( image, ele ); + this.originalShowImage( image, $ele ); // resizeListener should have been saved assert.notStrictEqual( this.resizeListener, undefined, 'Saved listener !' ); @@ -316,7 +316,8 @@ keydown.which = 38; // Up arrow $document.trigger( keydown ); - assert.strictEqual( Math.round( $.scrollTo().scrollTop() ), lightbox.panel.$imageMetadata.height() + 1, + assert.strictEqual( Math.round( $.scrollTo().scrollTop() ), + lightbox.panel.$imageMetadata.height() + 1, 'scrollTo scrollTop should be set to the metadata height + 1 after pressing up arrow' ); assert.ok( lightbox.panel.$dragIcon.hasClass( 'pointing-down' ), 'Chevron pointing down after pressing up arrow' ); @@ -333,7 +334,8 @@ lightbox.panel.$dragIcon.click(); - assert.strictEqual( Math.round( $.scrollTo().scrollTop() ), lightbox.panel.$imageMetadata.height() + 1, + assert.strictEqual( Math.round( $.scrollTo().scrollTop() ), + lightbox.panel.$imageMetadata.height() + 1, 'scrollTo scrollTop should be set to the metadata height + 1 after clicking the chevron once' ); assert.ok( lightbox.panel.$dragIcon.hasClass( 'pointing-down' ), 'Chevron pointing down after clicking the chevron once' ); @@ -385,4 +387,44 @@ mw.mmv.mediaViewer.lightbox = oldMWLightbox; mw.mmv.mediaViewer.ui = oldMWUI; } ); + + QUnit.test( 'Unblur', 3, function ( assert ) { + var lightbox = new mw.mmv.LightboxInterface( mw.mediaViewer ), + oldAnimate = $.fn.animate; + + $.fn.animate = function ( target, options ) { + var self = this, + lastValue; + + $.each( target, function ( key, value ) { + lastValue = self.key = value; + } ); + + if ( options ) { + if ( options.step ) { + options.step.call( this, lastValue ); + } + + if ( options.complete ) { + options.complete.call( this ); + } + } + }; + + // Attach lightbox to testing fixture to avoid interference with other tests. + lightbox.attach( '#qunit-fixture' ); + lightbox.$image = $( '' ); + + lightbox.unblur(); + + assert.ok( !lightbox.$image.css( '-webkit-filter' ) || !lightbox.$image.css( '-webkit-filter' ).length, + 'Image has no filter left' ); + assert.strictEqual( parseInt( lightbox.$image.css( 'opacity' ), 10 ), 1, + 'Image is fully opaque' ); + assert.ok( !lightbox.$image.hasClass( 'blurred' ), 'Image has no "blurred" class' ); + + lightbox.unattach(); + + $.fn.animate = oldAnimate; + } ); }( mediaWiki, jQuery ) ); diff --git a/tests/qunit/mmv/mmv.test.js b/tests/qunit/mmv/mmv.test.js index 38235cee8..229640660 100644 --- a/tests/qunit/mmv/mmv.test.js +++ b/tests/qunit/mmv/mmv.test.js @@ -152,4 +152,228 @@ window.location.hash = ''; } ); + + QUnit.test( 'Progress', 1, function ( assert ) { + var imageDeferred = $.Deferred(), + viewer = new mw.mmv.MultimediaViewer(), + oldImageGet = mw.mmv.provider.Image.prototype.get, + oldImageInfoGet = mw.mmv.provider.ImageInfo.prototype.get, + oldThumbnailInfoGet = mw.mmv.provider.ThumbnailInfo.prototype.get; + + viewer.thumbs = []; + viewer.displayPlaceholderThumbnail = $.noop; + viewer.setImage = $.noop; + viewer.scroll = $.noop; + viewer.preloadFullscreenThumbnail = $.noop; + viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve(); }; + viewer.lightbox = { iface : { + setupForLoad : $.noop, + showImage : $.noop, + getCurrentImageWidths : function () { return { real : 0 }; }, + panel : { setImageInfo : $.noop, + percent : function ( percent ) { + assert.strictEqual( percent, 45, + 'Percentage correctly funneled to panel UI' ); + } } + }, + open : $.noop }; + + mw.mmv.provider.Image.prototype.get = function() { return imageDeferred.promise(); }; + mw.mmv.provider.ImageInfo.prototype.get = function() { return $.Deferred().resolve(); }; + mw.mmv.provider.ThumbnailInfo.prototype.get = function() { return $.Deferred().resolve( {} ); }; + + viewer.loadImage( { filePageTitle : new mw.Title( 'File:Stuff.jpg' ) }, new Image() ); + + imageDeferred.notify( 'response', 45 ); + imageDeferred.resolve(); + + viewer.close(); + + mw.mmv.provider.Image.prototype.get = oldImageGet; + mw.mmv.provider.ImageInfo.prototype.get = oldImageInfoGet; + mw.mmv.provider.ThumbnailInfo.prototype.get = oldThumbnailInfoGet; + } ); + + QUnit.test( 'resetBlurredThumbnailStates', 4, function ( assert ) { + var viewer = new mw.mmv.MultimediaViewer(); + + assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + + viewer.realThumbnailShown = true; + viewer.blurredThumbnailShown = true; + + viewer.resetBlurredThumbnailStates(); + + assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'Placeholder first, then real thumbnail', 4, function ( assert ) { + var viewer = new mw.mmv.MultimediaViewer(); + + viewer.setImage = $.noop; + viewer.lightbox = { iface : { + showImage : $.noop + } }; + + viewer.displayPlaceholderThumbnail( + { width : 300 }, + undefined, + $( '' ).width( 100 ), + { css : 300, real : 300 } + ); + + assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + + viewer.displayRealThumbnail(); + + assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'Real thumbnail first, then placeholder', 4, function ( assert ) { + var viewer = new mw.mmv.MultimediaViewer(); + + viewer.setImage = $.noop; + viewer.lightbox = { iface : { + showImage : $.noop + } }; + + viewer.displayRealThumbnail(); + + assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + + viewer.displayPlaceholderThumbnail( + { width : 300 }, + undefined, + $( '' ).width( 100 ), + { css : 300, real : 300 } + ); + + assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'displayRealThumbnail', 1, function ( assert ) { + var viewer = new mw.mmv.MultimediaViewer(); + + viewer.setImage = $.noop; + viewer.lightbox = { iface : { + unblur : function () { assert.ok( false, 'Image should not be unblurred yet' ); } + } }; + viewer.blurredThumbnailShown = true; + + // Should not result in an unblur (image cache from cache) + viewer.displayRealThumbnail( undefined, undefined, undefined, 5 ); + + viewer.lightbox.iface.unblur = function () { + 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 ); + } ); + + QUnit.test( 'displayPlaceholderThumbnail: placeholder big enough that it doesn\'t need blurring, actual image bigger than the lightbox', 5, function ( assert ) { + var $image, + viewer = new mw.mmv.MultimediaViewer(); + + viewer.setImage = $.noop; + viewer.lightbox = { iface : { + showImage : function () { assert.ok ( true, 'Placeholder shown'); } + } }; + + $image = $( '' ).width( 200 ).height( 100 ); + + viewer.displayPlaceholderThumbnail( + { width : 1000, height : 500 }, + undefined, + $image, + { css : 300, real : 300 } + ); + + assert.strictEqual( $image.width(), 300, 'Placeholder has the right width' ); + assert.strictEqual( $image.height(), 150, 'Placeholder has the right height' ); + assert.ok( !$image.hasClass( 'blurred' ), 'Placeholder is not blurred' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'displayPlaceholderThumbnail: big-enough placeholder that needs blurring, actual image bigger than the lightbox', 5, function ( assert ) { + var $image, + viewer = new mw.mmv.MultimediaViewer(); + + viewer.setImage = $.noop; + viewer.lightbox = { iface : { + showImage : function () { assert.ok ( true, 'Placeholder shown'); } + } }; + + $image = $( '' ).width( 100 ).height( 50 ); + + viewer.displayPlaceholderThumbnail( + { width : 1000, height : 500 }, + undefined, + $image, + { css : 300, real : 300 } + ); + + assert.strictEqual( $image.width(), 300, 'Placeholder has the right width' ); + assert.strictEqual( $image.height(), 150, 'Placeholder has the right height' ); + assert.ok( $image.hasClass( 'blurred' ), 'Placeholder is blurred' ); + assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); + + QUnit.test( 'displayPlaceholderThumbnail: big-enough placeholder that needs blurring, actual image smaller than the lightbox', 5, function ( assert ) { + var $image, + viewer = new mw.mmv.MultimediaViewer(), + oldDevicePixelRatio = $.devicePixelRatio; + + $.devicePixelRatio = function () { return 2; }; + + viewer.setImage = $.noop; + viewer.lightbox = { iface : { + showImage : function () { assert.ok ( true, 'Placeholder shown'); } + } }; + + $image = $( '' ).width( 100 ).height( 50 ); + + viewer.displayPlaceholderThumbnail( + { width : 1000, height : 500 }, + undefined, + $image, + { css : 1200, real : 1200 } + ); + + assert.strictEqual( $image.width(), 1000, 'Placeholder has the right width' ); + assert.strictEqual( $image.height(), 500, 'Placeholder has the right height' ); + assert.ok( $image.hasClass( 'blurred' ), 'Placeholder is blurred' ); + assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + + $.devicePixelRatio = oldDevicePixelRatio; + } ); + + QUnit.test( 'displayPlaceholderThumbnail: placeholder too small to be displayed, actual image bigger than the lightbox', 4, function ( assert ) { + var $image, + viewer = new mw.mmv.MultimediaViewer(); + + viewer.lightbox = { iface : { + showImage : function () { assert.ok ( false, 'Placeholder shown when it should not'); } + } }; + + $image = $( '' ).width( 10 ).height( 5 ); + + viewer.displayPlaceholderThumbnail( + { width : 1000, height : 500 }, + undefined, + $image, + { css : 300, real : 300 } + ); + + assert.strictEqual( $image.width(), 10, 'Placeholder has the right width' ); + assert.strictEqual( $image.height(), 5, 'Placeholder has the right height' ); + assert.ok( !$image.hasClass( 'blurred' ), 'Placeholder is not blurred' ); + assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); + } ); }( mediaWiki, jQuery ) ); diff --git a/tests/qunit/mmv/provider/mmv.provider.Image.test.js b/tests/qunit/mmv/provider/mmv.provider.Image.test.js index 3afed08b1..cd0359f65 100644 --- a/tests/qunit/mmv/provider/mmv.provider.Image.test.js +++ b/tests/qunit/mmv/provider/mmv.provider.Image.test.js @@ -24,7 +24,7 @@ assert.ok( imageProvider ); } ); - QUnit.test( 'Image load success test', 2, function ( assert ) { + QUnit.test( 'Image load success', 2, function ( assert ) { var url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0' + 'iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH' + '8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC', @@ -42,7 +42,7 @@ } ); } ); - QUnit.test( 'Image caching test', 6, function ( assert ) { + QUnit.test( 'Image caching', 6, function ( assert ) { var url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0' + 'iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH' + '8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC', @@ -75,10 +75,78 @@ assert.strictEqual( image.src, url2, 'image src is correct'); QUnit.start(); } ); - } ); - QUnit.asyncTest( 'Image load fail test', 1, function ( assert ) { + QUnit.test( 'Image load XHR progress funneling', 7, function ( assert ) { + var i = 0, + imageProvider = new mw.mmv.provider.Image(), + oldPerformance = imageProvider.performance, + fakeURL = 'fakeURL', + response = 'response'; + + imageProvider.performance.delay = 0; + imageProvider.imagePreloadingSupported = function () { return true; }; + imageProvider.rawGet = function () { return $.Deferred().resolve(); }; + + imageProvider.performance.newXHR = function () { + return { readyState: 4, + response: response, + send: function () { + var self = this; + + // The timeout is necessary because without it notify() happens before + // the imageProvider has time to chain its progress() to the returned deferred + setTimeout( function () { + self.onprogress( { lengthComputable: true, loaded : 10, total : 20 } ); + self.onreadystatechange(); + } ); + }, + + open: $.noop }; + }; + + QUnit.stop(); + + imageProvider.performance.recordEntry = function ( type, total, url ) { + QUnit.start(); + + assert.strictEqual( type, 'image', 'Type matches' ); + assert.strictEqual( url, fakeURL, 'URL matches' ); + + imageProvider.performance = oldPerformance; + + return $.Deferred().resolve(); + }; + + QUnit.stop(); + + imageProvider.get( fakeURL ) + .fail( function () { + QUnit.start(); + + assert.ok( false, 'Image failed to (pretend to) load' ); + } ) + .then( function () { + QUnit.start(); + + assert.ok( true, 'Image was pretend-loaded' ); + } ) + .progress( function ( response, percent ) { + if ( i === 0 ) { + assert.strictEqual( percent, 50, 'Correctly propagated a 50% progress event' ); + assert.strictEqual( response, response, 'Partial response propagated' ); + } else if ( i === 1 ) { + assert.strictEqual( percent, 100, 'Correctly propagated a 100% progress event' ); + assert.strictEqual( response, response, 'Partial response propagated' ); + } else { + assert.ok( false, 'Only 2 progress events should propagate' ); + } + + i++; + } ); + } ); + + QUnit.asyncTest( 'Image load fail', 1, function ( assert ) { var imageProvider = new mw.mmv.provider.Image(); imageProvider.imagePreloadingSupported = function () { return false; }; @@ -90,7 +158,7 @@ } ); } ); - QUnit.test( 'Image load test with preloading supported', 1, function ( assert ) { + QUnit.test( 'Image load with preloading supported', 1, function ( assert ) { var url = mw.config.get( 'wgScriptPath' ) + '/skins/vector/images/search-ltr.png', imageProvider = new mw.mmv.provider.Image(), endsWith = function ( a, b ) { return a.indexOf( b ) === a.length - b.length; }; @@ -112,7 +180,7 @@ } ); } ); - QUnit.test( 'Failed image load test with preloading supported', 1, function ( assert ) { + QUnit.test( 'Failed image load with preloading supported', 1, function ( assert ) { var url = 'nosuchimage.png', imageProvider = new mw.mmv.provider.Image(); diff --git a/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js b/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js index 4f79a81f4..e8ae8d029 100644 --- a/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js +++ b/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js @@ -15,7 +15,8 @@ '$usernameLi', '$locationLi', '$repoLi', - '$datetimeLi' + '$datetimeLi', + '$progress' ]; QUnit.module( 'mmv.ui.metadataPanel', QUnit.newMwEnvironment() ); @@ -193,4 +194,34 @@ assert.strictEqual( result, date1, 'Invalid date is correctly ignored' ); } ); + + QUnit.test( 'Progress bar', 8, function ( assert ) { + var $qf = $( '#qunit-fixture' ), + panel = new mw.mmv.ui.MetadataPanel( $qf, $( '
' ).appendTo( $qf ) ), + oldAnimate = $.fn.animate; + + $.fn.animate = function ( target ) { + $( this ).css( target ); + }; + + assert.ok( panel.$progress.hasClass( 'empty' ), 'Progress bar is hidden' ); + assert.strictEqual( panel.$percent.width(), 0, 'Progress bar\'s indicator is at 0' ); + + panel.percent( 0 ); + + assert.ok( !panel.$progress.hasClass( 'empty' ), 'Progress bar is visible' ); + assert.strictEqual( panel.$percent.width(), 0, 'Progress bar\'s indicator is at 0' ); + + panel.percent( 50 ); + + assert.ok( !panel.$progress.hasClass( 'empty' ), 'Progress bar is visible' ); + assert.strictEqual( panel.$percent.width(), $qf.width() / 2, 'Progress bar\'s indicator is at half' ); + + panel.percent( 100 ); + + assert.ok( panel.$progress.hasClass( 'empty' ), 'Progress bar is hidden' ); + assert.strictEqual( panel.$percent.width(), 0, 'Progress bar\'s indicator is at 0' ); + + $.fn.animate = oldAnimate; + } ); }( mediaWiki, jQuery ) );