]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - tests/phpunit/includes/OutputPageTest.php
MediaWiki 1.30.2-scripts
[autoinstallsdev/mediawiki.git] / tests / phpunit / includes / OutputPageTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 /**
6  *
7  * @author Matthew Flaschen
8  *
9  * @group Database
10  * @group Output
11  *
12  * @todo factor tests in this class into providers and test methods
13  */
14 class OutputPageTest extends MediaWikiTestCase {
15         const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
16         const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
17
18         /**
19          * @covers OutputPage::addMeta
20          * @covers OutputPage::getMetaTags
21          * @covers OutputPage::getHeadLinksArray
22          */
23         public function testMetaTags() {
24                 $outputPage = $this->newInstance();
25                 $outputPage->addMeta( 'http:expires', '0' );
26                 $outputPage->addMeta( 'keywords', 'first' );
27                 $outputPage->addMeta( 'keywords', 'second' );
28                 $outputPage->addMeta( 'og:title', 'Ta-duh' );
29
30                 $expected = [
31                         [ 'http:expires', '0' ],
32                         [ 'keywords', 'first' ],
33                         [ 'keywords', 'second' ],
34                         [ 'og:title', 'Ta-duh' ],
35                 ];
36                 $this->assertSame( $expected, $outputPage->getMetaTags() );
37
38                 $links = $outputPage->getHeadLinksArray();
39                 $this->assertContains( '<meta http-equiv="expires" content="0"/>', $links );
40                 $this->assertContains( '<meta name="keywords" content="first"/>', $links );
41                 $this->assertContains( '<meta name="keywords" content="second"/>', $links );
42                 $this->assertContains( '<meta property="og:title" content="Ta-duh"/>', $links );
43                 $this->assertArrayNotHasKey( 'meta-robots', $links );
44         }
45
46         /**
47          * @covers OutputPage::setIndexPolicy
48          * @covers OutputPage::setFollowPolicy
49          * @covers OutputPage::getHeadLinksArray
50          */
51         public function testRobotsPolicies() {
52                 $outputPage = $this->newInstance();
53                 $outputPage->setIndexPolicy( 'noindex' );
54                 $outputPage->setFollowPolicy( 'nofollow' );
55
56                 $links = $outputPage->getHeadLinksArray();
57                 $this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
58         }
59
60         /**
61          * Tests a particular case of transformCssMedia, using the given input, globals,
62          * expected return, and message
63          *
64          * Asserts that $expectedReturn is returned.
65          *
66          * options['printableQuery'] - value of query string for printable, or omitted for none
67          * options['handheldQuery'] - value of query string for handheld, or omitted for none
68          * options['media'] - passed into the method under the same name
69          * options['expectedReturn'] - expected return value
70          * options['message'] - PHPUnit message for assertion
71          *
72          * @param array $args Key-value array of arguments as shown above
73          */
74         protected function assertTransformCssMediaCase( $args ) {
75                 $queryData = [];
76                 if ( isset( $args['printableQuery'] ) ) {
77                         $queryData['printable'] = $args['printableQuery'];
78                 }
79
80                 if ( isset( $args['handheldQuery'] ) ) {
81                         $queryData['handheld'] = $args['handheldQuery'];
82                 }
83
84                 $fauxRequest = new FauxRequest( $queryData, false );
85                 $this->setMwGlobals( [
86                         'wgRequest' => $fauxRequest,
87                 ] );
88
89                 $actualReturn = OutputPage::transformCssMedia( $args['media'] );
90                 $this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] );
91         }
92
93         /**
94          * Tests print requests
95          * @covers OutputPage::transformCssMedia
96          */
97         public function testPrintRequests() {
98                 $this->assertTransformCssMediaCase( [
99                         'printableQuery' => '1',
100                         'media' => 'screen',
101                         'expectedReturn' => null,
102                         'message' => 'On printable request, screen returns null'
103                 ] );
104
105                 $this->assertTransformCssMediaCase( [
106                         'printableQuery' => '1',
107                         'media' => self::SCREEN_MEDIA_QUERY,
108                         'expectedReturn' => null,
109                         'message' => 'On printable request, screen media query returns null'
110                 ] );
111
112                 $this->assertTransformCssMediaCase( [
113                         'printableQuery' => '1',
114                         'media' => self::SCREEN_ONLY_MEDIA_QUERY,
115                         'expectedReturn' => null,
116                         'message' => 'On printable request, screen media query with only returns null'
117                 ] );
118
119                 $this->assertTransformCssMediaCase( [
120                         'printableQuery' => '1',
121                         'media' => 'print',
122                         'expectedReturn' => '',
123                         'message' => 'On printable request, media print returns empty string'
124                 ] );
125         }
126
127         /**
128          * Tests screen requests, without either query parameter set
129          * @covers OutputPage::transformCssMedia
130          */
131         public function testScreenRequests() {
132                 $this->assertTransformCssMediaCase( [
133                         'media' => 'screen',
134                         'expectedReturn' => 'screen',
135                         'message' => 'On screen request, screen media type is preserved'
136                 ] );
137
138                 $this->assertTransformCssMediaCase( [
139                         'media' => 'handheld',
140                         'expectedReturn' => 'handheld',
141                         'message' => 'On screen request, handheld media type is preserved'
142                 ] );
143
144                 $this->assertTransformCssMediaCase( [
145                         'media' => self::SCREEN_MEDIA_QUERY,
146                         'expectedReturn' => self::SCREEN_MEDIA_QUERY,
147                         'message' => 'On screen request, screen media query is preserved.'
148                 ] );
149
150                 $this->assertTransformCssMediaCase( [
151                         'media' => self::SCREEN_ONLY_MEDIA_QUERY,
152                         'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY,
153                         'message' => 'On screen request, screen media query with only is preserved.'
154                 ] );
155
156                 $this->assertTransformCssMediaCase( [
157                         'media' => 'print',
158                         'expectedReturn' => 'print',
159                         'message' => 'On screen request, print media type is preserved'
160                 ] );
161         }
162
163         /**
164          * Tests handheld behavior
165          * @covers OutputPage::transformCssMedia
166          */
167         public function testHandheld() {
168                 $this->assertTransformCssMediaCase( [
169                         'handheldQuery' => '1',
170                         'media' => 'handheld',
171                         'expectedReturn' => '',
172                         'message' => 'On request with handheld querystring and media is handheld, returns empty string'
173                 ] );
174
175                 $this->assertTransformCssMediaCase( [
176                         'handheldQuery' => '1',
177                         'media' => 'screen',
178                         'expectedReturn' => null,
179                         'message' => 'On request with handheld querystring and media is screen, returns null'
180                 ] );
181         }
182
183         public static function provideTransformFilePath() {
184                 $baseDir = dirname( __DIR__ ) . '/data/media';
185                 return [
186                         // File that matches basePath, and exists. Hash found and appended.
187                         [
188                                 'baseDir' => $baseDir, 'basePath' => '/w',
189                                 '/w/test.jpg',
190                                 '/w/test.jpg?edcf2'
191                         ],
192                         // File that matches basePath, but not found on disk. Empty query.
193                         [
194                                 'baseDir' => $baseDir, 'basePath' => '/w',
195                                 '/w/unknown.png',
196                                 '/w/unknown.png?'
197                         ],
198                         // File not matching basePath. Ignored.
199                         [
200                                 'baseDir' => $baseDir, 'basePath' => '/w',
201                                 '/files/test.jpg'
202                         ],
203                         // Empty string. Ignored.
204                         [
205                                 'baseDir' => $baseDir, 'basePath' => '/w',
206                                 '',
207                                 ''
208                         ],
209                         // Similar path, but with domain component. Ignored.
210                         [
211                                 'baseDir' => $baseDir, 'basePath' => '/w',
212                                 '//example.org/w/test.jpg'
213                         ],
214                         [
215                                 'baseDir' => $baseDir, 'basePath' => '/w',
216                                 'https://example.org/w/test.jpg'
217                         ],
218                         // Unrelated path with domain component. Ignored.
219                         [
220                                 'baseDir' => $baseDir, 'basePath' => '/w',
221                                 'https://example.org/files/test.jpg'
222                         ],
223                         [
224                                 'baseDir' => $baseDir, 'basePath' => '/w',
225                                 '//example.org/files/test.jpg'
226                         ],
227                         // Unrelated path with domain, and empty base path (root mw install). Ignored.
228                         [
229                                 'baseDir' => $baseDir, 'basePath' => '',
230                                 'https://example.org/files/test.jpg'
231                         ],
232                         [
233                                 'baseDir' => $baseDir, 'basePath' => '',
234                                 // T155310
235                                 '//example.org/files/test.jpg'
236                         ],
237                         // Check UploadPath before ResourceBasePath (T155146)
238                         [
239                                 'baseDir' => dirname( $baseDir ), 'basePath' => '',
240                                 'uploadDir' => $baseDir, 'uploadPath' => '/images',
241                                 '/images/test.jpg',
242                                 '/images/test.jpg?edcf2'
243                         ],
244                 ];
245         }
246
247         /**
248          * @dataProvider provideTransformFilePath
249          * @covers OutputPage::transformFilePath
250          * @covers OutputPage::transformResourcePath
251          */
252         public function testTransformResourcePath( $baseDir, $basePath, $uploadDir = null,
253                 $uploadPath = null, $path = null, $expected = null
254         ) {
255                 if ( $path === null ) {
256                         // Skip optional $uploadDir and $uploadPath
257                         $path = $uploadDir;
258                         $expected = $uploadPath;
259                         $uploadDir = "$baseDir/images";
260                         $uploadPath = "$basePath/images";
261                 }
262                 $this->setMwGlobals( 'IP', $baseDir );
263                 $conf = new HashConfig( [
264                         'ResourceBasePath' => $basePath,
265                         'UploadDirectory' => $uploadDir,
266                         'UploadPath' => $uploadPath,
267                 ] );
268
269                 MediaWiki\suppressWarnings();
270                 $actual = OutputPage::transformResourcePath( $conf, $path );
271                 MediaWiki\restoreWarnings();
272
273                 $this->assertEquals( $expected ?: $path, $actual );
274         }
275
276         public static function provideMakeResourceLoaderLink() {
277                 // @codingStandardsIgnoreStart Generic.Files.LineLength
278                 return [
279                         // Single only=scripts load
280                         [
281                                 [ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ],
282                                 "<script>(window.RLQ=window.RLQ||[]).push(function(){"
283                                         . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.foo\u0026only=scripts\u0026skin=fallback");'
284                                         . "});</script>"
285                         ],
286                         // Multiple only=styles load
287                         [
288                                 [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
289
290                                 '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback"/>'
291                         ],
292                         // Private embed (only=scripts)
293                         [
294                                 [ 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ],
295                                 "<script>(window.RLQ=window.RLQ||[]).push(function(){"
296                                         . "mw.test.baz({token:123});\nmw.loader.state({\"test.quux\":\"ready\"});"
297                                         . "});</script>"
298                         ],
299                 ];
300                 // @codingStandardsIgnoreEnd
301         }
302
303         /**
304          * See ResourceLoaderClientHtmlTest for full coverage.
305          *
306          * @dataProvider provideMakeResourceLoaderLink
307          * @covers OutputPage::makeResourceLoaderLink
308          */
309         public function testMakeResourceLoaderLink( $args, $expectedHtml ) {
310                 $this->setMwGlobals( [
311                         'wgResourceLoaderDebug' => false,
312                         'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
313                 ] );
314                 $class = new ReflectionClass( 'OutputPage' );
315                 $method = $class->getMethod( 'makeResourceLoaderLink' );
316                 $method->setAccessible( true );
317                 $ctx = new RequestContext();
318                 $ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
319                 $ctx->setLanguage( 'en' );
320                 $out = new OutputPage( $ctx );
321                 $rl = $out->getResourceLoader();
322                 $rl->setMessageBlobStore( new NullMessageBlobStore() );
323                 $rl->register( [
324                         'test.foo' => new ResourceLoaderTestModule( [
325                                 'script' => 'mw.test.foo( { a: true } );',
326                                 'styles' => '.mw-test-foo { content: "style"; }',
327                         ] ),
328                         'test.bar' => new ResourceLoaderTestModule( [
329                                 'script' => 'mw.test.bar( { a: true } );',
330                                 'styles' => '.mw-test-bar { content: "style"; }',
331                         ] ),
332                         'test.baz' => new ResourceLoaderTestModule( [
333                                 'script' => 'mw.test.baz( { a: true } );',
334                                 'styles' => '.mw-test-baz { content: "style"; }',
335                         ] ),
336                         'test.quux' => new ResourceLoaderTestModule( [
337                                 'script' => 'mw.test.baz( { token: 123 } );',
338                                 'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
339                                 'group' => 'private',
340                         ] ),
341                 ] );
342                 $links = $method->invokeArgs( $out, $args );
343                 $actualHtml = strval( $links );
344                 $this->assertEquals( $expectedHtml, $actualHtml );
345         }
346
347         public static function provideBuildExemptModules() {
348                 return [
349                         'empty' => [
350                                 'exemptStyleModules' => [],
351                                 '<meta name="ResourceLoaderDynamicStyles" content=""/>',
352                         ],
353                         'empty sets' => [
354                                 'exemptStyleModules' => [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ],
355                                 '<meta name="ResourceLoaderDynamicStyles" content=""/>',
356                         ],
357                         // @codingStandardsIgnoreStart Generic.Files.LineLength
358                         'default logged-out' => [
359                                 'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ],
360                                 '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
361                                 '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>',
362                         ],
363                         'default logged-in' => [
364                                 'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ],
365                                 '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
366                                 '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>' . "\n" .
367                                 '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=user.styles&amp;only=styles&amp;skin=fallback&amp;version=1e9z0ox"/>',
368                         ],
369                         'custom modules' => [
370                                 'exemptStyleModules' => [
371                                         'site' => [ 'site.styles', 'example.site.a', 'example.site.b' ],
372                                         'user' => [ 'user.styles', 'example.user' ],
373                                 ],
374                                 '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
375                                 '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=example.site.a%2Cb&amp;only=styles&amp;skin=fallback"/>' . "\n" .
376                                 '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>' . "\n" .
377                                 '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=example.user&amp;only=styles&amp;skin=fallback&amp;version=0a56zyi"/>' . "\n" .
378                                 '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=user.styles&amp;only=styles&amp;skin=fallback&amp;version=1e9z0ox"/>',
379                         ],
380                         // @codingStandardsIgnoreEnd Generic.Files.LineLength
381                 ];
382         }
383
384         /**
385          * @dataProvider provideBuildExemptModules
386          * @covers OutputPage::buildExemptModules
387          */
388         public function testBuildExemptModules( array $exemptStyleModules, $expect ) {
389                 $this->setMwGlobals( [
390                         'wgResourceLoaderDebug' => false,
391                         'wgLoadScript' => '/w/load.php',
392                         // Stub wgCacheEpoch as it influences getVersionHash used for the
393                         // urls in the expected HTML
394                         'wgCacheEpoch' => '20140101000000',
395                 ] );
396
397                 // Set up stubs
398                 $ctx = new RequestContext();
399                 $ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
400                 $ctx->setLanguage( 'en' );
401                 $outputPage = $this->getMockBuilder( 'OutputPage' )
402                         ->setConstructorArgs( [ $ctx ] )
403                         ->setMethods( [ 'isUserCssPreview', 'buildCssLinksArray' ] )
404                         ->getMock();
405                 $outputPage->expects( $this->any() )
406                         ->method( 'isUserCssPreview' )
407                         ->willReturn( false );
408                 $outputPage->expects( $this->any() )
409                         ->method( 'buildCssLinksArray' )
410                         ->willReturn( [] );
411                 $rl = $outputPage->getResourceLoader();
412                 $rl->setMessageBlobStore( new NullMessageBlobStore() );
413
414                 // Register custom modules
415                 $rl->register( [
416                         'example.site.a' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
417                         'example.site.b' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
418                         'example.user' => new ResourceLoaderTestModule( [ 'group' => 'user' ] ),
419                 ] );
420
421                 $outputPage = TestingAccessWrapper::newFromObject( $outputPage );
422                 $outputPage->rlExemptStyleModules = $exemptStyleModules;
423                 $this->assertEquals(
424                         $expect,
425                         strval( $outputPage->buildExemptModules() )
426                 );
427         }
428
429         /**
430          * @dataProvider provideVaryHeaders
431          * @covers OutputPage::addVaryHeader
432          * @covers OutputPage::getVaryHeader
433          * @covers OutputPage::getKeyHeader
434          */
435         public function testVaryHeaders( $calls, $vary, $key ) {
436                 // get rid of default Vary fields
437                 $outputPage = $this->getMockBuilder( 'OutputPage' )
438                         ->setConstructorArgs( [ new RequestContext() ] )
439                         ->setMethods( [ 'getCacheVaryCookies' ] )
440                         ->getMock();
441                 $outputPage->expects( $this->any() )
442                         ->method( 'getCacheVaryCookies' )
443                         ->will( $this->returnValue( [] ) );
444                 TestingAccessWrapper::newFromObject( $outputPage )->mVaryHeader = [];
445
446                 foreach ( $calls as $call ) {
447                         call_user_func_array( [ $outputPage, 'addVaryHeader' ], $call );
448                 }
449                 $this->assertEquals( $vary, $outputPage->getVaryHeader(), 'Vary:' );
450                 $this->assertEquals( $key, $outputPage->getKeyHeader(), 'Key:' );
451         }
452
453         public function provideVaryHeaders() {
454                 // note: getKeyHeader() automatically adds Vary: Cookie
455                 return [
456                         [ // single header
457                                 [
458                                         [ 'Cookie' ],
459                                 ],
460                                 'Vary: Cookie',
461                                 'Key: Cookie',
462                         ],
463                         [ // non-unique headers
464                                 [
465                                         [ 'Cookie' ],
466                                         [ 'Accept-Language' ],
467                                         [ 'Cookie' ],
468                                 ],
469                                 'Vary: Cookie, Accept-Language',
470                                 'Key: Cookie,Accept-Language',
471                         ],
472                         [ // two headers with single options
473                                 [
474                                         [ 'Cookie', [ 'param=phpsessid' ] ],
475                                         [ 'Accept-Language', [ 'substr=en' ] ],
476                                 ],
477                                 'Vary: Cookie, Accept-Language',
478                                 'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
479                         ],
480                         [ // one header with multiple options
481                                 [
482                                         [ 'Cookie', [ 'param=phpsessid', 'param=userId' ] ],
483                                 ],
484                                 'Vary: Cookie',
485                                 'Key: Cookie;param=phpsessid;param=userId',
486                         ],
487                         [ // Duplicate option
488                                 [
489                                         [ 'Cookie', [ 'param=phpsessid' ] ],
490                                         [ 'Cookie', [ 'param=phpsessid' ] ],
491                                         [ 'Accept-Language', [ 'substr=en', 'substr=en' ] ],
492                                 ],
493                                 'Vary: Cookie, Accept-Language',
494                                 'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
495                         ],
496                         [ // Same header, different options
497                                 [
498                                         [ 'Cookie', [ 'param=phpsessid' ] ],
499                                         [ 'Cookie', [ 'param=userId' ] ],
500                                 ],
501                                 'Vary: Cookie',
502                                 'Key: Cookie;param=phpsessid;param=userId',
503                         ],
504                 ];
505         }
506
507         /**
508          * @covers OutputPage::haveCacheVaryCookies
509          */
510         public function testHaveCacheVaryCookies() {
511                 $request = new FauxRequest();
512                 $context = new RequestContext();
513                 $context->setRequest( $request );
514                 $outputPage = new OutputPage( $context );
515
516                 // No cookies are set.
517                 $this->assertFalse( $outputPage->haveCacheVaryCookies() );
518
519                 // 'Token' is present but empty, so it shouldn't count.
520                 $request->setCookie( 'Token', '' );
521                 $this->assertFalse( $outputPage->haveCacheVaryCookies() );
522
523                 // 'Token' present and nonempty.
524                 $request->setCookie( 'Token', '123' );
525                 $this->assertTrue( $outputPage->haveCacheVaryCookies() );
526         }
527
528         /*
529          * @covers OutputPage::addCategoryLinks
530          * @covers OutputPage::getCategories
531          */
532         public function testGetCategories() {
533                 $fakeResultWrapper = new FakeResultWrapper( [
534                         (object)[
535                                 'pp_value' => 1,
536                                 'page_title' => 'Test'
537                         ],
538                         (object)[
539                                 'page_title' => 'Test2'
540                         ]
541                 ] );
542                 $outputPage = $this->getMockBuilder( 'OutputPage' )
543                         ->setConstructorArgs( [ new RequestContext() ] )
544                         ->setMethods( [ 'addCategoryLinksToLBAndGetResult' ] )
545                         ->getMock();
546                 $outputPage->expects( $this->any() )
547                         ->method( 'addCategoryLinksToLBAndGetResult' )
548                         ->will( $this->returnValue( $fakeResultWrapper ) );
549
550                 $outputPage->addCategoryLinks( [
551                         'Test' => 'Test',
552                         'Test2' => 'Test2',
553                 ] );
554                 $this->assertEquals( [ 0 => 'Test', '1' => 'Test2' ], $outputPage->getCategories() );
555                 $this->assertEquals( [ 0 => 'Test2' ], $outputPage->getCategories( 'normal' ) );
556                 $this->assertEquals( [ 0 => 'Test' ], $outputPage->getCategories( 'hidden' ) );
557         }
558
559         /**
560          * @dataProvider provideLinkHeaders
561          * @covers OutputPage::addLinkHeader
562          * @covers OutputPage::getLinkHeader
563          */
564         public function testLinkHeaders( $headers, $result ) {
565                 $outputPage = $this->newInstance();
566
567                 foreach ( $headers as $header ) {
568                         $outputPage->addLinkHeader( $header );
569                 }
570
571                 $this->assertEquals( $result, $outputPage->getLinkHeader() );
572         }
573
574         public function provideLinkHeaders() {
575                 return [
576                         [
577                                 [],
578                                 false
579                         ],
580                         [
581                                 [ '<https://foo/bar.jpg>;rel=preload;as=image' ],
582                                 'Link: <https://foo/bar.jpg>;rel=preload;as=image',
583                         ],
584                         [
585                                 [ '<https://foo/bar.jpg>;rel=preload;as=image','<https://foo/baz.jpg>;rel=preload;as=image' ],
586                                 'Link: <https://foo/bar.jpg>;rel=preload;as=image,<https://foo/baz.jpg>;rel=preload;as=image',
587                         ],
588                 ];
589         }
590
591         /**
592          * @dataProvider providePreloadLinkHeaders
593          * @covers OutputPage::addLogoPreloadLinkHeaders
594          * @covers ResourceLoaderSkinModule::getLogo
595          */
596         public function testPreloadLinkHeaders( $config, $result, $baseDir = null ) {
597                 if ( $baseDir ) {
598                         $this->setMwGlobals( 'IP', $baseDir );
599                 }
600                 $out = TestingAccessWrapper::newFromObject( $this->newInstance( $config ) );
601                 $out->addLogoPreloadLinkHeaders();
602
603                 $this->assertEquals( $result, $out->getLinkHeader() );
604         }
605
606         public function providePreloadLinkHeaders() {
607                 return [
608                         [
609                                 [
610                                         'ResourceBasePath' => '/w',
611                                         'Logo' => '/img/default.png',
612                                         'LogoHD' => [
613                                                 '1.5x' => '/img/one-point-five.png',
614                                                 '2x' => '/img/two-x.png',
615                                         ],
616                                 ],
617                                 'Link: </img/default.png>;rel=preload;as=image;media=' .
618                                 'not all and (min-resolution: 1.5dppx),' .
619                                 '</img/one-point-five.png>;rel=preload;as=image;media=' .
620                                 '(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' .
621                                 '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
622                         ],
623                         [
624                                 [
625                                         'ResourceBasePath' => '/w',
626                                         'Logo' => '/img/default.png',
627                                         'LogoHD' => false,
628                                 ],
629                                 'Link: </img/default.png>;rel=preload;as=image'
630                         ],
631                         [
632                                 [
633                                         'ResourceBasePath' => '/w',
634                                         'Logo' => '/img/default.png',
635                                         'LogoHD' => [
636                                                 '2x' => '/img/two-x.png',
637                                         ],
638                                 ],
639                                 'Link: </img/default.png>;rel=preload;as=image;media=' .
640                                 'not all and (min-resolution: 2dppx),' .
641                                 '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
642                         ],
643                         [
644                                 [
645                                         'ResourceBasePath' => '/w',
646                                         'Logo' => '/w/test.jpg',
647                                         'LogoHD' => false,
648                                         'UploadPath' => '/w/images',
649                                 ],
650                                 'Link: </w/test.jpg?edcf2>;rel=preload;as=image',
651                                 'baseDir' => dirname( __DIR__ ) . '/data/media',
652                         ],
653                 ];
654         }
655
656         /**
657          * @return OutputPage
658          */
659         private function newInstance( $config = [] ) {
660                 $context = new RequestContext();
661
662                 $context->setConfig( new HashConfig( $config + [
663                         'AppleTouchIcon' => false,
664                         'DisableLangConversion' => true,
665                         'EnableAPI' => false,
666                         'EnableCanonicalServerLink' => false,
667                         'Favicon' => false,
668                         'Feed' => false,
669                         'LanguageCode' => false,
670                         'ReferrerPolicy' => false,
671                         'RightsPage' => false,
672                         'RightsUrl' => false,
673                         'UniversalEditButton' => false,
674                 ] ) );
675
676                 return new OutputPage( $context );
677         }
678 }
679
680 /**
681  * MessageBlobStore that doesn't do anything
682  */
683 class NullMessageBlobStore extends MessageBlobStore {
684         public function get( ResourceLoader $resourceLoader, $modules, $lang ) {
685                 return [];
686         }
687
688         public function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) {
689                 return false;
690         }
691
692         public function updateModule( $name, ResourceLoaderModule $module, $lang ) {
693         }
694
695         public function updateMessage( $key ) {
696         }
697
698         public function clear() {
699         }
700 }