diff --git a/MultimediaViewer.php b/MultimediaViewer.php index fbb449265..d6aea6f18 100644 --- a/MultimediaViewer.php +++ b/MultimediaViewer.php @@ -117,6 +117,14 @@ if ( !isset( $wgMediaViewerImageQueryParameter ) ) { $wgMediaViewerImageQueryParameter = false; } +if ( !isset( $wgMediaViewerRecordViewDuration ) ) { + /** + * If set, record the view duration via a HEAD request. + * @var bool + */ + $wgMediaViewerRecordViewDuration = false; +} + $wgMessagesDirs['MultimediaViewer'] = __DIR__ . '/i18n'; $wgExtensionMessagesFiles['MultimediaViewer'] = __DIR__ . '/MultimediaViewer.i18n.php'; @@ -898,6 +906,7 @@ $wgResourceModules += array( 'mmv.routing', 'mmv.logging.DurationLogger', 'mmv.logging.DimensionLogger', + 'mmv.logging.ViewLogger', 'jquery.fullscreen', 'jquery.hidpi', 'jquery.scrollTo', @@ -1003,6 +1012,17 @@ $wgResourceModules += array( ), ), + 'mmv.logging.ViewLogger' => $wgMediaViewerResourceTemplate + array( + 'scripts' => array( + 'mmv/logging/mmv.logging.ViewLogger.js', + ), + + 'dependencies' => array( + 'mediawiki.Uri', + 'mmv.base', + ), + ), + 'mmv.head' => $wgMediaViewerResourceTemplate + array( 'scripts' => array( 'mmv/mmv.head.js', diff --git a/MultimediaViewerHooks.php b/MultimediaViewerHooks.php index 5e87291be..55a906799 100644 --- a/MultimediaViewerHooks.php +++ b/MultimediaViewerHooks.php @@ -142,7 +142,9 @@ class MultimediaViewerHooks { global $wgMediaViewerActionLoggingSamplingFactorMap, $wgNetworkPerformanceSamplingFactor, $wgMediaViewerDurationLoggingSamplingFactor, $wgMediaViewerDurationLoggingLoggedinSamplingFactor, $wgMediaViewerAttributionLoggingSamplingFactor, $wgMediaViewerDimensionLoggingSamplingFactor, - $wgMediaViewerIsInBeta, $wgMediaViewerUseThumbnailGuessing, $wgMediaViewerImageQueryParameter; + $wgMediaViewerIsInBeta, $wgMediaViewerUseThumbnailGuessing, $wgMediaViewerImageQueryParameter, + $wgMediaViewerRecordViewDuration; + $vars['wgMultimediaViewer'] = array( 'infoLink' => self::$infoLink, 'discussionLink' => self::$discussionLink, @@ -155,6 +157,7 @@ class MultimediaViewerHooks { 'attributionSamplingFactor' => $wgMediaViewerAttributionLoggingSamplingFactor, 'dimensionSamplingFactor' => $wgMediaViewerDimensionLoggingSamplingFactor, 'imageQueryParameter' => $wgMediaViewerImageQueryParameter, + 'recordViewDuration' => $wgMediaViewerRecordViewDuration, 'tooltipDelay' => 1000, ); $vars['wgMediaViewer'] = true; @@ -199,6 +202,7 @@ class MultimediaViewerHooks { 'tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js', 'tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js', 'tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js', + 'tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js', 'tests/qunit/mmv/model/mmv.model.test.js', 'tests/qunit/mmv/model/mmv.model.IwTitle.test.js', 'tests/qunit/mmv/model/mmv.model.TaskQueue.test.js', diff --git a/resources/mmv/logging/mmv.logging.ViewLogger.js b/resources/mmv/logging/mmv.logging.ViewLogger.js new file mode 100644 index 000000000..2c48d2078 --- /dev/null +++ b/resources/mmv/logging/mmv.logging.ViewLogger.js @@ -0,0 +1,172 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer 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. + * + * MultimediaViewer 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 MultimediaViewer. If not, see . + */ + +( function ( mw, $ ) { + var VL; + + /** + * Tracks how long users are viewing images for + * @class mw.mmv.logging.ViewLogger + * @extends mw.Api + * @constructor + * @param {mw.Map} mwConfig mw.config + * @param {Object} window Browser window object + * @param {mw.mmv.logging.ActionLogger} actionLogger ActionLogger object + */ + function ViewLogger( mwConfig, windowObject, actionLogger ) { + var config = mwConfig && mwConfig.get ? mwConfig.get( 'wgMultimediaViewer' ) : false; + + /** + * Was the last image view logged or was logging skipped? + * @property {boolean} + */ + this.wasLastViewLogged = false; + + /** + * Record when the user started looking at the current image + * @property {number} + */ + this.viewStartTime = 0; + + /** + * How long the user has been looking at the current image + * @property {number} + */ + this.viewDuration = 0; + + /** + * The image URL to hit with a HEAD request + * @property {string} + */ + this.url = ''; + + /** + * Should the view duration be recorded through a HEAD request + * @property {boolean} + */ + this.shouldRecordViewDuration = config ? config.recordViewDuration : false; + + /** + * Browser window + * @property {Object} + */ + this.window = windowObject; + + /** + * Action logger + * @property {mw.mmv.logging.ActionLogger} + */ + this.actionLogger = actionLogger; + } + + VL = ViewLogger.prototype; + + /** + * Tracks the unview event of the current image if appropriate + */ + VL.unview = function () { + if ( !this.wasLastViewLogged ) { + return; + } + + this.wasLastViewLogged = false; + this.actionLogger.log( 'image-unview', true ); + }; + + /** + * Starts recording a viewing window for the current image + */ + VL.startViewDuration = function () { + this.viewStartTime = $.now(); + }; + + /** + * Stops recording the viewing window for the current image + */ + VL.stopViewDuration = function () { + if ( this.viewStartTime ) { + this.viewDuration += $.now() - this.viewStartTime; + this.viewStartTime = 0; + } + }; + + /** + * Records the amount of time the current image has been viewed + */ + VL.recordViewDuration = function () { + var uri; + + this.stopViewDuration(); + + if ( this.shouldRecordViewDuration && this.viewDuration > 0 ) { + uri = new mw.Uri( this.url ); + uri.extend( { viewDuration: this.viewDuration } ); + + $.ajax( { + type: 'HEAD', + url: uri.toString() + } ); + + mw.log( 'Image has been viewed for ', this.viewDuration ); + } + + this.viewDuration = 0; + + this.unview(); + }; + + /** + * Sets up the view tracking for the current image + * @param {string} url URL of the image to send a HEAD request to + */ + VL.attach = function ( url ) { + var view = this; + + this.url = url; + this.startViewDuration(); + + $( this.window ) + .off( '.mmv-view-logger' ) + .on( 'beforeunload.mmv-view-logger', function() { + view.recordViewDuration(); + } ) + .on( 'focus.mmv-view-logger', function() { + view.startViewDuration(); + } ) + .on( 'blur.mmv-view-logger', function() { + view.stopViewDuration(); + } ); + }; + + /* + * Stops listening to events + */ + VL.unattach = function() { + $( this.window ).off( '.mmv-view-logger' ); + this.stopViewDuration(); + }; + + /** + * Tracks whether or not the image view event was logged or not (i.e. was it in the logging sample) + * @param {boolean} wasEventLogged Whether the image view event was logged + */ + VL.setLastViewLogged = function ( wasEventLogged ) { + this.wasLastViewLogged = wasEventLogged; + }; + + mw.mmv.logging.ViewLogger = ViewLogger; +}( mediaWiki, jQuery ) ); diff --git a/resources/mmv/mmv.bootstrap.js b/resources/mmv/mmv.bootstrap.js index ad51921ed..053c66b0e 100644 --- a/resources/mmv/mmv.bootstrap.js +++ b/resources/mmv/mmv.bootstrap.js @@ -467,7 +467,7 @@ */ MMVB.getViewer = function () { if ( this.viewer === undefined ) { - this.viewer = new mw.mmv.MultimediaViewer(); + this.viewer = new mw.mmv.MultimediaViewer( mw.config ); this.viewer.setupEventHandlers(); } diff --git a/resources/mmv/mmv.js b/resources/mmv/mmv.js index 11392a351..99ebb9207 100644 --- a/resources/mmv/mmv.js +++ b/resources/mmv/mmv.js @@ -24,10 +24,17 @@ * to manage the viewing experience of such content. * @class mw.mmv.MultimediaViewer * @constructor + * @param {mw.Map} mwConfig mw.config */ - function MultimediaViewer() { + function MultimediaViewer( mwConfig ) { var apiCacheMaxAge = 86400; // one day + /** + * @property {mw.Map} + * @private + */ + this.mwConfig = mwConfig; + /** * @property {mw.mmv.provider.Image} * @private @@ -40,7 +47,7 @@ */ this.imageInfoProvider = new mw.mmv.provider.ImageInfo( new mw.mmv.logging.Api( 'imageinfo' ), // Short-circuit, don't fallback, to save some tiny amount of time - { language: mw.config.get( 'wgUserLanguage', false ) || mw.config.get( 'wgContentLanguage', 'en' ) } + { language: this.mwConfig.get( 'wgUserLanguage', false ) || this.mwConfig.get( 'wgContentLanguage', 'en' ) } ); /** @@ -106,10 +113,9 @@ this.documentTitle = document.title; /** - * Was the last image view logged or was logging skipped? - * @property {boolean} + * @property {mw.mmv.logging.ViewLogger} view - */ - this.wasLastViewLogged = false; + this.viewLogger = new mw.mmv.logging.ViewLogger( this.mwConfig, window, mw.mmv.actionLogger ); } MMVP = MultimediaViewer.prototype; @@ -424,14 +430,10 @@ this.ui.canvas.unblur(); } - mw.mmv.actionLogger.log( 'image-view' ).then( function ( wasEventLogged ) { - viewer.wasLastViewLogged = wasEventLogged; + this.viewLogger.attach( thumbnail.url ); - if ( viewer.wasLastViewLogged ) { - $( window ).on( 'beforeunload.unview', function() { viewer.unview(); } ); - } else { - $( window ).off( 'beforeunload.unview' ); - } + mw.mmv.actionLogger.log( 'image-view' ).then( function ( wasEventLogged ) { + viewer.viewLogger.setLastViewLogged( wasEventLogged ); } ); }; @@ -797,6 +799,8 @@ var thumb; if ( index < this.thumbs.length && index >= 0 ) { + this.viewLogger.recordViewDuration(); + thumb = this.thumbs[ index ]; this.loadImage( thumb.image, thumb.$thumb.clone()[0] ); } @@ -806,7 +810,6 @@ * Opens the next image */ MMVP.nextImage = function () { - this.unview(); mw.mmv.actionLogger.log( 'next-image' ); this.loadIndex( this.currentIndex + 1 ); }; @@ -815,7 +818,6 @@ * Opens the previous image */ MMVP.prevImage = function () { - this.unview(); mw.mmv.actionLogger.log( 'prev-image' ); this.loadIndex( this.currentIndex - 1 ); }; @@ -826,7 +828,8 @@ MMVP.close = function () { var windowTitle; - this.unview(); + this.viewLogger.recordViewDuration(); + this.viewLogger.unattach(); windowTitle = this.createDocumentTitle( null ); @@ -951,17 +954,5 @@ mw.loader.load( [ 'mmv.ui.reuse.share', 'mmv.ui.reuse.embed', 'mmv.ui.reuse.download', 'moment' ] ); }; - /** - * Tracks the unview event of the current image if appropriate - */ - MMVP.unview = function () { - if ( !this.wasLastViewLogged ) { - return; - } - - this.wasLastViewLogged = false; - mw.mmv.actionLogger.log( 'image-unview', true ); - }; - mw.mmv.MultimediaViewer = MultimediaViewer; }( mediaWiki, jQuery ) ); diff --git a/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js b/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js new file mode 100644 index 000000000..fefff6909 --- /dev/null +++ b/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js @@ -0,0 +1,75 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.logging.ViewLogger', QUnit.newMwEnvironment( { + setup: function () { + this.clock = this.sandbox.useFakeTimers(); + } + } ) ); + + QUnit.test( 'unview()', 4, function ( assert ) { + var logger = { log : $.noop }, + viewLogger = new mw.mmv.logging.ViewLogger( {}, {}, logger ); + + this.sandbox.stub( logger, 'log' ); + + viewLogger.unview(); + + assert.ok( !logger.log.called, 'action logger not called' ); + + viewLogger.setLastViewLogged( false ); + viewLogger.unview(); + + assert.ok( !logger.log.called, 'action logger not called' ); + + viewLogger.setLastViewLogged( true ); + viewLogger.unview(); + + assert.ok( logger.log.calledOnce, 'action logger called' ); + + viewLogger.unview(); + + assert.ok( logger.log.calledOnce, 'action logger not called again' ); + } ); + + QUnit.test( 'focus and blur', 1, function ( assert ) { + var fakeWindow = $( '
' ), + viewLogger = new mw.mmv.logging.ViewLogger( {}, fakeWindow, { log : $.noop } ); + + this.clock.tick( 1 ); // This is just so that $.now() > 0 in the fake timer environment + + viewLogger.attach(); + + this.clock.tick( 5 ); + + fakeWindow.triggerHandler( 'blur' ); + + this.clock.tick( 2 ); + + fakeWindow.triggerHandler( 'focus' ); + + this.clock.tick( 3 ); + + fakeWindow.triggerHandler( 'blur' ); + + this.clock.tick( 4 ); + + assert.strictEqual( viewLogger.viewDuration, 8, 'Only focus duration was logged' ); + } ); + + QUnit.test( 'stopViewDuration before startViewDuration', 1, function ( assert ) { + var viewLogger = new mw.mmv.logging.ViewLogger( {}, {}, { log : $.noop } ); + + this.clock.tick( 1 ); // This is just so that $.now() > 0 in the fake timer environment + + viewLogger.stopViewDuration(); + + this.clock.tick( 2 ); + + viewLogger.startViewDuration(); + + this.clock.tick( 3 ); + + viewLogger.stopViewDuration(); + + assert.strictEqual( viewLogger.viewDuration, 3, 'Only last timeframe was logged' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/mmv/mmv.bootstrap.test.js b/tests/qunit/mmv/mmv.bootstrap.test.js index 53dda695d..2be14f9d4 100644 --- a/tests/qunit/mmv/mmv.bootstrap.test.js +++ b/tests/qunit/mmv/mmv.bootstrap.test.js @@ -275,7 +275,7 @@ var bootstrap, $div, $link, - viewer = new mw.mmv.MultimediaViewer(), + viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), fname = 'valid', imgSrc = '/' + fname + '.jpg/300px-' + fname + '.jpg', imgRegex = new RegExp( imgSrc + '$' ); diff --git a/tests/qunit/mmv/mmv.lightboxinterface.test.js b/tests/qunit/mmv/mmv.lightboxinterface.test.js index 1a61748f5..b79152b0c 100644 --- a/tests/qunit/mmv/mmv.lightboxinterface.test.js +++ b/tests/qunit/mmv/mmv.lightboxinterface.test.js @@ -136,7 +136,7 @@ var buttonOffset, panelBottom, oldRevealButtonsAndFadeIfNeeded, lightbox = new mw.mmv.LightboxInterface(), - viewer = new mw.mmv.MultimediaViewer(), + viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), oldFnEnterFullscreen = $.fn.enterFullscreen, oldFnExitFullscreen = $.fn.exitFullscreen; @@ -245,7 +245,7 @@ } ); QUnit.test( 'Keyboard prev/next', 2, function ( assert ) { - var viewer = new mw.mmv.MultimediaViewer(), + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), lightbox = new mw.mmv.LightboxInterface(); viewer.setupEventHandlers(); diff --git a/tests/qunit/mmv/mmv.test.js b/tests/qunit/mmv/mmv.test.js index 14eb36f51..50b1e091e 100644 --- a/tests/qunit/mmv/mmv.test.js +++ b/tests/qunit/mmv/mmv.test.js @@ -2,7 +2,7 @@ QUnit.module( 'mmv', QUnit.newMwEnvironment() ); QUnit.test( 'eachPrealoadableLightboxIndex()', 11, function ( assert ) { - var viewer = new mw.mmv.MultimediaViewer(), + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), expectedIndices, i; @@ -31,7 +31,7 @@ QUnit.test( 'Hash handling', 7, function ( assert ) { var oldUnattach, - viewer = new mw.mmv.MultimediaViewer(), + viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), ui = new mw.mmv.LightboxInterface(), imageSrc = 'Foo bar.jpg', image = { filePageTitle: new mw.Title( 'File:' + imageSrc ) }; @@ -102,7 +102,7 @@ QUnit.test( 'Progress', 4, function ( assert ) { var imageDeferred = $.Deferred(), - viewer = new mw.mmv.MultimediaViewer(); + viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ); viewer.thumbs = []; viewer.displayPlaceholderThumbnail = $.noop; @@ -158,7 +158,7 @@ secondImageDeferred = $.Deferred(), firstImage = { index: 1, filePageTitle : new mw.Title( 'File:First.jpg' ) }, secondImage = { index: 2, filePageTitle : new mw.Title( 'File:Second.jpg' ) }, - viewer = new mw.mmv.MultimediaViewer(); + viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ); viewer.thumbs = []; viewer.displayPlaceholderThumbnail = $.noop; @@ -256,7 +256,7 @@ } ); QUnit.test( 'resetBlurredThumbnailStates', 4, function ( assert ) { - var viewer = new mw.mmv.MultimediaViewer(); + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ); assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); @@ -271,7 +271,7 @@ } ); QUnit.test( 'Placeholder first, then real thumbnail', 4, function ( assert ) { - var viewer = new mw.mmv.MultimediaViewer(); + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ); viewer.setImage = $.noop; viewer.ui = { canvas : { @@ -286,14 +286,14 @@ assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); - viewer.displayRealThumbnail(); + viewer.displayRealThumbnail( { url : undefined } ); assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' ); assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); } ); QUnit.test( 'Placeholder first, then real thumbnail - missing size', 4, function ( assert ) { - var viewer = new mw.mmv.MultimediaViewer(); + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ); viewer.currentIndex = 1; viewer.setImage = $.noop; @@ -309,14 +309,14 @@ assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' ); assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' ); - viewer.displayRealThumbnail(); + viewer.displayRealThumbnail( { url : undefined } ); 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(); + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ); viewer.setImage = $.noop; viewer.ui = { @@ -326,7 +326,7 @@ unblur: $.noop } }; - viewer.displayRealThumbnail(); + viewer.displayRealThumbnail( { url : undefined } ); assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' ); assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' ); @@ -338,7 +338,7 @@ } ); QUnit.test( 'displayRealThumbnail', 2, function ( assert ) { - var viewer = new mw.mmv.MultimediaViewer(); + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ); viewer.setImage = $.noop; viewer.ui = { canvas : { @@ -348,16 +348,16 @@ viewer.blurredThumbnailShown = true; // Should not result in an unblurWithAnimation animation (image cache from cache) - viewer.displayRealThumbnail( undefined, undefined, undefined, 5 ); + viewer.displayRealThumbnail( { url : undefined }, undefined, undefined, 5 ); assert.ok( !viewer.ui.canvas.unblurWithAnimation.called, 'There should not be an unblurWithAnimation animation' ); // Should result in an unblurWithAnimation (image didn't come from cache) - viewer.displayRealThumbnail( undefined, undefined, undefined, 1000 ); + viewer.displayRealThumbnail( { url : undefined }, undefined, undefined, 1000 ); assert.ok( viewer.ui.canvas.unblurWithAnimation.called, 'There should be an unblurWithAnimation animation' ); } ); QUnit.test( 'New image loaded while another one is loading', 5, function ( assert ) { - var viewer = new mw.mmv.MultimediaViewer(), + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), firstImageDeferred = $.Deferred(), secondImageDeferred = $.Deferred(), firstLigthboxInfoDeferred = $.Deferred(), @@ -419,7 +419,7 @@ QUnit.test( 'Events are not trapped after the viewer is closed', 0, function( assert ) { var i, j, k, eventParameters, - viewer = new mw.mmv.MultimediaViewer(), + viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), $document = $( document ), $qf = $( '#qunit-fixture' ), eventTypes = [ 'keydown', 'keyup', 'keypress', 'click', 'mousedown', 'mouseup' ], @@ -490,7 +490,7 @@ } ); QUnit.test( 'Refuse to load too-big thumbnails', 1, function ( assert ) { - var viewer = new mw.mmv.MultimediaViewer(), + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), intendedWidth = 50, title = mw.Title.newFromText( 'File:Foobar.svg' ); @@ -507,7 +507,7 @@ thumbnailInfoStub, imageStub, promise, - viewer = new mw.mmv.MultimediaViewer(), + viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), sandbox = this.sandbox, oldUseThumbnailGuessing = mw.config.get( 'wgMultimediaViewer' ).useThumbnailGuessing, file = new mw.Title( 'File:Copyleft.svg' ), @@ -607,7 +607,7 @@ } ); QUnit.test( 'document.title', 2, function ( assert ) { - var viewer = new mw.mmv.MultimediaViewer(), + var viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ), bootstrap = new mw.mmv.MultimediaViewerBootstrap(), title = new mw.Title( 'File:This_should_show_up_in_document_title.png'), oldDocumentTitle = document.title; diff --git a/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js b/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js index c94fff073..e3b1ecf25 100644 --- a/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js +++ b/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js @@ -22,7 +22,7 @@ var i = 0, $qf = $( '#qunit-fixture' ), buttons = new mw.mmv.ui.CanvasButtons( $qf, $( '
' ), $( '
' ) ), - viewer = new mw.mmv.MultimediaViewer(); + viewer = new mw.mmv.MultimediaViewer( { get : $.noop } ); viewer.ui = {};