Merge "Type hint against LinkTarget in WatchedItemStore"
[lhc/web/wiklou.git] / tests / phpunit / includes / resourceloader / ResourceLoaderWikiModuleTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use Wikimedia\Rdbms\IDatabase;
5 use Wikimedia\TestingAccessWrapper;
6
7 /**
8 * @covers ResourceLoaderWikiModule
9 */
10 class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase {
11
12 /**
13 * @dataProvider provideConstructor
14 */
15 public function testConstructor( $params ) {
16 $module = new ResourceLoaderWikiModule( $params );
17 $this->assertInstanceOf( ResourceLoaderWikiModule::class, $module );
18 }
19
20 public static function provideConstructor() {
21 yield 'null' => [ null ];
22 yield 'empty' => [ [] ];
23 yield 'unknown settings' => [ [ 'foo' => 'baz' ] ];
24 yield 'real settings' => [ [ 'MediaWiki:Common.js' ] ];
25 }
26
27 private function prepareTitleInfo( array $mockInfo ) {
28 $module = TestingAccessWrapper::newFromClass( ResourceLoaderWikiModule::class );
29 $info = [];
30 foreach ( $mockInfo as $key => $val ) {
31 $info[ $module->makeTitleKey( Title::newFromText( $key ) ) ] = $val;
32 }
33 return $info;
34 }
35
36 /**
37 * @dataProvider provideGetPages
38 */
39 public function testGetPages( $params, Config $config, $expected ) {
40 $module = new ResourceLoaderWikiModule( $params );
41 $module->setConfig( $config );
42
43 // Because getPages is protected..
44 $getPages = new ReflectionMethod( $module, 'getPages' );
45 $getPages->setAccessible( true );
46 $out = $getPages->invoke( $module, ResourceLoaderContext::newDummyContext() );
47 $this->assertSame( $expected, $out );
48 }
49
50 public static function provideGetPages() {
51 $settings = self::getSettings() + [
52 'UseSiteJs' => true,
53 'UseSiteCss' => true,
54 ];
55
56 $params = [
57 'styles' => [ 'MediaWiki:Common.css' ],
58 'scripts' => [ 'MediaWiki:Common.js' ],
59 ];
60
61 return [
62 [ [], new HashConfig( $settings ), [] ],
63 [ $params, new HashConfig( $settings ), [
64 'MediaWiki:Common.js' => [ 'type' => 'script' ],
65 'MediaWiki:Common.css' => [ 'type' => 'style' ]
66 ] ],
67 [ $params, new HashConfig( [ 'UseSiteCss' => false ] + $settings ), [
68 'MediaWiki:Common.js' => [ 'type' => 'script' ],
69 ] ],
70 [ $params, new HashConfig( [ 'UseSiteJs' => false ] + $settings ), [
71 'MediaWiki:Common.css' => [ 'type' => 'style' ],
72 ] ],
73 [ $params,
74 new HashConfig(
75 [ 'UseSiteJs' => false, 'UseSiteCss' => false ]
76 ),
77 []
78 ],
79 ];
80 }
81
82 /**
83 * @dataProvider provideGetGroup
84 */
85 public function testGetGroup( $params, $expected ) {
86 $module = new ResourceLoaderWikiModule( $params );
87 $this->assertSame( $expected, $module->getGroup() );
88 }
89
90 public static function provideGetGroup() {
91 yield 'no group' => [ [], null ];
92 yield 'some group' => [ [ 'group' => 'foobar' ], 'foobar' ];
93 }
94
95 /**
96 * @dataProvider provideGetType
97 */
98 public function testGetType( $params, $expected ) {
99 $module = new ResourceLoaderWikiModule( $params );
100 $this->assertSame( $expected, $module->getType() );
101 }
102
103 public static function provideGetType() {
104 yield 'empty' => [
105 [],
106 ResourceLoaderWikiModule::LOAD_GENERAL,
107 ];
108 yield 'scripts' => [
109 [ 'scripts' => [ 'Example.js' ] ],
110 ResourceLoaderWikiModule::LOAD_GENERAL,
111 ];
112 yield 'styles' => [
113 [ 'styles' => [ 'Example.css' ] ],
114 ResourceLoaderWikiModule::LOAD_STYLES,
115 ];
116 yield 'styles and scripts' => [
117 [ 'styles' => [ 'Example.css' ], 'scripts' => [ 'Example.js' ] ],
118 ResourceLoaderWikiModule::LOAD_GENERAL,
119 ];
120 }
121
122 /**
123 * @dataProvider provideIsKnownEmpty
124 */
125 public function testIsKnownEmpty( $titleInfo, $group, $dependencies, $expected ) {
126 $module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
127 ->disableOriginalConstructor()
128 ->setMethods( [ 'getTitleInfo', 'getGroup', 'getDependencies' ] )
129 ->getMock();
130 $module->method( 'getTitleInfo' )
131 ->willReturn( $this->prepareTitleInfo( $titleInfo ) );
132 $module->method( 'getGroup' )
133 ->willReturn( $group );
134 $module->method( 'getDependencies' )
135 ->willReturn( $dependencies );
136 $context = $this->createMock( ResourceLoaderContext::class );
137 $this->assertSame( $expected, $module->isKnownEmpty( $context ) );
138 }
139
140 public static function provideIsKnownEmpty() {
141 yield 'nothing' => [
142 [],
143 null,
144 [],
145 // No pages exist, considered empty.
146 true,
147 ];
148
149 yield 'an empty page exists (no group)' => [
150 [ 'Project:Example/foo.js' => [ 'page_len' => 0 ] ],
151 null,
152 [],
153 // There is an existing page, so we should let the module be queued.
154 // Its emptiness might be temporary, hence considered non-empty (T70488).
155 false,
156 ];
157 yield 'an empty page exists (site group)' => [
158 [ 'MediaWiki:Foo.js' => [ 'page_len' => 0 ] ],
159 'site',
160 [],
161 // There is an existing page, hence considered non-empty.
162 false,
163 ];
164 yield 'an empty page exists (user group)' => [
165 [ 'User:Example/foo.js' => [ 'page_len' => 0 ] ],
166 'user',
167 [],
168 // There is an existing page, but it is empty.
169 // For user-specific modules, don't bother loading a known-empty module.
170 // Given user-specific HTML output, this will vary and re-appear if/when
171 // the page becomes non-empty again.
172 true,
173 ];
174
175 yield 'no pages but having dependencies (no group)' => [
176 [],
177 null,
178 [ 'another-module' ],
179 false,
180 ];
181 yield 'no pages but having dependencies (site group)' => [
182 [],
183 'site',
184 [ 'another-module' ],
185 false,
186 ];
187 yield 'no pages but having dependencies (user group)' => [
188 [],
189 'user',
190 [ 'another-module' ],
191 false,
192 ];
193
194 yield 'a non-empty page exists (user group)' => [
195 [ 'User:Example/foo.js' => [ 'page_len' => 25 ] ],
196 'user',
197 [],
198 false,
199 ];
200 yield 'a non-empty page exists (site group)' => [
201 [ 'MediaWiki:Foo.js' => [ 'page_len' => 25 ] ],
202 'site',
203 [],
204 false,
205 ];
206 }
207
208 public function testGetTitleInfo() {
209 $pages = [
210 'MediaWiki:Common.css' => [ 'type' => 'styles' ],
211 'mediawiki: fallback.css' => [ 'type' => 'styles' ],
212 ];
213 $titleInfo = $this->prepareTitleInfo( [
214 'MediaWiki:Common.css' => [ 'page_len' => 1234 ],
215 'MediaWiki:Fallback.css' => [ 'page_len' => 0 ],
216 ] );
217 $expected = $titleInfo;
218
219 $module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
220 ->setMethods( [ 'getPages', 'getTitleInfo' ] )
221 ->getMock();
222 $module->method( 'getPages' )->willReturn( $pages );
223 $module->method( 'getTitleInfo' )->willReturn( $titleInfo );
224
225 $context = $this->getMockBuilder( ResourceLoaderContext::class )
226 ->disableOriginalConstructor()
227 ->getMock();
228
229 $module = TestingAccessWrapper::newFromObject( $module );
230 $this->assertSame( $expected, $module->getTitleInfo( $context ), 'Title info' );
231 }
232
233 public function testGetPreloadedTitleInfo() {
234 $pages = [
235 'MediaWiki:Common.css' => [ 'type' => 'styles' ],
236 // Regression against T145673. It's impossible to statically declare page names in
237 // a canonical way since the canonical prefix is localised. As such, the preload
238 // cache computed the right cache key, but failed to find the results when
239 // doing an intersect on the canonical result, producing an empty array.
240 'mediawiki: fallback.css' => [ 'type' => 'styles' ],
241 ];
242 $titleInfo = $this->prepareTitleInfo( [
243 'MediaWiki:Common.css' => [ 'page_len' => 1234 ],
244 'MediaWiki:Fallback.css' => [ 'page_len' => 0 ],
245 ] );
246 $expected = $titleInfo;
247
248 $module = $this->getMockBuilder( TestResourceLoaderWikiModule::class )
249 ->setMethods( [ 'getPages' ] )
250 ->getMock();
251 $module->method( 'getPages' )->willReturn( $pages );
252 // Can't mock static methods
253 $module::$returnFetchTitleInfo = $titleInfo;
254
255 $rl = new EmptyResourceLoader();
256 $context = new ResourceLoaderContext( $rl, new FauxRequest() );
257
258 TestResourceLoaderWikiModule::invalidateModuleCache(
259 Title::newFromText( 'MediaWiki:Common.css' ),
260 null,
261 null,
262 wfWikiID()
263 );
264 TestResourceLoaderWikiModule::preloadTitleInfo(
265 $context,
266 $this->createMock( IDatabase::class ),
267 [ 'testmodule' ]
268 );
269
270 $module = TestingAccessWrapper::newFromObject( $module );
271 $this->assertSame( $expected, $module->getTitleInfo( $context ), 'Title info' );
272 }
273
274 public function testGetPreloadedBadTitle() {
275 // Set up
276 TestResourceLoaderWikiModule::$returnFetchTitleInfo = [];
277 $rl = new EmptyResourceLoader();
278 $rl->getConfig()->set( 'UseSiteJs', true );
279 $rl->getConfig()->set( 'UseSiteCss', true );
280 $rl->register( 'testmodule', [
281 'class' => TestResourceLoaderWikiModule::class,
282 // Covers preloadTitleInfo branch for invalid page name
283 'styles' => [ '[x]' ],
284 ] );
285 $context = new ResourceLoaderContext( $rl, new FauxRequest() );
286
287 // Act
288 TestResourceLoaderWikiModule::preloadTitleInfo(
289 $context,
290 $this->createMock( IDatabase::class ),
291 [ 'testmodule' ]
292 );
293
294 // Assert
295 $module = TestingAccessWrapper::newFromObject( $rl->getModule( 'testmodule' ) );
296 $this->assertSame( [], $module->getTitleInfo( $context ), 'Title info' );
297 }
298
299 public function testGetPreloadedTitleInfoEmpty() {
300 $context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest() );
301 // This covers the early return case
302 $this->assertSame(
303 null,
304 ResourceLoaderWikiModule::preloadTitleInfo(
305 $context,
306 $this->createMock( IDatabase::class ),
307 []
308 )
309 );
310 }
311
312 public static function provideGetContent() {
313 yield 'Bad title' => [ null, '[x]' ];
314 yield 'Dead redirect' => [ null, [
315 'text' => 'Dead redirect',
316 'title' => 'Dead_redirect',
317 'redirect' => 1,
318 ] ];
319 yield 'Bad content model' => [ null, [
320 'text' => 'MediaWiki:Wikitext',
321 'ns' => NS_MEDIAWIKI,
322 'title' => 'Wikitext',
323 ] ];
324 yield 'No JS content found' => [ null, [
325 'text' => 'MediaWiki:Script.js',
326 'ns' => NS_MEDIAWIKI,
327 'title' => 'Script.js',
328 ] ];
329 yield 'No CSS content found' => [ null, [
330 'text' => 'MediaWiki:Styles.css',
331 'ns' => NS_MEDIAWIKI,
332 'title' => 'Script.css',
333 ] ];
334 }
335
336 /**
337 * @dataProvider provideGetContent
338 */
339 public function testGetContent( $expected, $title ) {
340 $context = $this->getResourceLoaderContext( [], new EmptyResourceLoader );
341 $module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
342 ->setMethods( [ 'getContentObj' ] )->getMock();
343 $module->method( 'getContentObj' )
344 ->willReturn( null );
345
346 if ( is_array( $title ) ) {
347 $title += [ 'ns' => NS_MAIN, 'id' => 1, 'len' => 1, 'redirect' => 0 ];
348 $titleText = $title['text'];
349 // Mock Title db access via LinkCache
350 MediaWikiServices::getInstance()->getLinkCache()->addGoodLinkObj(
351 $title['id'],
352 new TitleValue( $title['ns'], $title['title'] ),
353 $title['len'],
354 $title['redirect']
355 );
356 } else {
357 $titleText = $title;
358 }
359
360 $module = TestingAccessWrapper::newFromObject( $module );
361 $this->assertSame(
362 $expected,
363 $module->getContent( $titleText, $context )
364 );
365 }
366
367 public function testContentOverrides() {
368 $pages = [
369 'MediaWiki:Common.css' => [ 'type' => 'style' ],
370 ];
371
372 $module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
373 ->setMethods( [ 'getPages' ] )
374 ->getMock();
375 $module->method( 'getPages' )->willReturn( $pages );
376
377 $rl = new EmptyResourceLoader();
378 $context = new DerivativeResourceLoaderContext(
379 new ResourceLoaderContext( $rl, new FauxRequest() )
380 );
381 $context->setContentOverrideCallback( function ( Title $t ) {
382 if ( $t->getPrefixedText() === 'MediaWiki:Common.css' ) {
383 return new CssContent( '.override{}' );
384 }
385 return null;
386 } );
387
388 $this->assertTrue( $module->shouldEmbedModule( $context ) );
389 $this->assertSame( [
390 'all' => [
391 "/*\nMediaWiki:Common.css\n*/\n.override{}"
392 ]
393 ], $module->getStyles( $context ) );
394
395 $context->setContentOverrideCallback( function ( Title $t ) {
396 if ( $t->getPrefixedText() === 'MediaWiki:Skin.css' ) {
397 return new CssContent( '.override{}' );
398 }
399 return null;
400 } );
401 $this->assertFalse( $module->shouldEmbedModule( $context ) );
402 }
403
404 public function testGetContentForRedirects() {
405 // Set up context and module object
406 $context = new DerivativeResourceLoaderContext(
407 $this->getResourceLoaderContext( [], new EmptyResourceLoader )
408 );
409 $module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
410 ->setMethods( [ 'getPages' ] )
411 ->getMock();
412 $module->method( 'getPages' )
413 ->willReturn( [
414 'MediaWiki:Redirect.js' => [ 'type' => 'script' ]
415 ] );
416 $context->setContentOverrideCallback( function ( Title $title ) {
417 if ( $title->getPrefixedText() === 'MediaWiki:Redirect.js' ) {
418 $handler = new JavaScriptContentHandler();
419 return $handler->makeRedirectContent(
420 Title::makeTitle( NS_MEDIAWIKI, 'Target.js' )
421 );
422 } elseif ( $title->getPrefixedText() === 'MediaWiki:Target.js' ) {
423 return new JavaScriptContent( 'target;' );
424 } else {
425 return null;
426 }
427 } );
428
429 // Mock away Title's db queries with LinkCache
430 MediaWikiServices::getInstance()->getLinkCache()->addGoodLinkObj(
431 1, // id
432 new TitleValue( NS_MEDIAWIKI, 'Redirect.js' ),
433 1, // len
434 1 // redirect
435 );
436
437 $this->assertSame(
438 "/*\nMediaWiki:Redirect.js\n*/\ntarget;\n",
439 $module->getScript( $context ),
440 'Redirect resolved by getContent'
441 );
442 }
443
444 public function tearDown() {
445 Title::clearCaches();
446 parent::tearDown();
447 }
448 }
449
450 class TestResourceLoaderWikiModule extends ResourceLoaderWikiModule {
451 public static $returnFetchTitleInfo = null;
452
453 protected static function fetchTitleInfo( IDatabase $db, array $pages, $fname = null ) {
454 $ret = self::$returnFetchTitleInfo;
455 self::$returnFetchTitleInfo = null;
456 return $ret;
457 }
458 }