Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / structure / ResourcesTest.php
bloba3fed843f4eb7518570f8de974cf88609510f65b
1 <?php
3 use JsonSchema\Validator;
4 use MediaWiki\MainConfigNames;
5 use MediaWiki\MediaWikiServices;
6 use MediaWiki\Request\FauxRequest;
7 use MediaWiki\ResourceLoader as RL;
8 use Wikimedia\Minify\CSSMin;
9 use Wikimedia\TestingAccessWrapper;
11 /**
12 * Checks for making sure registered resources are sensible.
14 * @author Antoine Musso
15 * @author Niklas Laxström
16 * @author Santhosh Thottingal
17 * @copyright © 2012, Antoine Musso
18 * @copyright © 2012, Niklas Laxström
19 * @copyright © 2012, Santhosh Thottingal
21 * @coversNothing
22 * @group Database
24 class ResourcesTest extends MediaWikiIntegrationTestCase {
26 public function testStyleMedia() {
27 foreach ( self::provideMediaStylesheets() as [ $moduleName, $media, $filename, $css ] ) {
28 $cssText = CSSMin::minify( $css->cssText );
30 $this->assertStringNotContainsString(
31 '@media',
32 $cssText,
33 'Stylesheets should not both specify "media" and contain @media'
38 /**
39 * Verify that all modules specified as dependencies of other modules actually
40 * exist and are not illegal.
42 * @todo Modules can dynamically choose dependencies based on context. This method
43 * does not find all such variations.
45 public function testValidDependencies() {
46 $data = self::getAllModules();
47 $illegalDeps = [ 'startup' ];
48 // Can't depend on modules in the `noscript` group, find all such module names
49 // to add to $illegalDeps. See T291735
50 /** @var RL\Module $module */
51 foreach ( $data['modules'] as $moduleName => $module ) {
52 if ( $module->getGroup() === 'noscript' ) {
53 $illegalDeps[] = $moduleName;
57 // Avoid an assert for each module to keep the test fast.
58 // Instead, perform a single assertion against everything at once.
59 // When all is good, actual/expected are both empty arrays.
60 // When we find issues, add the violations to 'actual' and add an empty
61 // key to 'expected'. These keys in expected are because the PHPUnit diff
62 // (as of 6.5) only goes one level deep.
63 $actualUnknown = [];
64 $expectedUnknown = [];
65 $actualIllegal = [];
66 $expectedIllegal = [];
68 /** @var RL\Module $module */
69 foreach ( $data['modules'] as $moduleName => $module ) {
70 foreach ( $module->getDependencies( $data['context'] ) as $dep ) {
71 if ( !isset( $data['modules'][$dep] ) ) {
72 $actualUnknown[$moduleName][] = $dep;
73 $expectedUnknown[$moduleName] = [];
75 if ( in_array( $dep, $illegalDeps, true ) ) {
76 $actualIllegal[$moduleName][] = $dep;
77 $expectedIllegal[$moduleName] = [];
81 $this->assertEquals( $expectedUnknown, $actualUnknown, 'Dependencies that do not exist' );
82 $this->assertEquals( $expectedIllegal, $actualIllegal, 'Dependencies that are not legal' );
85 public function testSchema() {
86 $data = include __DIR__ . '/../../../resources/Resources.php';
87 $schemaPath = __DIR__ . '/../../../docs/extension.schema.v2.json';
89 // Replace inline functions with fake callables
90 array_walk_recursive( $data, static function ( &$item, $key ) {
91 if ( $item instanceof Closure ) {
92 $item = 'Test::test';
94 } );
95 // Convert PHP associative arrays to stdClass objects recursively
96 $data = json_decode( json_encode( $data ) );
98 $validator = new Validator;
99 $validator->validate( $data, (object)[ '$ref' => 'file://' . $schemaPath . '#/properties/ResourceModules' ] );
101 $this->assertEquals(
103 $validator->getErrors(),
104 'Found errors when validating Resources.php against the ResourceModules schema: ' .
105 json_encode( $validator->getErrors(), JSON_PRETTY_PRINT )
110 * Verify that all specified messages actually exist.
112 public function testMissingMessages() {
113 $data = self::getAllModules();
114 $lang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( 'en' );
116 /** @var RL\Module $module */
117 foreach ( $data['modules'] as $moduleName => $module ) {
118 foreach ( $module->getMessages() as $msgKey ) {
119 $this->assertTrue(
120 wfMessage( $msgKey )->useDatabase( false )->inLanguage( $lang )->exists(),
121 "Message '$msgKey' required by '$moduleName' must exist"
128 * Get all registered modules from ResouceLoader.
129 * @return array
131 protected static function getAllModules() {
132 global $wgEnableJavaScriptTest;
134 // Test existance of test suite files as well
135 // (can't use setUp or setMwGlobals because providers are static)
136 $org_wgEnableJavaScriptTest = $wgEnableJavaScriptTest;
137 $wgEnableJavaScriptTest = true;
139 // Get main ResourceLoader
140 $rl = MediaWikiServices::getInstance()->getResourceLoader();
142 $modules = [];
144 foreach ( $rl->getModuleNames() as $moduleName ) {
145 $modules[$moduleName] = $rl->getModule( $moduleName );
148 // Restore settings
149 $wgEnableJavaScriptTest = $org_wgEnableJavaScriptTest;
151 return [
152 'modules' => $modules,
153 'resourceloader' => $rl,
154 'context' => new RL\Context( $rl, new FauxRequest() )
159 * Get all stylesheet files from modules that are an instance of
160 * RL\FileModule (or one of its subclasses).
162 public static function provideMediaStylesheets() {
163 $data = self::getAllModules();
164 $context = $data['context'];
166 foreach ( $data['modules'] as $moduleName => $module ) {
167 if ( !$module instanceof RL\FileModule ) {
168 continue;
171 $moduleProxy = TestingAccessWrapper::newFromObject( $module );
173 $styleFiles = $moduleProxy->getStyleFiles( $context );
175 foreach ( $styleFiles as $media => $files ) {
176 if ( $media && $media !== 'all' ) {
177 foreach ( $files as $file ) {
178 yield [
179 $moduleName,
180 $media,
181 $file,
182 // XXX: Wrapped in an object to keep it out of PHPUnit output
183 (object)[
184 'cssText' => $moduleProxy->readStyleFile( $file, $context )
194 * Check all resource files from RL\FileModule modules.
196 public function testResourceFiles() {
197 $this->overrideConfigValues( [
198 MainConfigNames::Logo => false,
199 MainConfigNames::Logos => [],
200 ] );
202 $data = self::getAllModules();
204 // See also RL\FileModule::__construct
205 $filePathProps = [
206 // Lists of file paths
207 'lists' => [
208 'scripts',
209 'debugScripts',
210 'styles',
211 'packageFiles',
214 // Collated lists of file paths
215 'nested-lists' => [
216 'languageScripts',
217 'skinScripts',
218 'skinStyles',
222 foreach ( $data['modules'] as $moduleName => $module ) {
223 if ( !$module instanceof RL\FileModule ) {
224 continue;
227 $moduleProxy = TestingAccessWrapper::newFromObject( $module );
229 $files = [];
231 foreach ( $filePathProps['lists'] as $propName ) {
232 $list = $moduleProxy->$propName;
233 if ( $list === null ) {
234 continue;
236 foreach ( $list as $key => $value ) {
237 // 'scripts' are numeral arrays.
238 // 'styles' can be numeral or associative.
239 // In case of associative the key is the file path
240 // and the value is the 'media' attribute.
241 if ( is_int( $key ) ) {
242 $files[] = $value;
243 } else {
244 $files[] = $key;
249 foreach ( $filePathProps['nested-lists'] as $propName ) {
250 $lists = $moduleProxy->$propName;
251 foreach ( $lists as $list ) {
252 foreach ( $list as $key => $value ) {
253 // We need the same filter as for 'lists',
254 // due to 'skinStyles'.
255 if ( is_int( $key ) ) {
256 $files[] = $value;
257 } else {
258 $files[] = $key;
264 foreach ( $files as $key => $file ) {
265 $fileInfo = $moduleProxy->expandFileInfo( $data['context'], $file, "files[$key]" );
266 if ( !isset( $fileInfo['filePath'] ) ) {
267 continue;
269 $relativePath = $fileInfo['filePath']->getPath();
270 $localPath = $fileInfo['filePath']->getLocalPath();
271 $this->assertFileExists(
272 $localPath,
273 "File '$relativePath' referenced by '$moduleName' must exist."
277 // To populate missingLocalFileRefs. Not sure how sensible this is inside this test...
278 $moduleProxy->readStyleFiles(
279 $module->getStyleFiles( $data['context'] ),
280 $data['context']
283 $missingLocalFileRefs = $moduleProxy->missingLocalFileRefs;
285 foreach ( $missingLocalFileRefs as $file ) {
286 $this->assertFileExists(
287 $file,
288 "File '$file' referenced by '$moduleName' must exist."
295 * Check all image files from RL\ImageModule modules.
297 public function testImageFiles() {
298 $data = self::getAllModules();
300 foreach ( $data['modules'] as $moduleName => $module ) {
301 if ( !$module instanceof RL\ImageModule ) {
302 continue;
305 $imagesFiles = $module->getImages( $data['context'] );
306 foreach ( $imagesFiles as $file ) {
307 $relativePath = $file->getName();
308 $this->assertFileExists(
309 $file->getPath( $data['context'] ),
310 "File '$relativePath' referenced by '$moduleName' must exist."
316 public static function provideRespond() {
317 $services = MediaWikiServices::getInstance();
318 $rl = $services->getResourceLoader();
319 $skinFactory = $services->getSkinFactory();
320 foreach ( array_keys( $skinFactory->getInstalledSkins() ) as $skin ) {
321 foreach ( $rl->getModuleNames() as $moduleName ) {
322 yield [ $moduleName, $skin ];
328 * @dataProvider provideRespond
329 * @param string $moduleName
330 * @param string $skin
332 public function testRespond( $moduleName, $skin ) {
333 $rl = $this->getServiceContainer()->getResourceLoader();
334 $module = $rl->getModule( $moduleName );
335 if ( $module->shouldSkipStructureTest() ) {
336 // Private modules cannot be served from load.php
337 $this->assertTrue( true );
338 return;
340 // Test only general (scripts) or only=styles responses.
341 $only = $module->getType() === RL\Module::LOAD_STYLES ? 'styles' : null;
342 $context = new RL\Context(
343 $rl,
344 new FauxRequest( [ 'modules' => $moduleName, 'only' => $only, 'skin' => $skin ] )
346 ob_start();
347 $rl->respond( $context );
348 ob_end_clean();
349 $this->assertSame( [], $rl->getErrors() );