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