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