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