Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / skins / SkinTest.php
blob9a2d085c0901214862ef4aa5816d9f8fa3773c6c
1 <?php
3 use MediaWiki\Context\RequestContext;
4 use MediaWiki\HookContainer\HookContainer;
5 use MediaWiki\Interwiki\InterwikiLookup;
6 use MediaWiki\Linker\LinkTarget;
7 use MediaWiki\MainConfigNames;
8 use MediaWiki\Output\OutputPage;
9 use MediaWiki\Page\PageReferenceValue;
10 use MediaWiki\Permissions\Authority;
11 use MediaWiki\Request\FauxRequest;
12 use MediaWiki\Tests\Unit\MockBlockTrait;
13 use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
14 use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
15 use MediaWiki\Title\Title;
16 use MediaWiki\Title\TitleValue;
17 use MediaWiki\User\UserIdentity;
18 use MediaWiki\User\UserIdentityLookup;
19 use MediaWiki\User\UserIdentityValue;
21 /**
22 * @covers \Skin
23 * @group Database
25 class SkinTest extends MediaWikiIntegrationTestCase {
26 use MockAuthorityTrait;
27 use MockBlockTrait;
28 use TempUserTestTrait;
30 /**
31 * @covers \Skin
33 public function testGetSkinName() {
34 $skin = new SkinFallback();
35 $this->assertEquals( 'fallback', $skin->getSkinName(), 'Default' );
36 $skin = new SkinFallback( 'testname' );
37 $this->assertEquals( 'testname', $skin->getSkinName(), 'Constructor argument' );
40 public function testGetDefaultModules() {
41 $skin = $this->getMockBuilder( Skin::class )
42 ->onlyMethods( [ 'outputPage' ] )
43 ->getMock();
45 $modules = $skin->getDefaultModules();
46 $this->assertTrue( isset( $modules['core'] ), 'core key is set by default' );
47 $this->assertTrue( isset( $modules['styles'] ), 'style key is set by default' );
50 /**
51 * @param bool $isSyndicated
52 * @param string $html
53 * @return OutputPage
55 private function getMockOutputPage( $isSyndicated, $html ) {
56 $mock = $this->createMock( OutputPage::class );
57 $mock->expects( $this->once() )
58 ->method( 'isSyndicated' )
59 ->willReturn( $isSyndicated );
60 $mock->method( 'getHTML' )
61 ->willReturn( $html );
62 return $mock;
65 public static function provideGetDefaultModulesForOutput() {
66 return [
68 false,
69 '',
73 true,
74 '',
75 [ 'mediawiki.feedlink' ]
78 false,
79 'FOO mw-ui-button BAR',
80 [ 'mediawiki.ui.button' ]
83 true,
84 'FOO mw-ui-button BAR',
85 [ 'mediawiki.ui.button', 'mediawiki.feedlink' ]
90 /**
91 * @dataProvider provideGetDefaultModulesForOutput
93 public function testGetDefaultModulesForContent( $isSyndicated, $html, array $expectedModuleStyles ) {
94 $skin = new class extends Skin {
95 public function outputPage() {
98 $fakeContext = new RequestContext();
99 $fakeContext->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
100 $fakeContext->setOutput( $this->getMockOutputPage( $isSyndicated, $html ) );
101 $skin->setContext( $fakeContext );
103 $modules = $skin->getDefaultModules();
105 $actualStylesModule = array_merge( ...array_values( $modules['styles'] ) );
106 foreach ( $expectedModuleStyles as $expected ) {
107 $this->assertContains( $expected, $actualStylesModule );
111 public function provideGetDefaultModulesForRights() {
112 yield 'no rights' => [
113 $this->mockRegisteredNullAuthority(), // $authority
114 false, // $hasModule
116 yield 'has all rights' => [
117 $this->mockRegisteredUltimateAuthority(), // $authority
118 true, // $hasModule
123 * @dataProvider provideGetDefaultModulesForRights
125 public function testGetDefaultModulesForRights( Authority $authority, bool $hasModule ) {
126 $skin = new class extends Skin {
127 public function outputPage() {
130 $fakeContext = new RequestContext();
131 $fakeContext->setAuthority( $authority );
132 $fakeContext->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
133 $skin->setContext( $fakeContext );
135 $defaultModules = $skin->getDefaultModules();
136 $this->assertArrayHasKey( 'watch', $defaultModules );
137 if ( $hasModule ) {
138 $this->assertContains( 'mediawiki.page.watch.ajax', $defaultModules['watch'] );
139 } else {
140 $this->assertNotContains( 'mediawiki.page.watch.ajax', $defaultModules['watch'] );
144 public function providGetPageClasses() {
145 yield 'normal page has namespace' => [
146 new TitleValue( NS_MAIN, 'Test' ), // $title
147 $this->mockRegisteredUltimateAuthority(), // $authority
148 [ 'ns-0' ], // $expectedClasses
150 yield 'valid special page' => [
151 new TitleValue( NS_SPECIAL, 'Userlogin' ), // $title
152 $this->mockRegisteredUltimateAuthority(), // $authority
153 [ 'mw-special-Userlogin' ], // $expectedClasses
155 yield 'invalid special page' => [
156 new TitleValue( NS_SPECIAL, 'BLABLABLABLA_I_AM_INVALID' ), // $title
157 $this->mockRegisteredUltimateAuthority(), // $authority
158 [ 'mw-invalidspecialpage' ], // $expectedClasses
160 yield 'talk page' => [
161 new TitleValue( NS_TALK, 'Test' ), // $title
162 $this->mockRegisteredUltimateAuthority(), // $authority
163 [ 'ns-talk' ], // $expectedClasses
165 yield 'subject' => [
166 new TitleValue( NS_MAIN, 'Test' ), // $title
167 $this->mockRegisteredUltimateAuthority(), // $authority
168 [ 'ns-subject' ], // $expectedClasses
170 yield 'editable' => [
171 new TitleValue( NS_MAIN, 'Test' ), // $title
172 $this->mockRegisteredAuthorityWithPermissions( [ 'edit' ] ), // $authority
173 [ 'mw-editable' ], // $expectedClasses
175 yield 'not editable' => [
176 new TitleValue( NS_MAIN, 'Test' ), // $title
177 $this->mockRegisteredNullAuthority(), // $authority
178 [], // $expectedClasses
179 [ 'mw-editable' ], // $unexpectedClasses
184 * @dataProvider providGetPageClasses
186 public function testGetPageClasses(
187 LinkTarget $title,
188 Authority $authority,
189 array $expectedClasses,
190 array $unexpectedClasses = []
192 $skin = new class extends Skin {
193 public function outputPage() {
196 $fakeContext = new RequestContext();
197 $fakeContext->setAuthority( $authority );
198 $skin->setContext( $fakeContext );
199 $classes = $skin->getPageClasses( Title::newFromLinkTarget( $title ) );
200 foreach ( $expectedClasses as $class ) {
201 $this->assertStringContainsString( $class, $classes );
203 foreach ( $unexpectedClasses as $class ) {
204 $this->assertStringNotContainsString( $class, $classes );
209 * @dataProvider provideSkinResponsiveOptions
211 public function testIsResponsive( array $options, bool $expected ) {
212 $skin = new class( $options ) extends Skin {
215 * @inheritDoc
217 public function outputPage() {
221 * @inheritDoc
223 public function getUser() {
224 $user = TestUserRegistry::getImmutableTestUser( [] )->getUser();
225 \MediaWiki\MediaWikiServices::getInstance()->getUserOptionsManager()->setOption(
226 $user,
227 'skin-responsive',
228 $this->options['userPreference']
230 return $user;
234 $this->assertSame( $expected, $skin->isResponsive() );
237 public static function provideSkinResponsiveOptions() {
238 yield 'responsive not set' => [
239 [ 'name' => 'test', 'userPreference' => true ],
240 false
242 yield 'responsive false' => [
243 [ 'name' => 'test', 'responsive' => false, 'userPreference' => true ],
244 false
246 yield 'responsive true' => [
247 [ 'name' => 'test', 'responsive' => true, 'userPreference' => true ],
248 true
250 yield 'responsive true, user preference false' => [
251 [ 'name' => 'test', 'responsive' => true, 'userPreference' => false ],
252 false
254 yield 'responsive false, user preference false' => [
255 [ 'name' => 'test', 'responsive' => false, 'userPreference' => false ],
256 false
260 public static function provideMakeLink() {
261 return [
262 'Empty href with link class' => [
264 'text' => 'Test',
265 'href' => '',
266 'class' => [
267 'class1',
268 'class2'
271 [ 'link-class' => 'link-class' ],
272 '<a href="" class="class1 class2 link-class">Test</a>',
274 'link with link-html' => [
276 'text' => '',
277 'href' => '#go',
278 'link-html' => '<i>label</i>'
280 [ 'text-wrapper' => [ 'tag' => 'span' ] ],
281 '<a href="#go"><i>label</i> </a>',
283 'Basic text wrapper' => [
285 'text' => 'Test',
287 [ 'text-wrapper' => [ 'tag' => 'span' ] ],
288 '<span>Test</span>'
290 'Text wrapper with tooltip ID in id attribute' => [
292 'text' => 'Test',
293 'id' => 'ii'
295 [ 'text-wrapper' => [ 'tag' => 'span' ] ],
296 '<span title="(tooltip-ii)">Test</span>'
298 'Text wrapper with tooltip ID in single-id' => [
300 'text' => 'Test',
301 'id' => 'foo',
302 'single-id' => 'ii'
304 [ 'text-wrapper' => [ 'tag' => 'span' ] ],
305 '<span title="(tooltip-ii)">Test</span>'
307 'Multi-level text wrapper with tooltip' => [
309 'text' => 'Test',
310 'id' => 'ii'
312 [ 'text-wrapper' => [
313 [ 'tag' => 'b' ],
314 [ 'tag' => 'i' ]
315 ] ],
316 '<b title="(tooltip-ii)"><i>Test</i></b>'
318 'Multi-level text wrapper with link' => [
320 'text' => 'Test',
321 'id' => 'ii',
322 'href' => '#',
324 [ 'text-wrapper' => [
325 [ 'tag' => 'b' ],
326 [ 'tag' => 'i' ]
327 ] ],
328 '<a id="ii" href="#" title="(tooltip-ii)(word-separator)(brackets: (accesskey-ii))" ' .
329 'accesskey="(accesskey-ii)"><b><i>Test</i></b></a>'
331 'Specified HTML' => [
333 'html' => '<b>1</b>',
336 '<b>1</b>'
338 'Data attribute' => [
340 'text' => 'Test',
341 'href' => '#',
342 'data' => [ 'foo' => 'bar' ]
345 '<a href="#" data-foo="bar">Test</a>'
347 'tooltip only' => [
349 'text' => 'Save',
350 'id' => 'save',
351 'href' => '#',
352 'tooltiponly' => true,
355 '<a id="save" href="#" title="(tooltip-save)">Save</a>'
361 * @dataProvider provideMakeLink
362 * @param array $data
363 * @param array $options
364 * @param string $expected
366 public function testMakeLinkLink( array $data, array $options, string $expected ) {
367 $this->setUserLang( 'qqx' );
368 $skin = new class extends Skin {
369 public function outputPage() {
373 $link = $skin->makeLink(
374 'test',
375 $data,
376 $options
379 $this->assertHTMLEquals(
380 $expected,
381 $link
385 public static function provideGetPersonalToolsForMakeListItem() {
386 return [
389 'foo' => [
390 'class' => 'foo',
391 'link-html' => '<i>text</i>',
392 'text' => 'Hello',
395 false,
397 'foo' => [
398 'links' => [
400 'single-id' => 'pt-foo',
401 'text' => 'Hello',
402 'link-html' => '<i>text</i>',
403 'class' => 'foo',
406 'id' => 'pt-foo',
407 'icon' => null,
413 'foo' => [
414 'class' => 'foo',
415 'link-html' => '<i>text</i>',
416 'text' => 'Hello',
419 true,
421 'foo' => [
422 'links' => [
424 'single-id' => 'pt-foo',
425 'text' => 'Hello',
426 'link-html' => '<i>text</i>',
429 'id' => 'pt-foo',
430 'icon' => null,
431 'class' => 'foo',
439 * @dataProvider provideGetPersonalToolsForMakeListItem
440 * @param array $urls
441 * @param bool $applyClassesToListItems
442 * @param array $expected
444 public function testGetPersonalToolsForMakeListItem( array $urls, bool $applyClassesToListItems, array $expected ) {
445 $skin = new class extends Skin {
446 public function outputPage() {
450 $this->assertSame(
451 $expected,
452 $skin->getPersonalToolsForMakeListItem(
453 $urls,
454 $applyClassesToListItems
459 public function testGetRelevantUser_get_set() {
460 $skin = new class extends Skin {
461 public function outputPage() {
464 $relevantUser = UserIdentityValue::newRegistered( 1, 'SomeUser' );
465 $skin->setRelevantUser( $relevantUser );
466 $this->assertSame( $relevantUser, $skin->getRelevantUser() );
468 $this->installMockBlockManager(
470 'target' => $relevantUser,
471 'hideName' => true,
475 $ctx = RequestContext::getMain();
476 $ctx->setAuthority( $this->mockAnonNullAuthority() );
477 $skin->setContext( $ctx );
478 $this->assertNull( $skin->getRelevantUser() );
480 $ctx->setAuthority( $this->mockAnonUltimateAuthority() );
481 $skin->setContext( $ctx );
482 $skin->setRelevantUser( $relevantUser );
483 $skin->setRelevantUser( $relevantUser );
484 $this->assertSame( $relevantUser, $skin->getRelevantUser() );
487 public static function provideGetRelevantUser_load_from_title() {
488 yield 'Not user namespace' => [
489 'relevantPage' => PageReferenceValue::localReference( NS_MAIN, '123.123.123.123' ),
490 'expectedUser' => null
492 yield 'User namespace' => [
493 'relevantPage' => PageReferenceValue::localReference( NS_USER, '123.123.123.123' ),
494 'expectedUser' => UserIdentityValue::newAnonymous( '123.123.123.123' )
496 yield 'User talk namespace' => [
497 'relevantPage' => PageReferenceValue::localReference( NS_USER_TALK, '123.123.123.123' ),
498 'expectedUser' => UserIdentityValue::newAnonymous( '123.123.123.123' )
500 yield 'User page subpage' => [
501 'relevantPage' => PageReferenceValue::localReference( NS_USER, '123.123.123.123/bla' ),
502 'expectedUser' => UserIdentityValue::newAnonymous( '123.123.123.123' )
504 yield 'Non-registered user with name' => [
505 'relevantPage' => PageReferenceValue::localReference( NS_USER, 'I_DO_NOT_EXIST' ),
506 'expectedUser' => null
511 * @dataProvider provideGetRelevantUser_load_from_title
513 public function testGetRelevantUser_load_from_title(
514 PageReferenceValue $relevantPage,
515 ?UserIdentity $expectedUser
517 $skin = new class extends Skin {
518 public function outputPage() {
521 $skin->setRelevantTitle( Title::newFromPageReference( $relevantPage ) );
522 $relevantUser = $skin->getRelevantUser();
523 if ( $expectedUser ) {
524 $this->assertTrue( $expectedUser->equals( $relevantUser ) );
525 } else {
526 $this->assertNull( $relevantUser );
530 public function testGetRelevantUser_load_existing() {
531 $skin = new class extends Skin {
532 public function outputPage() {
535 $user = new UserIdentityValue( 42, 'foo' );
536 $userIdentityLookup = $this->createMock( UserIdentityLookup::class );
537 $userIdentityLookup->method( 'getUserIdentityByName' )
538 ->willReturnCallback( function ( $name ) use ( $user ) {
539 if ( $name === $user->getName() ) {
540 return $user;
542 return $this->createMock( UserIdentity::class );
543 } );
544 $this->setService( 'UserIdentityLookup', $userIdentityLookup );
545 $skin->setRelevantTitle(
546 Title::makeTitle( NS_USER, $user->getName() )
548 $this->assertTrue( $user->equals( $skin->getRelevantUser() ) );
549 $this->assertSame( $user->getId(), $skin->getRelevantUser()->getId() );
552 public function testBuildSidebarCache() {
553 // T303007: Skin subclasses and Skin hooks may vary their sidebar contents.
554 $this->overrideConfigValues( [
555 MainConfigNames::UseDatabaseMessages => true,
556 MainConfigNames::EnableSidebarCache => true,
557 MainConfigNames::SidebarCacheExpiry => 3600,
558 ] );
559 // Mock time (T344191)
560 $clock = 1301644800.0;
561 $this->getServiceContainer()->getMainWANObjectCache()->setMockTime( $clock );
562 $id = 0;
563 $this->setTemporaryHook( 'SkinBuildSidebar',
564 static function ( Skin $skin, array &$bar ) use ( &$id, &$clock ) {
565 $id++;
566 $clock += 1.0;
567 if ( $skin->getSkinName() === 'foo' ) {
568 $bar['myhook'] = "foo $id";
570 if ( $skin->getSkinName() === 'bar' ) {
571 $bar['myhook'] = "bar $id";
575 $context = RequestContext::newExtraneousContext( Title::makeTitle( NS_SPECIAL, 'Blankpage' ) );
576 $foo1 = new class( 'foo' ) extends Skin {
577 public function outputPage() {
580 $foo2 = new class( 'foo' ) extends Skin {
581 public function outputPage() {
584 $bar = new class( 'bar' ) extends Skin {
585 public function outputPage() {
588 $foo1->setContext( $context );
589 $foo2->setContext( $context );
590 $bar->setContext( $context );
591 $this->assertArrayContains( [ 'myhook' => 'foo 1' ], $foo1->buildSidebar(), 'fresh' );
592 $clock += 0.01;
593 $this->assertArrayContains( [ 'myhook' => 'foo 1' ], $foo2->buildSidebar(), 'cache hit' );
594 $this->assertArrayContains( [ 'myhook' => 'bar 2' ], $bar->buildSidebar(), 'cache miss' );
597 public function testBuildSidebarWithUserAddedContent() {
598 $this->overrideConfigValues( [
599 MainConfigNames::UseDatabaseMessages => true,
600 MainConfigNames::EnableSidebarCache => false
601 ] );
602 $foo1 = new class( 'foo' ) extends Skin {
603 public function outputPage() {
606 $this->editPage( 'MediaWiki:Sidebar', <<<EOS
607 * navigation
608 ** mainpage|mainpage-description
609 ** recentchanges-url|recentchanges
610 ** randompage-url|randompage
611 ** helppage|help-mediawiki
612 * SEARCH
613 * TOOLBOX
614 ** A|B
615 * LANGUAGES
616 ** C|D
617 EOS );
619 $context = RequestContext::newExtraneousContext( Title::makeTitle( NS_MAIN, 'Main Page' ) );
620 $foo1->setContext( $context );
622 $this->assertArrayContains( [ [ 'id' => 'n-B', 'text' => 'B' ] ], $foo1->buildSidebar()['TOOLBOX'], 'Toolbox has user defined links' );
624 $hasUserDefinedLinks = false;
625 $languageLinks = $foo1->buildSidebar()['LANGUAGES'];
626 foreach ( $languageLinks as $languageLink ) {
627 if ( $languageLink['id'] === 'n-D' ) {
628 $hasUserDefinedLinks = true;
629 break;
633 $this->assertSame( false, $hasUserDefinedLinks, 'Languages does not support user defined links' );
636 public function testBuildSidebarForContributionsPageOfTemporaryAccount() {
637 // Don't allow extensions to modify the TOOLBOX array as we assert pretty strictly against it.
638 $this->clearHook( 'SidebarBeforeOutput' );
640 $this->overrideConfigValues( [
641 MainConfigNames::UploadNavigationUrl => false,
642 MainConfigNames::EnableUploads => false,
643 MainConfigNames::EnableSpecialMute => true,
644 ] );
645 $foo1 = new class( 'foo' ) extends Skin {
646 public function outputPage() {
650 // Simulate the settings and context for Special:Contributions for a temporary account
651 // (no article related and relevant user set).
652 $this->enableAutoCreateTempUser();
653 $tempUser = $this->getServiceContainer()->getTempUserCreator()
654 ->create( null, new FauxRequest() )->getUser();
655 $context = RequestContext::newExtraneousContext(
656 Title::makeTitle( NS_SPECIAL, 'Contributions/' . $tempUser->getName() )
658 $context->setUser( $this->getTestSysop()->getUser() );
659 $foo1->setContext( $context );
660 $foo1->setRelevantUser( $tempUser );
661 $foo1->getOutput()->setArticleRelated( false );
663 // Verify that the "userrights" key is not present, by checking that the list of keys is as expected.
664 $this->assertArrayEquals(
665 [ 'contributions', 'log', 'blockip', 'mute', 'print', 'specialpages' ],
666 array_keys( $foo1->buildSidebar()['TOOLBOX'] )
670 public function testGetLanguagesHidden() {
671 $this->overrideConfigValues( [
672 MainConfigNames::HideInterlanguageLinks => true,
673 ] );
674 $skin = new class extends Skin {
675 public function outputPage() {
678 $this->assertSame( [], $skin->getLanguages() );
681 public function testGetLanguages() {
682 $this->overrideConfigValues( [
683 MainConfigNames::HideInterlanguageLinks => false,
684 MainConfigNames::InterlanguageLinkCodeMap => [ 'talk' => 'fr' ],
685 MainConfigNames::LanguageCode => 'qqx',
686 ] );
688 $mockOutputPage = $this->createMock( OutputPage::class );
689 $mockOutputPage->method( 'getLanguageLinks' )
690 // The 'talk' interwiki is a deliberate conflict with the
691 // Talk namespace (T363538)
692 ->willReturn( [ 'en:Foo', 'talk:Page' ] );
694 $fakeContext = new RequestContext();
695 $fakeContext->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
696 $fakeContext->setOutput( $mockOutputPage );
697 $fakeContext->setLanguage( 'en' );
699 $hookContainer = $this->createMock( HookContainer::class );
700 $this->setService( 'HookContainer', $hookContainer );
702 $mockIwLookup = $this->createMock( InterwikiLookup::class );
703 $mockIwLookup->method( 'isValidInterwiki' )->willReturn( true );
704 $mockIwLookup->method( 'fetch' )->willReturnCallback( static function ( string $prefix ) {
705 return new Interwiki(
706 $prefix,
707 "https://$prefix.example.com/$1"
709 } );
710 $this->setService( 'InterwikiLookup', $mockIwLookup );
712 $skin = new class extends Skin {
713 public function outputPage() {
716 $skin->setContext( $fakeContext );
718 $this->assertSame( [
720 'href' => 'https://en.example.com/Foo',
721 'text' => 'English',
722 'title' => 'Foo – English',
723 'class' => 'interlanguage-link interwiki-en',
724 'link-class' => 'interlanguage-link-target',
725 'lang' => 'en',
726 'hreflang' => 'en',
727 'data-title' => 'Foo',
728 'data-language-autonym' => 'English',
729 'data-language-local-name' => 'English',
732 'href' => 'https://talk.example.com/Page',
733 'text' => 'Français',
734 'title' => 'Page – français',
735 'class' => 'interlanguage-link interwiki-talk',
736 'link-class' => 'interlanguage-link-target',
737 'lang' => 'fr',
738 'hreflang' => 'fr',
739 'data-title' => 'Page',
740 'data-language-autonym' => 'Français',
741 'data-language-local-name' => 'français',
743 ], $skin->getLanguages() );