Adopt mediawiki.router

Bug: T77258
Change-Id: Id4df92b0ebed3fb4f4b9269862c952f3968bf957
This commit is contained in:
Simon Legner 2019-05-16 15:19:44 +02:00
parent 870735c0e9
commit b1ade19388
18 changed files with 113 additions and 780 deletions

View file

@ -46,11 +46,6 @@
"mmv/logging/mmv.logging.DimensionLogger.js",
"mmv/logging/mmv.logging.ViewLogger.js",
"mmv/logging/mmv.logging.PerformanceLogger.js",
"mmv/routing/mmv.routing.js",
"mmv/routing/mmv.routing.Route.js",
"mmv/routing/mmv.routing.ThumbnailRoute.js",
"mmv/routing/mmv.routing.MainFileRoute.js",
"mmv/routing/mmv.routing.Router.js",
"mmv/model/mmv.model.js",
"mmv/model/mmv.model.IwTitle.js",
"mmv/model/mmv.model.License.js",
@ -109,6 +104,7 @@
"mediawiki.user",
"mediawiki.util",
"oojs",
"oojs-router",
"jquery.fullscreen",
"jquery.throttle-debounce",
"jquery.color",
@ -350,7 +346,8 @@
"mediawiki.notify",
"mediawiki.storage",
"mmv.head",
"oojs"
"oojs",
"oojs-router"
],
"messages": [
"multimediaviewer-view-expanded",
@ -414,9 +411,6 @@
"tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js",
"tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js",
"tests/qunit/mmv/provider/mmv.provider.Image.test.js",
"tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js",
"tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js",
"tests/qunit/mmv/routing/mmv.routing.Router.test.js",
"tests/qunit/mmv/ui/mmv.ui.test.js",
"tests/qunit/mmv/ui/mmv.ui.canvas.test.js",
"tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js",

View file

@ -5,6 +5,7 @@
{
"name": "Base",
"classes": [
"mw.mmv",
"mw.mmv.Config",
"mw.mmv.EmbedFileFormatter",
"mw.mmv.HtmlUtils",
@ -33,12 +34,6 @@
"mw.mmv.provider.*"
]
},
{
"name": "Routers",
"classes": [
"mw.mmv.routing.*"
]
},
{
"name": "Interface",
"classes": [
@ -74,6 +69,12 @@
"String"
]
},
{
"name": "OOjs Router",
"classes": [
"OO.Router"
]
},
{
"name": "OOUI",
"classes": [

View file

@ -46,6 +46,11 @@
* An HTML <img> element.
*/
/**
* @class OO.Router
* <https://doc.wikimedia.org/oojs-router/master/#!/api/OO.Router>
*/
/**
* @class OO.ui.MenuOptionWidget
* <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/OO.ui.MenuOptionWidget>

View file

@ -59,7 +59,6 @@
// e.g. via a VE edit or pagination in a multipage file
mw.hook( 'wikipage.content' ).add( $.proxy( this, 'processThumbs' ) );
this.browserHistory = window.history;
}
MMVB = MultimediaViewerBootstrap.prototype;
@ -417,7 +416,7 @@
this.ensureEventHandlersAreSetUp();
return this.loadViewer( true ).then( function ( viewer ) {
viewer.loadImageByTitle( title, true );
viewer.loadImageByTitle( title, false );
} );
};
@ -461,8 +460,8 @@
* @private
*/
MMVB.isViewerHash = function () {
return window.location.hash.indexOf( '#mediaviewer/' ) === 0 ||
window.location.hash.indexOf( '#/media/' ) === 0;
var path = OO.Router.prototype.getPath();
return path.match( mw.mmv.ROUTE_REGEXP ) || path.match( mw.mmv.LEGACY_ROUTE_REGEXP );
};
/**
@ -479,13 +478,8 @@
return;
}
if ( this.skipNextHashHandling ) {
this.skipNextHashHandling = false;
return;
}
this.loadViewer( this.isViewerHash() ).then( function ( viewer ) {
viewer.hash();
viewer.router.checkRoute();
// this is an ugly temporary fix to avoid a black screen of death when
// the page is loaded with an invalid MMV url
if ( !viewer.isOpen ) {
@ -498,35 +492,6 @@
} );
};
/**
* Handles hash change requests coming from mmv
*
* @param {jQuery.Event} e Custom mmv-hash event
*/
MMVB.internalHashChange = function ( e ) {
var hash = e.hash,
title = e.title;
// The advantage of using pushState when it's available is that it has to ability to truly
// clear the hash, not leaving "#" in the history
// An entry with "#" in the history has the side-effect of resetting the scroll position when navigating the history
if ( this.browserHistory && this.browserHistory.pushState ) {
// In order to truly clear the hash, we need to reconstruct the hash-free URL
if ( hash === '#' ) {
hash = window.location.href.replace( /#.*$/, '' );
}
window.history.pushState( null, title, hash );
} else {
// Since we voluntarily changed the hash, we don't want MMVB.hash (which will trigger on hashchange event) to treat it
this.skipNextHashHandling = true;
window.location.hash = hash;
}
document.title = title;
};
/**
* Instantiates a new viewer if necessary
*
@ -551,15 +516,11 @@
/** @property {boolean} eventHandlersHaveBeenSetUp tracks domready event handler state */
this.eventHandlersHaveBeenSetUp = true;
$( window ).on( this.browserHistory && this.browserHistory.pushState ? 'popstate.mmvb' : 'hashchange', function () {
self.hash();
} );
// Interpret any hash that might already be in the url
self.hash( true );
$( document ).on( 'mmv-hash', function ( e ) {
self.internalHashChange( e );
$( document ).on( 'mmv-setup-overlay', function () {
self.setupOverlay();
} ).on( 'mmv-cleanup-overlay', function () {
self.cleanupOverlay();
} );
@ -569,8 +530,7 @@
* Cleans up event handlers, used for tests
*/
MMVB.cleanupEventHandlers = function () {
$( window ).off( 'hashchange popstate.mmvb' );
$( document ).off( 'mmv-hash' );
$( document ).off( 'mmv-setup-overlay mmv-cleanup-overlay' );
this.eventHandlersHaveBeenSetUp = false;
};

View file

@ -17,5 +17,31 @@
// Included on every page which has images so keep it lightweight.
( function () {
mw.mmv = {};
mw.mmv = {
/**
* The media route prefix
* @member mw.mmv
*/
ROUTE: 'media',
/**
* RegExp representing the media route
* @member mw.mmv
*/
ROUTE_REGEXP: /^\/media\/(.+)$/,
/**
* @property
* RegExp representing the legacy media route
* @member mw.mmv
*/
LEGACY_ROUTE_REGEXP: /^mediaviewer\/(.+)$/,
/**
* Returns the location hash (route string) for the given file title.
* @param {string} imageFileTitle the file title
* @return {string} the location hash
* @member mw.mmv
*/
getMediaHash: function ( imageFileTitle ) {
return '#/' + mw.mmv.ROUTE + '/' + imageFileTitle;
}
};
}() );

View file

@ -27,11 +27,6 @@
function EmbedFileFormatter() {
/** @property {mw.mmv.HtmlUtils} htmlUtils - */
this.htmlUtils = new mw.mmv.HtmlUtils();
/**
* @property {mw.mmv.routing.Router} router -
*/
this.router = new mw.mmv.routing.Router();
}
EFFP = EmbedFileFormatter.prototype;
@ -251,8 +246,7 @@
* @return {string} URL
*/
EFFP.getLinkUrl = function ( info ) {
var route = new mw.mmv.routing.ThumbnailRoute( info.imageInfo.title );
return this.router.createHashedUrl( route, info.imageInfo.descriptionUrl );
return info.imageInfo.descriptionUrl + mw.mmv.getMediaHash( info.imageInfo.title );
};
mw.mmv.EmbedFileFormatter = EmbedFileFormatter;

View file

@ -28,12 +28,6 @@
*/
function Share( $container ) {
mw.mmv.ui.reuse.Tab.call( this, $container );
/**
* @property {mw.mmv.routing.Router} router -
*/
this.router = new mw.mmv.routing.Router();
this.init();
}
OO.inheritClass( Share, mw.mmv.ui.reuse.Tab );
@ -115,8 +109,7 @@
* @param {mw.mmv.model.Image} image
*/
SP.set = function ( image ) {
var route = new mw.mmv.routing.ThumbnailRoute( image.title ),
url = this.router.createHashedUrl( route, image.descriptionUrl );
var url = image.descriptionUrl + mw.mmv.getMediaHash( image.title );
this.pageInput.setValue( url );

View file

@ -79,9 +79,11 @@
this.currentIndex = 0;
/**
* @property {mw.mmv.routing.Router} router -
* @property {OO.Router} router
*/
this.router = new mw.mmv.routing.Router();
this.router = new OO.Router();
this.setupRouter();
comingFromHashChange = false;
/**
* UI object used to display the pictures in the page.
@ -245,8 +247,9 @@
*
* @param {mw.mmv.LightboxImage} image
* @param {HTMLImageElement} initialImage A thumbnail to use as placeholder while the image loads
* @param {boolean} useReplaceState Whether to update history entry to avoid long history queues
*/
MMVP.loadImage = function ( image, initialImage ) {
MMVP.loadImage = function ( image, initialImage, useReplaceState ) {
var imageWidths,
canvasDimensions,
imagePromise,
@ -264,12 +267,14 @@
this.currentImageFileTitle = image.filePageTitle;
if ( !this.isOpen ) {
$( document ).trigger( $.Event( 'mmv-setup-overlay' ) );
this.ui.open();
this.isOpen = true;
} else {
this.ui.empty();
}
this.setHash();
this.setMediaHash( useReplaceState );
// 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
@ -384,29 +389,25 @@
viewer.ui.panel.scroller.animateMetadataOnce();
viewer.preloadDependencies();
} );
this.comingFromHashChange = false;
};
/**
* Loads an image by its title
*
* @param {mw.Title} title
* @param {boolean} updateHash Viewer should update the location hash when true
* @param {boolean} useReplaceState Whether to update history entry to avoid long history queues
*/
MMVP.loadImageByTitle = function ( title, updateHash ) {
MMVP.loadImageByTitle = function ( title, useReplaceState ) {
var i, thumb;
if ( !this.thumbs || !this.thumbs.length ) {
return;
}
this.comingFromHashChange = !updateHash;
for ( i = 0; i < this.thumbs.length; i++ ) {
thumb = this.thumbs[ i ];
if ( thumb.title.getPrefixedText() === title.getPrefixedText() ) {
this.loadImage( thumb.image, thumb.$thumb.clone()[ 0 ], true );
this.loadImage( thumb.image, thumb.$thumb.clone()[ 0 ], useReplaceState );
return;
}
}
@ -877,17 +878,15 @@
* Handles close event coming from the lightbox
*/
MMVP.close = function () {
var windowTitle;
this.viewLogger.recordViewDuration();
this.viewLogger.unattach();
windowTitle = this.createDocumentTitle( null );
document.title = this.createDocumentTitle( null );
if ( comingFromHashChange === false ) {
$( document ).trigger( $.Event( 'mmv-hash', { hash: '#', title: windowTitle } ) );
} else {
if ( comingFromHashChange ) {
comingFromHashChange = false;
} else {
this.router.back();
}
// This has to happen after the hash reset, because setting the hash to # will reset the page scroll
@ -897,36 +896,56 @@
};
/**
* Handles a hash change coming from the browser
* Sets up the route handlers
*/
MMVP.hash = function () {
var route = this.router.parseLocation( window.location );
if ( route instanceof mw.mmv.routing.ThumbnailRoute ) {
document.title = this.createDocumentTitle( route.fileTitle );
this.loadImageByTitle( route.fileTitle );
} else if ( this.isOpen ) {
// This allows us to avoid the mmv-hash event that normally happens on close
MMVP.setupRouter = function () {
function route( fileName ) {
var fileTitle;
comingFromHashChange = true;
document.title = this.createDocumentTitle( null );
if ( this.ui ) {
// FIXME triggers mmv-close event, which calls viewer.close()
this.ui.unattach();
} else {
this.close();
fileName = decodeURIComponent( fileName );
try {
fileTitle = new mw.Title( fileName );
this.loadImageByTitle( fileTitle );
} catch ( err ) {
// ignore routes to invalid titles
mw.log.warn( err );
}
}
this.router.addRoute( mw.mmv.ROUTE_REGEXP, route.bind( this ) );
this.router.addRoute( mw.mmv.LEGACY_ROUTE_REGEXP, route.bind( this ) );
// handle empty hashes, and anchor links (page sections)
this.router.addRoute( /^[^/]*$/, function () {
comingFromHashChange = true;
if ( this.isOpen ) {
document.title = this.createDocumentTitle( null );
if ( this.ui ) {
// FIXME triggers mmv-close event, which calls viewer.close()
this.ui.unattach();
} else {
this.close();
}
}
}.bind( this ) );
};
MMVP.setHash = function () {
var route, windowTitle, hashFragment;
if ( !this.comingFromHashChange ) {
route = new mw.mmv.routing.ThumbnailRoute( this.currentImageFileTitle );
hashFragment = '#' + this.router.createHash( route );
windowTitle = this.createDocumentTitle( this.currentImageFileTitle );
$( document ).trigger( $.Event( 'mmv-hash', { hash: hashFragment, title: windowTitle } ) );
/**
* Updates the hash to reflect an open image file
* @param {boolean} useReplaceState Whether to update history entry to avoid long history queues
*/
MMVP.setMediaHash = function ( useReplaceState ) {
if ( useReplaceState === undefined ) {
useReplaceState = true;
}
if ( comingFromHashChange ) {
comingFromHashChange = false;
return;
}
document.title = this.createDocumentTitle( this.currentImageFileTitle );
this.router.navigateTo( document.title, {
path: mw.mmv.getMediaHash( this.currentImageFileTitle ),
useReplaceState: useReplaceState
} );
};
/**

View file

@ -1,30 +0,0 @@
/*
* This file is part of the MediaWiki extension MediaViewer.
*
* MediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
/**
* Route for showing the main image on the page, (whatever that means might depend on the page).
* This is typically used on file pages.
*
* @class mw.mmv.routing.MainFileRoute
* @extends mw.mmv.routing.Route
* @constructor
*/
function MainFileRoute() {}
OO.inheritClass( MainFileRoute, mw.mmv.routing.Route );
mw.mmv.routing.MainFileRoute = MainFileRoute;
}() );

View file

@ -1,28 +0,0 @@
/*
* This file is part of the MediaWiki extension MediaViewer.
*
* MediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
/**
* The base class for routes. Route classes don't really do anything, they are just simple
* containers which specify a certain way of referencing images.
*
* @class mw.mmv.routing.Route
* @constructor
*/
function Route() {}
mw.mmv.routing.Route = Route;
}() );

View file

@ -1,197 +0,0 @@
/*
* This file is part of the MediaWiki extension MediaViewer.
*
* MediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
var RP;
/**
* Converts between routes and their URL hash representations such as `mediaviewer/File:Foo`.
*
* @class mw.mmv.routing.Router
* @constructor
*/
function Router() {}
RP = Router.prototype;
/**
* The prefix originally used to namespace MediaViewer routing hashes. Since there are many links
* out there pointing to those URLs, we should keep them working.
*
* @protected
* @property {string}
*/
RP.legacyPrefix = 'mediaviewer';
/**
* The prefix used to namespace MediaViewer routing hashes
*
* @protected
* @property {string}
*/
RP.applicationPrefix = '/media';
/**
* Takes an URL hash and returns a route (or null if it could not be parsed).
* Returns null for URL hashes which were not created by MediaViewer; you should use
* #isMediaViewerHash() if you want to differentiate such hashes.
* The hash can contain the starting `#` but does not have to; it should be in raw (percent-
* encoded) form. Note that the percent-encoding behavior of location.hash is not consistent
* between browsers; location.href can be used instead.
*
* @param {string} hash
* @return {mw.mmv.routing.Route|null}
*/
RP.parseHash = function ( hash ) {
var hashParts, fileName;
hashParts = this.tokenizeHash( hash );
if ( hashParts.length === 0 ) {
return null;
} else if ( hashParts.length === 1 ) {
return new mw.mmv.routing.MainFileRoute();
} else if ( hashParts.length === 2 ) {
fileName = this.decodeRouteComponent( hashParts[ 1 ] );
return new mw.mmv.routing.ThumbnailRoute( new mw.Title( fileName ) );
}
return null;
};
/**
* Takes a route and returns a string representation which can be used in the URL fragment.
* The string does not contain the starting `#`, and it is encoded and guaranteed to be a
* valid URL.
*
* @param {mw.mmv.routing.Route} route
* @return {string}
*/
RP.createHash = function ( route ) {
if ( route instanceof mw.mmv.routing.ThumbnailRoute ) {
return this.applicationPrefix + '/' +
this.encodeRouteComponent( 'File:' + route.fileTitle.getMain() );
} else if ( route instanceof mw.mmv.routing.MainFileRoute ) {
return this.applicationPrefix;
} else if ( route instanceof mw.mmv.routing.Route ) {
throw new Error( 'mw.mmv.routing.Router.createHash: not implemented for ' + route.constructor.name );
} else {
throw new Error( 'mw.mmv.routing.Router.createHash: invalid argument' );
}
};
/**
* Like #parseHash(), but takes a window.location object. This is a helper function to make
* sure that hashes are decoded correctly in spite of browser inconsistencies.
*
* @param {{href: string}} location window.location object
* @return {mw.mmv.routing.Route|null}
*/
RP.parseLocation = function ( location ) {
// Firefox percent-decodes location.hash: https://bugzilla.mozilla.org/show_bug.cgi?id=483304
// which would cause inconsistent cross-browser behavior for files which have % or /
// characters in their names. Using location.href is safe.
return this.parseHash( location.href.split( '#' )[ 1 ] || '' );
};
/**
* Like #createHash(), but appends the hash to a specified URL
*
* @param {mw.mmv.routing.Route} route
* @param {string} baseUrl the URL of the page the image is on (can contain a hash part,
* which will be stripped)
* @return {string} an URL to the same page as baseUrl, with the hash for the given route
*/
RP.createHashedUrl = function ( route, baseUrl ) {
return baseUrl.replace( /#.*/, '' ) + '#' + this.createHash( route );
};
/**
* Returns true if this hash looks like it was created by MediaViewer.
* The hash can contain the starting `#` but does not have to.
*
* @param {string} hash
* @return {boolean}
*/
RP.isMediaViewerHash = function ( hash ) {
return this.tokenizeHash( hash ).length !== 0;
};
/**
* Returns "segments" of a hash. The first segment is always the #applicationPrefix.
* If the hash is not a MediaViewer routing hash, an empty array is returned.
* The input hash can contain the starting `#` but does not have to.
*
* @protected
* @param {string} hash
* @return {string[]}
*/
RP.tokenizeHash = function ( hash ) {
var prefix,
hashParts;
if ( hash[ 0 ] === '#' ) {
hash = hash.slice( 1 );
}
if ( hash.indexOf( this.legacyPrefix ) === 0 ) {
prefix = this.legacyPrefix;
}
if ( hash.indexOf( this.applicationPrefix ) === 0 ) {
prefix = this.applicationPrefix;
}
if ( prefix === undefined ) {
return [];
}
hash = hash.slice( prefix.length );
hashParts = hash.split( '/' );
hashParts[ 0 ] = prefix;
return hashParts;
};
/**
* URL-encodes a route component.
* Almost identical to mw.util.wikiUrlencode but makes sure there are no unencoded `/`
* characters left since we use those to delimit components.
*
* @protected
* @param {string} component
* @return {string}
*/
RP.encodeRouteComponent = function ( component ) {
return mw.util.wikiUrlencode( component ).replace( /\//g, '%2F' );
};
/**
* URL-decodes a route component.
* This is basically just a standard percent-decode, but for backwards compatibility with
* older schemes, we also replace spaces which underlines (the current scheme never has spaces).
*
* @protected
* @param {string} component
* @return {string}
*/
RP.decodeRouteComponent = function ( component ) {
return decodeURIComponent( component ).replace( / /g, '_' );
};
mw.mmv.routing.Router = Router;
}() );

View file

@ -1,36 +0,0 @@
/*
* This file is part of the MediaWiki extension MediaViewer.
*
* MediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
/**
* Route for a specific thumbnail on the current page. The thumbnail must be that of a wiki
* file (can't be e.g. an external image); can be a file from a remote repo though.
*
* @class mw.mmv.routing.ThumbnailRoute
* @extends mw.mmv.routing.Route
* @constructor
* @param {mw.Title} fileTitle the name of the image
*/
function ThumbnailRoute( fileTitle ) {
if ( !fileTitle ) {
throw new Error( 'mw.mmv.routing.ThumbnailRoute: fileTitle parameter is required' );
}
this.fileTitle = fileTitle;
}
OO.inheritClass( ThumbnailRoute, mw.mmv.routing.Route );
mw.mmv.routing.ThumbnailRoute = ThumbnailRoute;
}() );

View file

@ -1,20 +0,0 @@
/*
* This file is part of the MediaWiki extension MediaViewer.
*
* MediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
mw.mmv.routing = {};
}() );

View file

@ -78,8 +78,6 @@
// wait for if we want to see these tests through
mw.mmv.testHelpers.asyncMethod( bootstrap, 'loadViewer' );
bootstrap.setupEventHandlers();
// invalid hash, should not trigger MMV load
window.location.hash = 'Foo';
@ -89,6 +87,7 @@
// without us interfering with another immediate change
setTimeout( function () {
window.location.hash = hash;
bootstrap.hash();
} );
return mw.mmv.testHelpers.waitForAsync().then( function () {
@ -430,58 +429,6 @@
assert.strictEqual( bootstrap.setupOverlay.called, false, 'Overlay is not set up' );
} );
QUnit.test( 'internalHashChange', function ( assert ) {
var bootstrap = createBootstrap(),
hash = '#/media/foo',
callCount = 0,
clock = this.sandbox.useFakeTimers();
window.location.hash = '';
bootstrap.loadViewer = function () {
callCount++;
return $.Deferred().reject();
};
bootstrap.setupEventHandlers();
bootstrap.internalHashChange( { hash: hash } );
clock.tick( 10 );
assert.strictEqual( callCount, 0, 'Viewer should not be loaded' );
assert.strictEqual( window.location.hash, hash, 'Window\'s hash has been updated correctly' );
bootstrap.cleanupEventHandlers();
window.location.hash = '';
clock.restore();
} );
QUnit.test( 'internalHashChange (legacy)', function ( assert ) {
var bootstrap = createBootstrap(),
hash = '#mediaviewer/foo',
callCount = 0,
clock = this.sandbox.useFakeTimers();
window.location.hash = '';
bootstrap.loadViewer = function () {
callCount++;
return $.Deferred().reject();
};
bootstrap.setupEventHandlers();
bootstrap.internalHashChange( { hash: hash } );
clock.tick( 10 );
assert.strictEqual( callCount, 0, 'Viewer should not be loaded' );
assert.strictEqual( window.location.hash, hash, 'Window\'s hash has been updated correctly' );
bootstrap.cleanupEventHandlers();
window.location.hash = '';
clock.restore();
} );
QUnit.test( 'Restoring article scroll position', function ( assert ) {
var stubbedScrollTop,
bootstrap = createBootstrap(),

View file

@ -54,11 +54,10 @@
assert.strictEqual( viewer.isOpen, false, 'Viewer is closed' );
viewer.isOpen = true;
viewer.loadImageByTitle( image.filePageTitle );
// Verify that passing an invalid mmv hash when the mmv is open triggers unattach()
window.location.hash = 'Foo';
viewer.hash();
// Verify that mmv doesn't reset a foreign hash
assert.strictEqual( window.location.hash, '#Foo', 'Foreign hash remains intact' );
@ -71,7 +70,6 @@
// Verify that passing an invalid mmv hash when the mmv is closed doesn't trigger unattach()
window.location.hash = 'Bar';
viewer.hash();
// Verify that mmv doesn't reset a foreign hash
assert.strictEqual( window.location.hash, '#Bar', 'Foreign hash remains intact' );
@ -87,24 +85,19 @@
// Open a valid mmv hash link and check that the right image is requested.
// imageSrc contains a space without any encoding on purpose
window.location.hash = '/media/File:' + imageSrc;
viewer.hash();
// 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
window.location.hash = '';
viewer.hash();
// Try again with an URI-encoded imageSrc containing a space
window.location.hash = '/media/File:' + encodeURIComponent( imageSrc );
viewer.hash();
// Reset the hash
window.location.hash = '';
viewer.hash();
// Try again with a legacy hash
window.location.hash = 'mediaviewer/File:' + imageSrc;
viewer.hash();
viewer.cleanupEventHandlers();
@ -703,7 +696,7 @@
viewer.currentImageFileTitle = title;
bootstrap.setupEventHandlers();
viewer.setHash();
viewer.setMediaHash();
assert.ok( document.title.match( title.getNameText() ), 'File name is visible in title' );

View file

@ -1,24 +0,0 @@
/*
* This file is part of the MediaWiki extension MediaViewer.
*
* MediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
QUnit.module( 'mmv.routing.MainFileRoute', QUnit.newMwEnvironment() );
QUnit.test( 'Constructor sanity checks', function ( assert ) {
assert.ok( new mw.mmv.routing.MainFileRoute(), 'MainFileRoute created successfully' );
} );
}() );

View file

@ -1,232 +0,0 @@
/*
* This file is part of the MediaWiki extension MediaViewer.
*
* MediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
QUnit.module( 'mmv.routing.Router', QUnit.newMwEnvironment() );
QUnit.test( 'Constructor sanity checks', function ( assert ) {
var router;
router = new mw.mmv.routing.Router();
assert.ok( router, 'Router created successfully' );
} );
QUnit.test( 'isMediaViewerHash()', function ( assert ) {
var router = new mw.mmv.routing.Router();
assert.strictEqual( router.isMediaViewerHash( 'mediaviewer/foo' ), true, 'Legacy hash' );
assert.strictEqual( router.isMediaViewerHash( '#mediaviewer/foo' ), true, 'Legacy hash with #' );
assert.strictEqual( router.isMediaViewerHash( 'mediaviewer' ), true, 'Bare legacy hash' );
assert.strictEqual( router.isMediaViewerHash( '#mediaviewer' ), true, 'Bare legacy hash with #' );
assert.strictEqual( router.isMediaViewerHash( '/media/foo' ), true, 'Normal hash' );
assert.strictEqual( router.isMediaViewerHash( '#/media/foo' ), true, 'Normal hash with #' );
assert.strictEqual( router.isMediaViewerHash( '/media' ), true, 'Bare hash' );
assert.strictEqual( router.isMediaViewerHash( '#/media' ), true, 'Bare hash with #' );
assert.strictEqual( router.isMediaViewerHash( 'foo/media' ), false, 'Foreign hash' );
assert.strictEqual( router.isMediaViewerHash( '' ), false, 'Empty hash' );
} );
QUnit.test( 'createHash()/parseHash()', function ( assert ) {
var route, parsedRoute, hash, title,
router = new mw.mmv.routing.Router();
route = new mw.mmv.routing.MainFileRoute();
hash = router.createHash( route );
parsedRoute = router.parseHash( hash );
assert.deepEqual( parsedRoute, route, 'Bare hash' );
title = new mw.Title( 'File:Foo.png' );
route = new mw.mmv.routing.ThumbnailRoute( title );
hash = router.createHash( route );
parsedRoute = router.parseHash( hash );
assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
title.getPrefixedDb(), 'Normal hash' );
assert.ok( hash.match( /File:Foo.png/ ), 'Simple filenames remain readable' );
title = new mw.Title( 'File:Foo.png' );
route = new mw.mmv.routing.ThumbnailRoute( title );
hash = router.createHash( route );
assert.notEqual( hash[ 0 ], '#', 'Leading # is not included in the returned hash' );
parsedRoute = router.parseHash( '#' + hash );
assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
title.getPrefixedDb(), 'Leading # is accepted when parsing a hash' );
title = new mw.Title( 'File:Foo.png' );
route = new mw.mmv.routing.ThumbnailRoute( title );
hash = router.createHash( route );
parsedRoute = router.parseHash( hash );
assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
title.getPrefixedDb(), 'Normal hash' );
assert.ok( hash.match( /File:Foo.png/ ), 'Simple filenames remain readable' );
title = new mw.Title( 'File:Foo/bar.png' );
route = new mw.mmv.routing.ThumbnailRoute( title );
hash = router.createHash( route );
parsedRoute = router.parseHash( hash );
assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
title.getPrefixedDb(), 'Filename with /' );
assert.notOk( hash.match( 'Foo/bar' ), '/ is encoded' );
title = new mw.Title( 'File:Foo bar.png' );
route = new mw.mmv.routing.ThumbnailRoute( title );
hash = router.createHash( route );
parsedRoute = router.parseHash( hash );
assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
title.getPrefixedDb(), 'Filename with space' );
assert.notOk( hash.match( 'Foo bar' ), 'space is replaced...' );
assert.ok( hash.match( 'Foo_bar' ), '...with underscore' );
title = new mw.Title( 'File:看門狗 (遊戲).jpg' );
route = new mw.mmv.routing.ThumbnailRoute( title );
hash = router.createHash( route );
parsedRoute = router.parseHash( hash );
assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
title.getPrefixedDb(), 'Unicode filename' );
title = new mw.Title( 'File:%!"$&\'()*,-./:;=?@\\^_`~+.jpg' );
if ( title ) {
route = new mw.mmv.routing.ThumbnailRoute( title );
hash = router.createHash( route );
parsedRoute = router.parseHash( hash );
assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
title.getPrefixedDb(), 'Special characters' );
} else {
// mw.Title depends on $wgLegalTitleChars - do not fail test if it is non-standard
assert.ok( true, 'Skipped' );
}
} );
QUnit.test( 'createHash() error handling', function ( assert ) {
var router = new mw.mmv.routing.Router();
assert.ok( mw.mmv.testHelpers.getException( function () { return new mw.mmv.routing.ThumbnailRoute(); } ),
'Exception thrown then ThumbnailRoute has no title' );
assert.ok( mw.mmv.testHelpers.getException( function () {
router.createHash( this.sandbox.createStubInstance( mw.mmv.routing.Route ) );
} ), 'Exception thrown for unknown Route subclass' );
assert.ok( mw.mmv.testHelpers.getException( function () {
router.createHash( {} );
} ), 'Exception thrown for non-Route class' );
} );
QUnit.test( 'parseHash() with invalid hashes', function ( assert ) {
var router = new mw.mmv.routing.Router();
assert.notOk( router.parseHash( 'foo' ), 'Non-MMV hash is rejected.' );
assert.notOk( router.parseHash( '#foo' ), 'Non-MMV hash is rejected (with #).' );
assert.notOk( router.parseHash( '/media/foo/bar' ), 'Invalid MMV hash is rejected.' );
assert.notOk( router.parseHash( '#/media/foo/bar' ), 'Invalid MMV hash is rejected (with #).' );
} );
QUnit.test( 'parseHash() backwards compatibility', function ( assert ) {
var route,
router = new mw.mmv.routing.Router();
route = router.parseHash( '#mediaviewer/File:Foo bar.png' );
assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Foo_bar.png',
'Old urls (with space) are handled' );
route = router.parseHash( '#mediaviewer/File:Mexican \'Alien\' Piñata.jpg' );
assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Mexican_\'Alien\'_Piñata.jpg',
'Old urls (without percent-encoding) are handled' );
} );
QUnit.test( 'createHashedUrl()', function ( assert ) {
var url,
route = new mw.mmv.routing.MainFileRoute(),
router = new mw.mmv.routing.Router();
url = router.createHashedUrl( route, 'http://example.com/' );
assert.strictEqual( url, 'http://example.com/#/media', 'Url generation works' );
url = router.createHashedUrl( route, 'http://example.com/#foo' );
assert.strictEqual( url, 'http://example.com/#/media', 'Urls with fragments are handled' );
} );
QUnit.test( 'parseLocation()', function ( assert ) {
var location, route,
router = new mw.mmv.routing.Router();
location = { href: 'http://example.com/foo#mediaviewer/File:Foo.png' };
route = router.parseLocation( location );
assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Foo.png', 'Reading location works' );
location = { href: 'http://example.com/foo#/media/File:Foo.png' };
route = router.parseLocation( location );
assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Foo.png', 'Reading location works' );
location = { href: 'http://example.com/foo' };
route = router.parseLocation( location );
assert.notOk( route, 'Reading location without fragment part works' );
} );
QUnit.test( 'parseLocation() with real location', function ( assert ) {
var route, title, hash,
router = new mw.mmv.routing.Router();
// mw.Title does not accept % in page names
this.sandbox.stub( mw, 'Title', function ( name ) {
return {
name: name,
getMain: function () { return name.replace( /^File:/, '' ); }
};
} );
title = new mw.Title( 'File:%40.png' );
hash = router.createHash( new mw.mmv.routing.ThumbnailRoute( title ) );
window.location.hash = hash;
route = router.parseLocation( window.location );
assert.strictEqual( route.fileTitle.getMain(), '%40.png',
'Reading location set via location.hash works' );
if ( window.history ) {
window.history.pushState( null, null, '#' + hash );
route = router.parseLocation( window.location );
assert.strictEqual( route.fileTitle.getMain(), '%40.png',
'Reading location set via pushState() works' );
} else {
assert.ok( true, 'Skipped pushState() test, not supported on this browser' );
}
// reset location, might interfere with other tests
window.location.hash = '#';
} );
QUnit.test( 'tokenizeHash()', function ( assert ) {
var router = new mw.mmv.routing.Router();
router.legacyPrefix = 'legacy';
router.applicationPrefix = 'prefix';
assert.deepEqual( router.tokenizeHash( '#foo/bar' ), [], 'No known prefix' );
assert.deepEqual( router.tokenizeHash( '#prefix' ), [ 'prefix' ], 'Current prefix, with #' );
assert.deepEqual( router.tokenizeHash( 'prefix' ), [ 'prefix' ], 'Current prefix, without #' );
assert.deepEqual( router.tokenizeHash( '#prefix/bar' ), [ 'prefix', 'bar' ], 'Current prefix, with # and element' );
assert.deepEqual( router.tokenizeHash( 'prefix/bar' ), [ 'prefix', 'bar' ], 'Current prefix, with element without #' );
assert.deepEqual( router.tokenizeHash( '#prefix/bar/baz' ), [ 'prefix', 'bar', 'baz' ], 'Current prefix, with # and 2 elements' );
assert.deepEqual( router.tokenizeHash( 'prefix/bar/baz' ), [ 'prefix', 'bar', 'baz' ], 'Current prefix, with 2 elements without #' );
assert.deepEqual( router.tokenizeHash( '#legacy' ), [ 'legacy' ], 'Legacy prefix, with #' );
assert.deepEqual( router.tokenizeHash( 'legacy' ), [ 'legacy' ], 'Legacy prefix, without #' );
assert.deepEqual( router.tokenizeHash( '#legacy/bar' ), [ 'legacy', 'bar' ], 'Legacy prefix, with # and element' );
assert.deepEqual( router.tokenizeHash( 'legacy/bar' ), [ 'legacy', 'bar' ], 'Legacy prefix, with element without #' );
assert.deepEqual( router.tokenizeHash( '#legacy/bar/baz' ), [ 'legacy', 'bar', 'baz' ], 'Legacy prefix, with # and 2 elements' );
assert.deepEqual( router.tokenizeHash( 'legacy/bar/baz' ), [ 'legacy', 'bar', 'baz' ], 'Legacy prefix, with 2 elements without #' );
} );
}() );

View file

@ -1,32 +0,0 @@
/*
* This file is part of the MediaWiki extension MediaViewer.
*
* MediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
QUnit.module( 'mmv.routing.ThumbnailRoute', QUnit.newMwEnvironment() );
QUnit.test( 'Constructor sanity checks', function ( assert ) {
var route,
title = new mw.Title( 'File:Foo.png' );
route = new mw.mmv.routing.ThumbnailRoute( title );
assert.ok( route, 'ThumbnailRoute created successfully' );
assert.ok( mw.mmv.testHelpers.getException( function () {
return new mw.mmv.routing.ThumbnailRoute();
} ), 'Exception is thrown when ThumbnailRoute is created without arguments' );
} );
}() );