ResourcesTest.php 9.4 KB

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