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