jenkins-bot has submitted this change and it was merged. Change subject: Fixes hash handling issues ......................................................................
Fixes hash handling issues * Fixes the bug where the browser hash would get nuked on foreign anchor clicks * Moves the hash handling back into the mmv itself * Adds test coverage for the hash nuking bug Change-Id: Iea57d0f4b9090f96e622418223d3f774923e8038 Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/254 --- M resources/mmv/mmv.bootstrap.js M resources/mmv/mmv.js M tests/qunit/mmv.bootstrap.test.js M tests/qunit/mmv.test.js 4 files changed, 204 insertions(+), 160 deletions(-) Approvals: Gergő Tisza: Looks good to me, approved jenkins-bot: Verified diff --git a/resources/mmv/mmv.bootstrap.js b/resources/mmv/mmv.bootstrap.js index 1b69b69..cad5a87 100755 --- a/resources/mmv/mmv.bootstrap.js +++ b/resources/mmv/mmv.bootstrap.js @@ -19,12 +19,12 @@ var bootstrap, MMVB; /** - * Bootstrap code listening to thumb clicks and location.hash + * Bootstrap code listening to thumb clicks checking the initial location.hash * Loads the mmv and opens it if necessary * @class mw.mmv.MultimediaViewerBootstrap */ function MultimediaViewerBootstrap () { - var bs = this; + var self = this; this.validExtensions = { 'jpg' : true, @@ -41,19 +41,12 @@ this.processThumbs(); this.hash(); - $( window ).on( 'popstate.mmv', function() { - bs.hash(); + $( window ).on( 'popstate.mmvb', function () { + self.hash(); } ); } MMVB = MultimediaViewerBootstrap.prototype; - - /** - * Stops the boostrap - */ - MMVB.shutdown = function () { - $( window ).off( 'popstate.mmv' ); - }; /** * Loads the mmv module asynchronously and passes the thumb data to it @@ -166,22 +159,18 @@ }; /** - * Handles the browser's location.hash + * Handles the browser location hash on pageload or popstate */ MMVB.hash = function () { - var hash = decodeURIComponent( document.location.hash ), - linkState = hash.split( '/' ); - - if ( linkState[0] === '#mediaviewer' ) { - this.loadViewer().then( function () { - mw.mediaViewer.loadImageByTitle( linkState[ 1 ] ); - } ); - } else { - // If the hash is invalid (not a mmv hash) we close any open mmv - if ( mw.mediaViewer ) { - mw.mediaViewer.shutdown(); - } + // There is no point loading the mmv if it isn't loaded yet for hash changes unrelated to the mmv + // Such as anchor links on the page + if ( !this.viewerInitialized && window.location.hash.indexOf( '#mediaviewer/') !== 0 ) { + return; } + + this.loadViewer().then( function () { + mw.mediaViewer.hash(); + } ); }; mw.mmv.MultimediaViewerBootstrap = MultimediaViewerBootstrap; diff --git a/resources/mmv/mmv.js b/resources/mmv/mmv.js index 7871a92..23e57e3 100755 --- a/resources/mmv/mmv.js +++ b/resources/mmv/mmv.js @@ -551,28 +551,39 @@ }; /** - * Shuts down the viewer + * Handles a hash change coming from the browser */ - MMVP.shutdown = function () { - if ( this.lightbox && this.lightbox.iface ) { - this.lightbox.iface.unattach(); + MMVP.hash = function () { + var hash = decodeURIComponent( document.location.hash ), + linkState = hash.split( '/' ); + + if ( linkState[0] === '#mediaviewer' ) { + this.loadImageByTitle( linkState[ 1 ] ); + } else if ( this.isOpen ) { + // This allows us to avoid the pushState that normally happens on close + comingFromPopstate = true; + + if ( this.lightbox && this.lightbox.iface ) { + this.lightbox.iface.unattach(); + } else { + this.close(); + } } }; /** - * Registers all event handlers, on the document, for various MMV-specific - * events. + * Registers all event handlers */ MMVP.setupEventHandlers = function () { var viewer = this; - $( document ).on( 'mmv-close', function () { + $( document ).on( 'mmv-close.mmvp', function () { viewer.close(); - } ).on( 'mmv-next', function () { + } ).on( 'mmv-next.mmvp', function () { viewer.nextImage(); - } ).on( 'mmv-prev', function () { + } ).on( 'mmv-prev.mmvp', function () { viewer.prevImage(); - } ).on( 'mmv-resize', function () { + } ).on( 'mmv-resize.mmvp', function () { viewer.resize( viewer.lightbox.iface ); }); }; diff --git a/tests/qunit/mmv.bootstrap.test.js b/tests/qunit/mmv.bootstrap.test.js index 95340d4..a2511f7 100644 --- a/tests/qunit/mmv.bootstrap.test.js +++ b/tests/qunit/mmv.bootstrap.test.js @@ -1,6 +1,4 @@ ( function ( mw, $ ) { - var oldUsing; - QUnit.module( 'mmv.bootstrap', QUnit.newMwEnvironment() ); function createGallery ( imageSrc ) { @@ -22,25 +20,10 @@ return div; } - function setupUsing ( assert ) { - oldUsing = mw.loader.using; - - mw.loader.using = function( module ) { - if ( module === 'mmv' ) { - assert.ok( true, 'mmv requested to be loaded' ); - } - }; - } - - function restoreUsing () { - mw.loader.using = oldUsing; - } - QUnit.test( 'Check viewer invoked when clicking on legit image links', 2, function ( assert ) { // TODO: Is <div class="gallery"><span class="image"><img/></span></div> valid ??? - var div, link, link2, link3, bootstrap; - - setupUsing( assert ); + var div, link, link2, link3, bootstrap, + oldLoadImageByTitle = mw.mediaViewer.loadImageByTitle; // Create gallery with legit link image div = createGallery(); @@ -57,23 +40,30 @@ // Create a new bootstrap object to trigger the DOM scan, etc. bootstrap = new mw.mmv.MultimediaViewerBootstrap(); + mw.mediaViewer.loadImageByTitle = function() { + assert.ok( true, 'Image loaded' ); + }; + // Click on legit link link.trigger( { type : 'click', which : 1 } ); // Click on legit link link2.trigger( { type : 'click', which : 1 } ); + mw.mediaViewer.loadImageByTitle = function() { + assert.ok( false, 'Image should not be loaded' ); + }; + // Click on non-legit link link3.trigger( { type : 'click', which : 1 } ); - bootstrap.shutdown(); - restoreUsing(); + mw.mediaViewer.loadImageByTitle = oldLoadImageByTitle; + bootstrap.hash = $.noop; } ); QUnit.test( 'Skip images with invalid extensions', 0, function ( assert ) { - var div, link, bootstrap; - - setupUsing( assert ); + var div, link, bootstrap, + oldLoadImageByTitle = mw.mediaViewer.loadImageByTitle; // Create gallery with image that has invalid name extension div = createGallery( 'thumb.badext' ); @@ -82,17 +72,20 @@ // Create a new bootstrap object to trigger the DOM scan, etc. bootstrap = new mw.mmv.MultimediaViewerBootstrap(); + mw.mediaViewer.loadImageByTitle = function() { + assert.ok( false, 'Image should not be loaded' ); + }; + // Click on legit link with wrong image extension link.trigger( { type : 'click', which : 1 } ); - bootstrap.shutdown(); - restoreUsing(); + mw.mediaViewer.loadImageByTitle = oldLoadImageByTitle; + bootstrap.hash = $.noop; } ); QUnit.test( 'Accept only left clicks without modifier keys, skip the rest', 1, function ( assert ) { - var $div, $link, bootstrap; - - setupUsing( assert ); + var $div, $link, bootstrap, + oldLoadImageByTitle = mw.mediaViewer.loadImageByTitle; // Create gallery with image that has invalid name extension $div = createGallery(); @@ -102,8 +95,16 @@ $link = $div.find( 'a.image' ); + mw.mediaViewer.loadImageByTitle = function() { + assert.ok( true, 'Image loaded' ); + }; + // Handle valid left click, it should try to load the image $link.trigger( { type : 'click', which : 1 } ); + + mw.mediaViewer.loadImageByTitle = function() { + assert.ok( false, 'Image should not be loaded' ); + }; // Skip Ctrl-left-click, no image is loaded $link.trigger( { type : 'click', which : 1, ctrlKey : true } ); @@ -111,124 +112,88 @@ // Skip invalid right click, no image is loaded $link.trigger( { type : 'click', which : 2 } ); - bootstrap.shutdown(); - restoreUsing(); + mw.mediaViewer.loadImageByTitle = oldLoadImageByTitle; + bootstrap.hash = $.noop; } ); - QUnit.asyncTest( 'Ensure that the correct title is loaded when clicking', 1, function ( assert ) { - // Preload mmv - mw.loader.using( 'mmv', function () { - QUnit.start(); - - var bootstrap, - oldByTitle, - $div = createGallery( 'foo.jpg' ), - $link = $div.find( 'a.image' ); - - oldByTitle = mw.mediaViewer.loadImageByTitle; - - mw.mediaViewer.loadImageByTitle = function ( loadedTitle ) { - assert.strictEqual( loadedTitle, 'File:Foo.jpg', 'Titles are identical' ); - }; - - // Create a new bootstrap object to trigger the DOM scan, etc. - bootstrap = new mw.mmv.MultimediaViewerBootstrap(); - - $link.trigger( { type : 'click', which : 1 } ); - - bootstrap.shutdown(); - mw.mediaViewer.loadImageByTitle = oldByTitle; - } ); - } ); - - QUnit.asyncTest( 'Validate new LightboxImage object has sane constructor parameters', 6, function ( assert ) { - // Preload mmv - mw.loader.using( 'mmv', function () { - QUnit.start(); - - var bootstrap, - $div, - $link, - oldNewImage = mw.mediaViewer.createNewImage, - oldLoadImage = mw.mediaViewer.loadImage, - fname = 'valid', - imgSrc = '/' + fname + '.jpg/300px-' + fname + '.jpg', - imgRegex = new RegExp( imgSrc + '$' ); - - $div = createThumb( imgSrc, 'Blah blah' ); + QUnit.test( 'Ensure that the correct title is loaded when clicking', 1, function ( assert ) { + var bootstrap, + oldByTitle, + $div = createGallery( 'foo.jpg' ), $link = $div.find( 'a.image' ); - // Create a new bootstrap object to trigger the DOM scan, etc. - bootstrap = new mw.mmv.MultimediaViewerBootstrap(); + oldByTitle = mw.mediaViewer.loadImageByTitle; - mw.mediaViewer.loadImage = $.noop; + mw.mediaViewer.loadImageByTitle = function ( loadedTitle ) { + assert.strictEqual( loadedTitle, 'File:Foo.jpg', 'Titles are identical' ); + }; - mw.mediaViewer.createNewImage = function ( fileLink, filePageLink, fileTitle, index, thumb, caption ) { - assert.ok( fileLink.match( imgRegex ), 'Thumbnail URL used in creating new image object' ); - assert.strictEqual( filePageLink, '', 'File page link is sane when creating new image object' ); - assert.strictEqual( fileTitle.title, fname, 'Filename is correct when passed into new image constructor' ); - assert.strictEqual( index, 0, 'The only image we created in the gallery is set at index 0 in the images array' ); - assert.strictEqual( thumb.outerHTML, '<img src="' + imgSrc + '">', 'The image element passed in is the thumbnail we want.' ); - assert.strictEqual( caption, 'Blah blah', 'The caption passed in is correct' ); - }; + // Create a new bootstrap object to trigger the DOM scan, etc. + bootstrap = new mw.mmv.MultimediaViewerBootstrap(); - $link.trigger( { type : 'click', which : 1 } ); + $link.trigger( { type : 'click', which : 1 } ); - mw.mediaViewer.createNewImage = oldNewImage; - mw.mediaViewer.loadImage = oldLoadImage; - - bootstrap.shutdown(); - } ); + mw.mediaViewer.loadImageByTitle = oldByTitle; + bootstrap.hash = $.noop; } ); - QUnit.asyncTest( 'Hash handling', 3, function ( assert ) { - // Preload mmv - mw.loader.using( 'mmv', function () { - QUnit.start(); + QUnit.test( 'Validate new LightboxImage object has sane constructor parameters', 6, function ( assert ) { + var bootstrap, + $div, + $link, + oldNewImage = mw.mediaViewer.createNewImage, + oldLoadImage = mw.mediaViewer.loadImage, + fname = 'valid', + imgSrc = '/' + fname + '.jpg/300px-' + fname + '.jpg', + imgRegex = new RegExp( imgSrc + '$' ); - var bootstrap, - oldLoadImage = mw.mediaViewer.loadImageByTitle, - oldLightbox = mw.mediaViewer.lightbox, - imageSrc = 'Foo bar.jpg', - image = { filePageTitle: new mw.Title( 'File:' + imageSrc ) }; + $div = createThumb( imgSrc, 'Blah blah' ); + $link = $div.find( 'a.image' ); - // Create a new bootstrap object to trigger the DOM scan, etc. - bootstrap = new mw.mmv.MultimediaViewerBootstrap(); + mw.mediaViewer.loadImage = $.noop; - document.location.hash = ''; + mw.mediaViewer.createNewImage = function ( fileLink, filePageLink, fileTitle, index, thumb, caption ) { + assert.ok( fileLink.match( imgRegex ), 'Thumbnail URL used in creating new image object' ); + assert.strictEqual( filePageLink, '', 'File page link is sane when creating new image object' ); + assert.strictEqual( fileTitle.title, fname, 'Filename is correct when passed into new image constructor' ); + assert.strictEqual( index, 0, 'The only image we created in the gallery is set at index 0 in the images array' ); + assert.strictEqual( thumb.outerHTML, '<img src="' + imgSrc + '">', 'The image element passed in is the thumbnail we want.' ); + assert.strictEqual( caption, 'Blah blah', 'The caption passed in is correct' ); + }; - mw.mediaViewer.lightbox = { iface: { unattach: function() { - assert.ok( true, 'Interface unattached' ); - } } }; + // Create a new bootstrap object to trigger the DOM scan, etc. + bootstrap = new mw.mmv.MultimediaViewerBootstrap(); - // Verify that passing an invalid mmv hash triggers unattach() - document.location.hash = 'Foo'; + $link.trigger( { type : 'click', which : 1 } ); - mw.mediaViewer.lightbox = { images: [ image ] }; + mw.mediaViewer.createNewImage = oldNewImage; + mw.mediaViewer.loadImage = oldLoadImage; + bootstrap.hash = $.noop; + } ); - $( '#qunit-fixture' ).append( '<a class="image"><img src="' + imageSrc + '"></a>' ); + QUnit.test( 'Only load the viewer on a valid hash', 1, function ( assert ) { + var bootstrap; - mw.mediaViewer.loadImageByTitle = function( title ) { - assert.strictEqual( title, 'File:' + imageSrc, 'The title matches' ); - }; + window.location.hash = ''; - // Open a valid mmv hash link and check that the right image is requested. - // imageSrc contains a space without any encoding on purpose - document.location.hash = 'mediaviewer/File:' + imageSrc; + bootstrap = new mw.mmv.MultimediaViewerBootstrap(); - // Reset the hash, because for some browsers switching from the non-URI-encoded to - // the non-URI-encoded version of the same text with a space will not trigger a hash change - document.location.hash = ''; + bootstrap.loadViewer = function () { + assert.ok( false, 'Viewer should not be loaded' ); + return $.Deferred().reject(); + }; - // Try again with an URI-encoded imageSrc containing a space - document.location.hash = 'mediaviewer/File:' + encodeURIComponent( imageSrc ); + window.location.hash = 'Foo'; - mw.mediaViewer.lightbox = oldLightbox; - mw.mediaViewer.loadImageByTitle = oldLoadImage; + bootstrap.loadViewer = function () { + assert.ok( true, 'Viewer should be loaded' ); + return $.Deferred().reject(); + }; - document.location.hash = ''; + window.location.hash = 'mediaviewer/foo'; - bootstrap.shutdown(); - } ); + bootstrap.hash = $.noop; + + window.location.hash = ''; } ); }( mediaWiki, jQuery ) ); diff --git a/tests/qunit/mmv.test.js b/tests/qunit/mmv.test.js index 59a632a..8b7aafb 100644 --- a/tests/qunit/mmv.test.js +++ b/tests/qunit/mmv.test.js @@ -4,14 +4,19 @@ QUnit.test( 'Metadata div is only animated once', 4, function ( assert ) { localStorage.removeItem( 'mmv.hasOpenedMetadata' ); - var viewer = new mw.MultimediaViewer(), + var viewer, backupAnimation = $.fn.animate, - animationRan = false; + animationRan = false, + oldSetupEventHandlers = mw.MultimediaViewer.prototype.setupEventHandlers; $.fn.animate = function () { animationRan = true; return this; }; + + // Because we don't want that throwaway instance to listen to events, could interfere with other tests + mw.MultimediaViewer.prototype.setupEventHandlers = $.noop; + viewer = new mw.MultimediaViewer(); viewer.animateMetadataDivOnce(); assert.strictEqual( viewer.hasAnimatedMetadata, true, 'The first call to animateMetadataDivOnce set hasAnimatedMetadata to true' ); @@ -23,6 +28,8 @@ assert.strictEqual( animationRan, false, 'The second call to animateMetadataDivOnce did not lead to an animation' ); $.fn.animate = backupAnimation; + + mw.MultimediaViewer.prototype.setupEventHandlers = oldSetupEventHandlers; } ); QUnit.test( 'eachPrealoadableLightboxIndex()', 11, function ( assert ) { @@ -70,4 +77,76 @@ viewer.lightbox.currentIndex = oldPosition; } } ); + + QUnit.test( 'Hash handling', 7, function ( assert ) { + var oldUnattach, + multiLightbox = new window.MultiLightbox(), + lightbox = new mw.LightboxInterface( mw.mediaViewer ), + oldLoadImage = mw.mediaViewer.loadImageByTitle, + oldLightbox = mw.mediaViewer.lightbox, + imageSrc = 'Foo bar.jpg', + image = { filePageTitle: new mw.Title( 'File:' + imageSrc ) }; + + document.location.hash = ''; + + oldUnattach = lightbox.unattach; + + lightbox.unattach = function () { + assert.ok( true, 'Lightbox was unattached' ); + oldUnattach.call( this ); + }; + + mw.mediaViewer.lightbox = multiLightbox; + mw.mediaViewer.lightbox.iface = lightbox; + mw.mediaViewer.close(); + + assert.ok( !mw.mediaViewer.isOpen, 'Viewer is closed' ); + + // The mediaViewer won't be initialized through bootstrap by any other way than a valid action + document.location.hash = 'mediaviewer/Foo'; + mw.mediaViewer.isOpen = true; + // From now on we're certain that the viewer receives hash changes through bootstrap + + // Verify that passing an invalid mmv hash when the mmv is open triggers unattach() + document.location.hash = 'Foo'; + + // Verify that mmv doesn't reset a foreign hash + assert.strictEqual( document.location.hash, '#Foo', 'Foreign hash remains intact' ); + assert.ok( !mw.mediaViewer.isOpen, 'Viewer is closed' ); + + lightbox.unattach = function () { + assert.ok( false, 'Lightbox was not unattached' ); + oldUnattach.call( this ); + }; + + // Verify that passing an invalid mmv hash when the mmv is closed doesn't trigger unattach() + document.location.hash = 'Bar'; + + // Verify that mmv doesn't reset a foreign hash + assert.strictEqual( document.location.hash, '#Bar', 'Foreign hash remains intact' ); + + mw.mediaViewer.lightbox = { images: [ image ] }; + + $( '#qunit-fixture' ).append( '<a class="image"><img src="' + imageSrc + '"></a>' ); + + mw.mediaViewer.loadImageByTitle = function( title ) { + assert.strictEqual( title, 'File:' + imageSrc, 'The title matches' ); + }; + + // Open a valid mmv hash link and check that the right image is requested. + // imageSrc contains a space without any encoding on purpose + document.location.hash = 'mediaviewer/File:' + imageSrc; + + // Reset the hash, because for some browsers switching from the non-URI-encoded to + // the non-URI-encoded version of the same text with a space will not trigger a hash change + document.location.hash = ''; + + // Try again with an URI-encoded imageSrc containing a space + document.location.hash = 'mediaviewer/File:' + encodeURIComponent( imageSrc ); + + mw.mediaViewer.lightbox = oldLightbox; + mw.mediaViewer.loadImageByTitle = oldLoadImage; + + document.location.hash = ''; + } ); }( mediaWiki, jQuery ) ); -- To view, visit https://gerrit.wikimedia.org/r/114728 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Iea57d0f4b9090f96e622418223d3f774923e8038 Gerrit-PatchSet: 6 Gerrit-Project: mediawiki/extensions/MultimediaViewer Gerrit-Branch: master Gerrit-Owner: Gilles <gdu...@wikimedia.org> Gerrit-Reviewer: Aarcos <aarcos.w...@gmail.com> Gerrit-Reviewer: Gergő Tisza <gti...@wikimedia.org> Gerrit-Reviewer: Gilles <gdu...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits