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
;
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
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(
33 'Stylesheets should not both specify "media" and contain @media'
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.
64 $expectedUnknown = [];
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
) {
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' ] );
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 ) {
120 wfMessage( $msgKey )->useDatabase( false )->inLanguage( $lang )->exists(),
121 "Message '$msgKey' required by '$moduleName' must exist"
128 * Get all registered modules from ResouceLoader.
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();
144 foreach ( $rl->getModuleNames() as $moduleName ) {
145 $modules[$moduleName] = $rl->getModule( $moduleName );
149 $wgEnableJavaScriptTest = $org_wgEnableJavaScriptTest;
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
) {
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 ) {
182 // XXX: Wrapped in an object to keep it out of PHPUnit output
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
=> [],
202 $data = self
::getAllModules();
204 // See also RL\FileModule::__construct
206 // Lists of file paths
214 // Collated lists of file paths
222 foreach ( $data['modules'] as $moduleName => $module ) {
223 if ( !$module instanceof RL\FileModule
) {
227 $moduleProxy = TestingAccessWrapper
::newFromObject( $module );
231 foreach ( $filePathProps['lists'] as $propName ) {
232 $list = $moduleProxy->$propName;
233 if ( $list === null ) {
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 ) ) {
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 ) ) {
264 foreach ( $files as $key => $file ) {
265 $fileInfo = $moduleProxy->expandFileInfo( $data['context'], $file, "files[$key]" );
266 if ( !isset( $fileInfo['filePath'] ) ) {
269 $relativePath = $fileInfo['filePath']->getPath();
270 $localPath = $fileInfo['filePath']->getLocalPath();
271 $this->assertFileExists(
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'] ),
283 $missingLocalFileRefs = $moduleProxy->missingLocalFileRefs
;
285 foreach ( $missingLocalFileRefs as $file ) {
286 $this->assertFileExists(
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
) {
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 );
340 // Test only general (scripts) or only=styles responses.
341 $only = $module->getType() === RL\Module
::LOAD_STYLES ?
'styles' : null;
342 $context = new RL\
Context(
344 new FauxRequest( [ 'modules' => $moduleName, 'only' => $only, 'skin' => $skin ] )
347 $rl->respond( $context );
349 $this->assertSame( [], $rl->getErrors() );