ResourcesTest: Detect missing files in url(...) expressions
[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( 8, 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 = array( '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 dependencies of all modules are always satisfiable with the 'targets' defined
91 * for the involved modules.
92 *
93 * Example: A depends on B. A has targets: mobile, desktop. B has targets: desktop. Therefore the
94 * dependency is sometimes unsatisfiable: it's impossible to load module A on mobile.
95 */
96 public function testUnsatisfiableDependencies() {
97 $data = self::getAllModules();
98 $validDeps = array_keys( $data['modules'] );
99
100 /** @var ResourceLoaderModule $module */
101 foreach ( $data['modules'] as $moduleName => $module ) {
102 $moduleTargets = $module->getTargets();
103 foreach ( $module->getDependencies( $data['context'] ) as $dep ) {
104 if ( !isset( $data['modules'][$dep] ) ) {
105 // Missing dependencies reported by testMissingDependencies
106 continue;
107 }
108 $targets = $data['modules'][$dep]->getTargets();
109 foreach ( $moduleTargets as $moduleTarget ) {
110 $this->assertContains(
111 $moduleTarget,
112 $targets,
113 "The module '$moduleName' must not have target '$moduleTarget' "
114 . "because its dependency '$dep' does not have it"
115 );
116 }
117 }
118 }
119 }
120
121 /**
122 * Get all registered modules from ResouceLoader.
123 * @return array
124 */
125 protected static function getAllModules() {
126 global $wgEnableJavaScriptTest;
127
128 // Test existance of test suite files as well
129 // (can't use setUp or setMwGlobals because providers are static)
130 $org_wgEnableJavaScriptTest = $wgEnableJavaScriptTest;
131 $wgEnableJavaScriptTest = true;
132
133 // Initialize ResourceLoader
134 $rl = new ResourceLoader();
135
136 $modules = array();
137
138 foreach ( $rl->getModuleNames() as $moduleName ) {
139 $modules[$moduleName] = $rl->getModule( $moduleName );
140 }
141
142 // Restore settings
143 $wgEnableJavaScriptTest = $org_wgEnableJavaScriptTest;
144
145 return array(
146 'modules' => $modules,
147 'resourceloader' => $rl,
148 'context' => new ResourceLoaderContext( $rl, new FauxRequest() )
149 );
150 }
151
152 /**
153 * Get all stylesheet files from modules that are an instance of
154 * ResourceLoaderFileModule (or one of its subclasses).
155 */
156 public static function provideMediaStylesheets() {
157 $data = self::getAllModules();
158 $cases = array();
159
160 foreach ( $data['modules'] as $moduleName => $module ) {
161 if ( !$module instanceof ResourceLoaderFileModule ) {
162 continue;
163 }
164
165 $reflectedModule = new ReflectionObject( $module );
166
167 $getStyleFiles = $reflectedModule->getMethod( 'getStyleFiles' );
168 $getStyleFiles->setAccessible( true );
169
170 $readStyleFile = $reflectedModule->getMethod( 'readStyleFile' );
171 $readStyleFile->setAccessible( true );
172
173 $styleFiles = $getStyleFiles->invoke( $module, $data['context'] );
174
175 $flip = $module->getFlip( $data['context'] );
176
177 foreach ( $styleFiles as $media => $files ) {
178 if ( $media && $media !== 'all' ) {
179 foreach ( $files as $file ) {
180 $cases[] = array(
181 $moduleName,
182 $media,
183 $file,
184 // XXX: Wrapped in an object to keep it out of PHPUnit output
185 (object)array( 'cssText' => $readStyleFile->invoke( $module, $file, $flip ) ),
186 );
187 }
188 }
189 }
190 }
191
192 return $cases;
193 }
194
195 /**
196 * Get all resource files from modules that are an instance of
197 * ResourceLoaderFileModule (or one of its subclasses).
198 *
199 * Since the raw data is stored in protected properties, we have to
200 * overrride this through ReflectionObject methods.
201 */
202 public static function provideResourceFiles() {
203 $data = self::getAllModules();
204 $cases = array();
205
206 // See also ResourceLoaderFileModule::__construct
207 $filePathProps = array(
208 // Lists of file paths
209 'lists' => array(
210 'scripts',
211 'debugScripts',
212 'loaderScripts',
213 'styles',
214 ),
215
216 // Collated lists of file paths
217 'nested-lists' => array(
218 'languageScripts',
219 'skinScripts',
220 'skinStyles',
221 ),
222 );
223
224 foreach ( $data['modules'] as $moduleName => $module ) {
225 if ( !$module instanceof ResourceLoaderFileModule ) {
226 continue;
227 }
228
229 $reflectedModule = new ReflectionObject( $module );
230
231 $files = array();
232
233 foreach ( $filePathProps['lists'] as $propName ) {
234 $property = $reflectedModule->getProperty( $propName );
235 $property->setAccessible( true );
236 $list = $property->getValue( $module );
237 foreach ( $list as $key => $value ) {
238 // 'scripts' are numeral arrays.
239 // 'styles' can be numeral or associative.
240 // In case of associative the key is the file path
241 // and the value is the 'media' attribute.
242 if ( is_int( $key ) ) {
243 $files[] = $value;
244 } else {
245 $files[] = $key;
246 }
247 }
248 }
249
250 foreach ( $filePathProps['nested-lists'] as $propName ) {
251 $property = $reflectedModule->getProperty( $propName );
252 $property->setAccessible( true );
253 $lists = $property->getValue( $module );
254 foreach ( $lists as $list ) {
255 foreach ( $list as $key => $value ) {
256 // We need the same filter as for 'lists',
257 // due to 'skinStyles'.
258 if ( is_int( $key ) ) {
259 $files[] = $value;
260 } else {
261 $files[] = $key;
262 }
263 }
264 }
265 }
266
267 // Get method for resolving the paths to full paths
268 $method = $reflectedModule->getMethod( 'getLocalPath' );
269 $method->setAccessible( true );
270
271 // Populate cases
272 foreach ( $files as $file ) {
273 $cases[] = array(
274 $method->invoke( $module, $file ),
275 $moduleName,
276 ( $file instanceof ResourceLoaderFilePath ? $file->getPath() : $file ),
277 );
278 }
279
280 // To populate missingLocalFileRefs. Not sure how sane this is inside this test...
281 $module->readStyleFiles(
282 $module->getStyleFiles( $data['context'] ),
283 $module->getFlip( $data['context'] ),
284 $data['context']
285 );
286
287 $property = $reflectedModule->getProperty( 'missingLocalFileRefs' );
288 $property->setAccessible( true );
289 $missingLocalFileRefs = $property->getValue( $module );
290
291 foreach ( $missingLocalFileRefs as $file ) {
292 $cases[] = array(
293 $file,
294 $moduleName,
295 $file,
296 );
297 }
298 }
299
300 return $cases;
301 }
302 }