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>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&modules=test.styles.deprecated%2Cpure&only=styles&skin=fallback"/>' . "\n"
118 . '<style>.private{}</style>' . "\n"
119 . '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&skin=fallback"></script>';
121 $expected = self
::expandVariables( $expected );
123 $this->assertSame( $expected, (string)$client->getHeadHtml() );
127 * Confirm that 'target' is passed down to the startup module's load url.
129 public function testGetHeadHtmlWithTarget() {
130 $client = new ResourceLoaderClientHtml(
132 [ 'target' => 'example' ]
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&modules=startup&only=scripts&skin=fallback&target=example"></script>';
140 $this->assertSame( $expected, (string)$client->getHeadHtml() );
144 * Confirm that 'safemode' is passed down to startup.
146 public function testGetHeadHtmlWithSafemode() {
147 $client = new ResourceLoaderClientHtml(
149 [ 'safemode' => '1' ]
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&modules=startup&only=scripts&safemode=1&skin=fallback"></script>';
157 $this->assertSame( $expected, (string)$client->getHeadHtml() );
161 * Confirm that a null 'target' is the same as no target.
163 public function testGetHeadHtmlWithNullTarget() {
164 $client = new ResourceLoaderClientHtml(
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&modules=startup&only=scripts&skin=fallback"></script>';
174 $this->assertSame( $expected, (string)$client->getHeadHtml() );
177 public function testGetBodyHtml() {
178 $context = self
::makeContext();
179 $context->getResourceLoader()->register( self
::makeSampleModules() );
181 $client = new ResourceLoaderClientHtml( $context, [ 'nonce' => false ] );
182 $client->setConfig( [ 'key' => 'value' ] );
183 $client->setModules( [
185 'test.private.bottom',
187 $client->setModuleStyles( [
188 'test.styles.deprecated',
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.");'
196 $this->assertSame( $expected, (string)$client->getBodyHtml() );
199 public static function provideMakeLoad() {
200 // phpcs:disable Generic.Files.LineLength
204 'modules' => [ 'test.unknown' ],
205 'only' => ResourceLoaderModule
::TYPE_STYLES
,
211 'modules' => [ 'test.styles.private' ],
212 'only' => ResourceLoaderModule
::TYPE_STYLES
,
214 'output' => '<style>.private{}</style>',
218 'modules' => [ 'test.private' ],
219 'only' => ResourceLoaderModule
::TYPE_COMBINED
,
221 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",null,{"css":[]});});</script>',
225 // Eg. startup module
226 'modules' => [ 'test.scripts.raw' ],
227 'only' => ResourceLoaderModule
::TYPE_SCRIPTS
,
229 'output' => '<script async="" src="/w/load.php?lang=nl&modules=test.scripts.raw&only=scripts&skin=fallback"></script>',
233 'modules' => [ 'test.scripts.raw' ],
234 'only' => ResourceLoaderModule
::TYPE_SCRIPTS
,
235 'extra' => [ 'sync' => '1' ],
236 'output' => '<script src="/w/load.php?lang=nl&modules=test.scripts.raw&only=scripts&skin=fallback&sync=1"></script>',
240 'modules' => [ 'test.scripts.user' ],
241 'only' => ResourceLoaderModule
::TYPE_SCRIPTS
,
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>',
247 'modules' => [ 'test.user' ],
248 'only' => ResourceLoaderModule
::TYPE_COMBINED
,
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>',
253 'context' => [ 'debug' => 'true' ],
254 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
255 'only' => ResourceLoaderModule
::TYPE_STYLES
,
257 'output' => '<link rel="stylesheet" href="/w/load.php?debug=true&lang=nl&modules=test.styles.mixed&only=styles&skin=fallback"/>' . "\n"
258 . '<link rel="stylesheet" href="/w/load.php?debug=true&lang=nl&modules=test.styles.pure&only=styles&skin=fallback"/>',
261 'context' => [ 'debug' => 'false' ],
262 'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
263 'only' => ResourceLoaderModule
::TYPE_STYLES
,
265 'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.mixed%2Cpure&only=styles&skin=fallback"/>',
269 'modules' => [ 'test.styles.noscript' ],
270 'only' => ResourceLoaderModule
::TYPE_STYLES
,
272 'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.noscript&only=styles&skin=fallback"/></noscript>',
276 'modules' => [ 'test.shouldembed' ],
277 'only' => ResourceLoaderModule
::TYPE_COMBINED
,
279 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
283 'modules' => [ 'test.styles.shouldembed' ],
284 'only' => ResourceLoaderModule
::TYPE_STYLES
,
286 'output' => '<style>.shouldembed{}</style>',
290 'modules' => [ 'test.scripts.shouldembed' ],
291 'only' => ResourceLoaderModule
::TYPE_SCRIPTS
,
293 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
297 'modules' => [ 'test', 'test.shouldembed' ],
298 'only' => ResourceLoaderModule
::TYPE_COMBINED
,
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>',
304 'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ],
305 'only' => ResourceLoaderModule
::TYPE_STYLES
,
308 '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.styles.pure&only=styles&skin=fallback"/>' . "\n"
309 . '<style>.shouldembed{}</style>'
313 'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ],
314 'only' => ResourceLoaderModule
::TYPE_STYLES
,
317 '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.ordering.a%2Cb&only=styles&skin=fallback"/>' . "\n"
318 . '<style>.orderingC{}.orderingD{}</style>' . "\n"
319 . '<link rel="stylesheet" href="/w/load.php?lang=nl&modules=test.ordering.e&only=styles&skin=fallback"/>'
326 * @dataProvider provideMakeLoad
327 * @covers ResourceLoaderClientHtml
328 * @covers ResourceLoaderModule::getModuleContent
329 * @covers ResourceLoader
331 public function testMakeLoad(
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 );
345 public function testGetDocumentAttributes() {
346 $client = new ResourceLoaderClientHtml( self
::makeContext() );
347 $this->assertInternalType( 'array', $client->getDocumentAttributes() );
350 private static function expandVariables( $text ) {
351 return strtr( $text, [
352 '{blankVer}' => ResourceLoaderTestCase
::BLANK_VERSION
356 private static function makeContext( $extraQuery = [] ) {
357 $conf = new HashConfig( [
358 'ResourceModuleSkinStyles' => [],
359 'ResourceModules' => [],
360 'EnableJavaScriptTest' => false,
361 'LoadScript' => '/w/load.php',
363 return new ResourceLoaderContext(
364 new ResourceLoader( $conf ),
365 new FauxRequest( array_merge( [
367 'skin' => 'fallback',
369 'target' => 'phpunit',
374 private static function makeModule( array $options = [] ) {
375 return new ResourceLoaderTestModule( $options );
378 private static function makeSampleModules() {
381 'test.private' => [ 'group' => 'private' ],
382 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
383 'test.shouldembed' => [ 'shouldEmbed' => true ],
384 'test.user' => [ 'group' => 'user' ],
386 'test.styles.pure' => [ 'type' => ResourceLoaderModule
::LOAD_STYLES
],
387 'test.styles.mixed' => [],
388 'test.styles.noscript' => [
389 'type' => ResourceLoaderModule
::LOAD_STYLES
,
390 'group' => 'noscript',
392 'test.styles.user' => [
393 'type' => ResourceLoaderModule
::LOAD_STYLES
,
396 'test.styles.user.empty' => [
397 'type' => ResourceLoaderModule
::LOAD_STYLES
,
399 'isKnownEmpty' => true,
401 'test.styles.private' => [
402 'type' => ResourceLoaderModule
::LOAD_STYLES
,
403 'group' => 'private',
404 'styles' => '.private{}',
406 'test.styles.shouldembed' => [
407 'type' => ResourceLoaderModule
::LOAD_STYLES
,
408 'shouldEmbed' => true,
409 'styles' => '.shouldembed{}',
411 'test.styles.deprecated' => [
412 'type' => ResourceLoaderModule
::LOAD_STYLES
,
413 'deprecated' => 'Deprecation message.',
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 ],
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 ],
428 return array_map( function ( $options ) {
429 return self
::makeModule( $options );