wiki.techinc.nl/tests/phpunit/includes/ResourceLoader/SkinModuleTest.php
Jan Drewniak afdc92987f SkinModule - Ensure skins can easily override skin feature styles
Reorders ResourceLoader\\SkinModule style output so that all styles
produced by skins are output after styles that are produces by
skin "features" such as normalize.

Refactors & expands SkinModuleTest.php to include tests for default
values, print styles and correct style ordering.

Bug: T354975
Change-Id: I0000b4ad8eb2de40be293f0e693d873c282ea785
2024-02-14 17:44:01 -05:00

683 lines
18 KiB
PHP

<?php
namespace MediaWiki\Tests\ResourceLoader;
use Generator;
use InvalidArgumentException;
use MediaWiki\Config\HashConfig;
use MediaWiki\MainConfigNames;
use MediaWiki\ResourceLoader\Context;
use MediaWiki\ResourceLoader\FilePath;
use MediaWiki\ResourceLoader\SkinModule;
use ReflectionClass;
use ResourceLoaderTestCase;
use Wikimedia\TestingAccessWrapper;
/**
* @group ResourceLoader
* @covers \MediaWiki\ResourceLoader\SkinModule
*/
class SkinModuleTest extends ResourceLoaderTestCase {
public static function provideApplyFeaturesCompatibility() {
return [
[
[
'content-thumbnails' => true,
],
[
'content-media' => true,
],
true,
'The `content-thumbnails` feature is mapped to `content-media`.'
],
[
[
'content-parser-output' => true,
],
[
'content-body' => true,
],
true,
'The new `content-parser-output` module was renamed to `content-body`.'
],
[
[
'content' => true,
],
[
'content-media' => true,
],
true,
'The `content` feature is mapped to `content-media`.'
],
[
[
'content-links' => true,
],
[
'content-links-external' => true,
'content-links' => true,
],
true,
'The `content-links` feature will also enable `content-links-external` if it not specified.'
],
[
[
'element' => true,
],
[
'element' => true,
'content-links' => true,
],
true,
'The `element` feature will turn on `content-links` if not specified.'
],
[
[
'content-links-external' => false,
'content-links' => true,
],
[
'content-links-external' => false,
'content-links' => true,
],
true,
'The `content-links` feature has no impact on content-links-external value.'
],
[
[
'content-links' => true,
'content-thumbnails' => true,
],
[
'content-links' => true,
'content-media' => true,
],
false,
'applyFeaturesCompatibility should not opt the skin into things it does not want.' .
'It should only rename features.'
],
[
[
'element' => true,
],
[
'element' => true,
],
false,
'applyFeaturesCompatibility should not opt the skin into things it does not want.'
],
];
}
/**
* @dataProvider provideApplyFeaturesCompatibility
*/
public function testApplyFeaturesCompatibility( array $features, array $expected, bool $optInPolicy, $msg ) {
// Test protected method
$class = TestingAccessWrapper::newFromClass( SkinModule::class );
$actual = $class->applyFeaturesCompatibility( $features, $optInPolicy );
$this->assertEquals( $expected, $actual, $msg );
}
public static function provideGetAvailableLogos() {
return [
[
[
MainConfigNames::Logos => [],
MainConfigNames::Logo => '/logo.png',
],
[
'1x' => '/logo.png',
]
],
[
[
MainConfigNames::Logos => [
'svg' => '/logo.svg',
'2x' => 'logo-2x.png'
],
MainConfigNames::Logo => '/logo.png',
],
[
'svg' => '/logo.svg',
'2x' => 'logo-2x.png',
'1x' => '/logo.png',
]
],
[
[
MainConfigNames::Logos => [
'wordmark' => [
'src' => '/logo-wordmark.png',
'width' => 100,
'height' => 15,
],
'1x' => '/logo.png',
'svg' => '/logo.svg',
'2x' => 'logo-2x.png'
],
],
[
'wordmark' => [
'src' => '/logo-wordmark.png',
'width' => 100,
'height' => 15,
'style' => 'width: 6.25em; height: 0.9375em;',
],
'1x' => '/logo.png',
'svg' => '/logo.svg',
'2x' => 'logo-2x.png',
]
]
];
}
public static function provideGetStyles() {
return [
[
'parent' => [],
'logo' => '/logo.png',
'expected' => [
'all' => [ '.mw-wiki-logo { background-image: url(/logo.png); }' ],
],
],
[
'parent' => [
'screen' => '.example {}',
],
'logo' => '/logo.png',
'expected' => [
'screen' => [ '.example {}' ],
'all' => [ '.mw-wiki-logo { background-image: url(/logo.png); }' ],
],
],
[
'parent' => [],
'logo' => [
'1x' => '/logo.png',
'1.5x' => '/logo@1.5x.png',
'2x' => '/logo@2x.png',
],
'expected' => [
'all' => [
'.mw-wiki-logo { background-image: url(/logo.png); }',
],
'(-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx), (min-resolution: 144dpi)' => [
'.mw-wiki-logo { background-image: url(/logo@1.5x.png);background-size: 135px auto; }',
],
'(-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx), (min-resolution: 192dpi)' => [
'.mw-wiki-logo { background-image: url(/logo@2x.png);background-size: 135px auto; }',
],
],
],
[
'parent' => [],
'logo' => [
'1x' => '/logo.png',
'svg' => '/logo.svg',
],
'expected' => [
'all' => [
'.mw-wiki-logo { background-image: url(/logo.svg); }',
'.mw-wiki-logo { background-size: 135px auto; }',
],
],
],
];
// phpcs:enable
}
/**
* @dataProvider provideGetStyles
*/
public function testGetStyles( $parent, $logo, $expected ) {
$module = $this->getMockBuilder( SkinModule::class )
->onlyMethods( [ 'readStyleFiles', 'getLogoData' ] )
->getMock();
$module->expects( $this->once() )->method( 'readStyleFiles' )
->willReturn( $parent );
$module->expects( $this->once() )->method( 'getLogoData' )
->willReturn( $logo );
$module->setConfig( new HashConfig( [
MainConfigNames::ParserEnableLegacyMediaDOM => false,
] + self::getSettings() ) );
$ctx = $this->createMock( Context::class );
$this->assertEquals(
$expected,
$module->getStyles( $ctx )
);
}
/**
* @dataProvider provideGetAvailableLogos
*/
public function testGetAvailableLogos( $config, $expected ) {
$logos = SkinModule::getAvailableLogos( new HashConfig( $config ) );
$this->assertSame( $expected, $logos );
}
public function testGetAvailableLogosRuntimeException() {
$logos = SkinModule::getAvailableLogos( new HashConfig( [
MainConfigNames::Logo => false,
MainConfigNames::Logos => false,
MainConfigNames::LogoHD => false,
] ) );
$this->assertSame( [], $logos );
}
public function testIsKnownEmpty() {
$module = new SkinModule();
$ctx = $this->createMock( Context::class );
$this->assertFalse( $module->isKnownEmpty( $ctx ) );
}
/**
* @dataProvider provideGetLogoData
*/
public function testGetLogoData( $config, $expected ) {
// Allow testing of protected method
$module = TestingAccessWrapper::newFromObject( new SkinModule() );
$this->assertEquals(
$expected,
$module->getLogoData( new HashConfig( $config ) )
);
}
public static function provideGetLogoData() {
return [
'wordmark' => [
'config' => [
MainConfigNames::BaseDirectory => MW_INSTALL_PATH,
MainConfigNames::ResourceBasePath => '/w',
MainConfigNames::Logos => [
'1x' => '/img/default.png',
'wordmark' => [
'src' => '/img/wordmark.png',
'width' => 120,
'height' => 20,
],
],
],
'expected' => '/img/default.png',
],
'simple' => [
'config' => [
MainConfigNames::BaseDirectory => MW_INSTALL_PATH,
MainConfigNames::ResourceBasePath => '/w',
MainConfigNames::Logos => [
'1x' => '/img/default.png',
],
],
'expected' => '/img/default.png',
],
'default and 2x' => [
'config' => [
MainConfigNames::BaseDirectory => MW_INSTALL_PATH,
MainConfigNames::ResourceBasePath => '/w',
MainConfigNames::Logos => [
'1x' => '/img/default.png',
'2x' => '/img/two-x.png',
],
],
'expected' => [
'1x' => '/img/default.png',
'2x' => '/img/two-x.png',
],
],
'default and all HiDPIs' => [
'config' => [
MainConfigNames::BaseDirectory => MW_INSTALL_PATH,
MainConfigNames::ResourceBasePath => '/w',
MainConfigNames::Logos => [
'1x' => '/img/default.png',
'1.5x' => '/img/one-point-five.png',
'2x' => '/img/two-x.png',
],
],
'expected' => [
'1x' => '/img/default.png',
'1.5x' => '/img/one-point-five.png',
'2x' => '/img/two-x.png',
],
],
'default and SVG' => [
'config' => [
MainConfigNames::BaseDirectory => MW_INSTALL_PATH,
MainConfigNames::ResourceBasePath => '/w',
MainConfigNames::Logos => [
'1x' => '/img/default.png',
'svg' => '/img/vector.svg',
],
],
'expected' => [
'1x' => '/img/default.png',
'svg' => '/img/vector.svg',
],
],
'everything' => [
'config' => [
MainConfigNames::BaseDirectory => MW_INSTALL_PATH,
MainConfigNames::ResourceBasePath => '/w',
MainConfigNames::Logos => [
'1x' => '/img/default.png',
'1.5x' => '/img/one-point-five.png',
'2x' => '/img/two-x.png',
'svg' => '/img/vector.svg',
],
],
'expected' => [
'1x' => '/img/default.png',
'svg' => '/img/vector.svg',
],
],
'versioned url' => [
'config' => [
MainConfigNames::BaseDirectory => dirname( dirname( __DIR__ ) ) . '/data/media',
MainConfigNames::ResourceBasePath => '/w',
MainConfigNames::UploadPath => '/w/images',
MainConfigNames::Logos => [
'1x' => '/w/test.jpg',
],
],
'expected' => '/w/test.jpg?edcf2',
],
];
}
/**
* @dataProvider providePreloadLinks
*/
public function testPreloadLinkHeaders( $config, $lang, $result ) {
$ctx = $this->createMock( Context::class );
$ctx->method( 'getLanguage' )->willReturn( $lang );
$module = new SkinModule();
$module->setConfig( new HashConfig( $config + [
MainConfigNames::BaseDirectory => '/dummy',
MainConfigNames::ResourceBasePath => '/w',
MainConfigNames::Logo => false,
MainConfigNames::LogoHD => false
] + self::getSettings() ) );
$this->assertEquals( [ $result ], $module->getHeaders( $ctx ) );
}
public static function providePreloadLinks() {
return [
[
[
'Logos' => [
'1x' => '/img/default.png',
'1.5x' => '/img/one-point-five.png',
'2x' => '/img/two-x.png',
],
],
'en',
'Link: </img/default.png>;rel=preload;as=image;media=' .
'not all and (min-resolution: 1.5dppx),' .
'</img/one-point-five.png>;rel=preload;as=image;media=' .
'(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' .
'</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
],
[
[
'Logos' => [
'1x' => '/img/default.png',
],
],
'en',
'Link: </img/default.png>;rel=preload;as=image'
],
[
[
'Logos' => [
'1x' => '/img/default.png',
'2x' => '/img/two-x.png',
],
],
'en',
'Link: </img/default.png>;rel=preload;as=image;media=' .
'not all and (min-resolution: 2dppx),' .
'</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
],
[
[
'Logos' => [
'1x' => '/img/default.png',
'svg' => '/img/vector.svg',
],
],
'en',
'Link: </img/vector.svg>;rel=preload;as=image'
],
[
[
'BaseDirectory' => dirname( dirname( __DIR__ ) ) . '/data/media',
'Logos' => [
'1x' => '/w/test.jpg',
],
'UploadPath' => '/w/images',
],
'en',
'Link: </w/test.jpg?edcf2>;rel=preload;as=image',
],
[
[
'Logos' => [
'1x' => '/img/default.png',
'1.5x' => '/img/one-point-five.png',
'2x' => '/img/two-x.png',
'variants' => [
'zh-hans' => [
'1x' => '/img/default-zh-hans.png',
'1.5x' => '/img/one-point-five-zh-hans.png',
]
]
],
],
'zh-hans',
'Link: </img/default-zh-hans.png>;rel=preload;as=image;media=' .
'not all and (min-resolution: 1.5dppx),' .
'</img/one-point-five-zh-hans.png>;rel=preload;as=image;media=' .
'(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' .
'</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
],
];
}
public function testNoPreloadLogos() {
$module = new SkinModule( [ 'features' => [ 'logo' => false ] ] );
$context =
$this->createMock( Context::class );
$preloadLinks = $module->getPreloadLinks( $context );
$this->assertArrayEquals( [], $preloadLinks );
}
public function testPreloadLogos() {
$module = new SkinModule();
$module->setConfig( self::getMinimalConfig() );
$context = $this->createMock( Context::class );
$preloadLinks = $module->getPreloadLinks( $context );
$this->assertNotSameSize( [], $preloadLinks );
}
/**
* Covers SkinModule::FEATURE_FILES, but not annotatable.
*
* @dataProvider provideFeatureFiles
* @param string $file
*/
public function testFeatureFilesExist( string $file ): void {
$this->assertFileExists( $file );
}
public static function provideFeatureFiles(): Generator {
global $IP;
$featureFiles = ( new ReflectionClass( SkinModule::class ) )
->getConstant( 'FEATURE_FILES' );
foreach ( $featureFiles as $feature => $files ) {
foreach ( $files as $media => $stylesheets ) {
foreach ( $stylesheets as $stylesheet ) {
yield "$feature: $media: $stylesheet" => [ "$IP/$stylesheet" ];
}
}
}
}
public static function getSkinFeaturePath( $feature, $mediaType ) {
global $IP;
$featureFiles = ( new ReflectionClass( SkinModule::class ) )->getConstant( 'FEATURE_FILES' );
return new FilePath( $featureFiles[ $feature ][ $mediaType ][ 0 ], $IP, '/w' );
}
public static function provideGetStyleFilesFeatureStylesOrder() {
return [
[ 'The "logo" skin-feature is loaded when the "features" key is absent',
[ 'styles' => 'test.styles/styles.default.css' ],
[
'all' => [ self::getSkinFeaturePath( 'logo', 'all' ) ],
'print' => [ self::getSkinFeaturePath( 'logo', 'print' ) ],
'' => [ 'test.styles/styles.default.css' ],
],
],
[ 'The "normalize" skin-feature is always output first',
[ 'features' => [ 'elements', 'normalize' ],
'styles' => 'test.styles/styles.default.css' ],
[
'all' => [ self::getSkinFeaturePath( 'normalize', 'all' ) ],
'screen' => [ self::getSkinFeaturePath( 'elements', 'screen' ) ],
'print' => [ self::getSkinFeaturePath( 'elements', 'print' ) ],
'' => [ 'test.styles/styles.default.css' ],
],
],
[ 'Module styles that include media queries are grouped in correct media query block',
[ 'features' => [ 'elements', 'normalize' ],
'styles' => [
'test.styles/styles.screen.css' => [ 'media' => 'screen' ],
'test.styles/styles.print.css' => [ 'media' => 'print' ],
'test.styles/styles.all.css' => [ 'media' => 'all' ],
'test.styles/styles.default.css'
],
],
[
'all' => [ self::getSkinFeaturePath( 'normalize', 'all' ) ],
'screen' => [ self::getSkinFeaturePath( 'elements', 'screen' ), 'test.styles/styles.screen.css' ],
'print' => [ self::getSkinFeaturePath( 'elements', 'print' ), 'test.styles/styles.print.css' ],
'' => [ 'test.styles/styles.all.css', 'test.styles/styles.default.css' ],
],
],
[ 'Empty media query blocks are not included in output',
[ 'features' => [
'accessibility' => false,
'content-body' => false,
'interface-core' => false,
'toc' => false
],
'styles' => [
'test.styles/styles.print.css' => [ 'media' => 'print' ]
],
],
[
'print' => [ 'test.styles/styles.print.css' ],
],
],
[ 'Empty "features" key outputs default skin-features',
[
'features' => [],
],
[
'all' => [
self::getSkinFeaturePath( 'accessibility', 'all' ),
self::getSkinFeaturePath( 'toc', 'all' )
],
'screen' => [
self::getSkinFeaturePath( 'content-body', 'screen' ),
self::getSkinFeaturePath( 'interface-core', 'screen' ),
self::getSkinFeaturePath( 'toc', 'screen' ),
],
'print' => [
self::getSkinFeaturePath( 'content-body', 'print' ),
self::getSkinFeaturePath( 'interface-core', 'print' ),
self::getSkinFeaturePath( 'toc', 'print' )
]
],
],
[ 'skin-features are output in the order defined in SkinModule.php',
[
'features' => [ 'interface-message-box', 'normalize', 'accessibility' ],
],
[
'all' => [
self::getSkinFeaturePath( 'accessibility', 'all' ),
self::getSkinFeaturePath( 'normalize', 'all' ),
self::getSkinFeaturePath( 'interface-message-box', 'all' )
],
],
]
];
}
/**
* Test order and output of SkinModule styles.
*
* @dataProvider provideGetStyleFilesFeatureStylesOrder
* @param string $msg to show for debugging
* @param array $skinModuleConfig
* @param array $expectedStyleOrder
*/
public function testGetStyleFilesFeatureStylesOrder(
$msg, $skinModuleConfig, $expectedStyleOrder
): void {
$ctx = $this->createMock( Context::class );
$module = new SkinModule( $skinModuleConfig );
$module->setConfig( self::getMinimalConfig() );
$actual = $module->getStyleFiles( $ctx );
$this->assertEquals(
array_values( $expectedStyleOrder ),
array_values( $actual )
);
}
public static function provideInvalidFeatures() {
yield 'listed unknown' => [
[ 'logo', 'unknown' ],
];
yield 'enabled unknown' => [
[
'logo' => true,
'toc' => false,
'unknown' => true,
],
];
yield 'disbled unknown' => [
[
'logo' => true,
'toc' => false,
'unknown' => false,
],
];
}
/**
* @dataProvider provideInvalidFeatures
*/
public function testConstructInvalidFeatures( array $features ) {
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( "Feature 'unknown' is not recognised" );
$module = new SkinModule( [
'features' => $features,
] );
}
}