resourceloader: Use feature test instead of UA sniffing
authorTimo Tijhof <krinklemail@gmail.com>
Sat, 20 Feb 2016 15:22:22 +0000 (15:22 +0000)
committerOri.livneh <ori@wikimedia.org>
Mon, 18 Apr 2016 20:27:43 +0000 (20:27 +0000)
Converting to a feature test reduces complexity of the startup module, and
makes the blacklist more meaningful - allowing us to safely remove some
unnecessary feature guards and polyfills.

It'll also provide unsupported browsers a much better experience. Previously,
unsupported browsers got the payload for Grade A runtime. Many of which would
likely fatal due to unimplemented DOM and JS methods. With the capability
filter in this commit, the client will only initialise the environment if the
browser is capable - falling back to Grade C otherwise.

The feature-test that replaces the user-agent filter in this commit is 99.96%
effectively the same - based on a 7 day analytics campaign on all Wikimedia
wikis (see T102318 for details).

Bug: T102318
Change-Id: I847b3f5f80f1eef3f57bbe7518768468e271082b

resources/src/startup.js
tests/qunit/suites/resources/startup.test.js

index e53e5f3..312e745 100644 (file)
@@ -1,7 +1,7 @@
 /**
- * This script provides a function which is run to evaluate whether or not to
- * continue loading jQuery and the MediaWiki modules. This code should work on
- * even the most ancient of browsers, so be very careful when editing.
+ * Code in this file MUST work on even the most ancient of browsers!
+ *
+ * This file is where we decide whether to initialise the modern run-time.
  */
 /*jshint unused: false, evil: true */
 /*globals mw, RLQ: true, NORLQ: true, $VARS, $CODE, performance */
@@ -15,53 +15,60 @@ var mediaWikiLoadStart = ( new Date() ).getTime(),
 mwPerformance.mark( 'mwLoadStart' );
 
 /**
- * Returns false for Grade C supported browsers.
+ * See <https://www.mediawiki.org/wiki/Compatibility#Browsers>
+ *
+ * Capabilities required for modern run-time:
+ * - DOM Level 4 & Selectors API Level 1
+ * - HTML5 & Web Storage
+ * - DOM Level 2 Events
+ *
+ * Browsers we support in our modern run-time (Grade A):
+ * - Chrome
+ * - IE 9+
+ * - Firefox 3.5+
+ * - Safari 4+
+ * - Opera 10.5+
+ * - Mobile Safari (iOS 1+)
+ * - Android 2.0+
  *
- * This function should only be used by the Startup module, do not expand it to
- * be generally useful beyond startup.
+ * Browsers we support in our no-javascript run-time (Grade C):
+ * - IE 6+
+ * - Firefox 3+
+ * - Safari 3+
+ * - Opera 10+
+ * - WebOS < 1.5
+ * - PlayStation
+ * - Symbian-based browsers
+ * - NetFront-based browser
+ * - Opera Mini
+ * - Nokia's Ovi Browser
+ * - MeeGo's browser
+ * - Google Glass
  *
- * See also:
- * - https://www.mediawiki.org/wiki/Compatibility#Browsers
- * - https://jquery.com/browser-support/
+ * Other browsers that pass the check are considered Grade X.
  */
-function isCompatible( ua ) {
-       if ( ua === undefined ) {
-               ua = navigator.userAgent;
-       }
+function isCompatible( str ) {
+       var ua = str || navigator.userAgent;
+       return !!(
+               // http://caniuse.com/#feat=queryselector
+               'querySelector' in document
+
+               // http://caniuse.com/#feat=namevalue-storage
+               // https://developer.blackberry.com/html5/apis/v1_0/localstorage.html
+               // https://blog.whatwg.org/this-week-in-html-5-episode-30
+               && 'localStorage' in window
+
+               // http://caniuse.com/#feat=addeventlistener
+               && 'addEventListener' in window
 
-       // Browsers with outdated or limited JavaScript engines get the no-JS experience
-       return !(
-               // Internet Explorer < 9
-               ( ua.indexOf( 'MSIE' ) !== -1 && parseFloat( ua.split( 'MSIE' )[ 1 ] ) < 9 ) ||
-               // Firefox < 3
-               ( ua.indexOf( 'Firefox/' ) !== -1 && parseFloat( ua.split( 'Firefox/' )[ 1 ] ) < 3 ) ||
-               // Opera < 12
-               ( ua.indexOf( 'Opera/' ) !== -1 && ( ua.indexOf( 'Version/' ) === -1 ?
-                       // "Opera/x.y"
-                       parseFloat( ua.split( 'Opera/' )[ 1 ] ) < 10 :
-                       // "Opera/9.80 ... Version/x.y"
-                       parseFloat( ua.split( 'Version/' )[ 1 ] ) < 12
-               ) ) ||
-               // "Mozilla/0.0 ... Opera x.y"
-               ( ua.indexOf( 'Opera ' ) !== -1 && parseFloat( ua.split( ' Opera ' )[ 1 ] ) < 10 ) ||
-               // BlackBerry < 6
-               ua.match( /BlackBerry[^\/]*\/[1-5]\./ ) ||
-               // Open WebOS < 1.5
-               ua.match( /webOS\/1\.[0-4]/ ) ||
-               // Anything PlayStation based.
-               ua.match( /PlayStation/i ) ||
-               // Any Symbian based browsers
-               ua.match( /SymbianOS|Series60/ ) ||
-               // Any NetFront based browser
-               ua.match( /NetFront/ ) ||
-               // Opera Mini, all versions
-               ua.match( /Opera Mini/ ) ||
-               // Nokia's Ovi Browser
-               ua.match( /S40OviBrowser/ ) ||
-               // MeeGo's browser
-               ua.match( /MeeGo/ ) ||
-               // Google Glass browser groks JS but UI is too limited
-               ( ua.match( /Glass/ ) && ua.match( /Android/ ) )
+               // Hardcoded exceptions for browsers that pass the requirement but we don't want to
+               // support in the modern run-time.
+               && !(
+                       ua.match( /webOS\/1\.[0-4]/ ) ||
+                       ua.match( /PlayStation/i ) ||
+                       ua.match( /SymbianOS|Series60|NetFront|Opera Mini|S40OviBrowser|MeeGo/ ) ||
+                       ( ua.match( /Glass/ ) && ua.match( /Android/ ) )
+               )
        );
 }
 
index d3f528c..2934b39 100644 (file)
@@ -1,7 +1,9 @@
 /*global isCompatible: true */
 ( function ( $ ) {
        var testcases = {
-               gradeA: [
+               tested: [
+                       /* Grade A */
+
                        // Chrome
                        'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.205 Safari/534.16',
                        // Firefox 4+
                        'Mozilla/5.0 (ipod: U;CPU iPhone OS 2_2 like Mac OS X: es_es) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3',
                        'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3',
                        // Android
-                       'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17'
-               ],
-               gradeC: [
+                       'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17',
+
+                       /* Grade C */
+
                        // Internet Explorer < 9
                        'Mozilla/2.0 (compatible; MSIE 3.03; Windows 3.1)',
                        'Mozilla/4.0 (compatible; MSIE 4.01; Windows 95)',
                        // BlackBerry < 6
                        'BlackBerry9300/5.0.0.716 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/133',
                        'BlackBerry7250/4.0.0 Profile/MIDP-2.0 Configuration/CLDC-1.1',
-                       // Open WebOS < 1.5 (Palm Pre, Palm Pixi)
-                       'Mozilla/5.0 (webOS/1.0; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0',
-                       'Mozilla/5.0 (webOS/1.4.0; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pixi/1.1 ',
-                       // SymbianOS
-                       'NokiaN95_8GB-3;Mozilla/5.0 SymbianOS/9.2;U;Series60/3.1 NokiaN95_8GB-3/11.2.011 Profile/MIDP-2.0 Configuration/CLDC-1.1 AppleWebKit/413 (KHTML, like Gecko)',
-                       'Nokia7610/2.0 (5.0509.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0 ',
-                       'Mozilla/5.0 (SymbianOS/9.1; U; [en]; SymbianOS/91 Series60/3.0) AppleWebKit/413 (KHTML, like Gecko) Safari/413',
-                       'Mozilla/5.0 (SymbianOS/9.3; Series60/3.2 NokiaE52-2/091.003; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.1.34 Mobile Safari/533.4',
-                       // NetFront
-                       'Mozilla/4.0 (compatible; Linux 2.6.10) NetFront/3.3 Kindle/1.0 (screen 600x800)',
-                       'Mozilla/4.0 (compatible; Linux 2.6.22) NetFront/3.4 Kindle/2.0 (screen 824x1200; rotate)',
-                       'Mozilla/4.08 (Windows; Mobile Content Viewer/1.0) NetFront/3.2',
-                       // Opera Mini
-                       'Opera/9.80 (J2ME/MIDP; Opera Mini/3.1.10423/22.387; U; en) Presto/2.5.25 Version/10.54',
-                       'Opera/9.50 (J2ME/MIDP; Opera Mini/4.0.10031/298; U; en)',
-                       'Opera/9.80 (J2ME/MIDP; Opera Mini/6.24093/26.1305; U; en) Presto/2.8.119 Version/10.54',
-                       'Opera/9.80 (Android; Opera Mini/7.29530/27.1407; U; en) Presto/2.8.119 Version/11.10',
-                       // Ovi Browser
-                       'Mozilla/5.0 (Series40; NokiaX3-02/05.60; Profile/MIDP-2.1 Configuration/CLDC-1.1) Gecko/20100401 S40OviBrowser/3.2.0.0.6',
-                       'Mozilla/5.0 (Series40; Nokia305/05.92; Profile/MIDP-2.1 Configuration/CLDC-1.1) Gecko/20100401 S40OviBrowser/3.7.0.0.11',
-                       // Google Glass
-                       'Mozilla/5.0 (Linux; U; Android 4.0.4; en-us; Glass 1 Build/IMM76L; XE11) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
-                       // MeeGo
-                       'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13'
-               ],
-               // No explicit support for or against these browsers, they're given a shot at Grade A.
-               gradeX: [
+
+                       /* Grade X */
+
                        // Firefox 3.6
                        'Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3',
                        // Gecko
                        'I\'m an unknown browser',
                        // Empty
                        ''
+               ],
+               blacklisted: [
+                       /* Grade C */
+
+                       // Open WebOS < 1.5 (Palm Pre, Palm Pixi)
+                       'Mozilla/5.0 (webOS/1.0; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0',
+                       'Mozilla/5.0 (webOS/1.4.0; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pixi/1.1 ',
+                       // SymbianOS
+                       'NokiaN95_8GB-3;Mozilla/5.0 SymbianOS/9.2;U;Series60/3.1 NokiaN95_8GB-3/11.2.011 Profile/MIDP-2.0 Configuration/CLDC-1.1 AppleWebKit/413 (KHTML, like Gecko)',
+                       'Nokia7610/2.0 (5.0509.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0 ',
+                       'Mozilla/5.0 (SymbianOS/9.1; U; [en]; SymbianOS/91 Series60/3.0) AppleWebKit/413 (KHTML, like Gecko) Safari/413',
+                       'Mozilla/5.0 (SymbianOS/9.3; Series60/3.2 NokiaE52-2/091.003; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.1.34 Mobile Safari/533.4',
+                       // NetFront
+                       'Mozilla/4.0 (compatible; Linux 2.6.10) NetFront/3.3 Kindle/1.0 (screen 600x800)',
+                       'Mozilla/4.0 (compatible; Linux 2.6.22) NetFront/3.4 Kindle/2.0 (screen 824x1200; rotate)',
+                       'Mozilla/4.08 (Windows; Mobile Content Viewer/1.0) NetFront/3.2',
+                       // Opera Mini
+                       'Opera/9.80 (J2ME/MIDP; Opera Mini/3.1.10423/22.387; U; en) Presto/2.5.25 Version/10.54',
+                       'Opera/9.50 (J2ME/MIDP; Opera Mini/4.0.10031/298; U; en)',
+                       'Opera/9.80 (J2ME/MIDP; Opera Mini/6.24093/26.1305; U; en) Presto/2.8.119 Version/10.54',
+                       'Opera/9.80 (Android; Opera Mini/7.29530/27.1407; U; en) Presto/2.8.119 Version/11.10',
+                       // Ovi Browser
+                       'Mozilla/5.0 (Series40; NokiaX3-02/05.60; Profile/MIDP-2.1 Configuration/CLDC-1.1) Gecko/20100401 S40OviBrowser/3.2.0.0.6',
+                       'Mozilla/5.0 (Series40; Nokia305/05.92; Profile/MIDP-2.1 Configuration/CLDC-1.1) Gecko/20100401 S40OviBrowser/3.7.0.0.11',
+                       // Google Glass
+                       'Mozilla/5.0 (Linux; U; Android 4.0.4; en-us; Glass 1 Build/IMM76L; XE11) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+                       // MeeGo
+                       'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13'
                ]
        };
 
        QUnit.module( 'startup', QUnit.newMwEnvironment() );
 
-       QUnit.test( 'isCompatible( Grade A )', testcases.gradeA.length, function ( assert ) {
-               $.each( testcases.gradeA, function ( i, ua ) {
+       QUnit.test( 'isCompatible( featureTestable )', testcases.tested.length, function ( assert ) {
+               $.each( testcases.tested, function ( i, ua ) {
                                assert.strictEqual( isCompatible( ua ), true, ua );
                        }
                );
        } );
 
-       QUnit.test( 'isCompatible( Grade C )', testcases.gradeC.length, function ( assert ) {
-               $.each( testcases.gradeC, function ( i, ua ) {
+       QUnit.test( 'isCompatible( blacklisted )', testcases.blacklisted.length, function ( assert ) {
+               $.each( testcases.blacklisted, function ( i, ua ) {
                                assert.strictEqual( isCompatible( ua ), false, ua );
                        }
                );
        } );
-
-       QUnit.test( 'isCompatible( Grade X )', testcases.gradeX.length, function ( assert ) {
-               $.each( testcases.gradeX, function ( i, ua ) {
-                               assert.strictEqual( isCompatible( ua ), true, ua );
-                       }
-               );
-       } );
-
 }( jQuery ) );