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