Merge "Title: Title::getSubpage should not lose the interwiki prefix"
[lhc/web/wiklou.git] / tests / phpunit / includes / resourceloader / ResourceLoaderClientHtmlTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 /**
6 * @group ResourceLoader
7 * @covers ResourceLoaderClientHtml
8 */
9 class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase {
10
11 use MediaWikiCoversValidator;
12
13 public function testGetData() {
14 $context = self::makeContext();
15 $context->getResourceLoader()->register( self::makeSampleModules() );
16
17 $client = new ResourceLoaderClientHtml( $context );
18 $client->setModules( [
19 'test',
20 'test.private',
21 'test.shouldembed.empty',
22 'test.shouldembed',
23 'test.user',
24 'test.unregistered',
25 ] );
26 $client->setModuleStyles( [
27 'test.styles.mixed',
28 'test.styles.user.empty',
29 'test.styles.private',
30 'test.styles.pure',
31 'test.styles.shouldembed',
32 'test.styles.deprecated',
33 'test.unregistered.styles',
34 ] );
35
36 $expected = [
37 'states' => [
38 // The below are NOT queued for loading via `mw.loader.load(Array)`.
39 // Instead we tell the client to set their state to "loading" so that
40 // if they are needed as dependencies, the client will not try to
41 // load them on-demand, because the server is taking care of them already.
42 // Either:
43 // - Embedded as inline scripts in the HTML (e.g. user-private code, and
44 // previews). Once that script tag is reached, the state is "loaded".
45 // - Loaded directly from the HTML with a dedicated HTTP request (e.g.
46 // user scripts, which vary by a 'user' and 'version' parameter that
47 // the static user-agnostic startup module won't have).
48 'test.private' => 'loading',
49 'test.shouldembed' => 'loading',
50 'test.user' => 'loading',
51 // The below are known to the server to be empty scripts, or to be
52 // synchronously loaded stylesheets. These start in the "ready" state.
53 'test.shouldembed.empty' => 'ready',
54 'test.styles.pure' => 'ready',
55 'test.styles.user.empty' => 'ready',
56 'test.styles.private' => 'ready',
57 'test.styles.shouldembed' => 'ready',
58 'test.styles.deprecated' => 'ready',
59 ],
60 'general' => [
61 'test',
62 ],
63 'styles' => [
64 'test.styles.pure',
65 'test.styles.deprecated',
66 ],
67 'embed' => [
68 'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ],
69 'general' => [
70 'test.private',
71 'test.shouldembed',
72 'test.user',
73 ],
74 ],
75 'styleDeprecations' => [
76 Xml::encodeJsCall(
77 'mw.log.warn',
78 [ 'This page is using the deprecated ResourceLoader module "test.styles.deprecated".
79 Deprecation message.' ]
80 )
81 ],
82 ];
83
84 $access = TestingAccessWrapper::newFromObject( $client );
85 $this->assertEquals( $expected, $access->getData() );
86 }
87
88 public function testGetHeadHtml() {
89 $context = self::makeContext();
90 $context->getResourceLoader()->register( self::makeSampleModules() );
91
92 $client = new ResourceLoaderClientHtml( $context, [
93 'nonce' => false,
94 ] );
95 $client->setConfig( [ 'key' => 'value' ] );
96 $client->setModules( [
97 'test',
98 'test.private',
99 ] );
100 $client->setModuleStyles( [
101 'test.styles.pure',
102 'test.styles.private',
103 'test.styles.deprecated',
104 ] );
105 $client->setExemptStates( [
106 'test.exempt' => 'ready',
107 ] );
108
109 // phpcs:disable Generic.Files.LineLength
110 $expected = '<script>'
111 . 'document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");'
112 . 'RLCONF={"key":"value"};'
113 . 'RLSTATE={"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.styles.deprecated":"ready"};'
114 . 'RLPAGEMODULES=["test"];'
115 . '</script>' . "\n"
116 . '<script>(RLQ=window.RLQ||[]).push(function(){'
117 . 'mw.loader.implement("test.private@{blankVer}",null,{"css":[]});'
118 . '});</script>' . "\n"
119 . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.deprecated%2Cpure&amp;only=styles"/>' . "\n"
120 . '<style>.private{}</style>' . "\n"
121 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1"></script>';
122 // phpcs:enable
123 $expected = self::expandVariables( $expected );
124
125 $this->assertSame( $expected, (string)$client->getHeadHtml() );
126 }
127
128 /**
129 * Confirm that 'target' is passed down to the startup module's load url.
130 */
131 public function testGetHeadHtmlWithTarget() {
132 $client = new ResourceLoaderClientHtml(
133 self::makeContext(),
134 [ 'target' => 'example' ]
135 );
136
137 // phpcs:disable Generic.Files.LineLength
138 $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
139 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;target=example"></script>';
140 // phpcs:enable
141
142 $this->assertSame( $expected, (string)$client->getHeadHtml() );
143 }
144
145 /**
146 * Confirm that 'safemode' is passed down to startup.
147 */
148 public function testGetHeadHtmlWithSafemode() {
149 $client = new ResourceLoaderClientHtml(
150 self::makeContext(),
151 [ 'safemode' => '1' ]
152 );
153
154 // phpcs:disable Generic.Files.LineLength
155 $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
156 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;safemode=1"></script>';
157 // phpcs:enable
158
159 $this->assertSame( $expected, (string)$client->getHeadHtml() );
160 }
161
162 /**
163 * Confirm that a null 'target' is the same as no target.
164 */
165 public function testGetHeadHtmlWithNullTarget() {
166 $client = new ResourceLoaderClientHtml(
167 self::makeContext(),
168 [ 'target' => null ]
169 );
170
171 // phpcs:disable Generic.Files.LineLength
172 $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
173 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1"></script>';
174 // phpcs:enable
175
176 $this->assertSame( $expected, (string)$client->getHeadHtml() );
177 }
178
179 public function testGetBodyHtml() {
180 $context = self::makeContext();
181 $context->getResourceLoader()->register( self::makeSampleModules() );
182
183 $client = new ResourceLoaderClientHtml( $context, [ 'nonce' => false ] );
184 $client->setConfig( [ 'key' => 'value' ] );
185 $client->setModules( [
186 'test',
187 'test.private.bottom',
188 ] );
189 $client->setModuleStyles( [
190 'test.styles.deprecated',
191 ] );
192 // phpcs:disable Generic.Files.LineLength
193 $expected = '<script>(RLQ=window.RLQ||[]).push(function(){'
194 . 'mw.log.warn("This page is using the deprecated ResourceLoader module \"test.styles.deprecated\".\nDeprecation message.");'
195 . '});</script>';
196 // phpcs:enable
197
198 $this->assertSame( $expected, (string)$client->getBodyHtml() );
199 }
200
201 public static function provideMakeLoad() {
202 // phpcs:disable Generic.Files.LineLength
203 return [
204 [
205 'context' => [],
206 'modules' => [ 'test.unknown' ],
207 'only' => ResourceLoaderModule::TYPE_STYLES,
208 'extra' => [],
209 'output' => '',
210 ],
211 [
212 'context' => [],
213 'modules' => [ 'test.styles.private' ],
214 'only' => ResourceLoaderModule::TYPE_STYLES,
215 'extra' => [],
216 'output' => '<style>.private{}</style>',
217 ],
218 [
219 'context' => [],
220 'modules' => [ 'test.private' ],
221 'only' => ResourceLoaderModule::TYPE_COMBINED,
222 'extra' => [],
223 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",null,{"css":[]});});</script>',
224 ],
225 [
226 'context' => [],
227 'modules' => [ 'test.scripts' ],
228 'only' => ResourceLoaderModule::TYPE_SCRIPTS,
229 // Eg. startup module
230 'extra' => [ 'raw' => '1' ],
231 'output' => '<script async="" src="/w/load.php?lang=nl&amp;modules=test.scripts&amp;only=scripts&amp;raw=1"></script>',
232 ],
233 [
234 'context' => [],
235 'modules' => [ 'test.scripts' ],
236 'only' => ResourceLoaderModule::TYPE_SCRIPTS,
237 'extra' => [ 'raw' => '1', 'sync' => '1' ],
238 'output' => '<script src="/w/load.php?lang=nl&amp;modules=test.scripts&amp;only=scripts&amp;raw=1&amp;sync=1"></script>',
239 ],
240 [
241 'context' => [],
242 'modules' => [ 'test.scripts.user' ],
243 'only' => ResourceLoaderModule::TYPE_SCRIPTS,
244 'extra' => [],
245 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026user=Example\u0026version=0a56zyi");});</script>',
246 ],
247 [
248 'context' => [],
249 'modules' => [ 'test.user' ],
250 'only' => ResourceLoaderModule::TYPE_COMBINED,
251 'extra' => [],
252 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026user=Example\u0026version=0a56zyi");});</script>',
253 ],
254 [
255 'context' => [ 'debug' => 'true' ],
256 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
257 'only' => ResourceLoaderModule::TYPE_STYLES,
258 'extra' => [],
259 'output' => '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.mixed&amp;only=styles"/>' . "\n"
260 . '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.pure&amp;only=styles"/>',
261 ],
262 [
263 'context' => [ 'debug' => 'false' ],
264 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
265 'only' => ResourceLoaderModule::TYPE_STYLES,
266 'extra' => [],
267 'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.mixed%2Cpure&amp;only=styles"/>',
268 ],
269 [
270 'context' => [],
271 'modules' => [ 'test.styles.noscript' ],
272 'only' => ResourceLoaderModule::TYPE_STYLES,
273 'extra' => [],
274 'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.noscript&amp;only=styles"/></noscript>',
275 ],
276 [
277 'context' => [],
278 'modules' => [ 'test.shouldembed' ],
279 'only' => ResourceLoaderModule::TYPE_COMBINED,
280 'extra' => [],
281 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
282 ],
283 [
284 'context' => [],
285 'modules' => [ 'test.styles.shouldembed' ],
286 'only' => ResourceLoaderModule::TYPE_STYLES,
287 'extra' => [],
288 'output' => '<style>.shouldembed{}</style>',
289 ],
290 [
291 'context' => [],
292 'modules' => [ 'test.scripts.shouldembed' ],
293 'only' => ResourceLoaderModule::TYPE_SCRIPTS,
294 'extra' => [],
295 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
296 ],
297 [
298 'context' => [],
299 'modules' => [ 'test', 'test.shouldembed' ],
300 'only' => ResourceLoaderModule::TYPE_COMBINED,
301 'extra' => [],
302 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test");mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
303 ],
304 [
305 'context' => [],
306 'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ],
307 'only' => ResourceLoaderModule::TYPE_STYLES,
308 'extra' => [],
309 'output' =>
310 '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.pure&amp;only=styles"/>' . "\n"
311 . '<style>.shouldembed{}</style>'
312 ],
313 [
314 'context' => [],
315 'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ],
316 'only' => ResourceLoaderModule::TYPE_STYLES,
317 'extra' => [],
318 'output' =>
319 '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.a%2Cb&amp;only=styles"/>' . "\n"
320 . '<style>.orderingC{}.orderingD{}</style>' . "\n"
321 . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.e&amp;only=styles"/>'
322 ],
323 ];
324 // phpcs:enable
325 }
326
327 /**
328 * @dataProvider provideMakeLoad
329 * @covers ResourceLoaderClientHtml
330 * @covers ResourceLoaderModule::getModuleContent
331 * @covers ResourceLoader
332 */
333 public function testMakeLoad(
334 array $contextQuery,
335 array $modules,
336 $type,
337 array $extraQuery,
338 $expected
339 ) {
340 $context = self::makeContext( $contextQuery );
341 $context->getResourceLoader()->register( self::makeSampleModules() );
342 $actual = ResourceLoaderClientHtml::makeLoad( $context, $modules, $type, $extraQuery, false );
343 $expected = self::expandVariables( $expected );
344 $this->assertSame( $expected, (string)$actual );
345 }
346
347 public function testGetDocumentAttributes() {
348 $client = new ResourceLoaderClientHtml( self::makeContext() );
349 $this->assertInternalType( 'array', $client->getDocumentAttributes() );
350 }
351
352 private static function expandVariables( $text ) {
353 return strtr( $text, [
354 '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
355 ] );
356 }
357
358 private static function makeContext( $extraQuery = [] ) {
359 $conf = new HashConfig( [
360 'ResourceModuleSkinStyles' => [],
361 'ResourceModules' => [],
362 'EnableJavaScriptTest' => false,
363 'LoadScript' => '/w/load.php',
364 ] );
365 return new ResourceLoaderContext(
366 new ResourceLoader( $conf ),
367 new FauxRequest( array_merge( [
368 'lang' => 'nl',
369 'skin' => 'fallback',
370 'user' => 'Example',
371 'target' => 'phpunit',
372 ], $extraQuery ) )
373 );
374 }
375
376 private static function makeModule( array $options = [] ) {
377 return $options + [ 'class' => ResourceLoaderTestModule::class ];
378 }
379
380 private static function makeSampleModules() {
381 $modules = [
382 'test' => [],
383 'test.private' => [ 'group' => 'private' ],
384 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
385 'test.shouldembed' => [ 'shouldEmbed' => true ],
386 'test.user' => [ 'group' => 'user' ],
387
388 'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ],
389 'test.styles.mixed' => [],
390 'test.styles.noscript' => [
391 'type' => ResourceLoaderModule::LOAD_STYLES,
392 'group' => 'noscript',
393 ],
394 'test.styles.user' => [
395 'type' => ResourceLoaderModule::LOAD_STYLES,
396 'group' => 'user',
397 ],
398 'test.styles.user.empty' => [
399 'type' => ResourceLoaderModule::LOAD_STYLES,
400 'group' => 'user',
401 'isKnownEmpty' => true,
402 ],
403 'test.styles.private' => [
404 'type' => ResourceLoaderModule::LOAD_STYLES,
405 'group' => 'private',
406 'styles' => '.private{}',
407 ],
408 'test.styles.shouldembed' => [
409 'type' => ResourceLoaderModule::LOAD_STYLES,
410 'shouldEmbed' => true,
411 'styles' => '.shouldembed{}',
412 ],
413 'test.styles.deprecated' => [
414 'type' => ResourceLoaderModule::LOAD_STYLES,
415 'deprecated' => 'Deprecation message.',
416 ],
417
418 'test.scripts' => [],
419 'test.scripts.user' => [ 'group' => 'user' ],
420 'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
421 'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
422
423 'test.ordering.a' => [ 'shouldEmbed' => false ],
424 'test.ordering.b' => [ 'shouldEmbed' => false ],
425 'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
426 'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
427 'test.ordering.e' => [ 'shouldEmbed' => false ],
428 ];
429 return array_map( function ( $options ) {
430 return self::makeModule( $options );
431 }, $modules );
432 }
433 }