Krinkle has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/198287

Change subject: SpecialJavaScriptTest: Add export feature
......................................................................

SpecialJavaScriptTest: Add export feature

Add an 'export' subpage to SpecialJavaScriptTest which allows
one to request a self-sufficient JavaScript payload that will
bootstrap a ResourceLoader client and load the test suites.

This is needed for using Karma (which only loads JavaScript,
no full html pages). As such elements from the Skin and OutputPage
will not exist. While all QUnit tests in MediaWiki core and
most extensions I've seen already use #qunit-fixture, this is
now required. This to prevent leakage of elements from one
test to another, but it also prevents tests from depending
on elements provided by the server.

(cherry picked from commit ba50b32556fa1f2983c80cc6c06b5908f46f7cf4)

Change-Id: I3d4d0df43bb426d9579eb0349b8b5477281a7cfc
---
M includes/OutputPage.php
M includes/specials/SpecialJavaScriptTest.php
M languages/i18n/en.json
M languages/i18n/qqq.json
M resources/Resources.php
M resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
M tests/qunit/QUnitTestResources.php
7 files changed, 192 insertions(+), 89 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/87/198287/1

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..a5984fe 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,142 @@
        }
 
        /**
-        * 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 id="qunit-fixture"></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>
+<div id="qunit-fixture"></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/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',

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I3d4d0df43bb426d9579eb0349b8b5477281a7cfc
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: REL1_23
Gerrit-Owner: Krinkle <[email protected]>

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

Reply via email to