mediawiki-extensions-Multim.../tests/qunit/mmv/mmv.bootstrap.test.js
Gergő Tisza 4b6e44a2fc Make sure event handlers are set up even if onready handler is lost
Due to a jQuery bug, errors in local code (gadgets, user scripts)
can cause onready handlers to not be executed. For MMV this causes
catastrophic failure, with a black screen of death on exit.

This change makes sure that the setup code necessary for Media
Viewer to work is executed at latest when MV is invoked, even if some
onready handlers were skipped.

Opening MediaViewer via a hash-URL will still not work if the onready
handler fails, but that's hard to avoid and it is not a catastrophic
failure anymore. This change can be reverted when bug 70772 gets fixed.

Bug: 70756
Change-Id: Ida3b780791bc9dfec29303567d33e3aa4f44dd81
Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/891
2014-09-12 23:49:43 +00:00

461 lines
15 KiB
JavaScript

( function ( mw, $ ) {
QUnit.module( 'mmv.bootstrap', QUnit.newMwEnvironment( {
setup: function () {
mw.config.set( 'wgMediaViewer', true );
mw.config.set( 'wgMediaViewerOnClick', true );
this.sandbox.stub( mw.user, 'isAnon').returns( false );
this.clock = this.sandbox.useFakeTimers();
}
} ) );
function createGallery( imageSrc, caption ) {
var div = $( '<div>' ).addClass( 'gallery' ).appendTo( '#qunit-fixture' ),
galleryBox = $( '<div>' ).addClass( 'gallerybox' ).appendTo( div ),
thumbwrap = $( '<div>' ).addClass( 'thumb' ).appendTo( galleryBox ),
link = $( '<a>' ).addClass( 'image' ).appendTo( thumbwrap );
$( '<img>' ).attr( 'src', ( imageSrc || 'thumb.jpg' ) ).appendTo( link );
$( '<div>' ).addClass( 'gallerytext' ).text( caption || 'Foobar' ).appendTo( galleryBox );
return div;
}
function createThumb( imageSrc, caption ) {
var div = $( '<div>' ).addClass( 'thumb' ).appendTo( '#qunit-fixture' ),
link = $( '<a>' ).addClass( 'image' ).appendTo( div );
$( '<div>' ).addClass( 'thumbcaption' ).appendTo( div ).text( caption );
$( '<img>' ).attr( 'src', ( imageSrc || 'thumb.jpg' ) ).appendTo( link );
return div;
}
function createNormal( imageSrc, caption ) {
var link = $( '<a>' ).prop( 'title', caption ).addClass( 'image' ).appendTo( '#qunit-fixture' );
$( '<img>' ).prop( 'src', ( imageSrc || 'thumb.jpg' ) ).appendTo( link );
return link;
}
function createBootstrap( viewer ) {
var bootstrap = new mw.mmv.MultimediaViewerBootstrap();
// MultimediaViewerBootstrap.ensureEventHandlersAreSetUp() is a weird workaround for gadget bugs.
// MediaViewer should work without it, and so should the tests.
bootstrap.ensureEventHandlersAreSetUp = $.noop;
bootstrap.getViewer = function() { return viewer ? viewer : { initWithThumbs : $.noop }; };
return bootstrap;
}
function hashTest( bootstrap, assert ) {
var hash = 'mediaviewer/foo';
bootstrap.setupEventHandlers();
bootstrap.loadViewer = function () {
assert.ok( false, 'Viewer should not be loaded' );
return $.Deferred().reject();
};
window.location.hash = 'Foo';
bootstrap.loadViewer = function () {
QUnit.start();
assert.ok( true, 'Viewer should be loaded' );
bootstrap.cleanupEventHandlers();
window.location.hash = '';
return $.Deferred().reject();
};
QUnit.stop();
window.location.hash = hash;
}
QUnit.test( 'Promise does not hang on ResourceLoader errors', 3, function ( assert ) {
var bootstrap,
errorMessage = 'loading failed';
this.sandbox.stub( mw.loader, 'using' )
.callsArgWith( 2, new Error( errorMessage, ['mmv'] ) )
.withArgs( 'mediawiki.notification' ).returns( $.Deferred().reject() ); // needed for mw.notify
bootstrap = createBootstrap();
bootstrap.setupOverlay = function () {
assert.ok( true, 'Overlay was set up' );
};
bootstrap.cleanupOverlay = function () {
assert.ok( true, 'Overlay was cleaned up' );
};
QUnit.stop();
bootstrap.loadViewer().fail( function ( message ) {
assert.strictEqual( message, errorMessage, 'promise is rejected with the error message when loading fails' );
QUnit.start();
} );
} );
QUnit.test( 'Clicks are not captured once the loading fails', 4, function ( assert ) {
var event, returnValue,
bootstrap = new mw.mmv.MultimediaViewerBootstrap();
this.sandbox.stub( mw.loader, 'using' )
.callsArgWith( 2, new Error( 'loading failed', ['mmv'] ) )
.withArgs( 'mediawiki.notification' ).returns( $.Deferred().reject() ); // needed for mw.notify
bootstrap.ensureEventHandlersAreSetUp = $.noop;
event = new $.Event( 'click', { button: 0, which: 1 } );
returnValue = bootstrap.click( {}, event, 'foo' );
assert.ok( event.isDefaultPrevented(), 'First click is caught' );
assert.strictEqual( returnValue, false, 'First click is caught' );
event = new $.Event( 'click', { button: 0, which: 1 } );
returnValue = bootstrap.click( {}, event, 'foo' );
assert.ok( !event.isDefaultPrevented(), 'Click after loading failure is not caught' );
assert.notStrictEqual( returnValue, false, 'Click after loading failure is not caught' );
} );
QUnit.test( 'Check viewer invoked when clicking on legit image links', 9, function ( assert ) {
// TODO: Is <div class="gallery"><span class="image"><img/></span></div> valid ???
var div, link, link2, link3, link4, bootstrap,
viewer = { initWithThumbs : $.noop };
// Create gallery with legit link image
div = createGallery();
link = div.find( 'a.image' );
// Legit isolated thumbnail
link2 = $( '<a>' ).addClass( 'image' ).appendTo( '#qunit-fixture' );
$( '<img>' ).attr( 'src', 'thumb2.jpg' ).appendTo( link2 );
// Non-legit fragment
link3 = $( '<a>' ).addClass( 'noImage' ).appendTo( div );
$( '<img>' ).attr( 'src', 'thumb3.jpg' ).appendTo( link3 );
$( '<div>' ).addClass( 'fullMedia' ).appendTo( div );
$( '<img>' ).attr( 'src', 'thumb4.jpg' ).appendTo(
$( '<a>' )
.appendTo(
$( '<div>' )
.attr( 'id', 'file' )
.appendTo( '#qunit-fixture' )
)
);
// Create a new bootstrap object to trigger the DOM scan, etc.
bootstrap = createBootstrap( viewer );
link4 = $( '.fullMedia .mw-mmv-view-expanded' );
assert.ok( link4.length, 'Link for viewing expanded file was set up.' );
bootstrap.setupOverlay = function () {
assert.ok( true, 'Overlay was set up' );
};
viewer.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 } );
// Click on legit link
link4.trigger( { type: 'click', which: 1 } );
// Click on legit link even when preference says not to
mw.config.set( 'wgMediaViewerOnClick', false );
link4.trigger( { type: 'click', which: 1 } );
mw.config.set( 'wgMediaViewerOnClick', true );
bootstrap.setupOverlay = function () {
assert.ok( false, 'Overlay was not set up' );
};
viewer.loadImageByTitle = function() {
assert.ok( false, 'Image should not be loaded' );
};
// Click on non-legit link
link3.trigger( { type : 'click', which : 1 } );
// Click on legit links with preference off
mw.config.set( 'wgMediaViewerOnClick', false );
link.trigger( { type : 'click', which : 1 } );
link2.trigger( { type : 'click', which : 1 } );
} );
QUnit.test( 'Skip images with invalid extensions', 0, function ( assert ) {
var div, link, bootstrap,
viewer = { initWithThumbs : $.noop };
// Create gallery with image that has invalid name extension
div = createGallery( 'thumb.badext' );
link = div.find( 'a.image' );
// Create a new bootstrap object to trigger the DOM scan, etc.
bootstrap = createBootstrap( viewer );
viewer.loadImageByTitle = function() {
assert.ok( false, 'Image should not be loaded' );
};
// Click on legit link with wrong image extension
link.trigger( { type : 'click', which : 1 } );
} );
QUnit.test( 'Accept only left clicks without modifier keys, skip the rest', 2, function ( assert ) {
var $div, $link, bootstrap,
viewer = { initWithThumbs : $.noop };
// Create gallery with image that has valid name extension
$div = createGallery();
// Create a new bootstrap object to trigger the DOM scan, etc.
bootstrap = createBootstrap( viewer );
$link = $div.find( 'a.image' );
bootstrap.setupOverlay = function () {
assert.ok( true, 'Overlay was set up' );
};
viewer.loadImageByTitle = function() {
assert.ok( true, 'Image loaded' );
};
// Handle valid left click, it should try to load the image
$link.trigger( { type : 'click', which : 1 } );
bootstrap.setupOverlay = function () {
assert.ok( false, 'Overlay was not set up' );
};
viewer.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 } );
// Skip invalid right click, no image is loaded
$link.trigger( { type : 'click', which : 2 } );
} );
QUnit.test( 'Ensure that the correct title is loaded when clicking', 2, function ( assert ) {
var bootstrap,
viewer = { initWithThumbs : $.noop },
$div = createGallery( 'foo.jpg' ),
$link = $div.find( 'a.image' );
viewer.loadImageByTitle = function ( loadedTitle ) {
assert.strictEqual( loadedTitle.getPrefixedDb(), 'File:Foo.jpg', 'Titles are identical' );
};
// Create a new bootstrap object to trigger the DOM scan, etc.
bootstrap = createBootstrap( viewer );
bootstrap.setupOverlay = function () {
assert.ok( true, 'Overlay was set up' );
};
$link.trigger( { type : 'click', which : 1 } );
} );
QUnit.test( 'Validate new LightboxImage object has sane constructor parameters', 7, function ( assert ) {
var bootstrap,
$div,
$link,
viewer = new mw.mmv.MultimediaViewer(),
fname = 'valid',
imgSrc = '/' + fname + '.jpg/300px-' + fname + '.jpg',
imgRegex = new RegExp( imgSrc + '$' );
$div = createThumb( imgSrc, 'Blah blah' );
$link = $div.find( 'a.image' );
viewer.loadImage = $.noop;
viewer.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 = createBootstrap( viewer );
bootstrap.setupOverlay = function () {
assert.ok( true, 'Overlay was set up' );
};
$link.trigger( { type : 'click', which : 1 } );
} );
QUnit.test( 'Only load the viewer on a valid hash (modern browsers)', 1, function ( assert ) {
var bootstrap;
window.location.hash = '';
bootstrap = createBootstrap();
hashTest( bootstrap, assert );
} );
QUnit.test( 'Only load the viewer on a valid hash (old browsers)', 1, function ( assert ) {
var bootstrap;
window.location.hash = '';
bootstrap = createBootstrap();
bootstrap.browserHistory = undefined;
hashTest( bootstrap, assert );
} );
QUnit.test( 'internalHashChange', 1, function ( assert ) {
var bootstrap = createBootstrap(),
hash = '#mediaviewer/foo';
window.location.hash = '';
bootstrap.setupEventHandlers();
bootstrap.loadViewer = function () {
assert.ok( false, 'Viewer should not be loaded' );
return $.Deferred().reject();
};
bootstrap.internalHashChange( { hash: hash } );
assert.strictEqual( window.location.hash, hash, 'Window\'s hash has been updated correctly' );
bootstrap.cleanupEventHandlers();
window.location.hash = '';
} );
QUnit.test( 'isCSSReady', 3, function ( assert ) {
var bootstrap = createBootstrap(),
deferred = $.Deferred(),
CSSclass = 'foo-' + $.now(),
$style = $( '<style type="text/css" />' )
.text( '.' + CSSclass + ' { display: inline; }' );
bootstrap.readinessCSSClass = CSSclass;
bootstrap.isCSSReady( deferred );
assert.strictEqual( deferred.state(), 'pending', 'The style isn\'t on the page yet' );
QUnit.stop();
deferred.then( function() {
QUnit.start();
assert.ok( true, 'The style is on the page' );
assert.strictEqual( $( '.' + CSSclass ).length, 0, 'There are no leftover test elements' );
$style.remove();
} );
$style.appendTo( 'head' );
this.clock.tick( bootstrap.readinessWaitDuration );
} );
QUnit.test( 'Restoring article scroll position', 2, function ( assert ) {
var bootstrap = createBootstrap(),
scrollTop = 50,
scrollLeft = 60,
stubbedScrollTop = scrollTop,
stubbedScrollLeft = scrollLeft;
this.sandbox.stub( $, 'scrollTo', function ( target ) {
if ( target ) {
stubbedScrollTop = target.top;
stubbedScrollLeft = target.left;
} else {
return {
scrollTop : function () { return stubbedScrollTop; },
scrollLeft : function () { return stubbedScrollLeft; }
};
}
} );
bootstrap.setupOverlay();
// Calling this a second time because it can happen in history navigation context
bootstrap.setupOverlay();
bootstrap.cleanupOverlay();
assert.strictEqual( stubbedScrollTop, scrollTop, 'Scroll is correctly reset to original top position' );
assert.strictEqual( stubbedScrollLeft, scrollLeft, 'Scroll is correctly reset to original left position' );
} );
QUnit.test( 'Preload JS/CSS dependencies on thumb hover', 2, function ( assert ) {
var $div, bootstrap,
viewer = { initWithThumbs : $.noop };
// Create gallery with image that has valid name extension
$div = createThumb();
// Create a new bootstrap object to trigger the DOM scan, etc.
bootstrap = createBootstrap( viewer );
this.sandbox.stub( mw.loader, 'load' );
$div.mouseenter();
this.clock.tick( bootstrap.hoverWaitDuration - 50 );
$div.mouseleave();
assert.ok( !mw.loader.load.called, 'Dependencies should not be preloaded if the thumb is not hovered long enough' );
$div.mouseenter();
this.clock.tick( bootstrap.hoverWaitDuration + 50 );
$div.mouseleave();
assert.ok( mw.loader.load.called, 'Dependencies should be preloaded if the thumb is hovered long enough' );
} );
QUnit.test( 'isAllowedThumb', 5, function ( assert ) {
var $container = $( '<div>' ),
$thumb = $( '<img>' ).appendTo( $container ),
bootstrap = createBootstrap();
assert.ok( bootstrap.isAllowedThumb( $thumb ), 'Normal image in a div is allowed.' );
$container.addClass( 'metadata' );
assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image in a metadata container is disallowed.' );
$container.prop( 'class', '' );
$container.addClass( 'noviewer' );
assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image in a noviewer container is disallowed.' );
$container.prop( 'class', '' );
$container.addClass( 'noarticletext' );
assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image in an empty article is disallowed.' );
$container.prop( 'class', '' );
$thumb.addClass( 'noviewer' );
assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image with a noviewer class is disallowed.' );
} );
QUnit.test( 'findCaption', 3, function ( assert ) {
var gallery = createGallery( 'foo.jpg', 'Baz' ),
thumb = createThumb( 'foo.jpg', 'Quuuuux' ),
link = createNormal( 'foo.jpg', 'Foobar' ),
bootstrap = createBootstrap();
assert.strictEqual( bootstrap.findCaption( gallery.find( '.thumb' ), gallery.find( 'a.image' ) ), 'Baz', 'A gallery caption is found.' );
assert.strictEqual( bootstrap.findCaption( thumb, thumb.find( 'a.image' ) ), 'Quuuuux', 'A thumbnail caption is found.' );
assert.strictEqual( bootstrap.findCaption( $(), link ), 'Foobar', 'The caption is found even if the image is not a thumbnail.' );
} );
}( mediaWiki, jQuery ) );