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