Merge "docs: Fix typo"
[mediawiki.git] / tests / phpunit / includes / ResourceLoader / ClientHtmlTest.php
blobab56858618d00e79d2a91a37fda45e73cd7c0ad3
1 <?php
3 namespace MediaWiki\Tests\ResourceLoader;
5 use MediaWiki\Config\HashConfig;
6 use MediaWiki\Request\FauxRequest;
7 use MediaWiki\ResourceLoader\ClientHtml;
8 use MediaWiki\ResourceLoader\Context;
9 use MediaWiki\ResourceLoader\Module;
10 use MediaWiki\ResourceLoader\ResourceLoader;
11 use MediaWikiCoversValidator;
12 use PHPUnit\Framework\TestCase;
13 use Wikimedia\TestingAccessWrapper;
15 /**
16 * @group ResourceLoader
17 * @covers \MediaWiki\ResourceLoader\ClientHtml
19 class ClientHtmlTest extends TestCase {
21 use MediaWikiCoversValidator;
23 public function testGetData() {
24 $context = self::makeContext();
25 $context->getResourceLoader()->register( self::makeSampleModules() );
27 $client = new ClientHtml( $context );
28 $client->setModules( [
29 'test',
30 'test.private',
31 'test.shouldembed.empty',
32 'test.shouldembed',
33 'test.user',
34 'test.unregistered',
35 ] );
36 $client->setModuleStyles( [
37 'test.styles.mixed',
38 'test.styles.user.empty',
39 'test.styles.private',
40 'test.styles.pure',
41 'test.styles.shouldembed',
42 'test.styles.deprecated',
43 'test.unregistered.styles',
44 ] );
46 $expected = [
47 'states' => [
48 // The below are NOT queued for loading via `mw.loader.load(Array)`.
49 // Instead we tell the client to set their state to "loading" so that
50 // if they are needed as dependencies, the client will not try to
51 // load them on-demand, because the server is taking care of them already.
52 // Either:
53 // - Embedded as inline scripts in the HTML (e.g. user-private code, and
54 // previews). Once that script tag is reached, the state is "loaded".
55 // - Loaded directly from the HTML with a dedicated HTTP request (e.g.
56 // user scripts, which vary by a 'user' and 'version' parameter that
57 // the static user-agnostic startup module won't have).
58 'test.private' => 'loading',
59 'test.shouldembed' => 'loading',
60 'test.user' => 'loading',
61 // The below are known to the server to be empty scripts, or to be
62 // synchronously loaded stylesheets. These start in the "ready" state.
63 'test.shouldembed.empty' => 'ready',
64 'test.styles.pure' => 'ready',
65 'test.styles.user.empty' => 'ready',
66 'test.styles.private' => 'ready',
67 'test.styles.shouldembed' => 'ready',
68 'test.styles.deprecated' => 'ready',
70 'general' => [
71 'test',
73 'styles' => [
74 'test.styles.pure',
75 'test.styles.deprecated',
77 'embed' => [
78 'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ],
79 'general' => [
80 'test.private',
81 'test.shouldembed',
82 'test.user',
85 'styleDeprecations' => [
86 // phpcs:ignore Generic.Files.LineLength.TooLong
87 "This page is using the deprecated ResourceLoader module \"test.styles.deprecated\".\nDeprecation message."
91 $access = TestingAccessWrapper::newFromObject( $client );
92 $this->assertEquals( $expected, $access->getData() );
95 public function testGetHeadHtml() {
96 $context = self::makeContext();
97 $context->getResourceLoader()->register( self::makeSampleModules() );
99 $client = new ClientHtml( $context );
100 $client->setConfig( [ 'key' => 'value' ] );
101 $client->setModules( [
102 'test',
103 'test.private',
104 ] );
105 $client->setModuleStyles( [
106 'test.styles.pure',
107 'test.styles.private',
108 'test.styles.deprecated',
109 ] );
110 $client->setExemptStates( [
111 'test.exempt' => 'ready',
112 ] );
113 $expected = '<script>'
114 . 'document.documentElement.className="client-js";'
115 . 'RLCONF={"key":"value"};'
116 . 'RLSTATE={"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.styles.deprecated":"ready"};'
117 . 'RLPAGEMODULES=["test"];'
118 . '</script>' . "\n"
119 . '<script>(RLQ=window.RLQ||[]).push(function(){'
120 . 'mw.loader.impl(function(){return["test.private@{blankVer}",null,{"css":[]}];});'
121 . '});</script>' . "\n"
122 . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.deprecated%2Cpure&amp;only=styles">' . "\n"
123 . '<style>.private{}</style>' . "\n"
124 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1"></script>';
125 // phpcs:enable
126 $expected = self::expandVariables( $expected );
128 $this->assertSame( $expected, (string)$client->getHeadHtml() );
132 * Confirm that 'target' is passed down to the startup module's load url.
134 public function testGetHeadHtmlWithTarget() {
135 $client = new ClientHtml(
136 self::makeContext(),
137 [ 'target' => 'example' ]
139 $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
140 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;target=example"></script>';
141 // phpcs:enable
143 $this->assertSame( $expected, (string)$client->getHeadHtml() );
147 * Confirm that 'safemode' is passed down to startup.
149 public function testGetHeadHtmlWithSafemode() {
150 $client = new ClientHtml(
151 self::makeContext(),
152 [ 'safemode' => '1' ]
154 $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
155 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1&amp;safemode=1"></script>';
156 // phpcs:enable
158 $this->assertSame( $expected, (string)$client->getHeadHtml() );
162 * Confirm that a null 'target' is the same as no target.
164 public function testGetHeadHtmlWithNullTarget() {
165 $client = new ClientHtml(
166 self::makeContext(),
167 [ 'target' => null ]
169 $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
170 . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;raw=1"></script>';
171 // phpcs:enable
173 $this->assertSame( $expected, (string)$client->getHeadHtml() );
176 public function testGetBodyHtml() {
177 $context = self::makeContext();
178 $context->getResourceLoader()->register( self::makeSampleModules() );
180 $client = new ClientHtml( $context );
181 $client->setConfig( [ 'key' => 'value' ] );
182 $client->setModules( [
183 'test',
184 'test.private.bottom',
185 ] );
186 $client->setModuleStyles( [
187 'test.styles.deprecated',
188 ] );
189 $expected = '<script>(RLQ=window.RLQ||[]).push(function(){'
190 . 'mw.log.warn("This page is using the deprecated ResourceLoader module \"test.styles.deprecated\".\nDeprecation message.");'
191 . '});</script>';
192 // phpcs:enable
194 $this->assertSame( $expected, (string)$client->getBodyHtml() );
197 public static function provideMakeLoad() {
198 return [
200 'context' => [],
201 'modules' => [ 'test.unknown' ],
202 'only' => Module::TYPE_STYLES,
203 'extra' => [],
204 'output' => '',
207 'context' => [],
208 'modules' => [ 'test.styles.private' ],
209 'only' => Module::TYPE_STYLES,
210 'extra' => [],
211 'output' => '<style>.private{}</style>',
214 'context' => [],
215 'modules' => [ 'test.private' ],
216 'only' => Module::TYPE_COMBINED,
217 'extra' => [],
218 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.impl(function(){return["test.private@{blankVer}",null,{"css":[]}];});});</script>',
221 'context' => [],
222 'modules' => [ 'test.scripts' ],
223 'only' => Module::TYPE_SCRIPTS,
224 // Eg. startup module
225 'extra' => [ 'raw' => '1' ],
226 'output' => '<script async="" src="/w/load.php?lang=nl&amp;modules=test.scripts&amp;only=scripts&amp;raw=1"></script>',
229 'context' => [],
230 'modules' => [ 'test.scripts.user' ],
231 'only' => Module::TYPE_SCRIPTS,
232 'extra' => [],
233 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026user=Example\u0026version={blankCombi}");});</script>',
236 'context' => [],
237 'modules' => [ 'test.user' ],
238 'only' => Module::TYPE_COMBINED,
239 'extra' => [],
240 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026user=Example\u0026version={blankCombi}");});</script>',
243 'context' => [ 'debug' => 'true' ],
244 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
245 'only' => Module::TYPE_STYLES,
246 'extra' => [],
247 'output' => '<link rel="stylesheet" href="/w/load.php?debug=1&amp;lang=nl&amp;modules=test.styles.mixed&amp;only=styles">' . "\n"
248 . '<link rel="stylesheet" href="/w/load.php?debug=1&amp;lang=nl&amp;modules=test.styles.pure&amp;only=styles">',
251 'context' => [ 'debug' => 'false' ],
252 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
253 'only' => Module::TYPE_STYLES,
254 'extra' => [],
255 'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.mixed%2Cpure&amp;only=styles">',
258 'context' => [],
259 'modules' => [ 'test.styles.noscript' ],
260 'only' => Module::TYPE_STYLES,
261 'extra' => [],
262 'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.noscript&amp;only=styles"></noscript>',
265 'context' => [],
266 'modules' => [ 'test.shouldembed' ],
267 'only' => Module::TYPE_COMBINED,
268 'extra' => [],
269 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.impl(function(){return["test.shouldembed@{blankVer}",null,{"css":[]}];});});</script>',
272 'context' => [],
273 'modules' => [ 'test.styles.shouldembed' ],
274 'only' => Module::TYPE_STYLES,
275 'extra' => [],
276 'output' => '<style>.shouldembed{}</style>',
279 'context' => [],
280 'modules' => [ 'test.scripts.shouldembed' ],
281 'only' => Module::TYPE_SCRIPTS,
282 'extra' => [],
283 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
286 'context' => [],
287 'modules' => [ 'test', 'test.shouldembed' ],
288 'only' => Module::TYPE_COMBINED,
289 'extra' => [],
290 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test");mw.loader.impl(function(){return["test.shouldembed@{blankVer}",null,{"css":[]}];});});</script>',
293 'context' => [],
294 'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ],
295 'only' => Module::TYPE_STYLES,
296 'extra' => [],
297 'output' =>
298 '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.pure&amp;only=styles">' . "\n"
299 . '<style>.shouldembed{}</style>'
302 'context' => [],
303 'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ],
304 'only' => Module::TYPE_STYLES,
305 'extra' => [],
306 'output' =>
307 '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.a%2Cb&amp;only=styles">' . "\n"
308 . '<style>.orderingC{}.orderingD{}</style>' . "\n"
309 . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.e&amp;only=styles">'
312 // phpcs:enable
316 * @dataProvider provideMakeLoad
317 * @covers \MediaWiki\ResourceLoader\ClientHtml
318 * @covers \MediaWiki\ResourceLoader\Module
319 * @covers \MediaWiki\ResourceLoader\ResourceLoader
321 public function testMakeLoad(
322 array $contextQuery,
323 array $modules,
324 $type,
325 array $extraQuery,
326 $expected
328 $context = self::makeContext( $contextQuery );
329 $context->getResourceLoader()->register( self::makeSampleModules() );
330 $actual = ClientHtml::makeLoad( $context, $modules, $type, $extraQuery, false );
331 $expected = self::expandVariables( $expected );
332 $this->assertSame( $expected, (string)$actual );
335 public function testGetDocumentAttributes() {
336 $client = new ClientHtml( self::makeContext() );
337 $this->assertIsArray( $client->getDocumentAttributes() );
340 private static function expandVariables( $text ) {
341 return strtr( $text, [
342 '{blankCombi}' => ResourceLoaderTestCase::BLANK_COMBI,
343 '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
344 ] );
347 private static function makeContext( $extraQuery = [] ) {
348 $conf = new HashConfig( [] );
349 return new Context(
350 new ResourceLoader( $conf, null, null, [
351 'loadScript' => '/w/load.php',
352 ] ),
353 new FauxRequest( array_merge( [
354 'lang' => 'nl',
355 'skin' => 'fallback',
356 'user' => 'Example',
357 'target' => 'phpunit',
358 ], $extraQuery ) )
362 private static function makeModule( array $options = [] ) {
363 return $options + [ 'class' => ResourceLoaderTestModule::class ];
366 private static function makeSampleModules() {
367 $modules = [
368 'test' => [],
369 'test.private' => [ 'group' => 'private' ],
370 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
371 'test.shouldembed' => [ 'shouldEmbed' => true ],
372 'test.user' => [ 'group' => 'user' ],
374 'test.styles.pure' => [ 'type' => Module::LOAD_STYLES ],
375 'test.styles.mixed' => [],
376 'test.styles.noscript' => [
377 'type' => Module::LOAD_STYLES,
378 'group' => 'noscript',
380 'test.styles.user' => [
381 'type' => Module::LOAD_STYLES,
382 'group' => 'user',
384 'test.styles.user.empty' => [
385 'type' => Module::LOAD_STYLES,
386 'group' => 'user',
387 'isKnownEmpty' => true,
389 'test.styles.private' => [
390 'type' => Module::LOAD_STYLES,
391 'group' => 'private',
392 'styles' => '.private{}',
394 'test.styles.shouldembed' => [
395 'type' => Module::LOAD_STYLES,
396 'shouldEmbed' => true,
397 'styles' => '.shouldembed{}',
399 'test.styles.deprecated' => [
400 'type' => Module::LOAD_STYLES,
401 'deprecated' => 'Deprecation message.',
404 'test.scripts' => [],
405 'test.scripts.user' => [ 'group' => 'user' ],
406 'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
407 'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
409 'test.ordering.a' => [ 'shouldEmbed' => false ],
410 'test.ordering.b' => [ 'shouldEmbed' => false ],
411 'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
412 'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
413 'test.ordering.e' => [ 'shouldEmbed' => false ],
415 return array_map( static function ( $options ) {
416 return self::makeModule( $options );
417 }, $modules );