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. While the Karma setup is still in the pipeline (might land before this commit loses WIP status), for now this can be tested via the 'Special:JavaScriptTest/qunit/plain' subpage. Refactor: * Use HTTP status code 404 in the response for "noframework". * Simplify HTML footprint by using <div id="qunit"> instead of hardcoding the full structure. This feature was added to QUnit since v1.3.0 (Feb 2012), we're using v1.14.0 (Jan 2014). QUnit's header is automatically derived from document.title. * Remove redundant addModules() for 'test.mediawiki.qunit.testrunner'. This is already added by default. * Move allowClickjacking() call so that it applies to other modes as well. The exported javascript needs to have wgBreakFrame set to false so that test runners can frame it. * Change mediawiki.special.javaScriptTest to not depend on QUnit. It caused QUnit to load on error pages. And in theory the page is suited for other frameworks and shouldn't load QUnit this way. Bug: T74063 Change-Id: I3d4d0df43bb426d9579eb0349b8b5477281a7cfc
This commit is contained in:
parent
d9360b5fcc
commit
ba50b32556
7 changed files with 189 additions and 88 deletions
|
|
@ -2729,7 +2729,7 @@ class OutputPage extends ContextSource {
|
|||
* call rather than a "<script src='...'>" tag.
|
||||
* @return string The html "<script>", "<link>" and "<style>" tags
|
||||
*/
|
||||
protected function makeResourceLoaderLink( $modules, $only, $useESI = false,
|
||||
public function makeResourceLoaderLink( $modules, $only, $useESI = false,
|
||||
array $extraQuery = array(), $loadCall = false
|
||||
) {
|
||||
$modules = (array)$modules;
|
||||
|
|
@ -3138,7 +3138,7 @@ class OutputPage extends ContextSource {
|
|||
* have to be purged on configuration changes.
|
||||
* @return array
|
||||
*/
|
||||
private function getJSVars() {
|
||||
public function getJSVars() {
|
||||
global $wgContLang;
|
||||
|
||||
$curRevisionId = 0;
|
||||
|
|
|
|||
|
|
@ -26,12 +26,10 @@
|
|||
*/
|
||||
class SpecialJavaScriptTest extends SpecialPage {
|
||||
/**
|
||||
* @var 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.
|
||||
*/
|
||||
private static $frameworks = array(
|
||||
'qunit' => 'initQUnitTesting',
|
||||
'qunit',
|
||||
);
|
||||
|
||||
public function __construct() {
|
||||
|
|
@ -44,43 +42,70 @@ class SpecialJavaScriptTest extends SpecialPage {
|
|||
$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()
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,7 +116,7 @@ class SpecialJavaScriptTest extends SpecialPage {
|
|||
*/
|
||||
private function getFrameworkListHtml() {
|
||||
$list = '<ul>';
|
||||
foreach ( self::$frameworks as $framework => $initFn ) {
|
||||
foreach ( self::$frameworks as $framework ) {
|
||||
$list .= Html::rawElement(
|
||||
'li',
|
||||
array(),
|
||||
|
|
@ -109,59 +134,132 @@ class SpecialJavaScriptTest extends SpecialPage {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
$out = $this->getOutput();
|
||||
|
||||
$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( 'https://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing' )
|
||||
->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 test frameworks to embed it into a test client window,
|
||||
// we need to allow iframing of this page.
|
||||
$out->allowClickjacking();
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -170,7 +268,7 @@ HTML;
|
|||
* @return string[] subpages
|
||||
*/
|
||||
public function getSubpagesForPrefixSearch() {
|
||||
return array_keys( self::$frameworks );
|
||||
return self::$frameworks;
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
|
|
|
|||
|
|
@ -2337,14 +2337,14 @@
|
|||
"import-logentry-interwiki-detail": "$1 {{PLURAL:$1|revision|revisions}} imported 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",
|
||||
|
|
|
|||
|
|
@ -2501,14 +2501,14 @@
|
|||
"import-logentry-interwiki-detail": "Used as success message and log entry. 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}}",
|
||||
|
|
|
|||
|
|
@ -1466,7 +1466,9 @@ return array(
|
|||
'colon-separator',
|
||||
'javascripttest-pagetext-skins',
|
||||
) ),
|
||||
'dependencies' => array( 'jquery.qunit' ),
|
||||
'dependencies' => array(
|
||||
'mediawiki.Uri',
|
||||
),
|
||||
'position' => 'top',
|
||||
'targets' => array( 'desktop', 'mobile' ),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
location.href = QUnit.url( { useskin: $( this ).val() } );
|
||||
var url = new mw.Uri();
|
||||
location.href = url.extend( { useskin: $( this ).val() } );
|
||||
} )
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ return array(
|
|||
'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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue