Merge "Allow partially blocked users to import images"
[lhc/web/wiklou.git] / tests / phpunit / structure / ResourcesTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use Wikimedia\TestingAccessWrapper;
5
6 /**
7 * Sanity checks for making sure registered resources are sane.
8 *
9 * @author Antoine Musso
10 * @author Niklas Laxström
11 * @author Santhosh Thottingal
12 * @copyright © 2012, Antoine Musso
13 * @copyright © 2012, Niklas Laxström
14 * @copyright © 2012, Santhosh Thottingal
15 */
16 class ResourcesTest extends MediaWikiTestCase {
17
18 /**
19 * @dataProvider provideResourceFiles
20 */
21 public function testFileExistence( $filename, $module, $resource ) {
22 $this->assertFileExists( $filename,
23 "File '$resource' referenced by '$module' must exist."
24 );
25 }
26
27 /**
28 * @dataProvider provideMediaStylesheets
29 */
30 public function testStyleMedia( $moduleName, $media, $filename, $css ) {
31 $cssText = CSSMin::minify( $css->cssText );
32
33 $this->assertTrue(
34 strpos( $cssText, '@media' ) === false,
35 'Stylesheets should not both specify "media" and contain @media'
36 );
37 }
38
39 /**
40 * Verify that all modules specified as dependencies of other modules actually
41 * exist and are not illegal.
42 *
43 * @todo Modules can dynamically choose dependencies based on context. This method
44 * does not find all such variations. The same applies to testUnsatisfiableDependencies().
45 */
46 public function testValidDependencies() {
47 $data = self::getAllModules();
48 $knownDeps = array_keys( $data['modules'] );
49 $illegalDeps = [ 'startup' ];
50
51 // Avoid an assert for each module to keep the test fast.
52 // Instead, perform a single assertion against everything at once.
53 // When all is good, actual/expected are both empty arrays.
54 // When we find issues, add the violations to 'actual' and add an empty
55 // key to 'expected'. These keys in expected are because the PHPUnit diff
56 // (as of 6.5) only goes one level deep.
57 $actualUnknown = [];
58 $expectedUnknown = [];
59 $actualIllegal = [];
60 $expectedIllegal = [];
61
62 /** @var ResourceLoaderModule $module */
63 foreach ( $data['modules'] as $moduleName => $module ) {
64 foreach ( $module->getDependencies( $data['context'] ) as $dep ) {
65 if ( !in_array( $dep, $knownDeps, true ) ) {
66 $actualUnknown[$moduleName][] = $dep;
67 $expectedUnknown[$moduleName] = [];
68 }
69 if ( in_array( $dep, $illegalDeps, true ) ) {
70 $actualIllegal[$moduleName][] = $dep;
71 $expectedIllegal[$moduleName] = [];
72 }
73 }
74 }
75 $this->assertEquals( $expectedUnknown, $actualUnknown, 'Dependencies that do not exist' );
76 $this->assertEquals( $expectedIllegal, $actualIllegal, 'Dependencies that are not legal' );
77 }
78
79 /**
80 * Verify that all specified messages actually exist.
81 */
82 public function testMissingMessages() {
83 $data = self::getAllModules();
84 $lang = Language::factory( 'en' );
85
86 /** @var ResourceLoaderModule $module */
87 foreach ( $data['modules'] as $moduleName => $module ) {
88 foreach ( $module->getMessages() as $msgKey ) {
89 $this->assertTrue(
90 wfMessage( $msgKey )->useDatabase( false )->inLanguage( $lang )->exists(),
91 "Message '$msgKey' required by '$moduleName' must exist"
92 );
93 }
94 }
95 }
96
97 /**
98 * Verify that all dependencies of all modules are always satisfiable with the 'targets' defined
99 * for the involved modules.
100 *
101 * Example: A depends on B. A has targets: mobile, desktop. B has targets: desktop. Therefore the
102 * dependency is sometimes unsatisfiable: it's impossible to load module A on mobile.
103 */
104 public function testUnsatisfiableDependencies() {
105 $data = self::getAllModules();
106
107 /** @var ResourceLoaderModule $module */
108 foreach ( $data['modules'] as $moduleName => $module ) {
109 $moduleTargets = $module->getTargets();
110 foreach ( $module->getDependencies( $data['context'] ) as $dep ) {
111 if ( !isset( $data['modules'][$dep] ) ) {
112 // Missing dependencies reported by testMissingDependencies
113 continue;
114 }
115 $targets = $data['modules'][$dep]->getTargets();
116 foreach ( $moduleTargets as $moduleTarget ) {
117 $this->assertContains(
118 $moduleTarget,
119 $targets,
120 "The module '$moduleName' must not have target '$moduleTarget' "
121 . "because its dependency '$dep' does not have it"
122 );
123 }
124 }
125 }
126 }
127
128 /**
129 * CSSMin::getLocalFileReferences should ignore url(...) expressions
130 * that have been commented out.
131 */
132 public function testCommentedLocalFileReferences() {
133 $basepath = __DIR__ . '/../data/css/';
134 $css = file_get_contents( $basepath . 'comments.css' );
135 $files = CSSMin::getLocalFileReferences( $css, $basepath );
136 $expected = [ $basepath . 'not-commented.gif' ];
137 $this->assertSame(
138 $expected,
139 $files,
140 'Url(...) expression in comment should be omitted.'
141 );
142 }
143
144 /**
145 * Get all registered modules from ResouceLoader.
146 * @return array
147 */
148 protected static function getAllModules() {
149 global $wgEnableJavaScriptTest;
150
151 // Test existance of test suite files as well
152 // (can't use setUp or setMwGlobals because providers are static)
153 $org_wgEnableJavaScriptTest = $wgEnableJavaScriptTest;
154 $wgEnableJavaScriptTest = true;
155
156 // Get main ResourceLoader
157 $rl = MediaWikiServices::getInstance()->getResourceLoader();
158
159 $modules = [];
160
161 foreach ( $rl->getModuleNames() as $moduleName ) {
162 $modules[$moduleName] = $rl->getModule( $moduleName );
163 }
164
165 // Restore settings
166 $wgEnableJavaScriptTest = $org_wgEnableJavaScriptTest;
167
168 return [
169 'modules' => $modules,
170 'resourceloader' => $rl,
171 'context' => new ResourceLoaderContext( $rl, new FauxRequest() )
172 ];
173 }
174
175 /**
176 * Get all stylesheet files from modules that are an instance of
177 * ResourceLoaderFileModule (or one of its subclasses).
178 */
179 public static function provideMediaStylesheets() {
180 $data = self::getAllModules();
181 $cases = [];
182
183 foreach ( $data['modules'] as $moduleName => $module ) {
184 if ( !$module instanceof ResourceLoaderFileModule ) {
185 continue;
186 }
187
188 $reflectedModule = new ReflectionObject( $module );
189
190 $getStyleFiles = $reflectedModule->getMethod( 'getStyleFiles' );
191 $getStyleFiles->setAccessible( true );
192
193 $readStyleFile = $reflectedModule->getMethod( 'readStyleFile' );
194 $readStyleFile->setAccessible( true );
195
196 $styleFiles = $getStyleFiles->invoke( $module, $data['context'] );
197
198 $flip = $module->getFlip( $data['context'] );
199
200 foreach ( $styleFiles as $media => $files ) {
201 if ( $media && $media !== 'all' ) {
202 foreach ( $files as $file ) {
203 $cases[] = [
204 $moduleName,
205 $media,
206 $file,
207 // XXX: Wrapped in an object to keep it out of PHPUnit output
208 (object)[
209 'cssText' => $readStyleFile->invoke(
210 $module,
211 $file,
212 $flip,
213 $data['context']
214 )
215 ],
216 ];
217 }
218 }
219 }
220 }
221
222 return $cases;
223 }
224
225 /**
226 * Get all resource files from modules that are an instance of
227 * ResourceLoaderFileModule (or one of its subclasses).
228 */
229 public static function provideResourceFiles() {
230 $data = self::getAllModules();
231 $cases = [];
232
233 // See also ResourceLoaderFileModule::__construct
234 $filePathProps = [
235 // Lists of file paths
236 'lists' => [
237 'scripts',
238 'debugScripts',
239 'styles',
240 ],
241
242 // Collated lists of file paths
243 'nested-lists' => [
244 'languageScripts',
245 'skinScripts',
246 'skinStyles',
247 ],
248 ];
249
250 foreach ( $data['modules'] as $moduleName => $module ) {
251 if ( !$module instanceof ResourceLoaderFileModule ) {
252 continue;
253 }
254
255 $moduleProxy = TestingAccessWrapper::newFromObject( $module );
256
257 $files = [];
258
259 foreach ( $filePathProps['lists'] as $propName ) {
260 $list = $moduleProxy->$propName;
261 foreach ( $list as $key => $value ) {
262 // 'scripts' are numeral arrays.
263 // 'styles' can be numeral or associative.
264 // In case of associative the key is the file path
265 // and the value is the 'media' attribute.
266 if ( is_int( $key ) ) {
267 $files[] = $value;
268 } else {
269 $files[] = $key;
270 }
271 }
272 }
273
274 foreach ( $filePathProps['nested-lists'] as $propName ) {
275 $lists = $moduleProxy->$propName;
276 foreach ( $lists as $list ) {
277 foreach ( $list as $key => $value ) {
278 // We need the same filter as for 'lists',
279 // due to 'skinStyles'.
280 if ( is_int( $key ) ) {
281 $files[] = $value;
282 } else {
283 $files[] = $key;
284 }
285 }
286 }
287 }
288
289 // Populate cases
290 foreach ( $files as $file ) {
291 $cases[] = [
292 $moduleProxy->getLocalPath( $file ),
293 $moduleName,
294 ( $file instanceof ResourceLoaderFilePath ? $file->getPath() : $file ),
295 ];
296 }
297
298 // To populate missingLocalFileRefs. Not sure how sane this is inside this test...
299 $moduleProxy->readStyleFiles(
300 $module->getStyleFiles( $data['context'] ),
301 $module->getFlip( $data['context'] ),
302 $data['context']
303 );
304
305 $missingLocalFileRefs = $moduleProxy->missingLocalFileRefs;
306
307 foreach ( $missingLocalFileRefs as $file ) {
308 $cases[] = [
309 $file,
310 $moduleName,
311 $file,
312 ];
313 }
314 }
315
316 return $cases;
317 }
318 }