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