Support custom offsets from SearchResultSet
[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 testGetModuleFactory() {
150 $factory = function( array $info ) {
151 $this->assertArrayHasKey( 'kitten', $info );
152 return new ResourceLoaderTestModule( $info );
153 };
154
155 $rl = new EmptyResourceLoader();
156 $rl->register( 'test', [ 'factory' => $factory, 'kitten' => 'little ball of fur' ] );
157 $this->assertInstanceOf(
158 ResourceLoaderTestModule::class,
159 $rl->getModule( 'test' )
160 );
161 }
162
163 /**
164 * @covers ResourceLoader::getModule
165 */
166 public function testGetModuleClassDefault() {
167 $rl = new EmptyResourceLoader();
168 $rl->register( 'test', [] );
169 $this->assertInstanceOf(
170 ResourceLoaderFileModule::class,
171 $rl->getModule( 'test' ),
172 'Array-style module registrations default to FileModule'
173 );
174 }
175
176 /**
177 * @covers ResourceLoaderFileModule::compileLessFile
178 */
179 public function testLessFileCompilation() {
180 $context = $this->getResourceLoaderContext();
181 $basePath = __DIR__ . '/../../data/less/module';
182 $module = new ResourceLoaderFileModule( [
183 'localBasePath' => $basePath,
184 'styles' => [ 'styles.less' ],
185 ] );
186 $module->setName( 'test.less' );
187 $styles = $module->getStyles( $context );
188 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
189 }
190
191 public static function providePackedModules() {
192 return [
193 [
194 'Example from makePackedModulesString doc comment',
195 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
196 'foo.bar,baz|bar.baz,quux',
197 ],
198 [
199 'Example from expandModuleNames doc comment',
200 [ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
201 'jquery.foo,bar|jquery.ui.baz,quux',
202 ],
203 [
204 'Regression fixed in r88706 with dotless names',
205 [ 'foo', 'bar', 'baz' ],
206 'foo,bar,baz',
207 ],
208 [
209 'Prefixless modules after a prefixed module',
210 [ 'single.module', 'foobar', 'foobaz' ],
211 'single.module|foobar,foobaz',
212 ],
213 [
214 'Ordering',
215 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
216 'foo|foo.baz,bar|baz.quux',
217 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
218 ]
219 ];
220 }
221
222 /**
223 * @dataProvider providePackedModules
224 * @covers ResourceLoader::makePackedModulesString
225 */
226 public function testMakePackedModulesString( $desc, $modules, $packed ) {
227 $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc );
228 }
229
230 /**
231 * @dataProvider providePackedModules
232 * @covers ResourceLoaderContext::expandModuleNames
233 */
234 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
235 $this->assertEquals(
236 $unpacked ?: $modules,
237 ResourceLoaderContext::expandModuleNames( $packed ),
238 $desc
239 );
240 }
241
242 public static function provideAddSource() {
243 return [
244 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
245 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
246 [
247 [
248 'foowiki' => 'https://example.org/w/load.php',
249 'bazwiki' => 'https://example.com/w/load.php',
250 ],
251 null,
252 [ 'foowiki', 'bazwiki' ]
253 ]
254 ];
255 }
256
257 /**
258 * @dataProvider provideAddSource
259 * @covers ResourceLoader::addSource
260 * @covers ResourceLoader::getSources
261 */
262 public function testAddSource( $name, $info, $expected ) {
263 $rl = new ResourceLoader;
264 $rl->addSource( $name, $info );
265 if ( is_array( $expected ) ) {
266 foreach ( $expected as $source ) {
267 $this->assertArrayHasKey( $source, $rl->getSources() );
268 }
269 } else {
270 $this->assertArrayHasKey( $expected, $rl->getSources() );
271 }
272 }
273
274 /**
275 * @covers ResourceLoader::addSource
276 */
277 public function testAddSourceDupe() {
278 $rl = new ResourceLoader;
279 $this->setExpectedException( 'MWException', 'ResourceLoader duplicate source addition error' );
280 $rl->addSource( 'foo', 'https://example.org/w/load.php' );
281 $rl->addSource( 'foo', 'https://example.com/w/load.php' );
282 }
283
284 /**
285 * @covers ResourceLoader::addSource
286 */
287 public function testAddSourceInvalid() {
288 $rl = new ResourceLoader;
289 $this->setExpectedException( 'MWException', 'with no "loadScript" key' );
290 $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
291 }
292
293 public static function provideLoaderImplement() {
294 return [
295 [ [
296 'title' => 'Implement scripts, styles and messages',
297
298 'name' => 'test.example',
299 'scripts' => 'mw.example();',
300 'styles' => [ 'css' => [ '.mw-example {}' ] ],
301 'messages' => [ 'example' => '' ],
302 'templates' => [],
303
304 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
305 mw.example();
306 }, {
307 "css": [
308 ".mw-example {}"
309 ]
310 }, {
311 "example": ""
312 } );',
313 ] ],
314 [ [
315 'title' => 'Implement scripts',
316
317 'name' => 'test.example',
318 'scripts' => 'mw.example();',
319 'styles' => [],
320
321 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
322 mw.example();
323 } );',
324 ] ],
325 [ [
326 'title' => 'Implement styles',
327
328 'name' => 'test.example',
329 'scripts' => [],
330 'styles' => [ 'css' => [ '.mw-example {}' ] ],
331
332 'expected' => 'mw.loader.implement( "test.example", [], {
333 "css": [
334 ".mw-example {}"
335 ]
336 } );',
337 ] ],
338 [ [
339 'title' => 'Implement scripts and messages',
340
341 'name' => 'test.example',
342 'scripts' => 'mw.example();',
343 'messages' => [ 'example' => '' ],
344
345 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
346 mw.example();
347 }, {}, {
348 "example": ""
349 } );',
350 ] ],
351 [ [
352 'title' => 'Implement scripts and templates',
353
354 'name' => 'test.example',
355 'scripts' => 'mw.example();',
356 'templates' => [ 'example.html' => '' ],
357
358 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
359 mw.example();
360 }, {}, {}, {
361 "example.html": ""
362 } );',
363 ] ],
364 [ [
365 'title' => 'Implement unwrapped user script',
366
367 'name' => 'user',
368 'scripts' => 'mw.example( 1 );',
369 'wrap' => false,
370
371 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
372 ] ],
373 ];
374 }
375
376 /**
377 * @dataProvider provideLoaderImplement
378 * @covers ResourceLoader::makeLoaderImplementScript
379 * @covers ResourceLoader::trimArray
380 */
381 public function testMakeLoaderImplementScript( $case ) {
382 $case += [
383 'wrap' => true,
384 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' )
385 ];
386 ResourceLoader::clearCache();
387 $this->setMwGlobals( 'wgResourceLoaderDebug', true );
388
389 $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' );
390 $this->assertEquals(
391 $case['expected'],
392 $rl->makeLoaderImplementScript(
393 $case['name'],
394 ( $case['wrap'] && is_string( $case['scripts'] ) )
395 ? new XmlJsCode( $case['scripts'] )
396 : $case['scripts'],
397 $case['styles'],
398 $case['messages'],
399 $case['templates']
400 )
401 );
402 }
403
404 /**
405 * @covers ResourceLoader::makeLoaderImplementScript
406 */
407 public function testMakeLoaderImplementScriptInvalid() {
408 $this->setExpectedException( 'MWException', 'Invalid scripts error' );
409 $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' );
410 $rl->makeLoaderImplementScript(
411 'test', // name
412 123, // scripts
413 null, // styles
414 null, // messages
415 null // templates
416 );
417 }
418
419 /**
420 * @covers ResourceLoader::makeLoaderRegisterScript
421 */
422 public function testMakeLoaderRegisterScript() {
423 $this->assertEquals(
424 'mw.loader.register( [
425 [
426 "test.name",
427 "1234567"
428 ]
429 ] );',
430 ResourceLoader::makeLoaderRegisterScript( [
431 [ 'test.name', '1234567' ],
432 ] ),
433 'Nested array parameter'
434 );
435
436 $this->assertEquals(
437 'mw.loader.register( "test.name", "1234567" );',
438 ResourceLoader::makeLoaderRegisterScript(
439 'test.name',
440 '1234567'
441 ),
442 'Variadic parameters'
443 );
444 }
445
446 /**
447 * @covers ResourceLoader::makeLoaderSourcesScript
448 */
449 public function testMakeLoaderSourcesScript() {
450 $this->assertEquals(
451 'mw.loader.addSource( "local", "/w/load.php" );',
452 ResourceLoader::makeLoaderSourcesScript( 'local', '/w/load.php' )
453 );
454 $this->assertEquals(
455 'mw.loader.addSource( {
456 "local": "/w/load.php"
457 } );',
458 ResourceLoader::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
459 );
460 $this->assertEquals(
461 'mw.loader.addSource( {
462 "local": "/w/load.php",
463 "example": "https://example.org/w/load.php"
464 } );',
465 ResourceLoader::makeLoaderSourcesScript( [
466 'local' => '/w/load.php',
467 'example' => 'https://example.org/w/load.php'
468 ] )
469 );
470 $this->assertEquals(
471 'mw.loader.addSource( [] );',
472 ResourceLoader::makeLoaderSourcesScript( [] )
473 );
474 }
475
476 private static function fakeSources() {
477 return [
478 'examplewiki' => [
479 'loadScript' => '//example.org/w/load.php',
480 'apiScript' => '//example.org/w/api.php',
481 ],
482 'example2wiki' => [
483 'loadScript' => '//example.com/w/load.php',
484 'apiScript' => '//example.com/w/api.php',
485 ],
486 ];
487 }
488
489 /**
490 * @covers ResourceLoader::getLoadScript
491 */
492 public function testGetLoadScript() {
493 $this->setMwGlobals( 'wgResourceLoaderSources', [] );
494 $rl = new ResourceLoader();
495 $sources = self::fakeSources();
496 $rl->addSource( $sources );
497 foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
498 $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
499 }
500
501 try {
502 $rl->getLoadScript( 'thiswasneverreigstered' );
503 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
504 } catch ( MWException $e ) {
505 $this->assertTrue( true );
506 }
507 }
508
509 protected function getFailFerryMock() {
510 $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
511 ->setMethods( [ 'getScript' ] )
512 ->getMock();
513 $mock->method( 'getScript' )->will( $this->throwException(
514 new Exception( 'Ferry not found' )
515 ) );
516 return $mock;
517 }
518
519 protected function getSimpleModuleMock( $script = '' ) {
520 $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
521 ->setMethods( [ 'getScript' ] )
522 ->getMock();
523 $mock->method( 'getScript' )->willReturn( $script );
524 return $mock;
525 }
526
527 /**
528 * @covers ResourceLoader::getCombinedVersion
529 */
530 public function testGetCombinedVersion() {
531 $rl = new EmptyResourceLoader();
532 $rl->register( [
533 'foo' => self::getSimpleModuleMock(),
534 'ferry' => self::getFailFerryMock(),
535 'bar' => self::getSimpleModuleMock(),
536 ] );
537 $context = $this->getResourceLoaderContext( [], $rl );
538
539 $this->assertEquals(
540 ResourceLoader::makeHash( self::BLANK_VERSION ),
541 $rl->getCombinedVersion( $context, [ 'foo' ] ),
542 'compute foo'
543 );
544
545 // Verify that getCombinedVersion() does not throw when ferry fails.
546 // Instead it gracefully continues to combine the remaining modules.
547 $this->assertEquals(
548 ResourceLoader::makeHash( self::BLANK_VERSION . self::BLANK_VERSION ),
549 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
550 'compute foo+ferry+bar (T152266)'
551 );
552 }
553
554 /**
555 * Verify that when building module content in a load.php response,
556 * an exception from one module will not break script output from
557 * other modules.
558 */
559 public function testMakeModuleResponseError() {
560 $modules = [
561 'foo' => self::getSimpleModuleMock( 'foo();' ),
562 'ferry' => self::getFailFerryMock(),
563 'bar' => self::getSimpleModuleMock( 'bar();' ),
564 ];
565 $rl = new EmptyResourceLoader();
566 $rl->register( $modules );
567 $context = $this->getResourceLoaderContext(
568 [
569 'modules' => 'foo|ferry|bar',
570 'only' => 'scripts',
571 ],
572 $rl
573 );
574
575 $response = $rl->makeModuleResponse( $context, $modules );
576 $errors = $rl->getErrors();
577
578 $this->assertCount( 1, $errors );
579 $this->assertRegExp( '/Ferry not found/', $errors[0] );
580 $this->assertEquals(
581 'foo();bar();mw.loader.state( {
582 "ferry": "error",
583 "foo": "ready",
584 "bar": "ready"
585 } );',
586 $response
587 );
588 }
589
590 /**
591 * Verify that when building the startup module response,
592 * an exception from one module class will not break the entire
593 * startup module response. See T152266.
594 */
595 public function testMakeModuleResponseStartupError() {
596 $rl = new EmptyResourceLoader();
597 $rl->register( [
598 'foo' => self::getSimpleModuleMock( 'foo();' ),
599 'ferry' => self::getFailFerryMock(),
600 'bar' => self::getSimpleModuleMock( 'bar();' ),
601 'startup' => [ 'class' => 'ResourceLoaderStartUpModule' ],
602 ] );
603 $context = $this->getResourceLoaderContext(
604 [
605 'modules' => 'startup',
606 'only' => 'scripts',
607 ],
608 $rl
609 );
610
611 $this->assertEquals(
612 [ 'foo', 'ferry', 'bar', 'startup' ],
613 $rl->getModuleNames(),
614 'getModuleNames'
615 );
616
617 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
618 $response = $rl->makeModuleResponse( $context, $modules );
619 $errors = $rl->getErrors();
620
621 $this->assertRegExp( '/Ferry not found/', $errors[0] );
622 $this->assertCount( 1, $errors );
623 $this->assertRegExp(
624 '/isCompatible.*function startUp/s',
625 $response,
626 'startup response undisrupted (T152266)'
627 );
628 $this->assertRegExp(
629 '/register\([^)]+"ferry",\s*""/s',
630 $response,
631 'startup response registers broken module'
632 );
633 $this->assertRegExp(
634 '/state\([^)]+"ferry":\s*"error"/s',
635 $response,
636 'startup response sets state to error'
637 );
638 }
639 }