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
;
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( [
31 'test.shouldembed.empty',
36 $client->setModuleStyles( [
38 'test.styles.user.empty',
39 'test.styles.private',
41 'test.styles.shouldembed',
42 'test.styles.deprecated',
43 'test.unregistered.styles',
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.
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',
75 'test.styles.deprecated',
78 'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ],
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( [
105 $client->setModuleStyles( [
107 'test.styles.private',
108 'test.styles.deprecated',
110 $client->setExemptStates( [
111 'test.exempt' => 'ready',
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"];'
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&modules=test.styles.deprecated%2Cpure&only=styles">' . "\n"
123 . '<style>.private{}</style>' . "\n"
124 . '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&raw=1"></script>';
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(
137 [ 'target' => 'example' ]
139 $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
140 . '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&raw=1&target=example"></script>';
143 $this->assertSame( $expected, (string)$client->getHeadHtml() );
147 * Confirm that 'safemode' is passed down to startup.
149 public function testGetHeadHtmlWithSafemode() {
150 $client = new ClientHtml(
152 [ 'safemode' => '1' ]
154 $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
155 . '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&raw=1&safemode=1"></script>';
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(
169 $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
170 . '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&raw=1"></script>';
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( [
184 'test.private.bottom',
186 $client->setModuleStyles( [
187 'test.styles.deprecated',
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.");'
194 $this->assertSame( $expected, (string)$client->getBodyHtml() );
197 public static function provideMakeLoad() {
201 'modules' => [ 'test.unknown' ],
202 'only' => Module
::TYPE_STYLES
,
208 'modules' => [ 'test.styles.private' ],
209 'only' => Module
::TYPE_STYLES
,
211 'output' => '<style>.private{}</style>',
215 'modules' => [ 'test.private' ],
216 'only' => Module
::TYPE_COMBINED
,
218 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.impl(function(){return["test.private@{blankVer}",null,{"css":[]}];});});</script>',
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&modules=test.scripts&only=scripts&raw=1"></script>',
230 'modules' => [ 'test.scripts.user' ],
231 'only' => Module
::TYPE_SCRIPTS
,
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>',
237 'modules' => [ 'test.user' ],
238 'only' => Module
::TYPE_COMBINED
,
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
,
247 'output' => '<link rel="stylesheet" href="/w/load.php?debug=1&lang=nl&modules=test.styles.mixed&only=styles">' . "\n"
248 . '<link rel="stylesheet" href="/w/load.php?debug=1&lang=nl&modules=test.styles.pure&only=styles">',
251 'context' => [ 'debug' => 'false' ],
252 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
253 'only' => Module
::TYPE_STYLES
,
255 'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.mixed%2Cpure&only=styles">',
259 'modules' => [ 'test.styles.noscript' ],
260 'only' => Module
::TYPE_STYLES
,
262 'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.noscript&only=styles"></noscript>',
266 'modules' => [ 'test.shouldembed' ],
267 'only' => Module
::TYPE_COMBINED
,
269 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.impl(function(){return["test.shouldembed@{blankVer}",null,{"css":[]}];});});</script>',
273 'modules' => [ 'test.styles.shouldembed' ],
274 'only' => Module
::TYPE_STYLES
,
276 'output' => '<style>.shouldembed{}</style>',
280 'modules' => [ 'test.scripts.shouldembed' ],
281 'only' => Module
::TYPE_SCRIPTS
,
283 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
287 'modules' => [ 'test', 'test.shouldembed' ],
288 'only' => Module
::TYPE_COMBINED
,
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>',
294 'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ],
295 'only' => Module
::TYPE_STYLES
,
298 '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.pure&only=styles">' . "\n"
299 . '<style>.shouldembed{}</style>'
303 'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ],
304 'only' => Module
::TYPE_STYLES
,
307 '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.ordering.a%2Cb&only=styles">' . "\n"
308 . '<style>.orderingC{}.orderingD{}</style>' . "\n"
309 . '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.ordering.e&only=styles">'
316 * @dataProvider provideMakeLoad
317 * @covers \MediaWiki\ResourceLoader\ClientHtml
318 * @covers \MediaWiki\ResourceLoader\Module
319 * @covers \MediaWiki\ResourceLoader\ResourceLoader
321 public function testMakeLoad(
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
347 private static function makeContext( $extraQuery = [] ) {
348 $conf = new HashConfig( [] );
350 new ResourceLoader( $conf, null, null, [
351 'loadScript' => '/w/load.php',
353 new FauxRequest( array_merge( [
355 'skin' => 'fallback',
357 'target' => 'phpunit',
362 private static function makeModule( array $options = [] ) {
363 return $options +
[ 'class' => ResourceLoaderTestModule
::class ];
366 private static function makeSampleModules() {
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
,
384 'test.styles.user.empty' => [
385 'type' => Module
::LOAD_STYLES
,
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 );