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