assertEquals( 'fallback', $skin->getSkinName(), 'Default' );
$skin = new SkinFallback( 'testname' );
$this->assertEquals( 'testname', $skin->getSkinName(), 'Constructor argument' );
}
public function testGetDefaultModules() {
$skin = $this->getMockBuilder( Skin::class )
->onlyMethods( [ 'outputPage' ] )
->getMock();
$modules = $skin->getDefaultModules();
$this->assertTrue( isset( $modules['core'] ), 'core key is set by default' );
$this->assertTrue( isset( $modules['styles'] ), 'style key is set by default' );
}
/**
* @param bool $isSyndicated
* @param string $html
* @return OutputPage
*/
private function getMockOutputPage( $isSyndicated, $html ) {
$mock = $this->createMock( OutputPage::class );
$mock->expects( $this->once() )
->method( 'isSyndicated' )
->willReturn( $isSyndicated );
$mock->method( 'getHTML' )
->willReturn( $html );
return $mock;
}
public static function provideGetDefaultModulesForOutput() {
return [
[
false,
'',
[]
],
[
true,
'',
[ 'mediawiki.feedlink' ]
],
[
false,
'FOO mw-ui-button BAR',
[ 'mediawiki.ui.button' ]
],
[
true,
'FOO mw-ui-button BAR',
[ 'mediawiki.ui.button', 'mediawiki.feedlink' ]
],
];
}
/**
* @dataProvider provideGetDefaultModulesForOutput
*/
public function testGetDefaultModulesForContent( $isSyndicated, $html, array $expectedModuleStyles ) {
$skin = new class extends Skin {
public function outputPage() {
}
};
$fakeContext = new RequestContext();
$fakeContext->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
$fakeContext->setOutput( $this->getMockOutputPage( $isSyndicated, $html ) );
$skin->setContext( $fakeContext );
$modules = $skin->getDefaultModules();
$actualStylesModule = array_merge( ...array_values( $modules['styles'] ) );
foreach ( $expectedModuleStyles as $expected ) {
$this->assertContains( $expected, $actualStylesModule );
}
}
public static function provideGetDefaultModulesForRights() {
yield 'no rights' => [
'null', // $authoritySpec
false, // $hasModule
];
yield 'has all rights' => [
'ultimate', // $authoritySpec
true, // $hasModule
];
}
/**
* @dataProvider provideGetDefaultModulesForRights
*/
public function testGetDefaultModulesForRights( string $authoritySpec, bool $hasModule ) {
$authority = $authoritySpec === 'ultimate'
? $this->mockRegisteredUltimateAuthority()
: $this->mockRegisteredNullAuthority();
$skin = new class extends Skin {
public function outputPage() {
}
};
$fakeContext = new RequestContext();
$fakeContext->setAuthority( $authority );
$fakeContext->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
$skin->setContext( $fakeContext );
$defaultModules = $skin->getDefaultModules();
$this->assertArrayHasKey( 'watch', $defaultModules );
if ( $hasModule ) {
$this->assertContains( 'mediawiki.page.watch.ajax', $defaultModules['watch'] );
} else {
$this->assertNotContains( 'mediawiki.page.watch.ajax', $defaultModules['watch'] );
}
}
public static function provideGetPageClasses() {
yield 'normal page has namespace' => [
new TitleValue( NS_MAIN, 'Test' ), // $title
'ultimate', // $authoritySpec
[ 'ns-0' ], // $expectedClasses
];
yield 'valid special page' => [
new TitleValue( NS_SPECIAL, 'Userlogin' ), // $title
'ultimate', // $authoritySpec
[ 'mw-special-Userlogin' ], // $expectedClasses
];
yield 'invalid special page' => [
new TitleValue( NS_SPECIAL, 'BLABLABLABLA_I_AM_INVALID' ), // $title
'ultimate', // $authoritySpec
[ 'mw-invalidspecialpage' ], // $expectedClasses
];
yield 'talk page' => [
new TitleValue( NS_TALK, 'Test' ), // $title
'ultimate', // $authoritySpec
[ 'ns-talk' ], // $expectedClasses
];
yield 'subject' => [
new TitleValue( NS_MAIN, 'Test' ), // $title
'ultimate', // $authoritySpec
[ 'ns-subject' ], // $expectedClasses
];
yield 'editable' => [
new TitleValue( NS_MAIN, 'Test' ), // $title
[ 'edit' ], // $authoritySpec
[ 'mw-editable' ], // $expectedClasses
];
yield 'not editable' => [
new TitleValue( NS_MAIN, 'Test' ), // $title
'null', // $authoritySpec
[], // $expectedClasses
[ 'mw-editable' ], // $unexpectedClasses
];
}
/**
* @dataProvider provideGetPageClasses
*/
public function testGetPageClasses(
LinkTarget $title,
$authoritySpec,
array $expectedClasses,
array $unexpectedClasses = []
) {
if ( is_array( $authoritySpec ) ) {
$authority = $this->mockRegisteredAuthorityWithPermissions( $authoritySpec );
} else {
$authority = $authoritySpec === 'ultimate'
? $this->mockRegisteredUltimateAuthority()
: $this->mockRegisteredNullAuthority();
}
$skin = new class extends Skin {
public function outputPage() {
}
};
$fakeContext = new RequestContext();
$fakeContext->setAuthority( $authority );
$skin->setContext( $fakeContext );
$classes = $skin->getPageClasses( Title::newFromLinkTarget( $title ) );
foreach ( $expectedClasses as $class ) {
$this->assertStringContainsString( $class, $classes );
}
foreach ( $unexpectedClasses as $class ) {
$this->assertStringNotContainsString( $class, $classes );
}
}
/**
* @dataProvider provideSkinResponsiveOptions
*/
public function testIsResponsive( array $options, bool $expected ) {
$skin = new class( $options ) extends Skin {
/**
* @inheritDoc
*/
public function outputPage() {
}
/**
* @inheritDoc
*/
public function getUser() {
$user = TestUserRegistry::getImmutableTestUser( [] )->getUser();
\MediaWiki\MediaWikiServices::getInstance()->getUserOptionsManager()->setOption(
$user,
'skin-responsive',
$this->options['userPreference']
);
return $user;
}
};
$this->assertSame( $expected, $skin->isResponsive() );
}
public static function provideSkinResponsiveOptions() {
yield 'responsive not set' => [
[ 'name' => 'test', 'userPreference' => true ],
false
];
yield 'responsive false' => [
[ 'name' => 'test', 'responsive' => false, 'userPreference' => true ],
false
];
yield 'responsive true' => [
[ 'name' => 'test', 'responsive' => true, 'userPreference' => true ],
true
];
yield 'responsive true, user preference false' => [
[ 'name' => 'test', 'responsive' => true, 'userPreference' => false ],
false
];
yield 'responsive false, user preference false' => [
[ 'name' => 'test', 'responsive' => false, 'userPreference' => false ],
false
];
}
public static function provideMakeLink() {
return [
'Empty href with link class' => [
[
'text' => 'Test',
'href' => '',
'class' => [
'class1',
'class2'
]
],
[ 'link-class' => 'link-class' ],
'Test',
],
'link with link-html' => [
[
'text' => '',
'href' => '#go',
'link-html' => 'label'
],
[ 'text-wrapper' => [ 'tag' => 'span' ] ],
'label ',
],
'Basic text wrapper' => [
[
'text' => 'Test',
],
[ 'text-wrapper' => [ 'tag' => 'span' ] ],
'Test'
],
'Text wrapper with tooltip ID in id attribute' => [
[
'text' => 'Test',
'id' => 'ii'
],
[ 'text-wrapper' => [ 'tag' => 'span' ] ],
'Test'
],
'Text wrapper with tooltip ID in single-id' => [
[
'text' => 'Test',
'id' => 'foo',
'single-id' => 'ii'
],
[ 'text-wrapper' => [ 'tag' => 'span' ] ],
'Test'
],
'Multi-level text wrapper with tooltip' => [
[
'text' => 'Test',
'id' => 'ii'
],
[ 'text-wrapper' => [
[ 'tag' => 'b' ],
[ 'tag' => 'i' ]
] ],
'Test'
],
'Multi-level text wrapper with link' => [
[
'text' => 'Test',
'id' => 'ii',
'href' => '#',
],
[ 'text-wrapper' => [
[ 'tag' => 'b' ],
[ 'tag' => 'i' ]
] ],
'Test'
],
'Specified HTML' => [
[
'html' => '1',
],
[],
'1'
],
'Data attribute' => [
[
'text' => 'Test',
'href' => '#',
'data' => [ 'foo' => 'bar' ]
],
[],
'Test'
],
'tooltip only' => [
[
'text' => 'Save',
'id' => 'save',
'href' => '#',
'tooltiponly' => true,
],
[],
'Save'
]
];
}
/**
* @dataProvider provideMakeLink
* @param array $data
* @param array $options
* @param string $expected
*/
public function testMakeLinkLink( array $data, array $options, string $expected ) {
$this->setUserLang( 'qqx' );
$skin = new class extends Skin {
public function outputPage() {
}
};
$link = $skin->makeLink(
'test',
$data,
$options
);
$this->assertHTMLEquals(
$expected,
$link
);
}
public static function provideGetPersonalToolsForMakeListItem() {
return [
[
[
'foo' => [
'class' => 'foo',
'link-html' => 'text',
'text' => 'Hello',
],
],
false,
[
'foo' => [
'links' => [
[
'single-id' => 'pt-foo',
'text' => 'Hello',
'link-html' => 'text',
'class' => 'foo',
]
],
'id' => 'pt-foo',
'icon' => null,
]
],
],
[
[
'foo' => [
'class' => 'foo',
'link-html' => 'text',
'text' => 'Hello',
],
],
true,
[
'foo' => [
'links' => [
[
'single-id' => 'pt-foo',
'text' => 'Hello',
'link-html' => 'text',
]
],
'id' => 'pt-foo',
'icon' => null,
'class' => 'foo',
]
],
]
];
}
/**
* @dataProvider provideGetPersonalToolsForMakeListItem
* @param array $urls
* @param bool $applyClassesToListItems
* @param array $expected
*/
public function testGetPersonalToolsForMakeListItem( array $urls, bool $applyClassesToListItems, array $expected ) {
$skin = new class extends Skin {
public function outputPage() {
}
};
$this->assertSame(
$expected,
$skin->getPersonalToolsForMakeListItem(
$urls,
$applyClassesToListItems
)
);
}
public function testGetRelevantUser_get_set() {
$skin = new class extends Skin {
public function outputPage() {
}
};
$relevantUser = UserIdentityValue::newRegistered( 1, 'SomeUser' );
$skin->setRelevantUser( $relevantUser );
$this->assertSame( $relevantUser, $skin->getRelevantUser() );
$this->installMockBlockManager(
[
'target' => new UserBlockTarget( $relevantUser ),
'hideName' => true,
]
);
$ctx = RequestContext::getMain();
$ctx->setAuthority( $this->mockAnonNullAuthority() );
$skin->setContext( $ctx );
$this->assertNull( $skin->getRelevantUser() );
$ctx->setAuthority( $this->mockAnonUltimateAuthority() );
$skin->setContext( $ctx );
$skin->setRelevantUser( $relevantUser );
$skin->setRelevantUser( $relevantUser );
$this->assertSame( $relevantUser, $skin->getRelevantUser() );
}
public static function provideGetRelevantUser_load_from_title() {
yield 'Not user namespace' => [
'relevantPage' => PageReferenceValue::localReference( NS_MAIN, '123.123.123.123' ),
'expectedUser' => null
];
yield 'User namespace' => [
'relevantPage' => PageReferenceValue::localReference( NS_USER, '123.123.123.123' ),
'expectedUser' => UserIdentityValue::newAnonymous( '123.123.123.123' )
];
yield 'User talk namespace' => [
'relevantPage' => PageReferenceValue::localReference( NS_USER_TALK, '123.123.123.123' ),
'expectedUser' => UserIdentityValue::newAnonymous( '123.123.123.123' )
];
yield 'User page subpage' => [
'relevantPage' => PageReferenceValue::localReference( NS_USER, '123.123.123.123/bla' ),
'expectedUser' => UserIdentityValue::newAnonymous( '123.123.123.123' )
];
yield 'Non-registered user with name' => [
'relevantPage' => PageReferenceValue::localReference( NS_USER, 'I_DO_NOT_EXIST' ),
'expectedUser' => null
];
}
/**
* @dataProvider provideGetRelevantUser_load_from_title
*/
public function testGetRelevantUser_load_from_title(
PageReferenceValue $relevantPage,
?UserIdentity $expectedUser
) {
$skin = new class extends Skin {
public function outputPage() {
}
};
$skin->setRelevantTitle( Title::newFromPageReference( $relevantPage ) );
$relevantUser = $skin->getRelevantUser();
if ( $expectedUser ) {
$this->assertTrue( $expectedUser->equals( $relevantUser ) );
} else {
$this->assertNull( $relevantUser );
}
}
public function testGetRelevantUser_load_existing() {
$skin = new class extends Skin {
public function outputPage() {
}
};
$user = new UserIdentityValue( 42, 'foo' );
$userIdentityLookup = $this->createMock( UserIdentityLookup::class );
$userIdentityLookup->method( 'getUserIdentityByName' )
->willReturnCallback( function ( $name ) use ( $user ) {
if ( $name === $user->getName() ) {
return $user;
}
return $this->createMock( UserIdentity::class );
} );
$this->setService( 'UserIdentityLookup', $userIdentityLookup );
$skin->setRelevantTitle(
Title::makeTitle( NS_USER, $user->getName() )
);
$this->assertTrue( $user->equals( $skin->getRelevantUser() ) );
$this->assertSame( $user->getId(), $skin->getRelevantUser()->getId() );
}
public function testBuildSidebarCache() {
// T303007: Skin subclasses and Skin hooks may vary their sidebar contents.
$this->overrideConfigValues( [
MainConfigNames::UseDatabaseMessages => true,
MainConfigNames::EnableSidebarCache => true,
MainConfigNames::SidebarCacheExpiry => 3600,
] );
// Mock time (T344191)
$clock = 1301644800.0;
$this->getServiceContainer()->getMainWANObjectCache()->setMockTime( $clock );
$id = 0;
$this->setTemporaryHook( 'SkinBuildSidebar',
static function ( Skin $skin, array &$bar ) use ( &$id, &$clock ) {
$id++;
$clock += 1.0;
if ( $skin->getSkinName() === 'foo' ) {
$bar['myhook'] = "foo $id";
}
if ( $skin->getSkinName() === 'bar' ) {
$bar['myhook'] = "bar $id";
}
}
);
$context = RequestContext::newExtraneousContext( Title::makeTitle( NS_SPECIAL, 'Blankpage' ) );
$foo1 = new class( 'foo' ) extends Skin {
public function outputPage() {
}
};
$foo2 = new class( 'foo' ) extends Skin {
public function outputPage() {
}
};
$bar = new class( 'bar' ) extends Skin {
public function outputPage() {
}
};
$foo1->setContext( $context );
$foo2->setContext( $context );
$bar->setContext( $context );
$this->assertArrayContains( [ 'myhook' => 'foo 1' ], $foo1->buildSidebar(), 'fresh' );
$clock += 0.01;
$this->assertArrayContains( [ 'myhook' => 'foo 1' ], $foo2->buildSidebar(), 'cache hit' );
$this->assertArrayContains( [ 'myhook' => 'bar 2' ], $bar->buildSidebar(), 'cache miss' );
}
public function testBuildSidebarWithUserAddedContent() {
$this->overrideConfigValues( [
MainConfigNames::UseDatabaseMessages => true,
MainConfigNames::EnableSidebarCache => false
] );
$foo1 = new class( 'foo' ) extends Skin {
public function outputPage() {
}
};
$this->editPage( 'MediaWiki:Sidebar', <<setContext( $context );
$this->assertArrayContains( [ [ 'id' => 'n-B', 'text' => 'B' ] ], $foo1->buildSidebar()['TOOLBOX'], 'Toolbox has user defined links' );
$hasUserDefinedLinks = false;
$languageLinks = $foo1->buildSidebar()['LANGUAGES'];
foreach ( $languageLinks as $languageLink ) {
if ( $languageLink['id'] === 'n-D' ) {
$hasUserDefinedLinks = true;
break;
}
}
$this->assertSame( false, $hasUserDefinedLinks, 'Languages does not support user defined links' );
}
public function testBuildSidebarForContributionsPageOfTemporaryAccount() {
// Don't allow extensions to modify the TOOLBOX array as we assert pretty strictly against it.
$this->clearHook( 'SidebarBeforeOutput' );
$this->overrideConfigValues( [
MainConfigNames::UploadNavigationUrl => false,
MainConfigNames::EnableUploads => false,
MainConfigNames::EnableSpecialMute => true,
] );
$foo1 = new class( 'foo' ) extends Skin {
public function outputPage() {
}
};
// Simulate the settings and context for Special:Contributions for a temporary account
// (no article related and relevant user set).
$this->enableAutoCreateTempUser();
$tempUser = $this->getServiceContainer()->getTempUserCreator()
->create( null, new FauxRequest() )->getUser();
$context = RequestContext::newExtraneousContext(
Title::makeTitle( NS_SPECIAL, 'Contributions/' . $tempUser->getName() )
);
$context->setUser( $this->getTestSysop()->getUser() );
$foo1->setContext( $context );
$foo1->setRelevantUser( $tempUser );
$foo1->getOutput()->setArticleRelated( false );
// Verify that the "userrights" key is not present, by checking that the list of keys is as expected.
$this->assertArrayEquals(
[ 'contributions', 'log', 'blockip', 'mute', 'print' ],
array_keys( $foo1->buildSidebar()['TOOLBOX'] )
);
}
public function testGetLanguagesHidden() {
$this->overrideConfigValues( [
MainConfigNames::HideInterlanguageLinks => true,
] );
$skin = new class extends Skin {
public function outputPage() {
}
};
$this->assertSame( [], $skin->getLanguages() );
}
public function testGetLanguages() {
$this->overrideConfigValues( [
MainConfigNames::HideInterlanguageLinks => false,
MainConfigNames::InterlanguageLinkCodeMap => [ 'talk' => 'fr' ],
MainConfigNames::LanguageCode => 'qqx',
] );
$mockOutputPage = $this->createMock( OutputPage::class );
$mockOutputPage->method( 'getLanguageLinks' )
// The 'talk' interwiki is a deliberate conflict with the
// Talk namespace (T363538)
->willReturn( [ 'en:Foo', 'talk:Page' ] );
$fakeContext = new RequestContext();
$fakeContext->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
$fakeContext->setOutput( $mockOutputPage );
$fakeContext->setLanguage( 'en' );
$hookContainer = $this->createMock( HookContainer::class );
$this->setService( 'HookContainer', $hookContainer );
$mockIwLookup = $this->createMock( InterwikiLookup::class );
$mockIwLookup->method( 'isValidInterwiki' )->willReturn( true );
$mockIwLookup->method( 'fetch' )->willReturnCallback( static function ( string $prefix ) {
return new Interwiki(
$prefix,
"https://$prefix.example.com/$1"
);
} );
$this->setService( 'InterwikiLookup', $mockIwLookup );
$skin = new class extends Skin {
public function outputPage() {
}
};
$skin->setContext( $fakeContext );
$this->assertSame( [
[
'href' => 'https://en.example.com/Foo',
'text' => 'English',
'title' => 'Foo – English',
'class' => 'interlanguage-link interwiki-en',
'link-class' => 'interlanguage-link-target',
'lang' => 'en',
'hreflang' => 'en',
'data-title' => 'Foo',
'data-language-autonym' => 'English',
'data-language-local-name' => 'English',
],
[
'href' => 'https://talk.example.com/Page',
'text' => 'Français',
'title' => 'Page – français',
'class' => 'interlanguage-link interwiki-talk',
'link-class' => 'interlanguage-link-target',
'lang' => 'fr',
'hreflang' => 'fr',
'data-title' => 'Page',
'data-language-autonym' => 'Français',
'data-language-local-name' => 'français',
],
], $skin->getLanguages() );
}
}