MediaWiki master
SpecialJavaScriptTest.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
23use HttpError;
32
38
39 public function __construct() {
40 parent::__construct( 'JavaScriptTest' );
41 }
42
43 public function execute( $par ) {
44 $this->getOutput()->disable();
45
46 if ( $par === 'qunit/export' ) {
47 // Send the JavaScript payload.
48 $this->exportJS();
49 } elseif ( $par === null || $par === '' || $par === 'qunit' || $par === 'qunit/plain' ) {
50 // Render the page
51 // (Support "/qunit" and "/qunit/plain" for backwards-compatibility)
52 $this->renderPage();
53 } else {
54 wfHttpError( 404, 'Unknown action', "Unknown action \"$par\"." );
55 }
56 }
57
61 private function getModulesForComponentOrThrow( ?string $component ): array {
62 $out = $this->getOutput();
63 $rl = $out->getResourceLoader();
64 $req = $this->getContext()->getRequest();
65
66 $modules = $rl->getTestSuiteModuleNames();
67 if ( $component !== null ) {
68 $module = 'test.' . $component;
69 if ( !in_array( 'test.' . $component, $modules ) ) {
70 throw new HttpError(
71 404,
72 "No test module found for the '$component' component.\n"
73 . "Make sure the extension is enabled via wfLoadExtension(),\n"
74 . "and register a test module via the QUnitTestModules attribute in extension.json.",
75 'Unknown test module',
76 );
77 }
78 $modules = [ 'test.' . $component ];
79 }
80
81 return $modules;
82 }
83
89 private function exportJS() {
90 $out = $this->getOutput();
91 $req = $this->getContext()->getRequest();
92 $rl = $out->getResourceLoader();
93
94 // Allow framing (disabling wgBreakFrames). Otherwise, mediawiki.page.ready
95 // will close this tab when running from CLI using karma-qunit.
96 $out->getMetadata()->setPreventClickjacking( false );
97
98 $query = [
99 'lang' => 'qqx',
100 'skin' => 'fallback',
101 'debug' => $req->getRawVal( 'debug' ),
102 'target' => 'test',
103 ];
104 $embedContext = new RL\Context( $rl, new FauxRequest( $query ) );
105 $query['only'] = 'scripts';
106 $startupContext = new RL\Context( $rl, new FauxRequest( $query ) );
107
108 $component = $req->getRawVal( 'component' );
109 $modules = $this->getModulesForComponentOrThrow( $component );
110
111 // Disable module storage.
112 // The unit test for mw.loader.store will enable it (with a mock timers).
113 $config = new MultiConfig( [
114 new HashConfig( [ MainConfigNames::ResourceLoaderStorageEnabled => false ] ),
115 $rl->getConfig(),
116 ] );
117
118 // The below is essentially a pure-javascript version of OutputPage::headElement().
119 $startupModule = $rl->getModule( 'startup' );
120 $startupModule->setConfig( $config );
121 $code = $rl->makeModuleResponse( $startupContext, [ 'startup' => $startupModule ] );
122 // The following has to be deferred via RLQ because the startup module is asynchronous.
123 $code .= ResourceLoader::makeLoaderConditionalScript(
124 // Embed page-specific mw.config variables.
125 //
126 // For compatibility with older tests, these will come from the user
127 // action "viewing Special:JavaScripTest".
128 //
129 // This is deprecated since MediaWiki 1.25 and slowly being phased out in favour of:
130 // 1. tests explicitly mocking the configuration they depend on.
131 // 2. tests explicitly skipping or not loading code that is only meant
132 // for real page views (e.g. not loading as dependency, or using a QUnit
133 // conditional).
134 //
135 // See https://phabricator.wikimedia.org/T89434.
136 // Keep a select few that are commonly referenced.
137 ResourceLoader::makeConfigSetScript( [
138 // used by mediawiki.util
139 'wgPageName' => 'Special:Badtitle/JavaScriptTest',
140 // used as input for mw.Title
141 'wgRelevantPageName' => 'Special:Badtitle/JavaScriptTest',
142 ] )
143 // Embed private modules as they're not allowed to be loaded dynamically
144 . $rl->makeModuleResponse( $embedContext, [
145 'user.options' => $rl->getModule( 'user.options' ),
146 ] )
147 // Load all the test modules
148 . Html::encodeJsCall( 'mw.loader.load', [ $modules ] )
149 );
150 $encModules = Html::encodeJsVar( $modules );
151 $code .= ResourceLoader::makeInlineCodeWithModule( 'mediawiki.base', <<<JAVASCRIPT
152 // Wait for each module individually, so that partial failures wont break the page
153 // completely by rejecting the promise before all/ any modules are loaded.
154 var promises = $encModules.map( function( module ) {
155 return mw.loader.using( module ).promise();
156 } );
157 Promise.allSettled( promises ).then( QUnit.start );
158JAVASCRIPT
159 );
160
161 header( 'Content-Type: text/javascript; charset=utf-8' );
162 header( 'Cache-Control: private, no-cache, must-revalidate' );
163 echo $code;
164 }
165
166 private function renderPage() {
167 $req = $this->getContext()->getRequest();
168 $component = $req->getRawVal( 'component' );
169 // If set, validate
170 $this->getModulesForComponentOrThrow( $component );
171
172 $basePath = $this->getConfig()->get( MainConfigNames::ResourceBasePath );
173 $headHtml = implode( "\n", [
174 Html::linkedStyle( "$basePath/resources/lib/qunitjs/qunit.css" ),
175 Html::linkedStyle( "$basePath/resources/src/qunitjs/qunit-local.css" ),
176 ] );
177
178 $scriptUrl = $this->getPageTitle( 'qunit/export' )->getFullURL( [
179 'debug' => (string)ResourceLoader::inDebugMode(),
180 'component' => $component,
181 ] );
182 $script = implode( "\n", [
183 Html::linkedScript( "$basePath/resources/lib/qunitjs/qunit.js" ),
184 Html::inlineScript( 'QUnit.config.autostart = false;' ),
185 Html::linkedScript( $scriptUrl ),
186 ] );
187
188 header( 'Content-Type: text/html; charset=utf-8' );
189 echo <<<HTML
190<!DOCTYPE html>
191<title>QUnit</title>
192$headHtml
193<div id="qunit"></div>
194<div id="qunit-fixture"></div>
195$script
196HTML;
197 }
198
199 protected function getGroupName() {
200 return 'other';
201 }
202}
203
205class_alias( SpecialJavaScriptTest::class, 'SpecialJavaScriptTest' );
wfHttpError( $code, $label, $desc)
Provide a simple HTTP error.
Show an error that looks like an HTTP server error.
Definition HttpError.php:33
A Config instance which stores all settings as a member variable.
Provides a fallback sequence for Config objects.
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
A class containing constants representing the names of configuration variables.
const ResourceBasePath
Name constant for the ResourceBasePath setting, for use with Config::get()
const ResourceLoaderStorageEnabled
Name constant for the ResourceLoaderStorageEnabled setting, for use with Config::get()
WebRequest clone which takes values from a provided array.
Context object that contains information about the state of a specific ResourceLoader web request.
Definition Context.php:46
ResourceLoader is a loading system for JavaScript and CSS resources.
Parent class for all special pages.
getContext()
Gets the context this SpecialPage is executed in.
getOutput()
Get the OutputPage being used for this instance.
execute( $par)
Default execute method Checks user permissions.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...