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