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