diff --git a/resources/skins.minerva.scripts/DownloadIcon.js b/resources/skins.minerva.scripts/DownloadIcon.js index 753ee9a..fab7f3e 100644 --- a/resources/skins.minerva.scripts/DownloadIcon.js +++ b/resources/skins.minerva.scripts/DownloadIcon.js @@ -1,9 +1,31 @@ ( function ( M, track ) { - var msg = mw.msg, MAX_PRINT_TIMEOUT = 3000, GLYPH = 'mf-download', - Icon = M.require( 'mobile.startup/Icon' ); + Icon = M.require( 'mobile.startup/Icon' ), + browser = M.require( 'mobile.startup/Browser' ).getSingleton(); + + /** + * Helper function to retreive the Android version + * @ignore + * @param {String} userAgent User Agent + * @return {Integer} + */ + function getAndroidVersion( userAgent ) { + var match = userAgent.toLowerCase().match( /android\s(\d\.]*)/ ); + return match ? parseInt( match[1] ) : false; + } + + /** + * Helper function to retrieve the Chrome/Chromium version + * @ignore + * @param {String} userAgent User Agent + * @return {Integer} + */ + function getChromeVersion( userAgent ) { + var match = userAgent.toLowerCase().match( /chrom(e|ium)\/(\d+)\./ ); + return match ? parseInt( match[2] ) : false; + } /** * A download icon for triggering print functionality @@ -11,11 +33,13 @@ * @extends Icon * * @param {Skin} skin + * @param {Number[]} [supportedNamespaces] * @constructor */ - function DownloadIcon( skin ) { + function DownloadIcon( skin, supportedNamespaces ) { var options = {}; this.skin = skin; + this.supportedNamespaces = supportedNamespaces || [ 0 ]; options.tagName = 'li'; options.title = msg( 'minerva-download' ); options.name = GLYPH; @@ -23,6 +47,34 @@ } OO.mfExtend( DownloadIcon, Icon, { + /** + * Checks whether DownloadIcon is available for given user agent + * @param {string} userAgent User agent + * @return {Boolean} + */ + isAvailable: function ( userAgent ) { + var androidVersion = getAndroidVersion( userAgent ), + chromeVersion = getChromeVersion( userAgent ), + page = this.skin.page; + + // Download button is restricted to certain namespaces T181152. + // Defaults to 0, in case cached JS has been served. + if ( this.supportedNamespaces.indexOf( page.getNamespaceId() ) === -1 || + page.isMainPage() ) { + // namespace is not supported or it's a main page + return false; + } + + if ( browser.isIos() || chromeVersion === false ) { + // we support only chrome/chromium on desktop/android + return false; + } + if ( ( androidVersion && androidVersion < 5 ) || chromeVersion < 41 ) { + return false; + } + return true; + }, + /** * Replace download icon with a spinner */ diff --git a/resources/skins.minerva.scripts/init.js b/resources/skins.minerva.scripts/init.js index 327399f..ea1ac1a 100644 --- a/resources/skins.minerva.scripts/init.js +++ b/resources/skins.minerva.scripts/init.js @@ -1,6 +1,5 @@ -( function ( M, track, $ ) { +( function ( M, track, config, $ ) { var - config = mw.config, toast = M.require( 'mobile.startup/toast' ), time = M.require( 'mobile.startup/time' ), skin = M.require( 'mobile.init/skin' ), @@ -220,6 +219,29 @@ } ); } + /** + * Initialize and inject the download button + * + * There are many restrictions when we can show the download button, this function should handle + * all device/os/operating system related checks and if device supports printing it will inject + * the Download icon + * @ignore + */ + function appendDownloadButton() { + var downloadIcon = new DownloadIcon( skin, config.get( 'wgMinervaDownloadNamespaces' ) ); + + if ( downloadIcon.isAvailable( navigator.userAgent ) ) { + // Because the page actions are floated to the right, their order in the + // DOM is reversed in the display. The watchstar is last in the DOM and + // left-most in the display. Since we want the download button to be to + // the left of the watchstar, we put it after it in the DOM. + downloadIcon.$el.insertAfter( '#ca-watch' ); + track( 'minerva.downloadAsPDF', { + action: 'buttonVisible' + } ); + } + } + $( function () { // Update anything else that needs enhancing (e.g. watchlist) initModifiedInfo(); @@ -227,29 +249,8 @@ initHistoryLink( $( '.last-modifier-tagline a' ) ); M.on( 'resize', loadTabletModules ); loadTabletModules(); - - if ( - // Download button is restricted to certain namespaces T181152. - // Defaults to 0, in case cached JS has been served. - config.get( 'wgMinervaDownloadNamespaces', [ 0 ] ) - .indexOf( config.get( 'wgNamespaceNumber' ) ) > -1 && - !page.isMainPage() && - // The iOS print dialog does not provide pdf functionality (see T177215) - !browser.isIos() && - // Currently restricted to Chrome (T179529) until we have good fallbacks for browsers - // which do not provide print functionality - window.chrome !== undefined - ) { - // Because the page actions are floated to the right, their order in the - // DOM is reversed in the display. The watchstar is last in the DOM and - // left-most in the display. Since we want the download button to be to - // the left of the watchstar, we put it after it in the DOM. - new DownloadIcon( skin ).$el.insertAfter( '#ca-watch' ); - track( 'minerva.downloadAsPDF', { - action: 'buttonVisible' - } ); - } + appendDownloadButton(); } ); M.define( 'skins.minerva.scripts/overlayManager', overlayManager ); -}( mw.mobileFrontend, mw.track, jQuery ) ); +}( mw.mobileFrontend, mw.track, mw.config, jQuery ) ); diff --git a/tests/qunit/skins.minerva.scripts/test_DownloadIcon.js b/tests/qunit/skins.minerva.scripts/test_DownloadIcon.js index 4ab4e60..37c1050 100644 --- a/tests/qunit/skins.minerva.scripts/test_DownloadIcon.js +++ b/tests/qunit/skins.minerva.scripts/test_DownloadIcon.js @@ -1,7 +1,11 @@ ( function ( M ) { - var Skin = M.require( 'mobile.startup/Skin' ), + var VALID_UA = 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Mobile Safari/537.36', + VALID_SUPPORTED_NAMESPACES = [ 0 ], + Skin = M.require( 'mobile.startup/Skin' ), Deferred = $.Deferred, - DownloadIcon = M.require( 'skins.minerva.scripts/DownloadIcon' ); + DownloadIcon = M.require( 'skins.minerva.scripts/DownloadIcon' ), + browser = M.require( 'mobile.startup/Browser' ).getSingleton(), + Page = M.require( 'mobile.startup/Page' ); QUnit.module( 'Minerva DownloadIcon', { setup: function () { @@ -64,4 +68,81 @@ return d; } ); + + QUnit.module( 'Minerva DownloadIcon.isAvailable()', { + setup: function () { + this.skin = new Skin( { + page: new Page( { + id: 0, + title: 'Test', + isMainPage: false + } ) + } ); + } + } ); + + QUnit.test( 'isAvailable() handles properly correct namespace', function ( assert ) { + var icon = new DownloadIcon( this.skin, VALID_SUPPORTED_NAMESPACES ); + assert.ok( icon.isAvailable( VALID_UA ) ); + } ); + + QUnit.test( 'isAvailable() handles properly not supported namespace', function ( assert ) { + var icon = new DownloadIcon( this.skin, [ 9999 ] ); + assert.notOk( icon.isAvailable( VALID_UA ) ); + } ); + + QUnit.test( 'isAvailable() handles properly main page', function ( assert ) { + var icon; + this.skin.page = new Page( { + id: 0, + title: 'Test', + isMainPage: true + } ); + icon = new DownloadIcon( this.skin, VALID_SUPPORTED_NAMESPACES ); + assert.notOk( icon.isAvailable( VALID_UA ) ); + } ); + + QUnit.test( 'isAvailable() returns false for iOS', function ( assert ) { + var icon; + this.sandbox.stub( browser, 'isIos' ).returns( true ); + icon = new DownloadIcon( this.skin, VALID_SUPPORTED_NAMESPACES ); + assert.notOk( icon.isAvailable( VALID_UA ) ); + } ); + + QUnit.test( 'isAvailable() handles properly non-chrome browsers', function ( assert ) { + var icon = new DownloadIcon( this.skin, VALID_SUPPORTED_NAMESPACES ); + // IPhone 6 Safari + assert.notOk( icon.isAvailable( 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_0_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13A405 Safari/601.1' ) ); + // Nokia Lumia 930 Windows Phone 8.1 + assert.notOk( icon.isAvailable( 'Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; Microsoft; Virtual) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537' ) ); + // Firefox @ Ubuntu + assert.notOk( icon.isAvailable( 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0' ) ); + } ); + + QUnit.test( 'isAvailable() handles properly old devices', function ( assert ) { + var icon = new DownloadIcon( this.skin, VALID_SUPPORTED_NAMESPACES ); + // Samsung Galaxy S5, Android 4.4, Chrome 28 + assert.notOk( icon.isAvailable( 'Mozilla/5.0 (Linux; Android 4.4.2; en-us; SAMSUNG SM-G900F Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.6 Chrome/28.0.1500.94 Mobile Safari/537.36' ) ); + // Samsung Galaxyu S1, Android 4.2.2 Cyanogenmod + built in Samsung Browser + assert.notOk( icon.isAvailable( 'Mozilla/5.0 (Linux; U; Android 4.2.2; en-ca; GT-I9000 Build/JDQ39E) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30 CyanogenMod/10.1.0/galaxysmtd' ) ); + // Samsung Galaxy S3 + assert.notOk( icon.isAvailable( 'Mozilla/5.0 (Linux; Android 4.3; GT-I9300 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36' ) ); + } ); + + QUnit.test( 'isAvailable() handles properly supported browsers', function ( assert ) { + var icon = new DownloadIcon( this.skin, VALID_SUPPORTED_NAMESPACES ); + // Samsung Galaxy S7, Android 6, Chrome 44 + assert.ok( icon.isAvailable( 'Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-G930F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36' ) ); + // Samsung Galaxy A5, Android 7, Samsung Browser 5.2 + assert.ok( icon.isAvailable( 'Mozilla/5.0 (Linux; Android 7.0; SAMSUNG SM-A510F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/5.2 Chrome/51.0.2704.106 Mobile Safari/537.36' ) ); + // Galaxy J2, Android 5, Chrome 65 + assert.ok( icon.isAvailable( 'Mozilla/5.0 (Linux; Android 5.1.1; SM-J200G Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3320.0 Mobile Safari/537.36' ) ); + // Desktop, Chrome 63 + assert.ok( icon.isAvailable( 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36' ) ); + // Desktop, Ubuntu, Chromium 61 + assert.ok( icon.isAvailable( 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/61.0.3163.100 Chrome/61.0.3163.100 Safari/537.36' ) ); + // Galaxy S8, Android 8, Samsung Browser 6.2 + assert.ok( icon.isAvailable( 'Mozilla/5.0 (Linux; Android 7.0; SAMSUNG SM-G950U1 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/6.2 Chrome/56.0.2924.87 Mobile Safari/537.36' ) ); + } ); + }( mw.mobileFrontend ) );