jenkins-bot has submitted this change and it was merged.

Change subject: build: Add Karma task for automated QUnit testing in browsers
......................................................................


build: Add Karma task for automated QUnit testing in browsers

To use, first run 'npm install'. Then run 'grunt qunit' to start
the test suite in Chrome.

Squashed cherry-picks from master:
* ba50b32556: SpecialJavaScriptTest: Add export feat
* 7605f112e4: jquery.mwExtension.test: Fix qunit-fixture conflict
* 365b6f3af9: mediawiki.jqueryMsg.test: Fix crazy concurrency
* 945c1efe37: build: Add Karma task
* 8d92aaf83e: build: Clean up Gruntfile
* 2258f25053: build: Add assert-mw-env task
* dcbbc0489c: build: Increase browserNoActivityTimeout to 60s
* fa4ba8dbd7: build: Declare grunt-cli dependency

Change-Id: I4e96da137340a28789b38940e75d4b6b8bc5d76a
---
M Gruntfile.js
M includes/OutputPage.php
M includes/specials/SpecialJavaScriptTest.php
M languages/i18n/en.json
M languages/i18n/qqq.json
M package.json
M resources/Resources.php
M resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
M tests/qunit/QUnitTestResources.php
M tests/qunit/data/testrunner.js
M tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
M tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
12 files changed, 376 insertions(+), 169 deletions(-)

Approvals:
  Krinkle: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Gruntfile.js b/Gruntfile.js
index 9badf03..e4d96a0 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -4,6 +4,13 @@
        grunt.loadNpmTasks( 'grunt-contrib-jshint' );
        grunt.loadNpmTasks( 'grunt-contrib-watch' );
        grunt.loadNpmTasks( 'grunt-jscs-checker' );
+       grunt.loadNpmTasks( 'grunt-karma' );
+
+       var wgServer = process.env.MW_SERVER,
+               wgScriptPath = process.env.MW_SCRIPT_PATH,
+               karmaProxy = {};
+
+       karmaProxy[wgScriptPath] = wgServer + wgScriptPath;
 
        grunt.initConfig( {
                pkg: grunt.file.readJSON( 'package.json' ),
@@ -38,6 +45,29 @@
                        ],
                        tasks: 'test'
                },
+               karma: {
+                       options: {
+                               proxies: karmaProxy,
+                               files: [ {
+                                       pattern: wgServer + wgScriptPath + 
'/index.php?title=Special:JavaScriptTest/qunit/export',
+                                       watched: false,
+                                       included: true,
+                                       served: false
+                               } ],
+                               frameworks: [ 'qunit' ],
+                               reporters: [ 'dots' ],
+                               singleRun: true,
+                               autoWatch: false,
+                               // Some tests in extensions don't yield for 
more than the default 10s (T89075)
+                               browserNoActivityTimeout: 60 * 1000
+                       },
+                       main: {
+                               browsers: [ 'Chrome' ]
+                       },
+                       more: {
+                               browsers: [ 'Chrome', 'Firefox' ]
+                       }
+               },
                copy: {
                        jsduck: {
                                src: 'resources/**/*',
@@ -50,7 +80,22 @@
                }
        } );
 
+       grunt.registerTask( 'assert-mw-env', function () {
+               if ( !process.env.MW_SERVER ) {
+                       grunt.log.error( 'Environment variable MW_SERVER must 
be set.\n' +
+                               'Set this like $wgServer, e.g. 
"http://localhost";'
+                       );
+               }
+               if ( !process.env.MW_SCRIPT_PATH ) {
+                       grunt.log.error( 'Environment variable MW_SCRIPT_PATH 
must be set.\n' +
+                               'Set this like $wgScriptPath, e.g. "/w"');
+               }
+               return !!( process.env.MW_SERVER && process.env.MW_SCRIPT_PATH 
);
+       } );
+
        grunt.registerTask( 'lint', ['jshint', 'jscs'] );
+       grunt.registerTask( 'qunit', [ 'assert-mw-env', 'karma:main' ] );
+
        grunt.registerTask( 'test', ['lint'] );
-       grunt.registerTask( 'default', ['test'] );
+       grunt.registerTask( 'default', 'test' );
 };
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index 4a72ba3..4e4ed1c 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -2632,7 +2632,7 @@
         * @param bool $loadCall If true, output an (asynchronous) 
mw.loader.load() call rather than a "<script src='...'>" tag
         * @return string The html "<script>", "<link>" and "<style>" tags
         */
-       protected function makeResourceLoaderLink( $modules, $only, $useESI = 
false, array $extraQuery = array(), $loadCall = false ) {
+       public function makeResourceLoaderLink( $modules, $only, $useESI = 
false, array $extraQuery = array(), $loadCall = false ) {
                global $wgResourceLoaderUseESI;
 
                $modules = (array)$modules;
diff --git a/includes/specials/SpecialJavaScriptTest.php 
b/includes/specials/SpecialJavaScriptTest.php
index 7982d5c..c852a06 100644
--- a/includes/specials/SpecialJavaScriptTest.php
+++ b/includes/specials/SpecialJavaScriptTest.php
@@ -27,12 +27,10 @@
 class SpecialJavaScriptTest extends SpecialPage {
 
        /**
-        * @var $frameworks Array: Mapping of framework ids and their 
initilizer methods
-        * in this class. If a framework is requested but not in this array,
-        * the 'unknownframework' error is served.
+       * @var array Supported frameworks.
         */
-       static $frameworks = array(
-               'qunit' => 'initQUnitTesting',
+       private static $frameworks = array(
+               'qunit',
        );
 
        public function __construct() {
@@ -45,43 +43,70 @@
                $this->setHeaders();
                $out->disallowUserJs();
 
+               if ( $par === null ) {
+                       // No framework specified
+                       $out->setStatusCode( 404 );
+                       $out->setPageTitle( $this->msg( 'javascripttest' ) );
+                       $out->addHTML(
+                               $this->msg( 
'javascripttest-pagetext-noframework' )->parseAsBlock()
+                               . $this->getFrameworkListHtml()
+                       );
+                       return;
+               }
+
+               // Determine framework and mode
+               $pars = explode( '/', $par, 2 );
+
+               $framework = $pars[0];
+               if ( !in_array( $framework, self::$frameworks ) ) {
+                       // Framework not found
+                       $out->setStatusCode( 404 );
+                       $out->addHTML(
+                               '<div class="error">'
+                               . $this->msg( 
'javascripttest-pagetext-unknownframework' )
+                                       ->plaintextParams( $par 
)->parseAsBlock()
+                               . '</div>'
+                               . $this->getFrameworkListHtml()
+                       );
+                       return;
+               }
+
+               // This special page is disabled by default 
($wgEnableJavaScriptTest), and contains
+               // no sensitive data. In order to allow TestSwarm to embed it 
into a test client window,
+               // we need to allow iframing of this page.
+               $out->allowClickjacking();
+               $out->setSubtitle(
+                       $this->msg( 'javascripttest-backlink' )
+                               ->rawParams( Linker::linkKnown( 
$this->getPageTitle() ) )
+               );
+
+               // Custom actions
+               if ( isset( $pars[1] ) ) {
+                       $action = $pars[1];
+                       if ( !in_array( $action, array( 'export', 'plain' ) ) ) 
{
+                               $out->setStatusCode( 404 );
+                               $out->addHTML(
+                                       '<div class="error">'
+                                       . $this->msg( 
'javascripttest-pagetext-unknownaction' )
+                                               ->plaintextParams( $action 
)->parseAsBlock()
+                                       . '</div>'
+                               );
+                               return;
+                       }
+                       $method = $action . ucfirst( $framework );
+                       $this->$method();
+                       return;
+               }
+
                $out->addModules( 'mediawiki.special.javaScriptTest' );
 
-               // Determine framework
-               $pars = explode( '/', $par );
-               $framework = strtolower( $pars[0] );
-
-               // No framework specified
-               if ( $par == '' ) {
-                       $out->setPageTitle( $this->msg( 'javascripttest' ) );
-                       $summary = $this->wrapSummaryHtml(
-                               $this->msg( 
'javascripttest-pagetext-noframework' )->escaped() .
-                                       $this->getFrameworkListHtml(),
-                               'noframework'
-                       );
-                       $out->addHtml( $summary );
-               } elseif ( isset( self::$frameworks[$framework] ) ) {
-                       // Matched! Display proper title and initialize the 
framework
-                       $out->setPageTitle( $this->msg(
-                               'javascripttest-title',
-                               // Messages: javascripttest-qunit-name
-                               $this->msg( "javascripttest-$framework-name" 
)->plain()
-                       ) );
-                       $out->setSubtitle( $this->msg( 
'javascripttest-backlink' )
-                               ->rawParams( Linker::linkKnown( 
$this->getPageTitle() ) ) );
-                       $this->{self::$frameworks[$framework]}();
-               } else {
-                       // Framework not found, display error
-                       $out->setPageTitle( $this->msg( 'javascripttest' ) );
-                       $summary = $this->wrapSummaryHtml(
-                               '<p class="error">' .
-                                       $this->msg( 
'javascripttest-pagetext-unknownframework', $par )->escaped() .
-                                       '</p>' .
-                                       $this->getFrameworkListHtml(),
-                               'unknownframework'
-                       );
-                       $out->addHtml( $summary );
-               }
+               $method = 'view' . ucfirst( $framework );
+               $this->$method();
+               $out->setPageTitle( $this->msg(
+                       'javascripttest-title',
+                       // Messages: javascripttest-qunit-name
+                       $this->msg( "javascripttest-$framework-name" )->plain()
+               ) );
        }
 
        /**
@@ -92,7 +117,7 @@
         */
        private function getFrameworkListHtml() {
                $list = '<ul>';
-               foreach ( self::$frameworks as $framework => $initFn ) {
+               foreach ( self::$frameworks as $framework ) {
                        $list .= Html::rawElement(
                                'li',
                                array(),
@@ -110,68 +135,140 @@
        }
 
        /**
-        * Function to wrap the summary.
-        * It must be given a valid state as a second parameter or an exception 
will
-        * be thrown.
-        * @param string $html The raw HTML.
-        * @param string $state State, one of 'noframework', 'unknownframework' 
or 'frameworkfound'
-        * @throws MWException
-        * @return string
+        * Wrap HTML contents in a summary container.
+        *
+        * @param string $html HTML contents to be wrapped
+        * @return string HTML
         */
-       private function wrapSummaryHtml( $html, $state ) {
-               $validStates = array( 'noframework', 'unknownframework', 
'frameworkfound' );
-
-               if ( !in_array( $state, $validStates ) ) {
-                       throw new MWException( __METHOD__
-                               . ' given an invalid state. Must be one of "'
-                               . join( '", "', $validStates ) . '".'
-                       );
-               }
-
-               return "<div id=\"mw-javascripttest-summary\" 
class=\"mw-javascripttest-$state\">$html</div>";
+       private function wrapSummaryHtml( $html ) {
+               return "<div id=\"mw-javascripttest-summary\">$html</div>";
        }
 
        /**
-        * Initialize the page for QUnit.
+        * Run the test suite on the Special page.
+        *
+        * Rendered by OutputPage and Skin.
         */
-       private function initQUnitTesting() {
+       private function viewQUnit() {
                global $wgJavaScriptTestConfig;
 
                $out = $this->getOutput();
+               $testConfig = $wgJavaScriptTestConfig;
 
-               $out->addModules( 'test.mediawiki.qunit.testrunner' );
-               $qunitTestModules = 
$out->getResourceLoader()->getTestModuleNames( 'qunit' );
-               $out->addModules( $qunitTestModules );
+               $modules = $out->getResourceLoader()->getTestModuleNames( 
'qunit' );
 
                $summary = $this->msg( 'javascripttest-qunit-intro' )
                        ->params( 
$wgJavaScriptTestConfig['qunit']['documentation'] )
                        ->parseAsBlock();
-               $header = $this->msg( 'javascripttest-qunit-heading' 
)->escaped();
-               $userDir = $this->getLanguage()->getDir();
 
                $baseHtml = <<<HTML
 <div class="mw-content-ltr">
-<div id="qunit-header"><span dir="$userDir">$header</span></div>
-<div id="qunit-banner"></div>
-<div id="qunit-testrunner-toolbar"></div>
-<div id="qunit-userAgent"></div>
-<ol id="qunit-tests"></ol>
-<div id="qunit-fixture">test markup, will be hidden</div>
+<div id="qunit"></div>
 </div>
 HTML;
-               $out->addHtml( $this->wrapSummaryHtml( $summary, 
'frameworkfound' ) . $baseHtml );
-
-               // This special page is disabled by default 
($wgEnableJavaScriptTest), and contains
-               // no sensitive data. In order to allow TestSwarm to embed it 
into a test client window,
-               // we need to allow iframing of this page.
-               $out->allowClickjacking();
 
                // Used in ./tests/qunit/data/testrunner.js, see also 
documentation of
                // $wgJavaScriptTestConfig in DefaultSettings.php
                $out->addJsConfigVars(
                        'QUnitTestSwarmInjectJSPath',
-                       $wgJavaScriptTestConfig['qunit']['testswarm-injectjs']
+                       $testConfig['qunit']['testswarm-injectjs']
                );
+
+               $out->addHtml( $this->wrapSummaryHtml( $summary ) . $baseHtml );
+
+               // The testrunner configures QUnit and essentially depends on 
it. However, test suites
+               // are reusable in environments that preload QUnit (or a 
compatibility interface to
+               // another framework). Therefore we have to load it ourselves.
+               $out->addHtml( Html::inlineScript(
+                       ResourceLoader::makeLoaderConditionalScript(
+                               Xml::encodeJsCall( 'mw.loader.using', array(
+                                       array( 'jquery.qunit', 
'jquery.qunit.completenessTest' ),
+                                       new XmlJsCode(
+                                               'function () {' . 
Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) . '}'
+                                       )
+                               ) )
+                       )
+               ) );
+       }
+
+       /**
+        * Generate self-sufficient JavaScript payload to run the tests 
elsewhere.
+        *
+        * Includes startup module to request modules from ResourceLoader.
+        *
+        * Note: This modifies the registry to replace 'jquery.qunit' with an
+        * empty module to allow external environment to preload QUnit with any
+        * neccecary framework adapters (e.g. Karma). Loading it again would
+        * re-define QUnit and dereference event handlers from Karma.
+        */
+       private function exportQUnit() {
+               $out = $this->getOutput();
+
+               $out->disable();
+
+               $rl = $out->getResourceLoader();
+
+               $query = array(
+                       'lang' => $this->getLanguage()->getCode(),
+                       'skin' => $this->getSkin()->getSkinName(),
+                       'debug' => ResourceLoader::inDebugMode() ? 'true' : 
'false',
+               );
+               $embedContext = new ResourceLoaderContext( $rl, new 
FauxRequest( $query ) );
+               $query['only'] = 'scripts';
+               $startupContext = new ResourceLoaderContext( $rl, new 
FauxRequest( $query ) );
+
+               $modules = $rl->getTestModuleNames( 'qunit' );
+
+               // The below is essentially a pure-javascript version of 
OutputPage::getHeadScripts.
+               $startup = $rl->makeModuleResponse( $startupContext, array(
+                       'startup' => $rl->getModule( 'startup' ),
+               ) );
+               // Embed page-specific mw.config variables.
+               // The current Special page shouldn't be relevant to tests, but 
various modules (which
+               // are loaded before the test suites), reference mw.config 
while initialising.
+               $code = ResourceLoader::makeConfigSetScript( $out->getJSVars() 
);
+               // Embed private modules as they're not allowed to be loaded 
dynamically
+               $code .= $rl->makeModuleResponse( $embedContext, array(
+                       'user.options' => $rl->getModule( 'user.options' ),
+                       'user.tokens' => $rl->getModule( 'user.tokens' ),
+               ) );
+               $code .= Xml::encodeJsCall( 'mw.loader.load', array( $modules ) 
);
+
+               header( 'Content-Type: text/javascript; charset=utf-8' );
+               header( 'Cache-Control: private, no-cache, must-revalidate' );
+               header( 'Pragma: no-cache' );
+               echo $startup;
+               echo "\n";
+               // Note: The following has to be wrapped in a script tag 
because the startup module also
+               // writes a script tag (the one loading mediawiki.js). Script 
tags are synchronous, block
+               // each other, and run in order. But they don't nest. The code 
appended after the startup
+               // module runs before the added script tag is parsed and 
executed.
+               echo Xml::encodeJsCall( 'document.write', array( 
Html::inlineScript( $code  ) ) );
+       }
+
+       private function plainQUnit() {
+               $out = $this->getOutput();
+               $out->disable();
+
+               $url = $this->getPageTitle( 'qunit/export' )->getFullURL( array(
+                       'debug' => ResourceLoader::inDebugMode() ? 'true' : 
'false',
+               ) );
+
+               $styles = $out->makeResourceLoaderLink( 'jquery.qunit', 
ResourceLoaderModule::TYPE_STYLES, false );
+               // Use 'raw' since this is a plain HTML page without 
ResourceLoader
+               $scripts = $out->makeResourceLoaderLink( 'jquery.qunit', 
ResourceLoaderModule::TYPE_SCRIPTS, false, array( 'raw' => 'true' ) );
+
+               $head = trim( $styles['html'] . $scripts['html'] );
+               $html = <<<HTML
+<!DOCTYPE html>
+<title>QUnit</title>
+$head
+<div id="qunit"></div>
+HTML;
+               $html .= "\n" . Html::linkedScript( $url );
+
+               header( 'Content-Type: text/html; charset=utf-8' );
+               echo $html;
        }
 
        protected function getGroupName() {
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index cdc8338..8b674fa 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -2329,14 +2329,14 @@
     "import-logentry-interwiki-detail": "$1 {{PLURAL:$1|revision|revisions}} 
from $2",
     "javascripttest": "JavaScript testing",
     "javascripttest-backlink": "< $1",
-    "javascripttest-title": "Running $1 tests",
+    "javascripttest-title": "$1",
     "javascripttest-pagetext-noframework": "This page is reserved for running 
JavaScript tests.",
     "javascripttest-pagetext-unknownframework": "Unknown testing framework 
\"$1\".",
+    "javascripttest-pagetext-unknownaction": "Unknown action \"$1\".",
     "javascripttest-pagetext-frameworks": "Please choose one of the following 
testing frameworks: $1",
     "javascripttest-pagetext-skins": "Choose a skin to run the tests with:",
     "javascripttest-qunit-name": "QUnit",
     "javascripttest-qunit-intro": "See [$1 testing documentation] on 
mediawiki.org.",
-    "javascripttest-qunit-heading": "MediaWiki JavaScript QUnit test suite",
     "accesskey-pt-userpage": ".",
     "accesskey-pt-anonuserpage": ".",
     "accesskey-pt-mytalk": "n",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index dab12a7..4d8fb9b 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -2492,14 +2492,15 @@
     "import-logentry-interwiki-detail": "Used as success message. 
Parameters:\n* $1 - number of succeeded revisions\n* $2 - interwiki name\nSee 
also:\n* {{msg-mw|Import-logentry-upload-detail}}",
     "javascripttest": "Title of the special page 
[[Special:JavaScriptTest]].\n\nSee also:\n* {{msg-mw|Javascripttest|title}}\n* 
{{msg-mw|Javascripttest-pagetext-noframework|summary}}\n* 
{{msg-mw|Javascripttest-pagetext-unknownframework|error message}}",
     "javascripttest-backlink": "{{optional}}\nUsed as subtitle in 
[[Special:JavaScriptTest]]. Parameters:\n* $1 - page title",
-    "javascripttest-title": "Title of the special page when running a test 
suite. Parameters:\n* $1 is the name of the framework, for example QUnit.",
+    "javascripttest-title": "{{Ignore}}",
     "javascripttest-pagetext-noframework": "Used as summary when no framework 
specified.\n\nSee also:\n* {{msg-mw|Javascripttest|title}}\n* 
{{msg-mw|Javascripttest-pagetext-noframework|summary}}\n* 
{{msg-mw|Javascripttest-pagetext-unknownframework|error message}}",
     "javascripttest-pagetext-unknownframework": "Error message when given 
framework ID is not found. Parameters:\n* $1 - the ID of the framework\nSee 
also:\n* {{msg-mw|Javascripttest|title}}\n* 
{{msg-mw|Javascripttest-pagetext-noframework|summary}}\n* 
{{msg-mw|Javascripttest-pagetext-unknownframework|error message}}",
+    "javascripttest-pagetext-unknownaction": "Error message when url specifies 
an unknown action. Parameters:\
+n* $1 - the action specified in the url.",
     "javascripttest-pagetext-frameworks": "Parameters:\n* $1 - frameworks list 
which contain a link text {{msg-mw|Javascripttest-qunit-name}}",
     "javascripttest-pagetext-skins": "Used as label in 
[[Special:JavaScriptTest]].",
     "javascripttest-qunit-name": "{{Ignore}}",
     "javascripttest-qunit-intro": "Used as summary. Parameters:\n* $1 - the 
configured URL to the documentation\nSee also:\n* 
{{msg-mw|Javascripttest-qunit-heading}}",
-    "javascripttest-qunit-heading": "See also:\n* 
{{msg-mw|Javascripttest-qunit-intro}}",
     "accesskey-pt-userpage": "{{doc-accesskey}}\nSee also:\n<!--* 
username-->\n* {{msg-mw|Accesskey-pt-userpage}}\n* 
{{msg-mw|Tooltip-pt-userpage}}",
     "accesskey-pt-anonuserpage": "{{doc-accesskey}}",
     "accesskey-pt-mytalk": "{{doc-accesskey}}\nSee also:\n* 
{{msg-mw|Mytalk}}\n* {{msg-mw|Accesskey-pt-mytalk}}\n* 
{{msg-mw|Tooltip-pt-mytalk}}",
diff --git a/package.json b/package.json
index a9b9148..9f27d5f 100644
--- a/package.json
+++ b/package.json
@@ -7,10 +7,17 @@
     "postdoc": "grunt copy:jsduck"
   },
   "devDependencies": {
-    "grunt": "0.4.2",
+    "grunt": "0.4.5",
+    "grunt-cli": "0.1.13",
     "grunt-contrib-jshint": "0.9.2",
     "grunt-contrib-copy": "0.8.0",
     "grunt-contrib-watch": "0.6.1",
-    "grunt-jscs-checker": "0.4.1"
+    "grunt-jscs-checker": "0.4.1",
+    "grunt-karma": "0.10.1",
+    "karma": "0.12.31",
+    "karma-chrome-launcher": "0.1.7",
+    "karma-firefox-launcher": "0.1.4",
+    "karma-qunit": "0.1.4",
+    "qunitjs": "1.16.0"
   }
 }
diff --git a/resources/Resources.php b/resources/Resources.php
index 05af927..12f889b 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1285,7 +1285,9 @@
                        'colon-separator',
                        'javascripttest-pagetext-skins',
                ) ),
-               'dependencies' => array( 'jquery.qunit' ),
+               'dependencies' => array(
+                       'mediawiki.Uri',
+               ),
                'position' => 'top',
                'targets' => array( 'desktop', 'mobile' ),
        ),
diff --git 
a/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js 
b/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
index 38f256c..04ea207 100644
--- a/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
+++ b/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
@@ -6,7 +6,7 @@
 
                // Create useskin dropdown menu and reload onchange to the 
selected skin
                // (only if a framework was found, not on error pages).
-               $( 
'#mw-javascripttest-summary.mw-javascripttest-frameworkfound' ).append( 
function () {
+               $( '#mw-javascripttest-summary' ).append( function () {
 
                        var $html = $( '<p><label for="useskin">'
                                        + mw.message( 
'javascripttest-pagetext-skins' ).escaped()
@@ -25,7 +25,8 @@
                        // Bind onchange event handler and append to form
                        $html.append(
                                $( select ).change( function () {
-                                       window.location = QUnit.url( { useskin: 
$( this ).val() } );
+                                       var url = new mw.Uri();
+                                       location.href = url.extend( { useskin: 
$( this ).val() } );
                                } )
                        );
 
diff --git a/tests/qunit/QUnitTestResources.php 
b/tests/qunit/QUnitTestResources.php
index 74ea58e..e599189 100644
--- a/tests/qunit/QUnitTestResources.php
+++ b/tests/qunit/QUnitTestResources.php
@@ -25,9 +25,9 @@
                        'tests/qunit/data/testrunner.js',
                ),
                'dependencies' => array(
+                       // Test runner configures QUnit but can't have it as 
dependency,
+                       // see SpecialJavaScriptTest::viewQUnit.
                        'jquery.getAttrs',
-                       'jquery.qunit',
-                       'jquery.qunit.completenessTest',
                        'mediawiki.page.ready',
                        'mediawiki.page.startup',
                        'test.sinonjs',
diff --git a/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js
index ab9aab1..aca8433e 100644
--- a/tests/qunit/data/testrunner.js
+++ b/tests/qunit/data/testrunner.js
@@ -112,6 +112,34 @@
                };
        }() );
 
+       // Extend QUnit.module to provide a fixture element.
+       ( function () {
+               var orgModule = QUnit.module;
+
+               QUnit.module = function ( name, localEnv ) {
+                       var fixture;
+                       localEnv = localEnv || {};
+                       orgModule( name, {
+                               setup: function () {
+                                       fixture = document.createElement( 'div' 
);
+                                       fixture.id = 'qunit-fixture';
+                                       document.body.appendChild( fixture );
+
+                                       if ( localEnv.setup ) {
+                                               localEnv.setup.call( this );
+                                       }
+                               },
+                               teardown: function () {
+                                       if ( localEnv.teardown ) {
+                                               localEnv.teardown.call( this );
+                                       }
+
+                                       fixture.parentNode.removeChild( fixture 
);
+                               }
+                       } );
+               };
+       }() );
+
        // Initiate when enabled
        if ( QUnit.urlParams.completenesstest ) {
 
diff --git a/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js 
b/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
index 7571b92..795c2bb 100644
--- a/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
+++ b/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
@@ -15,24 +15,22 @@
                assert.equal( $.escapeRE( '0123456789' ), '0123456789', 
'escapeRE - Leave numbers alone' );
        } );
 
-       QUnit.test( 'Is functions', 15, function ( assert ) {
-               assert.strictEqual( $.isDomElement( document.getElementById( 
'qunit-header' ) ), true,
-                       'isDomElement: #qunit-header Node' );
-               assert.strictEqual( $.isDomElement( document.getElementById( 
'random-name' ) ), false,
-                       'isDomElement: #random-name (null)' );
-               assert.strictEqual( $.isDomElement( 
document.getElementsByTagName( 'div' ) ), false,
-                       'isDomElement: getElementsByTagName Array' );
-               assert.strictEqual( $.isDomElement( 
document.getElementsByTagName( 'div' )[0] ), true,
-                       'isDomElement: getElementsByTagName(..)[0] Node' );
-               assert.strictEqual( $.isDomElement( $( 'div' ) ), false,
-                       'isDomElement: jQuery object' );
-               assert.strictEqual( $.isDomElement( $( 'div' ).get( 0 ) ), true,
-                       'isDomElement: jQuery object > Get node' );
+       QUnit.test( 'isDomElement', 6, function ( assert ) {
                assert.strictEqual( $.isDomElement( document.createElement( 
'div' ) ), true,
-                       'isDomElement: createElement' );
+                       'isDomElement: HTMLElement' );
+               assert.strictEqual( $.isDomElement( document.createTextNode( '' 
) ), true,
+                       'isDomElement: TextNode' );
+               assert.strictEqual( $.isDomElement( null ), false,
+                       'isDomElement: null' );
+               assert.strictEqual( $.isDomElement( 
document.getElementsByTagName( 'div' ) ), false,
+                       'isDomElement: NodeList' );
+               assert.strictEqual( $.isDomElement( $( 'div' ) ), false,
+                       'isDomElement: jQuery' );
                assert.strictEqual( $.isDomElement( { foo: 1 } ), false,
-                       'isDomElement: Object' );
+                       'isDomElement: Plain Object' );
+       } );
 
+       QUnit.test( 'isEmpty', 7, function ( assert ) {
                assert.strictEqual( $.isEmpty( 'string' ), false, 'isEmpty: 
"string"' );
                assert.strictEqual( $.isEmpty( '0' ), true, 'isEmpty: "0"' );
                assert.strictEqual( $.isEmpty( '' ), true, 'isEmpty: ""' );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js 
b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
index 995c1ed..2638ed0 100644
--- a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
@@ -1,6 +1,7 @@
 ( function ( mw, $ ) {
-       var mwLanguageCache = {}, formatText, formatParse, formatnumTests, 
specialCharactersPageName,
-               expectedListUsers, expectedEntrypoints;
+       var formatText, formatParse, formatnumTests, specialCharactersPageName, 
expectedListUsers, expectedEntrypoints,
+               mwLanguageCache = {},
+               hasOwn = Object.hasOwnProperty;
 
        // When the expected result is the same in both modes
        function assertBothModes( assert, parserArguments, expectedResult, 
assertMessage ) {
@@ -59,31 +60,52 @@
                }
        } ) );
 
-       function getMwLanguage( langCode, cb ) {
-               if ( mwLanguageCache[langCode] !== undefined ) {
-                       mwLanguageCache[langCode].add( cb );
-                       return;
-               }
-               mwLanguageCache[langCode] = $.Callbacks( 'once memory' );
-               mwLanguageCache[langCode].add( cb );
-               $.ajax( {
-                       url: mw.util.wikiScript( 'load' ),
-                       data: {
-                               skin: mw.config.get( 'skin' ),
-                               lang: langCode,
-                               debug: mw.config.get( 'debug' ),
-                               modules: [
-                                       'mediawiki.language.data',
-                                       'mediawiki.language'
-                               ].join( '|' ),
-                               only: 'scripts'
-                       },
-                       dataType: 'script'
-               } ).done(function () {
-                               mwLanguageCache[langCode].fire( mw.language );
-                       } ).fail( function () {
-                               mwLanguageCache[langCode].fire( false );
+       /**
+        * Be careful to no run this in parallel as it uses a global identifier 
(mw.language)
+        * to transport the module back to the test. It musn't be overwritten 
concurrentely.
+        *
+        * This function caches the mw.language data to avoid having to request 
the same module
+        * multiple times. There is more than one test case for any given 
language.
+        */
+       function getMwLanguage( langCode ) {
+               if ( !hasOwn.call( mwLanguageCache, langCode ) ) {
+                       mwLanguageCache[langCode] = $.ajax( {
+                               url: mw.util.wikiScript( 'load' ),
+                               data: {
+                                       skin: mw.config.get( 'skin' ),
+                                       lang: langCode,
+                                       debug: mw.config.get( 'debug' ),
+                                       modules: [
+                                               'mediawiki.language.data',
+                                               'mediawiki.language'
+                                       ].join( '|' ),
+                                       only: 'scripts'
+                               },
+                               dataType: 'script',
+                               cache: true
+                       } ).then( function () {
+                               return mw.language;
                        } );
+               }
+               return mwLanguageCache[langCode];
+       }
+
+       /**
+        * @param {Function[]} tasks List of functions that perform tasks
+        *  that may be asynchronous. Invoke the callback parameter when done.
+        * @param {Function} done When all tasks are done.
+        * @return
+        */
+       function process( tasks, done ) {
+               function run() {
+                       var task = tasks.shift();
+                       if ( task ) {
+                               task( run );
+                       } else {
+                               done();
+                       }
+               }
+               run();
        }
 
        QUnit.test( 'Replace', 9, function ( assert ) {
@@ -244,23 +266,27 @@
 
        QUnit.test( 'Match PHP parser', mw.libs.phpParserData.tests.length, 
function ( assert ) {
                mw.messages.set( mw.libs.phpParserData.messages );
-               $.each( mw.libs.phpParserData.tests, function ( i, test ) {
-                       QUnit.stop();
-                       getMwLanguage( test.lang, function ( langClass ) {
-                               QUnit.start();
-                               if ( !langClass ) {
-                                       assert.ok( false, 'Language "' + 
test.lang + '" failed to load' );
-                                       return;
-                               }
-                               mw.config.set( 'wgUserLanguage', test.lang );
-                               var parser = new mw.jqueryMsg.parser( { 
language: langClass } );
-                               assert.equal(
-                                       parser.parse( test.key, test.args 
).html(),
-                                       test.result,
-                                       test.name
-                               );
-                       } );
+               var tasks = $.map( mw.libs.phpParserData.tests, function ( test 
) {
+                       return function ( next ) {
+                               getMwLanguage( test.lang )
+                                       .done( function ( langClass ) {
+                                               mw.config.set( 
'wgUserLanguage', test.lang );
+                                               var parser = new 
mw.jqueryMsg.parser( { language: langClass } );
+                                               assert.equal(
+                                                       parser.parse( test.key, 
test.args ).html(),
+                                                       test.result,
+                                                       test.name
+                                               );
+                                       } )
+                                       .fail( function () {
+                                               assert.ok( false, 'Language "' 
+ test.lang + '" failed to load.' );
+                                       } )
+                                       .always( next );
+                       };
                } );
+
+               QUnit.stop();
+               process( tasks, QUnit.start );
        } );
 
        QUnit.test( 'Links', 6, function ( assert ) {
@@ -419,8 +445,8 @@
                );
        } );
 
-// Tests that getMessageFunction is used for non-plain messages with curly 
braces or
-// square brackets, but not otherwise.
+       // Tests that getMessageFunction is used for non-plain messages with 
curly braces or
+       // square brackets, but not otherwise.
        QUnit.test( 'mw.Message.prototype.parser monkey-patch', 22, function ( 
assert ) {
                var oldGMF, outerCalled, innerCalled;
 
@@ -571,25 +597,27 @@
 QUnit.test( 'formatnum', formatnumTests.length, function ( assert ) {
        mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' );
        mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' );
-       $.each( formatnumTests, function ( i, test ) {
-               QUnit.stop();
-               getMwLanguage( test.lang, function ( langClass ) {
-                       QUnit.start();
-                       if ( !langClass ) {
-                               assert.ok( false, 'Language "' + test.lang + '" 
failed to load' );
-                               return;
-                       }
-                       mw.messages.set(test.message );
-                       mw.config.set( 'wgUserLanguage', test.lang );
-                       var parser = new mw.jqueryMsg.parser( { language: 
langClass } );
-                       assert.equal(
-                               parser.parse( test.integer ? 
'formatnum-msg-int' : 'formatnum-msg',
-                                       [ test.number ] ).html(),
-                               test.result,
-                               test.description
-                       );
-               } );
+       var queue = $.map( formatnumTests, function ( test ) {
+               return function ( next ) {
+                       getMwLanguage( test.lang )
+                               .done( function ( langClass ) {
+                                       mw.config.set( 'wgUserLanguage', 
test.lang );
+                                       var parser = new mw.jqueryMsg.parser( { 
language: langClass } );
+                                       assert.equal(
+                                               parser.parse( test.integer ? 
'formatnum-msg-int' : 'formatnum-msg',
+                                                       [ test.number ] 
).html(),
+                                               test.result,
+                                               test.description
+                                       );
+                               } )
+                               .fail( function () {
+                                       assert.ok( false, 'Language "' + 
test.lang + '" failed to load' );
+                               } )
+                               .always( next );
+               };
        } );
+       QUnit.stop();
+       process( queue, QUnit.start );
 } );
 
 // HTML in wikitext

-- 
To view, visit https://gerrit.wikimedia.org/r/198288
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I4e96da137340a28789b38940e75d4b6b8bc5d76a
Gerrit-PatchSet: 3
Gerrit-Project: mediawiki/core
Gerrit-Branch: REL1_23
Gerrit-Owner: Krinkle <[email protected]>
Gerrit-Reviewer: Daniel Friesen <[email protected]>
Gerrit-Reviewer: Jack Phoenix <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Mattflaschen <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to