RCFilters: Style the Saved Links placeholder and add a title
[lhc/web/wiklou.git] / tests / phpunit / includes / resourceloader / ResourceLoaderTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 class ResourceLoaderTest extends ResourceLoaderTestCase {
6
7 protected function setUp() {
8 parent::setUp();
9
10 $this->setMwGlobals( [
11 'wgResourceLoaderLESSImportPaths' => [
12 dirname( dirname( __DIR__ ) ) . '/data/less/common',
13 ],
14 'wgResourceLoaderLESSVars' => [
15 'foo' => '2px',
16 'Foo' => '#eeeeee',
17 'bar' => 5,
18 ],
19 // Clear ResourceLoaderGetConfigVars hooks (called by StartupModule)
20 // to avoid notices during testMakeModuleResponse for missing
21 // wgResourceLoaderLESSVars keys in extension hooks.
22 'wgHooks' => [],
23 'wgShowExceptionDetails' => true,
24 ] );
25 }
26
27 /**
28 * Ensure the ResourceLoaderRegisterModules hook is called.
29 *
30 * @covers ResourceLoader::__construct
31 */
32 public function testConstructRegistrationHook() {
33 $resourceLoaderRegisterModulesHook = false;
34
35 $this->setMwGlobals( 'wgHooks', [
36 'ResourceLoaderRegisterModules' => [
37 function ( &$resourceLoader ) use ( &$resourceLoaderRegisterModulesHook ) {
38 $resourceLoaderRegisterModulesHook = true;
39 }
40 ]
41 ] );
42
43 $unused = new ResourceLoader();
44 $this->assertTrue(
45 $resourceLoaderRegisterModulesHook,
46 'Hook ResourceLoaderRegisterModules called'
47 );
48 }
49
50 /**
51 * @covers ResourceLoader::register
52 * @covers ResourceLoader::getModule
53 */
54 public function testRegisterValidObject() {
55 $module = new ResourceLoaderTestModule();
56 $resourceLoader = new EmptyResourceLoader();
57 $resourceLoader->register( 'test', $module );
58 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
59 }
60
61 /**
62 * @covers ResourceLoader::register
63 * @covers ResourceLoader::getModule
64 */
65 public function testRegisterValidArray() {
66 $module = new ResourceLoaderTestModule();
67 $resourceLoader = new EmptyResourceLoader();
68 // Covers case of register() setting $rl->moduleInfos,
69 // but $rl->modules lazy-populated by getModule()
70 $resourceLoader->register( 'test', [ 'object' => $module ] );
71 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
72 }
73
74 /**
75 * @covers ResourceLoader::register
76 */
77 public function testRegisterEmptyString() {
78 $module = new ResourceLoaderTestModule();
79 $resourceLoader = new EmptyResourceLoader();
80 $resourceLoader->register( '', $module );
81 $this->assertEquals( $module, $resourceLoader->getModule( '' ) );
82 }
83
84 /**
85 * @covers ResourceLoader::register
86 */
87 public function testRegisterInvalidName() {
88 $resourceLoader = new EmptyResourceLoader();
89 $this->setExpectedException( 'MWException', "name 'test!invalid' is invalid" );
90 $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() );
91 }
92
93 /**
94 * @covers ResourceLoader::register
95 */
96 public function testRegisterInvalidType() {
97 $resourceLoader = new EmptyResourceLoader();
98 $this->setExpectedException( 'MWException', 'ResourceLoader module info type error' );
99 $resourceLoader->register( 'test', new stdClass() );
100 }
101
102 /**
103 * @covers ResourceLoader::getModuleNames
104 */
105 public function testGetModuleNames() {
106 // Use an empty one so that core and extension modules don't get in.
107 $resourceLoader = new EmptyResourceLoader();
108 $resourceLoader->register( 'test.foo', new ResourceLoaderTestModule() );
109 $resourceLoader->register( 'test.bar', new ResourceLoaderTestModule() );
110 $this->assertEquals(
111 [ 'test.foo', 'test.bar' ],
112 $resourceLoader->getModuleNames()
113 );
114 }
115
116 /**
117 * @covers ResourceLoader::isModuleRegistered
118 */
119 public function testIsModuleRegistered() {
120 $rl = new EmptyResourceLoader();
121 $rl->register( 'test', new ResourceLoaderTestModule() );
122 $this->assertTrue( $rl->isModuleRegistered( 'test' ) );
123 $this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
124 }
125
126 /**
127 * @covers ResourceLoader::getModule
128 */
129 public function testGetModuleUnknown() {
130 $rl = new EmptyResourceLoader();
131 $this->assertSame( null, $rl->getModule( 'test' ) );
132 }
133
134 /**
135 * @covers ResourceLoader::getModule
136 */
137 public function testGetModuleClass() {
138 $rl = new EmptyResourceLoader();
139 $rl->register( 'test', [ 'class' => ResourceLoaderTestModule::class ] );
140 $this->assertInstanceOf(
141 ResourceLoaderTestModule::class,
142 $rl->getModule( 'test' )
143 );
144 }
145
146 /**
147 * @covers ResourceLoader::getModule
148 */
149 public function testGetModuleClassDefault() {
150 $rl = new EmptyResourceLoader();
151 $rl->register( 'test', [] );
152 $this->assertInstanceOf(
153 ResourceLoaderFileModule::class,
154 $rl->getModule( 'test' ),
155 'Array-style module registrations default to FileModule'
156 );
157 }
158
159 /**
160 * @covers ResourceLoaderFileModule::compileLessFile
161 */
162 public function testLessFileCompilation() {
163 $context = $this->getResourceLoaderContext();
164 $basePath = __DIR__ . '/../../data/less/module';
165 $module = new ResourceLoaderFileModule( [
166 'localBasePath' => $basePath,
167 'styles' => [ 'styles.less' ],
168 ] );
169 $module->setName( 'test.less' );
170 $styles = $module->getStyles( $context );
171 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
172 }
173
174 public static function providePackedModules() {
175 return [
176 [
177 'Example from makePackedModulesString doc comment',
178 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
179 'foo.bar,baz|bar.baz,quux',
180 ],
181 [
182 'Example from expandModuleNames doc comment',
183 [ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
184 'jquery.foo,bar|jquery.ui.baz,quux',
185 ],
186 [
187 'Regression fixed in r88706 with dotless names',
188 [ 'foo', 'bar', 'baz' ],
189 'foo,bar,baz',
190 ],
191 [
192 'Prefixless modules after a prefixed module',
193 [ 'single.module', 'foobar', 'foobaz' ],
194 'single.module|foobar,foobaz',
195 ],
196 [
197 'Ordering',
198 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
199 'foo|foo.baz,bar|baz.quux',
200 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
201 ]
202 ];
203 }
204
205 /**
206 * @dataProvider providePackedModules
207 * @covers ResourceLoader::makePackedModulesString
208 */
209 public function testMakePackedModulesString( $desc, $modules, $packed ) {
210 $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc );
211 }
212
213 /**
214 * @dataProvider providePackedModules
215 * @covers ResourceLoaderContext::expandModuleNames
216 */
217 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
218 $this->assertEquals(
219 $unpacked ?: $modules,
220 ResourceLoaderContext::expandModuleNames( $packed ),
221 $desc
222 );
223 }
224
225 public static function provideAddSource() {
226 return [
227 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
228 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
229 [
230 [
231 'foowiki' => 'https://example.org/w/load.php',
232 'bazwiki' => 'https://example.com/w/load.php',
233 ],
234 null,
235 [ 'foowiki', 'bazwiki' ]
236 ]
237 ];
238 }
239
240 /**
241 * @dataProvider provideAddSource
242 * @covers ResourceLoader::addSource
243 * @covers ResourceLoader::getSources
244 */
245 public function testAddSource( $name, $info, $expected ) {
246 $rl = new ResourceLoader;
247 $rl->addSource( $name, $info );
248 if ( is_array( $expected ) ) {
249 foreach ( $expected as $source ) {
250 $this->assertArrayHasKey( $source, $rl->getSources() );
251 }
252 } else {
253 $this->assertArrayHasKey( $expected, $rl->getSources() );
254 }
255 }
256
257 /**
258 * @covers ResourceLoader::addSource
259 */
260 public function testAddSourceDupe() {
261 $rl = new ResourceLoader;
262 $this->setExpectedException( 'MWException', 'ResourceLoader duplicate source addition error' );
263 $rl->addSource( 'foo', 'https://example.org/w/load.php' );
264 $rl->addSource( 'foo', 'https://example.com/w/load.php' );
265 }
266
267 /**
268 * @covers ResourceLoader::addSource
269 */
270 public function testAddSourceInvalid() {
271 $rl = new ResourceLoader;
272 $this->setExpectedException( 'MWException', 'with no "loadScript" key' );
273 $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
274 }
275
276 public static function provideLoaderImplement() {
277 return [
278 [ [
279 'title' => 'Implement scripts, styles and messages',
280
281 'name' => 'test.example',
282 'scripts' => 'mw.example();',
283 'styles' => [ 'css' => [ '.mw-example {}' ] ],
284 'messages' => [ 'example' => '' ],
285 'templates' => [],
286
287 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
288 mw.example();
289 }, {
290 "css": [
291 ".mw-example {}"
292 ]
293 }, {
294 "example": ""
295 } );',
296 ] ],
297 [ [
298 'title' => 'Implement scripts',
299
300 'name' => 'test.example',
301 'scripts' => 'mw.example();',
302 'styles' => [],
303
304 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
305 mw.example();
306 } );',
307 ] ],
308 [ [
309 'title' => 'Implement styles',
310
311 'name' => 'test.example',
312 'scripts' => [],
313 'styles' => [ 'css' => [ '.mw-example {}' ] ],
314
315 'expected' => 'mw.loader.implement( "test.example", [], {
316 "css": [
317 ".mw-example {}"
318 ]
319 } );',
320 ] ],
321 [ [
322 'title' => 'Implement scripts and messages',
323
324 'name' => 'test.example',
325 'scripts' => 'mw.example();',
326 'messages' => [ 'example' => '' ],
327
328 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
329 mw.example();
330 }, {}, {
331 "example": ""
332 } );',
333 ] ],
334 [ [
335 'title' => 'Implement scripts and templates',
336
337 'name' => 'test.example',
338 'scripts' => 'mw.example();',
339 'templates' => [ 'example.html' => '' ],
340
341 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
342 mw.example();
343 }, {}, {}, {
344 "example.html": ""
345 } );',
346 ] ],
347 [ [
348 'title' => 'Implement unwrapped user script',
349
350 'name' => 'user',
351 'scripts' => 'mw.example( 1 );',
352 'wrap' => false,
353
354 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
355 ] ],
356 ];
357 }
358
359 /**
360 * @dataProvider provideLoaderImplement
361 * @covers ResourceLoader::makeLoaderImplementScript
362 * @covers ResourceLoader::trimArray
363 */
364 public function testMakeLoaderImplementScript( $case ) {
365 $case += [
366 'wrap' => true,
367 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' )
368 ];
369 ResourceLoader::clearCache();
370 $this->setMwGlobals( 'wgResourceLoaderDebug', true );
371
372 $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' );
373 $this->assertEquals(
374 $case['expected'],
375 $rl->makeLoaderImplementScript(
376 $case['name'],
377 ( $case['wrap'] && is_string( $case['scripts'] ) )
378 ? new XmlJsCode( $case['scripts'] )
379 : $case['scripts'],
380 $case['styles'],
381 $case['messages'],
382 $case['templates']
383 )
384 );
385 }
386
387 /**
388 * @covers ResourceLoader::makeLoaderImplementScript
389 */
390 public function testMakeLoaderImplementScriptInvalid() {
391 $this->setExpectedException( 'MWException', 'Invalid scripts error' );
392 $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' );
393 $rl->makeLoaderImplementScript(
394 'test', // name
395 123, // scripts
396 null, // styles
397 null, // messages
398 null // templates
399 );
400 }
401
402 /**
403 * @covers ResourceLoader::makeLoaderRegisterScript
404 */
405 public function testMakeLoaderRegisterScript() {
406 $this->assertEquals(
407 'mw.loader.register( [
408 [
409 "test.name",
410 "1234567"
411 ]
412 ] );',
413 ResourceLoader::makeLoaderRegisterScript( [
414 [ 'test.name', '1234567' ],
415 ] ),
416 'Nested array parameter'
417 );
418
419 $this->assertEquals(
420 'mw.loader.register( "test.name", "1234567" );',
421 ResourceLoader::makeLoaderRegisterScript(
422 'test.name',
423 '1234567'
424 ),
425 'Variadic parameters'
426 );
427 }
428
429 /**
430 * @covers ResourceLoader::makeLoaderSourcesScript
431 */
432 public function testMakeLoaderSourcesScript() {
433 $this->assertEquals(
434 'mw.loader.addSource( "local", "/w/load.php" );',
435 ResourceLoader::makeLoaderSourcesScript( 'local', '/w/load.php' )
436 );
437 $this->assertEquals(
438 'mw.loader.addSource( {
439 "local": "/w/load.php"
440 } );',
441 ResourceLoader::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
442 );
443 $this->assertEquals(
444 'mw.loader.addSource( {
445 "local": "/w/load.php",
446 "example": "https://example.org/w/load.php"
447 } );',
448 ResourceLoader::makeLoaderSourcesScript( [
449 'local' => '/w/load.php',
450 'example' => 'https://example.org/w/load.php'
451 ] )
452 );
453 $this->assertEquals(
454 'mw.loader.addSource( [] );',
455 ResourceLoader::makeLoaderSourcesScript( [] )
456 );
457 }
458
459 private static function fakeSources() {
460 return [
461 'examplewiki' => [
462 'loadScript' => '//example.org/w/load.php',
463 'apiScript' => '//example.org/w/api.php',
464 ],
465 'example2wiki' => [
466 'loadScript' => '//example.com/w/load.php',
467 'apiScript' => '//example.com/w/api.php',
468 ],
469 ];
470 }
471
472 /**
473 * @covers ResourceLoader::getLoadScript
474 */
475 public function testGetLoadScript() {
476 $this->setMwGlobals( 'wgResourceLoaderSources', [] );
477 $rl = new ResourceLoader();
478 $sources = self::fakeSources();
479 $rl->addSource( $sources );
480 foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
481 $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
482 }
483
484 try {
485 $rl->getLoadScript( 'thiswasneverreigstered' );
486 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
487 } catch ( MWException $e ) {
488 $this->assertTrue( true );
489 }
490 }
491
492 protected function getFailFerryMock() {
493 $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
494 ->setMethods( [ 'getScript' ] )
495 ->getMock();
496 $mock->method( 'getScript' )->will( $this->throwException(
497 new Exception( 'Ferry not found' )
498 ) );
499 return $mock;
500 }
501
502 protected function getSimpleModuleMock( $script = '' ) {
503 $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
504 ->setMethods( [ 'getScript' ] )
505 ->getMock();
506 $mock->method( 'getScript' )->willReturn( $script );
507 return $mock;
508 }
509
510 /**
511 * @covers ResourceLoader::getCombinedVersion
512 */
513 public function testGetCombinedVersion() {
514 $rl = new EmptyResourceLoader();
515 $rl->register( [
516 'foo' => self::getSimpleModuleMock(),
517 'ferry' => self::getFailFerryMock(),
518 'bar' => self::getSimpleModuleMock(),
519 ] );
520 $context = $this->getResourceLoaderContext( [], $rl );
521
522 $this->assertEquals(
523 ResourceLoader::makeHash( self::BLANK_VERSION ),
524 $rl->getCombinedVersion( $context, [ 'foo' ] ),
525 'compute foo'
526 );
527
528 // Verify that getCombinedVersion() does not throw when ferry fails.
529 // Instead it gracefully continues to combine the remaining modules.
530 $this->assertEquals(
531 ResourceLoader::makeHash( self::BLANK_VERSION . self::BLANK_VERSION ),
532 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
533 'compute foo+ferry+bar (T152266)'
534 );
535 }
536
537 /**
538 * Verify that when building module content in a load.php response,
539 * an exception from one module will not break script output from
540 * other modules.
541 */
542 public function testMakeModuleResponseError() {
543 $modules = [
544 'foo' => self::getSimpleModuleMock( 'foo();' ),
545 'ferry' => self::getFailFerryMock(),
546 'bar' => self::getSimpleModuleMock( 'bar();' ),
547 ];
548 $rl = new EmptyResourceLoader();
549 $rl->register( $modules );
550 $context = $this->getResourceLoaderContext(
551 [
552 'modules' => 'foo|ferry|bar',
553 'only' => 'scripts',
554 ],
555 $rl
556 );
557
558 $response = $rl->makeModuleResponse( $context, $modules );
559 $errors = $rl->getErrors();
560
561 $this->assertCount( 1, $errors );
562 $this->assertRegExp( '/Ferry not found/', $errors[0] );
563 $this->assertEquals(
564 'foo();bar();mw.loader.state( {
565 "ferry": "error",
566 "foo": "ready",
567 "bar": "ready"
568 } );',
569 $response
570 );
571 }
572
573 /**
574 * Verify that when building the startup module response,
575 * an exception from one module class will not break the entire
576 * startup module response. See T152266.
577 */
578 public function testMakeModuleResponseStartupError() {
579 $rl = new EmptyResourceLoader();
580 $rl->register( [
581 'foo' => self::getSimpleModuleMock( 'foo();' ),
582 'ferry' => self::getFailFerryMock(),
583 'bar' => self::getSimpleModuleMock( 'bar();' ),
584 'startup' => [ 'class' => 'ResourceLoaderStartUpModule' ],
585 ] );
586 $context = $this->getResourceLoaderContext(
587 [
588 'modules' => 'startup',
589 'only' => 'scripts',
590 ],
591 $rl
592 );
593
594 $this->assertEquals(
595 [ 'foo', 'ferry', 'bar', 'startup' ],
596 $rl->getModuleNames(),
597 'getModuleNames'
598 );
599
600 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
601 $response = $rl->makeModuleResponse( $context, $modules );
602 $errors = $rl->getErrors();
603
604 $this->assertRegExp( '/Ferry not found/', $errors[0] );
605 $this->assertCount( 1, $errors );
606 $this->assertRegExp(
607 '/isCompatible.*function startUp/s',
608 $response,
609 'startup response undisrupted (T152266)'
610 );
611 $this->assertRegExp(
612 '/register\([^)]+"ferry",\s*""/s',
613 $response,
614 'startup response registers broken module'
615 );
616 $this->assertRegExp(
617 '/state\([^)]+"ferry":\s*"error"/s',
618 $response,
619 'startup response sets state to error'
620 );
621 }
622 }