9ab3a2dae3c6df9734d2ece3b57df703f0540c4e
[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>document.documentElement.className = document.documentElement.className.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );</script>' . "\n"
111 . '<script>(window.RLQ=window.RLQ||[]).push(function(){'
112 . 'mw.config.set({"key":"value"});'
113 . 'mw.loader.state({"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.styles.deprecated":"ready"});'
114 . 'mw.loader.implement("test.private@{blankVer}",null,{"css":[]});'
115 . 'RLPAGEMODULES=["test"];mw.loader.load(RLPAGEMODULES);'
116 . '});</script>' . "\n"
117 . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.deprecated%2Cpure&amp;only=styles&amp;skin=fallback"/>' . "\n"
118 . '<style>.private{}</style>' . "\n"
119 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;skin=fallback"></script>';
120 // phpcs:enable
121 $expected = self::expandVariables( $expected );
122
123 $this->assertSame( $expected, (string)$client->getHeadHtml() );
124 }
125
126 /**
127 * Confirm that 'target' is passed down to the startup module's load url.
128 */
129 public function testGetHeadHtmlWithTarget() {
130 $client = new ResourceLoaderClientHtml(
131 self::makeContext(),
132 [ 'target' => 'example' ]
133 );
134
135 // phpcs:disable Generic.Files.LineLength
136 $expected = '<script>document.documentElement.className = document.documentElement.className.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );</script>' . "\n"
137 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;skin=fallback&amp;target=example"></script>';
138 // phpcs:enable
139
140 $this->assertSame( $expected, (string)$client->getHeadHtml() );
141 }
142
143 /**
144 * Confirm that 'safemode' is passed down to startup.
145 */
146 public function testGetHeadHtmlWithSafemode() {
147 $client = new ResourceLoaderClientHtml(
148 self::makeContext(),
149 [ 'safemode' => '1' ]
150 );
151
152 // phpcs:disable Generic.Files.LineLength
153 $expected = '<script>document.documentElement.className = document.documentElement.className.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );</script>' . "\n"
154 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;safemode=1&amp;skin=fallback"></script>';
155 // phpcs:enable
156
157 $this->assertSame( $expected, (string)$client->getHeadHtml() );
158 }
159
160 /**
161 * Confirm that a null 'target' is the same as no target.
162 */
163 public function testGetHeadHtmlWithNullTarget() {
164 $client = new ResourceLoaderClientHtml(
165 self::makeContext(),
166 [ 'target' => null ]
167 );
168
169 // phpcs:disable Generic.Files.LineLength
170 $expected = '<script>document.documentElement.className = document.documentElement.className.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );</script>' . "\n"
171 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;skin=fallback"></script>';
172 // phpcs:enable
173
174 $this->assertSame( $expected, (string)$client->getHeadHtml() );
175 }
176
177 public function testGetBodyHtml() {
178 $context = self::makeContext();
179 $context->getResourceLoader()->register( self::makeSampleModules() );
180
181 $client = new ResourceLoaderClientHtml( $context, [ 'nonce' => false ] );
182 $client->setConfig( [ 'key' => 'value' ] );
183 $client->setModules( [
184 'test',
185 'test.private.bottom',
186 ] );
187 $client->setModuleStyles( [
188 'test.styles.deprecated',
189 ] );
190 // phpcs:disable Generic.Files.LineLength
191 $expected = '<script>(window.RLQ=window.RLQ||[]).push(function(){'
192 . 'mw.log.warn("This page is using the deprecated ResourceLoader module \"test.styles.deprecated\".\nDeprecation message.");'
193 . '});</script>';
194 // phpcs:enable
195
196 $this->assertSame( $expected, (string)$client->getBodyHtml() );
197 }
198
199 public static function provideMakeLoad() {
200 // phpcs:disable Generic.Files.LineLength
201 return [
202 [
203 'context' => [],
204 'modules' => [ 'test.unknown' ],
205 'only' => ResourceLoaderModule::TYPE_STYLES,
206 'extra' => [],
207 'output' => '',
208 ],
209 [
210 'context' => [],
211 'modules' => [ 'test.styles.private' ],
212 'only' => ResourceLoaderModule::TYPE_STYLES,
213 'extra' => [],
214 'output' => '<style>.private{}</style>',
215 ],
216 [
217 'context' => [],
218 'modules' => [ 'test.private' ],
219 'only' => ResourceLoaderModule::TYPE_COMBINED,
220 'extra' => [],
221 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",null,{"css":[]});});</script>',
222 ],
223 [
224 'context' => [],
225 // Eg. startup module
226 'modules' => [ 'test.scripts.raw' ],
227 'only' => ResourceLoaderModule::TYPE_SCRIPTS,
228 'extra' => [],
229 'output' => '<script async="" src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts&amp;skin=fallback"></script>',
230 ],
231 [
232 'context' => [],
233 'modules' => [ 'test.scripts.raw' ],
234 'only' => ResourceLoaderModule::TYPE_SCRIPTS,
235 'extra' => [ 'sync' => '1' ],
236 'output' => '<script src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts&amp;skin=fallback&amp;sync=1"></script>',
237 ],
238 [
239 'context' => [],
240 'modules' => [ 'test.scripts.user' ],
241 'only' => ResourceLoaderModule::TYPE_SCRIPTS,
242 'extra' => [],
243 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026skin=fallback\u0026user=Example\u0026version=0a56zyi");});</script>',
244 ],
245 [
246 'context' => [],
247 'modules' => [ 'test.user' ],
248 'only' => ResourceLoaderModule::TYPE_COMBINED,
249 'extra' => [],
250 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026skin=fallback\u0026user=Example\u0026version=0a56zyi");});</script>',
251 ],
252 [
253 'context' => [ 'debug' => 'true' ],
254 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
255 'only' => ResourceLoaderModule::TYPE_STYLES,
256 'extra' => [],
257 'output' => '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.mixed&amp;only=styles&amp;skin=fallback"/>' . "\n"
258 . '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.pure&amp;only=styles&amp;skin=fallback"/>',
259 ],
260 [
261 'context' => [ 'debug' => 'false' ],
262 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
263 'only' => ResourceLoaderModule::TYPE_STYLES,
264 'extra' => [],
265 'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.mixed%2Cpure&amp;only=styles&amp;skin=fallback"/>',
266 ],
267 [
268 'context' => [],
269 'modules' => [ 'test.styles.noscript' ],
270 'only' => ResourceLoaderModule::TYPE_STYLES,
271 'extra' => [],
272 'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.noscript&amp;only=styles&amp;skin=fallback"/></noscript>',
273 ],
274 [
275 'context' => [],
276 'modules' => [ 'test.shouldembed' ],
277 'only' => ResourceLoaderModule::TYPE_COMBINED,
278 'extra' => [],
279 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
280 ],
281 [
282 'context' => [],
283 'modules' => [ 'test.styles.shouldembed' ],
284 'only' => ResourceLoaderModule::TYPE_STYLES,
285 'extra' => [],
286 'output' => '<style>.shouldembed{}</style>',
287 ],
288 [
289 'context' => [],
290 'modules' => [ 'test.scripts.shouldembed' ],
291 'only' => ResourceLoaderModule::TYPE_SCRIPTS,
292 'extra' => [],
293 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
294 ],
295 [
296 'context' => [],
297 'modules' => [ 'test', 'test.shouldembed' ],
298 'only' => ResourceLoaderModule::TYPE_COMBINED,
299 'extra' => [],
300 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test\u0026skin=fallback");mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
301 ],
302 [
303 'context' => [],
304 'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ],
305 'only' => ResourceLoaderModule::TYPE_STYLES,
306 'extra' => [],
307 'output' =>
308 '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.pure&amp;only=styles&amp;skin=fallback"/>' . "\n"
309 . '<style>.shouldembed{}</style>'
310 ],
311 [
312 'context' => [],
313 'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ],
314 'only' => ResourceLoaderModule::TYPE_STYLES,
315 'extra' => [],
316 'output' =>
317 '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.a%2Cb&amp;only=styles&amp;skin=fallback"/>' . "\n"
318 . '<style>.orderingC{}.orderingD{}</style>' . "\n"
319 . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.e&amp;only=styles&amp;skin=fallback"/>'
320 ],
321 ];
322 // phpcs:enable
323 }
324
325 /**
326 * @dataProvider provideMakeLoad
327 * @covers ResourceLoaderClientHtml
328 * @covers ResourceLoaderModule::getModuleContent
329 * @covers ResourceLoader
330 */
331 public function testMakeLoad(
332 array $contextQuery,
333 array $modules,
334 $type,
335 array $extraQuery,
336 $expected
337 ) {
338 $context = self::makeContext( $contextQuery );
339 $context->getResourceLoader()->register( self::makeSampleModules() );
340 $actual = ResourceLoaderClientHtml::makeLoad( $context, $modules, $type, $extraQuery, false );
341 $expected = self::expandVariables( $expected );
342 $this->assertSame( $expected, (string)$actual );
343 }
344
345 public function testGetDocumentAttributes() {
346 $client = new ResourceLoaderClientHtml( self::makeContext() );
347 $this->assertInternalType( 'array', $client->getDocumentAttributes() );
348 }
349
350 private static function expandVariables( $text ) {
351 return strtr( $text, [
352 '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
353 ] );
354 }
355
356 private static function makeContext( $extraQuery = [] ) {
357 $conf = new HashConfig( [
358 'ResourceModuleSkinStyles' => [],
359 'ResourceModules' => [],
360 'EnableJavaScriptTest' => false,
361 'LoadScript' => '/w/load.php',
362 ] );
363 return new ResourceLoaderContext(
364 new ResourceLoader( $conf ),
365 new FauxRequest( array_merge( [
366 'lang' => 'nl',
367 'skin' => 'fallback',
368 'user' => 'Example',
369 'target' => 'phpunit',
370 ], $extraQuery ) )
371 );
372 }
373
374 private static function makeModule( array $options = [] ) {
375 return new ResourceLoaderTestModule( $options );
376 }
377
378 private static function makeSampleModules() {
379 $modules = [
380 'test' => [],
381 'test.private' => [ 'group' => 'private' ],
382 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
383 'test.shouldembed' => [ 'shouldEmbed' => true ],
384 'test.user' => [ 'group' => 'user' ],
385
386 'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ],
387 'test.styles.mixed' => [],
388 'test.styles.noscript' => [
389 'type' => ResourceLoaderModule::LOAD_STYLES,
390 'group' => 'noscript',
391 ],
392 'test.styles.user' => [
393 'type' => ResourceLoaderModule::LOAD_STYLES,
394 'group' => 'user',
395 ],
396 'test.styles.user.empty' => [
397 'type' => ResourceLoaderModule::LOAD_STYLES,
398 'group' => 'user',
399 'isKnownEmpty' => true,
400 ],
401 'test.styles.private' => [
402 'type' => ResourceLoaderModule::LOAD_STYLES,
403 'group' => 'private',
404 'styles' => '.private{}',
405 ],
406 'test.styles.shouldembed' => [
407 'type' => ResourceLoaderModule::LOAD_STYLES,
408 'shouldEmbed' => true,
409 'styles' => '.shouldembed{}',
410 ],
411 'test.styles.deprecated' => [
412 'type' => ResourceLoaderModule::LOAD_STYLES,
413 'deprecated' => 'Deprecation message.',
414 ],
415
416 'test.scripts' => [],
417 'test.scripts.user' => [ 'group' => 'user' ],
418 'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
419 'test.scripts.raw' => [ 'isRaw' => true ],
420 'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
421
422 'test.ordering.a' => [ 'shouldEmbed' => false ],
423 'test.ordering.b' => [ 'shouldEmbed' => false ],
424 'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
425 'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
426 'test.ordering.e' => [ 'shouldEmbed' => false ],
427 ];
428 return array_map( function ( $options ) {
429 return self::makeModule( $options );
430 }, $modules );
431 }
432 }