MediaWiki  master
SpecialJavaScriptTest.php
Go to the documentation of this file.
1 <?php
28 
29  public function __construct() {
30  parent::__construct( 'JavaScriptTest' );
31  }
32 
33  public function execute( $par ) {
34  $out = $this->getOutput();
35 
36  $this->setHeaders();
37  $out->disallowUserJs();
38 
39  // This special page is disabled by default ($wgEnableJavaScriptTest), and contains
40  // no sensitive data. In order to allow TestSwarm to embed it into a test client window,
41  // we need to allow iframing of this page.
42  $out->allowClickjacking();
43 
44  // Sub resource: Internal JavaScript export bundle for QUnit
45  if ( $par === 'qunit/export' ) {
46  $this->exportQUnit();
47  return;
48  }
49 
50  // Regular view: QUnit test runner
51  // (Support "/qunit" and "/qunit/plain" for backwards compatibility)
52  if ( $par === null || $par === '' || $par === 'qunit' || $par === 'qunit/plain' ) {
53  $this->plainQUnit();
54  return;
55  }
56 
57  // Unknown action
58  $out->setStatusCode( 404 );
59  $out->setPageTitle( $this->msg( 'javascripttest' ) );
60  $out->addHTML(
61  '<div class="error">'
62  . $this->msg( 'javascripttest-pagetext-unknownaction' )
63  ->plaintextParams( $par )->parseAsBlock()
64  . '</div>'
65  );
66  }
67 
73  private function getSummaryHtml() {
74  $summary = $this->msg( 'javascripttest-qunit-intro' )
75  ->params( 'https://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing' )
76  ->parseAsBlock();
77  return "<div id=\"mw-javascripttest-summary\">$summary</div>";
78  }
79 
90  private function exportQUnit() {
91  $out = $this->getOutput();
92  $out->disable();
93 
94  $rl = $out->getResourceLoader();
95 
96  $query = [
97  'lang' => $this->getLanguage()->getCode(),
98  'skin' => $this->getSkin()->getSkinName(),
99  'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false',
100  'target' => 'test',
101  ];
102  $embedContext = new ResourceLoaderContext( $rl, new FauxRequest( $query ) );
103  $query['only'] = 'scripts';
104  $startupContext = new ResourceLoaderContext( $rl, new FauxRequest( $query ) );
105 
106  $modules = $rl->getTestSuiteModuleNames();
107 
108  // Disable autostart because we load modules asynchronously. By default, QUnit would start
109  // at domready when there are no tests loaded and also fire 'QUnit.done' which then instructs
110  // Karma to exit the browser process before the tests even finished loading.
111  $qunitConfig = 'QUnit.config.autostart = false;'
112  . 'if (window.__karma__) {'
113  // karma-qunit's use of autostart=false and QUnit.start conflicts with ours.
114  // Hack around this by replacing 'karma.loaded' with a no-op and perform its duty of calling
115  // `__karma__.start()` ourselves. See <https://github.com/karma-runner/karma-qunit/issues/27>.
116  . 'window.__karma__.loaded = function () {};'
117  . '}';
118 
119  // The below is essentially a pure-javascript version of OutputPage::headElement().
120  $code = $rl->makeModuleResponse( $startupContext, [
121  'startup' => $rl->getModule( 'startup' ),
122  ] );
123  $code .= <<<JAVASCRIPT
124  // Disable module storage.
125  // The unit test for mw.loader.store will enable it
126  // explicitly with a mock timer.
127  mw.loader.store.enabled = false;
128 JAVASCRIPT;
129  // The following has to be deferred via RLQ because the startup module is asynchronous.
130  $code .= ResourceLoader::makeLoaderConditionalScript(
131  // Embed page-specific mw.config variables.
132  // The current Special page shouldn't be relevant to tests, but various modules (which
133  // are loaded before the test suites), reference mw.config while initialising.
134  ResourceLoader::makeConfigSetScript( $out->getJSVars() )
135  // Embed private modules as they're not allowed to be loaded dynamically
136  . $rl->makeModuleResponse( $embedContext, [
137  'user.options' => $rl->getModule( 'user.options' ),
138  'user.tokens' => $rl->getModule( 'user.tokens' ),
139  ] )
140  // Load all the test suites
141  . Xml::encodeJsCall( 'mw.loader.load', [ $modules ] )
142  );
143  $encModules = Xml::encodeJsVar( $modules );
144  $code .= ResourceLoader::makeInlineCodeWithModule( 'mediawiki.base', <<<JAVASCRIPT
145  var start = window.__karma__ ? window.__karma__.start : QUnit.start;
146  mw.loader.using( $encModules ).always( start );
147  mw.trackSubscribe( 'resourceloader.exception', function ( topic, err ) {
148  // Things like "dependency missing" or "unknown module".
149  // Re-throw so that they are reported as global exceptions by QUnit and Karma.
150  setTimeout( function () {
151  throw err;
152  } );
153  } );
154 JAVASCRIPT
155  );
156 
157  header( 'Content-Type: text/javascript; charset=utf-8' );
158  header( 'Cache-Control: private, no-cache, must-revalidate' );
159  header( 'Pragma: no-cache' );
160  echo $qunitConfig;
161  echo $code;
162  }
163 
164  private function plainQUnit() {
165  $out = $this->getOutput();
166  $out->disable();
167 
168  $styles = $out->makeResourceLoaderLink( 'jquery.qunit',
169  ResourceLoaderModule::TYPE_STYLES
170  );
171 
172  // Use 'raw' because QUnit loads before ResourceLoader initialises (omit mw.loader.state call)
173  // Use 'sync' to ensure OutputPage doesn't use the "async" attribute because QUnit must
174  // load before qunit/export.
175  $scripts = $out->makeResourceLoaderLink( 'jquery.qunit',
176  ResourceLoaderModule::TYPE_SCRIPTS,
177  [ 'raw' => '1', 'sync' => '1' ]
178  );
179 
180  $head = implode( "\n", [ $styles, $scripts ] );
181  $summary = $this->getSummaryHtml();
182  $html = <<<HTML
183 <!DOCTYPE html>
184 <title>QUnit</title>
185 $head
186 $summary
187 <div id="qunit"></div>
188 HTML;
189 
190  $url = $this->getPageTitle( 'qunit/export' )->getFullURL( [
191  'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false',
192  ] );
193  $html .= "\n" . Html::linkedScript( $url );
194 
195  header( 'Content-Type: text/html; charset=utf-8' );
196  echo $html;
197  }
198 
199  protected function getGroupName() {
200  return 'other';
201  }
202 }
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:672
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:33
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:792
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:33
SpecialJavaScriptTest\execute
execute( $par)
Default execute method Checks user permissions.
Definition: SpecialJavaScriptTest.php:33
SpecialJavaScriptTest\getSummaryHtml
getSummaryHtml()
Get summary text wrapped in a container.
Definition: SpecialJavaScriptTest.php:73
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:719
SpecialJavaScriptTest\getGroupName
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Definition: SpecialJavaScriptTest.php:199
SpecialPage\getSkin
getSkin()
Shortcut to get the skin being used for this instance.
Definition: SpecialPage.php:739
SpecialPage\getLanguage
getLanguage()
Shortcut to get user's language.
Definition: SpecialPage.php:749
Xml\encodeJsVar
static encodeJsVar( $value, $pretty=false)
Encode a variable of arbitrary type to JavaScript.
Definition: Xml.php:659
Xml\encodeJsCall
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition: Xml.php:677
SpecialJavaScriptTest\__construct
__construct()
Definition: SpecialJavaScriptTest.php:29
SpecialJavaScriptTest\plainQUnit
plainQUnit()
Definition: SpecialJavaScriptTest.php:164
$modules
$modules
Definition: HTMLFormElement.php:13
SpecialPage\setHeaders
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
Definition: SpecialPage.php:537
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:37
SpecialJavaScriptTest
Definition: SpecialJavaScriptTest.php:27
SpecialJavaScriptTest\exportQUnit
exportQUnit()
Generate self-sufficient JavaScript payload to run the tests elsewhere.
Definition: SpecialJavaScriptTest.php:90