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