less-test.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. /*jshint latedef: nofunc */
  2. module.exports = function() {
  3. var path = require('path'),
  4. fs = require('fs'),
  5. copyBom = require('./copy-bom')(),
  6. doBomTest = false;
  7. var less = require('../lib/less-node');
  8. var stylize = require('../lib/less-node/lessc-helper').stylize;
  9. var globals = Object.keys(global);
  10. var oneTestOnly = process.argv[2],
  11. isFinished = false;
  12. var isVerbose = process.env.npm_config_loglevel === 'verbose';
  13. var normalFolder = 'test/less';
  14. var bomFolder = 'test/less-bom';
  15. less.logger.addListener({
  16. info: function(msg) {
  17. if (isVerbose) {
  18. process.stdout.write(msg + "\n");
  19. }
  20. },
  21. warn: function(msg) {
  22. process.stdout.write(msg + "\n");
  23. },
  24. error: function(msg) {
  25. process.stdout.write(msg + "\n");
  26. }
  27. });
  28. var queueList = [],
  29. queueRunning = false;
  30. function queue(func) {
  31. if (queueRunning) {
  32. //console.log("adding to queue");
  33. queueList.push(func);
  34. } else {
  35. //console.log("first in queue - starting");
  36. queueRunning = true;
  37. func();
  38. }
  39. }
  40. function release() {
  41. if (queueList.length) {
  42. //console.log("running next in queue");
  43. var func = queueList.shift();
  44. setTimeout(func, 0);
  45. } else {
  46. //console.log("stopping queue");
  47. queueRunning = false;
  48. }
  49. }
  50. var totalTests = 0,
  51. failedTests = 0,
  52. passedTests = 0;
  53. less.functions.functionRegistry.addMultiple({
  54. add: function (a, b) {
  55. return new(less.tree.Dimension)(a.value + b.value);
  56. },
  57. increment: function (a) {
  58. return new(less.tree.Dimension)(a.value + 1);
  59. },
  60. _color: function (str) {
  61. if (str.value === "evil red") { return new(less.tree.Color)("600"); }
  62. }
  63. });
  64. function testSourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
  65. fs.readFile(path.join('test/', name) + '.json', 'utf8', function (e, expectedSourcemap) {
  66. process.stdout.write("- " + path.join(baseFolder, name) + ": ");
  67. if (sourcemap === expectedSourcemap) {
  68. ok('OK');
  69. } else if (err) {
  70. fail("ERROR: " + (err && err.message));
  71. if (isVerbose) {
  72. process.stdout.write("\n");
  73. process.stdout.write(err.stack + "\n");
  74. }
  75. } else {
  76. difference("FAIL", expectedSourcemap, sourcemap);
  77. }
  78. });
  79. }
  80. function testEmptySourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
  81. process.stdout.write("- " + path.join(baseFolder, name) + ": ");
  82. if (err) {
  83. fail("ERROR: " + (err && err.message));
  84. } else {
  85. var expectedSourcemap = undefined;
  86. if ( compiledLess !== "" ) {
  87. difference("\nCompiledLess must be empty", "", compiledLess);
  88. } else if (sourcemap !== expectedSourcemap) {
  89. fail("Sourcemap must be undefined");
  90. } else {
  91. ok('OK');
  92. }
  93. }
  94. }
  95. function testErrors(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
  96. fs.readFile(path.join(baseFolder, name) + '.txt', 'utf8', function (e, expectedErr) {
  97. process.stdout.write("- " + path.join(baseFolder, name) + ": ");
  98. expectedErr = doReplacements(expectedErr, baseFolder);
  99. if (!err) {
  100. if (compiledLess) {
  101. fail("No Error", 'red');
  102. } else {
  103. fail("No Error, No Output");
  104. }
  105. } else {
  106. var errMessage = less.formatError(err);
  107. if (errMessage === expectedErr) {
  108. ok('OK');
  109. } else {
  110. difference("FAIL", expectedErr, errMessage);
  111. }
  112. }
  113. });
  114. }
  115. function globalReplacements(input, directory) {
  116. var p = path.join(process.cwd(), directory),
  117. pathimport = path.join(process.cwd(), directory + "import/"),
  118. pathesc = p.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }),
  119. pathimportesc = pathimport.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); });
  120. return input.replace(/\{path\}/g, p)
  121. .replace(/\{pathesc\}/g, pathesc)
  122. .replace(/\{pathimport\}/g, pathimport)
  123. .replace(/\{pathimportesc\}/g, pathimportesc)
  124. .replace(/\r\n/g, '\n');
  125. }
  126. function checkGlobalLeaks() {
  127. return Object.keys(global).filter(function(v) {
  128. return globals.indexOf(v) < 0;
  129. });
  130. }
  131. function testSyncronous(options, filenameNoExtension) {
  132. if (oneTestOnly && ("Test Sync " + filenameNoExtension) !== oneTestOnly) {
  133. return;
  134. }
  135. totalTests++;
  136. queue(function() {
  137. var isSync = true;
  138. toCSS(options, path.join(normalFolder, filenameNoExtension + ".less"), function (err, result) {
  139. process.stdout.write("- Test Sync " + filenameNoExtension + ": ");
  140. if (isSync) {
  141. ok("OK");
  142. } else {
  143. fail("Not Sync");
  144. }
  145. release();
  146. });
  147. isSync = false;
  148. });
  149. }
  150. function prepBomTest() {
  151. copyBom.copyFolderWithBom(normalFolder, bomFolder);
  152. doBomTest = true;
  153. }
  154. function runTestSet(options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) {
  155. var options2 = options ? JSON.parse(JSON.stringify(options)) : {};
  156. runTestSetInternal(normalFolder, options, foldername, verifyFunction, nameModifier, doReplacements, getFilename);
  157. if (doBomTest) {
  158. runTestSetInternal(bomFolder, options2, foldername, verifyFunction, nameModifier, doReplacements, getFilename);
  159. }
  160. }
  161. function runTestSetNormalOnly(options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) {
  162. runTestSetInternal(normalFolder, options, foldername, verifyFunction, nameModifier, doReplacements, getFilename);
  163. }
  164. function runTestSetInternal(baseFolder, options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) {
  165. foldername = foldername || "";
  166. if (!doReplacements) {
  167. doReplacements = globalReplacements;
  168. }
  169. function getBasename(file) {
  170. return foldername + path.basename(file, '.less');
  171. }
  172. fs.readdirSync(path.join(baseFolder, foldername)).forEach(function (file) {
  173. if (! /\.less/.test(file)) { return; }
  174. var name = getBasename(file);
  175. if (oneTestOnly && name !== oneTestOnly) {
  176. return;
  177. }
  178. totalTests++;
  179. if (options.sourceMap && !options.sourceMap.sourceMapFileInline) {
  180. options.sourceMapOutputFilename = name + ".css";
  181. options.sourceMapBasepath = path.join(process.cwd(), baseFolder);
  182. options.sourceMapRootpath = "testweb/";
  183. // TODO separate options?
  184. options.sourceMap = options;
  185. }
  186. options.getVars = function(file) {
  187. return JSON.parse(fs.readFileSync(getFilename(getBasename(file), 'vars', baseFolder), 'utf8'));
  188. };
  189. var doubleCallCheck = false;
  190. queue(function() {
  191. toCSS(options, path.join(baseFolder, foldername + file), function (err, result) {
  192. if (doubleCallCheck) {
  193. totalTests++;
  194. fail("less is calling back twice");
  195. process.stdout.write(doubleCallCheck + "\n");
  196. process.stdout.write((new Error()).stack + "\n");
  197. return;
  198. }
  199. doubleCallCheck = (new Error()).stack;
  200. if (verifyFunction) {
  201. var verificationResult = verifyFunction(name, err, result && result.css, doReplacements, result && result.map, baseFolder);
  202. release();
  203. return verificationResult;
  204. }
  205. if (err) {
  206. fail("ERROR: " + (err && err.message));
  207. if (isVerbose) {
  208. process.stdout.write("\n");
  209. if (err.stack) {
  210. process.stdout.write(err.stack + "\n");
  211. } else {
  212. //this sometimes happen - show the whole error object
  213. console.log(err);
  214. }
  215. }
  216. release();
  217. return;
  218. }
  219. var css_name = name;
  220. if (nameModifier) { css_name = nameModifier(name); }
  221. fs.readFile(path.join('test/css', css_name) + '.css', 'utf8', function (e, css) {
  222. process.stdout.write("- " + path.join(baseFolder, css_name) + ": ");
  223. css = css && doReplacements(css, path.join(baseFolder, foldername));
  224. if (result.css === css) { ok('OK'); }
  225. else {
  226. difference("FAIL", css, result.css);
  227. }
  228. release();
  229. });
  230. });
  231. });
  232. });
  233. }
  234. function diff(left, right) {
  235. require('diff').diffLines(left, right).forEach(function(item) {
  236. if (item.added || item.removed) {
  237. var text = item.value && item.value.replace("\n", String.fromCharCode(182) + "\n").replace('\ufeff', '[[BOM]]');
  238. process.stdout.write(stylize(text, item.added ? 'green' : 'red'));
  239. } else {
  240. process.stdout.write(item.value && item.value.replace('\ufeff', '[[BOM]]'));
  241. }
  242. });
  243. process.stdout.write("\n");
  244. }
  245. function fail(msg) {
  246. process.stdout.write(stylize(msg, 'red') + "\n");
  247. failedTests++;
  248. endTest();
  249. }
  250. function difference(msg, left, right) {
  251. process.stdout.write(stylize(msg, 'yellow') + "\n");
  252. failedTests++;
  253. diff(left, right);
  254. endTest();
  255. }
  256. function ok(msg) {
  257. process.stdout.write(stylize(msg, 'green') + "\n");
  258. passedTests++;
  259. endTest();
  260. }
  261. function finished() {
  262. isFinished = true;
  263. endTest();
  264. }
  265. function endTest() {
  266. if (isFinished && ((failedTests + passedTests) >= totalTests)) {
  267. var leaked = checkGlobalLeaks();
  268. process.stdout.write("\n");
  269. if (failedTests > 0) {
  270. process.stdout.write(failedTests + stylize(" Failed", "red") + ", " + passedTests + " passed\n");
  271. } else {
  272. process.stdout.write(stylize("All Passed ", "green") + passedTests + " run\n");
  273. }
  274. if (leaked.length > 0) {
  275. process.stdout.write("\n");
  276. process.stdout.write(stylize("Global leak detected: ", "red") + leaked.join(', ') + "\n");
  277. }
  278. if (leaked.length || failedTests) {
  279. process.on('exit', function() { process.reallyExit(1); });
  280. }
  281. }
  282. }
  283. function contains(fullArray, obj) {
  284. for (var i = 0; i < fullArray.length; i++) {
  285. if (fullArray[i] === obj) {
  286. return true;
  287. }
  288. }
  289. return false;
  290. }
  291. function toCSS(options, path, callback) {
  292. options = options || {};
  293. var str = fs.readFileSync(path, 'utf8'), addPath = require('path').dirname(path);
  294. if (typeof options.paths !== "string") {
  295. options.paths = options.paths || [];
  296. if (!contains(options.paths, addPath)) {
  297. options.paths.push(addPath);
  298. }
  299. }
  300. options.filename = require('path').resolve(process.cwd(), path);
  301. options.optimization = options.optimization || 0;
  302. if (options.globalVars) {
  303. options.globalVars = options.getVars(path);
  304. } else if (options.modifyVars) {
  305. options.modifyVars = options.getVars(path);
  306. }
  307. if (options.plugin) {
  308. var Plugin = require(require('path').resolve(process.cwd(), options.plugin));
  309. options.plugins = [Plugin];
  310. }
  311. less.render(str, options, callback);
  312. }
  313. function testNoOptions() {
  314. if (oneTestOnly && "Integration" !== oneTestOnly) {
  315. return;
  316. }
  317. totalTests++;
  318. try {
  319. process.stdout.write("- Integration - creating parser without options: ");
  320. less.render("");
  321. } catch(e) {
  322. fail(stylize("FAIL\n", "red"));
  323. return;
  324. }
  325. ok(stylize("OK\n", "green"));
  326. }
  327. return {
  328. runTestSet: runTestSet,
  329. runTestSetNormalOnly: runTestSetNormalOnly,
  330. testSyncronous: testSyncronous,
  331. testErrors: testErrors,
  332. testSourcemap: testSourcemap,
  333. testEmptySourcemap: testEmptySourcemap,
  334. testNoOptions: testNoOptions,
  335. prepBomTest: prepBomTest,
  336. finished: finished
  337. };
  338. };