3 use Wikimedia\TestingAccessWrapper
;
6 * @group ResourceLoader
7 * @covers ResourceLoaderClientHtml
9 class ResourceLoaderClientHtmlTest
extends PHPUnit\Framework\TestCase
{
11 use MediaWikiCoversValidator
;
13 public function testGetData() {
14 $context = self
::makeContext();
15 $context->getResourceLoader()->register( self
::makeSampleModules() );
17 $client = new ResourceLoaderClientHtml( $context );
18 $client->setModules( [
21 'test.shouldembed.empty',
26 $client->setModuleStyles( [
28 'test.styles.user.empty',
29 'test.styles.private',
31 'test.styles.shouldembed',
32 'test.styles.deprecated',
33 'test.unregistered.styles',
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.
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',
65 'test.styles.deprecated',
68 'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ],
75 'styleDeprecations' => [
78 [ 'This page is using the deprecated ResourceLoader module "test.styles.deprecated".
79 Deprecation message.' ]
84 $access = TestingAccessWrapper
::newFromObject( $client );
85 $this->assertEquals( $expected, $access->getData() );
88 public function testGetHeadHtml() {
89 $context = self
::makeContext();
90 $context->getResourceLoader()->register( self
::makeSampleModules() );
92 $client = new ResourceLoaderClientHtml( $context, [
95 $client->setConfig( [ 'key' => 'value' ] );
96 $client->setModules( [
100 $client->setModuleStyles( [
102 'test.styles.private',
103 'test.styles.deprecated',
105 $client->setExemptStates( [
106 'test.exempt' => 'ready',
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"];'
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&modules=test.styles.deprecated%2Cpure&only=styles"/>' . "\n"
120 . '<style>.private{}</style>' . "\n"
121 . '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&raw=1"></script>';
123 $expected = self
::expandVariables( $expected );
125 $this->assertSame( $expected, (string)$client->getHeadHtml() );
129 * Confirm that 'target' is passed down to the startup module's load url.
131 public function testGetHeadHtmlWithTarget() {
132 $client = new ResourceLoaderClientHtml(
134 [ 'target' => 'example' ]
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&modules=startup&only=scripts&raw=1&target=example"></script>';
142 $this->assertSame( $expected, (string)$client->getHeadHtml() );
146 * Confirm that 'safemode' is passed down to startup.
148 public function testGetHeadHtmlWithSafemode() {
149 $client = new ResourceLoaderClientHtml(
151 [ 'safemode' => '1' ]
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&modules=startup&only=scripts&raw=1&safemode=1"></script>';
159 $this->assertSame( $expected, (string)$client->getHeadHtml() );
163 * Confirm that a null 'target' is the same as no target.
165 public function testGetHeadHtmlWithNullTarget() {
166 $client = new ResourceLoaderClientHtml(
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&modules=startup&only=scripts&raw=1"></script>';
176 $this->assertSame( $expected, (string)$client->getHeadHtml() );
179 public function testGetBodyHtml() {
180 $context = self
::makeContext();
181 $context->getResourceLoader()->register( self
::makeSampleModules() );
183 $client = new ResourceLoaderClientHtml( $context, [ 'nonce' => false ] );
184 $client->setConfig( [ 'key' => 'value' ] );
185 $client->setModules( [
187 'test.private.bottom',
189 $client->setModuleStyles( [
190 'test.styles.deprecated',
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.");'
198 $this->assertSame( $expected, (string)$client->getBodyHtml() );
201 public static function provideMakeLoad() {
202 // phpcs:disable Generic.Files.LineLength
206 'modules' => [ 'test.unknown' ],
207 'only' => ResourceLoaderModule
::TYPE_STYLES
,
213 'modules' => [ 'test.styles.private' ],
214 'only' => ResourceLoaderModule
::TYPE_STYLES
,
216 'output' => '<style>.private{}</style>',
220 'modules' => [ 'test.private' ],
221 'only' => ResourceLoaderModule
::TYPE_COMBINED
,
223 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",null,{"css":[]});});</script>',
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&modules=test.scripts&only=scripts&raw=1"></script>',
235 'modules' => [ 'test.scripts' ],
236 'only' => ResourceLoaderModule
::TYPE_SCRIPTS
,
237 'extra' => [ 'raw' => '1', 'sync' => '1' ],
238 'output' => '<script src="/w/load.php?lang=nl&modules=test.scripts&only=scripts&raw=1&sync=1"></script>',
242 'modules' => [ 'test.scripts.user' ],
243 'only' => ResourceLoaderModule
::TYPE_SCRIPTS
,
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>',
249 'modules' => [ 'test.user' ],
250 'only' => ResourceLoaderModule
::TYPE_COMBINED
,
252 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026user=Example\u0026version=0a56zyi");});</script>',
255 'context' => [ 'debug' => 'true' ],
256 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
257 'only' => ResourceLoaderModule
::TYPE_STYLES
,
259 'output' => '<link rel="stylesheet" href="/w/load.php?debug=true&lang=nl&modules=test.styles.mixed&only=styles"/>' . "\n"
260 . '<link rel="stylesheet" href="/w/load.php?debug=true&lang=nl&modules=test.styles.pure&only=styles"/>',
263 'context' => [ 'debug' => 'false' ],
264 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
265 'only' => ResourceLoaderModule
::TYPE_STYLES
,
267 'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.mixed%2Cpure&only=styles"/>',
271 'modules' => [ 'test.styles.noscript' ],
272 'only' => ResourceLoaderModule
::TYPE_STYLES
,
274 'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.noscript&only=styles"/></noscript>',
278 'modules' => [ 'test.shouldembed' ],
279 'only' => ResourceLoaderModule
::TYPE_COMBINED
,
281 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
285 'modules' => [ 'test.styles.shouldembed' ],
286 'only' => ResourceLoaderModule
::TYPE_STYLES
,
288 'output' => '<style>.shouldembed{}</style>',
292 'modules' => [ 'test.scripts.shouldembed' ],
293 'only' => ResourceLoaderModule
::TYPE_SCRIPTS
,
295 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
299 'modules' => [ 'test', 'test.shouldembed' ],
300 'only' => ResourceLoaderModule
::TYPE_COMBINED
,
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>',
306 'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ],
307 'only' => ResourceLoaderModule
::TYPE_STYLES
,
310 '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.pure&only=styles"/>' . "\n"
311 . '<style>.shouldembed{}</style>'
315 'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ],
316 'only' => ResourceLoaderModule
::TYPE_STYLES
,
319 '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.ordering.a%2Cb&only=styles"/>' . "\n"
320 . '<style>.orderingC{}.orderingD{}</style>' . "\n"
321 . '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.ordering.e&only=styles"/>'
328 * @dataProvider provideMakeLoad
329 * @covers ResourceLoaderClientHtml
330 * @covers ResourceLoaderModule::getModuleContent
331 * @covers ResourceLoader
333 public function testMakeLoad(
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 );
347 public function testGetDocumentAttributes() {
348 $client = new ResourceLoaderClientHtml( self
::makeContext() );
349 $this->assertInternalType( 'array', $client->getDocumentAttributes() );
352 private static function expandVariables( $text ) {
353 return strtr( $text, [
354 '{blankVer}' => ResourceLoaderTestCase
::BLANK_VERSION
358 private static function makeContext( $extraQuery = [] ) {
359 $conf = new HashConfig( [
360 'ResourceModuleSkinStyles' => [],
361 'ResourceModules' => [],
362 'EnableJavaScriptTest' => false,
363 'LoadScript' => '/w/load.php',
365 return new ResourceLoaderContext(
366 new ResourceLoader( $conf ),
367 new FauxRequest( array_merge( [
369 'skin' => 'fallback',
371 'target' => 'phpunit',
376 private static function makeModule( array $options = [] ) {
377 return new ResourceLoaderTestModule( $options );
380 private static function makeSampleModules() {
383 'test.private' => [ 'group' => 'private' ],
384 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
385 'test.shouldembed' => [ 'shouldEmbed' => true ],
386 'test.user' => [ 'group' => 'user' ],
388 'test.styles.pure' => [ 'type' => ResourceLoaderModule
::LOAD_STYLES
],
389 'test.styles.mixed' => [],
390 'test.styles.noscript' => [
391 'type' => ResourceLoaderModule
::LOAD_STYLES
,
392 'group' => 'noscript',
394 'test.styles.user' => [
395 'type' => ResourceLoaderModule
::LOAD_STYLES
,
398 'test.styles.user.empty' => [
399 'type' => ResourceLoaderModule
::LOAD_STYLES
,
401 'isKnownEmpty' => true,
403 'test.styles.private' => [
404 'type' => ResourceLoaderModule
::LOAD_STYLES
,
405 'group' => 'private',
406 'styles' => '.private{}',
408 'test.styles.shouldembed' => [
409 'type' => ResourceLoaderModule
::LOAD_STYLES
,
410 'shouldEmbed' => true,
411 'styles' => '.shouldembed{}',
413 'test.styles.deprecated' => [
414 'type' => ResourceLoaderModule
::LOAD_STYLES
,
415 'deprecated' => 'Deprecation message.',
418 'test.scripts' => [],
419 'test.scripts.user' => [ 'group' => 'user' ],
420 'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
421 'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
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 ],
429 return array_map( function ( $options ) {
430 return self
::makeModule( $options );