api.js 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842
  1. /*eslint new-cap: ["error", {"capIsNewExceptions": ["Color"]}]*/
  2. var assert = require('assert'),
  3. fs = require('fs'),
  4. path = require('path'),
  5. read = fs.readFileSync,
  6. sassPath = process.env.NODESASS_COV
  7. ? require.resolve('../lib-cov')
  8. : require.resolve('../lib'),
  9. sass = require(sassPath),
  10. fixture = path.join.bind(null, __dirname, 'fixtures'),
  11. resolveFixture = path.resolve.bind(null, __dirname, 'fixtures');
  12. describe('api', function() {
  13. describe('.render(options, callback)', function() {
  14. beforeEach(function() {
  15. delete process.env.SASS_PATH;
  16. });
  17. it('should compile sass to css with file', function(done) {
  18. var expected = read(fixture('simple/expected.css'), 'utf8').trim();
  19. sass.render({
  20. file: fixture('simple/index.scss')
  21. }, function(error, result) {
  22. assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
  23. done();
  24. });
  25. });
  26. it('should compile sass to css with outFile set to absolute url', function(done) {
  27. sass.render({
  28. file: fixture('simple/index.scss'),
  29. sourceMap: true,
  30. outFile: fixture('simple/index-test.css')
  31. }, function(error, result) {
  32. assert.equal(JSON.parse(result.map).file, 'index-test.css');
  33. done();
  34. });
  35. });
  36. it('should compile sass to css with outFile set to relative url', function(done) {
  37. sass.render({
  38. file: fixture('simple/index.scss'),
  39. sourceMap: true,
  40. outFile: './index-test.css'
  41. }, function(error, result) {
  42. assert.equal(JSON.parse(result.map).file, 'index-test.css');
  43. done();
  44. });
  45. });
  46. it('should compile sass to css with outFile and sourceMap set to relative url', function(done) {
  47. sass.render({
  48. file: fixture('simple/index.scss'),
  49. sourceMap: './deep/nested/index.map',
  50. outFile: './index-test.css'
  51. }, function(error, result) {
  52. assert.equal(JSON.parse(result.map).file, '../../index-test.css');
  53. done();
  54. });
  55. });
  56. it('should compile generate map with sourceMapRoot pass-through option', function(done) {
  57. sass.render({
  58. file: fixture('simple/index.scss'),
  59. sourceMap: './deep/nested/index.map',
  60. sourceMapRoot: 'http://test.com/',
  61. outFile: './index-test.css'
  62. }, function(error, result) {
  63. assert.equal(JSON.parse(result.map).sourceRoot, 'http://test.com/');
  64. done();
  65. });
  66. });
  67. it('should compile sass to css with data', function(done) {
  68. var src = read(fixture('simple/index.scss'), 'utf8');
  69. var expected = read(fixture('simple/expected.css'), 'utf8').trim();
  70. sass.render({
  71. data: src
  72. }, function(error, result) {
  73. assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
  74. done();
  75. });
  76. });
  77. it('should NOT compile empty data string', function(done) {
  78. sass.render({
  79. data: ''
  80. }, function(error) {
  81. assert.equal(error.message, 'No input specified: provide a file name or a source string to process');
  82. done();
  83. });
  84. });
  85. it('should NOT compile without parameters', function(done) {
  86. sass.render({ }, function(error) {
  87. assert.equal(error.message, 'No input specified: provide a file name or a source string to process');
  88. done();
  89. });
  90. });
  91. it('should compile sass to css using indented syntax', function(done) {
  92. var src = read(fixture('indent/index.sass'), 'utf8');
  93. var expected = read(fixture('indent/expected.css'), 'utf8').trim();
  94. sass.render({
  95. data: src,
  96. indentedSyntax: true
  97. }, function(error, result) {
  98. assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
  99. done();
  100. });
  101. });
  102. it('should throw error status 1 for bad input', function(done) {
  103. sass.render({
  104. data: '#navbar width 80%;'
  105. }, function(error) {
  106. assert(error.message);
  107. assert.equal(error.status, 1);
  108. done();
  109. });
  110. });
  111. it('should compile with include paths', function(done) {
  112. var src = read(fixture('include-path/index.scss'), 'utf8');
  113. var expected = read(fixture('include-path/expected.css'), 'utf8').trim();
  114. sass.render({
  115. data: src,
  116. includePaths: [
  117. fixture('include-path/functions'),
  118. fixture('include-path/lib')
  119. ]
  120. }, function(error, result) {
  121. assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
  122. done();
  123. });
  124. });
  125. it('should check SASS_PATH in the specified order', function(done) {
  126. var src = read(fixture('sass-path/index.scss'), 'utf8');
  127. var expectedRed = read(fixture('sass-path/expected-red.css'), 'utf8').trim();
  128. var expectedOrange = read(fixture('sass-path/expected-orange.css'), 'utf8').trim();
  129. var envIncludes = [
  130. fixture('sass-path/red'),
  131. fixture('sass-path/orange')
  132. ];
  133. process.env.SASS_PATH = envIncludes.join(path.delimiter);
  134. sass.render({
  135. data: src,
  136. includePaths: []
  137. }, function(error, result) {
  138. assert.equal(result.css.toString().trim(), expectedRed.replace(/\r\n/g, '\n'));
  139. });
  140. process.env.SASS_PATH = envIncludes.reverse().join(path.delimiter);
  141. sass.render({
  142. data: src,
  143. includePaths: []
  144. }, function(error, result) {
  145. assert.equal(result.css.toString().trim(), expectedOrange.replace(/\r\n/g, '\n'));
  146. done();
  147. });
  148. });
  149. it('should prefer include path over SASS_PATH', function(done) {
  150. var src = read(fixture('sass-path/index.scss'), 'utf8');
  151. var expectedRed = read(fixture('sass-path/expected-red.css'), 'utf8').trim();
  152. var expectedOrange = read(fixture('sass-path/expected-orange.css'), 'utf8').trim();
  153. var envIncludes = [
  154. fixture('sass-path/red')
  155. ];
  156. process.env.SASS_PATH = envIncludes.join(path.delimiter);
  157. sass.render({
  158. data: src,
  159. includePaths: []
  160. }, function(error, result) {
  161. assert.equal(result.css.toString().trim(), expectedRed.replace(/\r\n/g, '\n'));
  162. });
  163. sass.render({
  164. data: src,
  165. includePaths: [fixture('sass-path/orange')]
  166. }, function(error, result) {
  167. assert.equal(result.css.toString().trim(), expectedOrange.replace(/\r\n/g, '\n'));
  168. done();
  169. });
  170. });
  171. it('should render with precision option', function(done) {
  172. var src = read(fixture('precision/index.scss'), 'utf8');
  173. var expected = read(fixture('precision/expected.css'), 'utf8').trim();
  174. sass.render({
  175. data: src,
  176. precision: 10
  177. }, function(error, result) {
  178. assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
  179. done();
  180. });
  181. });
  182. it('should contain all included files in stats when data is passed', function(done) {
  183. var src = read(fixture('include-files/index.scss'), 'utf8');
  184. var expected = [
  185. fixture('include-files/bar.scss').replace(/\\/g, '/'),
  186. fixture('include-files/foo.scss').replace(/\\/g, '/')
  187. ];
  188. sass.render({
  189. data: src,
  190. includePaths: [fixture('include-files')]
  191. }, function(error, result) {
  192. assert.deepEqual(result.stats.includedFiles, expected);
  193. done();
  194. });
  195. });
  196. it('should render with indentWidth and indentType options', function(done) {
  197. sass.render({
  198. data: 'div { color: transparent; }',
  199. indentWidth: 7,
  200. indentType: 'tab'
  201. }, function(error, result) {
  202. assert.equal(result.css.toString().trim(), 'div {\n\t\t\t\t\t\t\tcolor: transparent; }');
  203. done();
  204. });
  205. });
  206. it('should render with linefeed option', function(done) {
  207. sass.render({
  208. data: 'div { color: transparent; }',
  209. linefeed: 'lfcr'
  210. }, function(error, result) {
  211. assert.equal(result.css.toString().trim(), 'div {\n\r color: transparent; }');
  212. done();
  213. });
  214. });
  215. });
  216. describe('.render(importer)', function() {
  217. var src = read(fixture('include-files/index.scss'), 'utf8');
  218. it('should respect the order of chained imports when using custom importers and one file is custom imported and the other is not.', function(done) {
  219. sass.render({
  220. file: fixture('include-files/chained-imports-with-custom-importer.scss'),
  221. importer: function(url, prev, done) {
  222. // NOTE: to see that this test failure is only due to the stated
  223. // issue do each of the following and see that the tests pass.
  224. //
  225. // a) add `return sass.NULL;` as the first line in this function to
  226. // cause non-custom importers to always be used.
  227. // b) comment out the conditional below to force our custom
  228. // importer to always be used.
  229. //
  230. // You will notice that the tests pass when either all native, or
  231. // all custom importers are used, but not when a native + custom
  232. // import chain is used.
  233. if (url !== 'file-processed-by-loader') {
  234. return sass.NULL;
  235. }
  236. done({
  237. file: fixture('include-files/' + url + '.scss')
  238. });
  239. }
  240. }, function(err, data) {
  241. assert.equal(err, null);
  242. assert.equal(
  243. data.css.toString().trim(),
  244. 'body {\n color: "red"; }'
  245. );
  246. done();
  247. });
  248. });
  249. it('should still call the next importer with the resolved prev path when the previous importer returned both a file and contents property - issue #1219', function(done) {
  250. sass.render({
  251. data: '@import "a";',
  252. importer: function(url, prev, done) {
  253. if (url === 'a') {
  254. done({
  255. file: '/Users/me/sass/lib/a.scss',
  256. contents: '@import "b"'
  257. });
  258. } else {
  259. console.log(prev);
  260. assert.equal(prev, '/Users/me/sass/lib/a.scss');
  261. done({
  262. file: '/Users/me/sass/lib/b.scss',
  263. contents: 'div {color: yellow;}'
  264. });
  265. }
  266. }
  267. }, function() {
  268. done();
  269. });
  270. });
  271. it('should override imports with "data" as input and fires callback with file and contents', function(done) {
  272. sass.render({
  273. data: src,
  274. importer: function(url, prev, done) {
  275. done({
  276. file: '/some/other/path.scss',
  277. contents: 'div {color: yellow;}'
  278. });
  279. }
  280. }, function(error, result) {
  281. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  282. done();
  283. });
  284. });
  285. it('should should resolve imports depth first', function (done) {
  286. var actualImportOrder = [];
  287. var expectedImportOrder = [
  288. 'a', '_common', 'vars', 'struct', 'a1', 'common', 'vars', 'struct', 'b', 'b1'
  289. ];
  290. var expected = read(fixture('depth-first/expected.css'));
  291. sass.render({
  292. file: fixture('depth-first/index.scss'),
  293. importer: function (url, prev, done) {
  294. actualImportOrder.push(url);
  295. done();
  296. }
  297. }, function(error, result) {
  298. assert.equal(result.css.toString().trim(), expected);
  299. assert.deepEqual(actualImportOrder, expectedImportOrder);
  300. done();
  301. });
  302. });
  303. it('should override imports with "file" as input and fires callback with file and contents', function(done) {
  304. sass.render({
  305. file: fixture('include-files/index.scss'),
  306. importer: function(url, prev, done) {
  307. done({
  308. file: '/some/other/path.scss',
  309. contents: 'div {color: yellow;}'
  310. });
  311. }
  312. }, function(error, result) {
  313. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  314. done();
  315. });
  316. });
  317. it('should override imports with "data" as input and returns file and contents', function(done) {
  318. sass.render({
  319. data: src,
  320. importer: function(url, prev) {
  321. return {
  322. file: prev + url,
  323. contents: 'div {color: yellow;}'
  324. };
  325. }
  326. }, function(error, result) {
  327. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  328. done();
  329. });
  330. });
  331. it('should override imports with "file" as input and returns file and contents', function(done) {
  332. sass.render({
  333. file: fixture('include-files/index.scss'),
  334. importer: function(url, prev) {
  335. return {
  336. file: prev + url,
  337. contents: 'div {color: yellow;}'
  338. };
  339. }
  340. }, function(error, result) {
  341. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  342. done();
  343. });
  344. });
  345. it('should override imports with "data" as input and fires callback with file', function(done) {
  346. sass.render({
  347. data: src,
  348. importer: function(url, /* jshint unused:false */ prev, done) {
  349. done({
  350. file: path.resolve(path.dirname(fixture('include-files/index.scss')), url + (path.extname(url) ? '' : '.scss'))
  351. });
  352. }
  353. }, function(error, result) {
  354. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  355. done();
  356. });
  357. });
  358. it('should override imports with "file" as input and fires callback with file', function(done) {
  359. sass.render({
  360. file: fixture('include-files/index.scss'),
  361. importer: function(url, prev, done) {
  362. done({
  363. file: path.resolve(path.dirname(prev), url + (path.extname(url) ? '' : '.scss'))
  364. });
  365. }
  366. }, function(error, result) {
  367. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  368. done();
  369. });
  370. });
  371. it('should override imports with "data" as input and returns file', function(done) {
  372. sass.render({
  373. data: src,
  374. importer: function(url) {
  375. return {
  376. file: path.resolve(path.dirname(fixture('include-files/index.scss')), url + (path.extname(url) ? '' : '.scss'))
  377. };
  378. }
  379. }, function(error, result) {
  380. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  381. done();
  382. });
  383. });
  384. it('should override imports with "file" as input and returns file', function(done) {
  385. sass.render({
  386. file: fixture('include-files/index.scss'),
  387. importer: function(url, prev) {
  388. return {
  389. file: path.resolve(path.dirname(prev), url + (path.extname(url) ? '' : '.scss'))
  390. };
  391. }
  392. }, function(error, result) {
  393. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  394. done();
  395. });
  396. });
  397. it('should fallback to default import behaviour if importer returns sass.NULL', function(done) {
  398. sass.render({
  399. file: fixture('include-files/index.scss'),
  400. importer: function(url, prev, done) {
  401. done(sass.NULL);
  402. }
  403. }, function(error, result) {
  404. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  405. done();
  406. });
  407. });
  408. it('should fallback to default import behaviour if importer returns null for backwards compatibility', function(done) {
  409. sass.render({
  410. file: fixture('include-files/index.scss'),
  411. importer: function(url, prev, done) {
  412. done(null);
  413. }
  414. }, function(error, result) {
  415. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  416. done();
  417. });
  418. });
  419. it('should fallback to default import behaviour if importer returns undefined for backwards compatibility', function(done) {
  420. sass.render({
  421. file: fixture('include-files/index.scss'),
  422. importer: function(url, prev, done) {
  423. done(undefined);
  424. }
  425. }, function(error, result) {
  426. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  427. done();
  428. });
  429. });
  430. it('should fallback to default import behaviour if importer returns false for backwards compatibility', function(done) {
  431. sass.render({
  432. file: fixture('include-files/index.scss'),
  433. importer: function(url, prev, done) {
  434. done(false);
  435. }
  436. }, function(error, result) {
  437. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  438. done();
  439. });
  440. });
  441. it('should override imports with "data" as input and fires callback with contents', function(done) {
  442. sass.render({
  443. data: src,
  444. importer: function(url, prev, done) {
  445. done({
  446. contents: 'div {color: yellow;}'
  447. });
  448. }
  449. }, function(error, result) {
  450. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  451. done();
  452. });
  453. });
  454. it('should override imports with "file" as input and fires callback with contents', function(done) {
  455. sass.render({
  456. file: fixture('include-files/index.scss'),
  457. importer: function(url, prev, done) {
  458. done({
  459. contents: 'div {color: yellow;}'
  460. });
  461. }
  462. }, function(error, result) {
  463. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  464. done();
  465. });
  466. });
  467. it('should override imports with "data" as input and returns contents', function(done) {
  468. sass.render({
  469. data: src,
  470. importer: function() {
  471. return {
  472. contents: 'div {color: yellow;}'
  473. };
  474. }
  475. }, function(error, result) {
  476. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  477. done();
  478. });
  479. });
  480. it('should override imports with "file" as input and returns contents', function(done) {
  481. sass.render({
  482. file: fixture('include-files/index.scss'),
  483. importer: function() {
  484. return {
  485. contents: 'div {color: yellow;}'
  486. };
  487. }
  488. }, function(error, result) {
  489. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  490. done();
  491. });
  492. });
  493. it('should accept arrays of importers and return respect the order', function(done) {
  494. sass.render({
  495. file: fixture('include-files/index.scss'),
  496. importer: [
  497. function() {
  498. return sass.NULL;
  499. },
  500. function() {
  501. return {
  502. contents: 'div {color: yellow;}'
  503. };
  504. }
  505. ]
  506. }, function(error, result) {
  507. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  508. done();
  509. });
  510. });
  511. it('should be able to see its options in this.options', function(done) {
  512. var fxt = fixture('include-files/index.scss');
  513. sass.render({
  514. file: fxt,
  515. importer: function() {
  516. assert.equal(fxt, this.options.file);
  517. return {};
  518. }
  519. }, function() {
  520. assert.equal(fxt, this.options.file);
  521. done();
  522. });
  523. });
  524. it('should be able to access a persistent options object', function(done) {
  525. sass.render({
  526. data: src,
  527. importer: function() {
  528. this.state = this.state || 0;
  529. this.state++;
  530. return {
  531. contents: 'div {color: yellow;}'
  532. };
  533. }
  534. }, function() {
  535. assert.equal(this.state, 2);
  536. done();
  537. });
  538. });
  539. it('should wrap importer options', function(done) {
  540. var options;
  541. options = {
  542. data: src,
  543. importer: function() {
  544. assert.notStrictEqual(this.options.importer, options.importer);
  545. return {
  546. contents: 'div {color: yellow;}'
  547. };
  548. }
  549. };
  550. sass.render(options, function() {
  551. done();
  552. });
  553. });
  554. it('should reflect user-defined error when returned as callback', function(done) {
  555. sass.render({
  556. data: src,
  557. importer: function(url, prev, done) {
  558. done(new Error('doesn\'t exist!'));
  559. }
  560. }, function(error) {
  561. assert(/doesn\'t exist!/.test(error.message));
  562. done();
  563. });
  564. });
  565. it('should reflect user-defined error with return', function(done) {
  566. sass.render({
  567. data: src,
  568. importer: function() {
  569. return new Error('doesn\'t exist!');
  570. }
  571. }, function(error) {
  572. assert(/doesn\'t exist!/.test(error.message));
  573. done();
  574. });
  575. });
  576. it('should throw exception when importer returns an invalid value', function(done) {
  577. sass.render({
  578. data: src,
  579. importer: function() {
  580. return { contents: new Buffer('i am not a string!') };
  581. }
  582. }, function(error) {
  583. assert(/returned value of `contents` must be a string/.test(error.message));
  584. done();
  585. });
  586. });
  587. });
  588. describe('.render(functions)', function() {
  589. it('should call custom defined nullary function', function(done) {
  590. sass.render({
  591. data: 'div { color: foo(); }',
  592. functions: {
  593. 'foo()': function() {
  594. return new sass.types.Number(42, 'px');
  595. }
  596. }
  597. }, function(error, result) {
  598. assert.equal(result.css.toString().trim(), 'div {\n color: 42px; }');
  599. done();
  600. });
  601. });
  602. it('should call custom function with multiple args', function(done) {
  603. sass.render({
  604. data: 'div { color: foo(3, 42px); }',
  605. functions: {
  606. 'foo($a, $b)': function(factor, size) {
  607. return new sass.types.Number(factor.getValue() * size.getValue(), size.getUnit());
  608. }
  609. }
  610. }, function(error, result) {
  611. assert.equal(result.css.toString().trim(), 'div {\n color: 126px; }');
  612. done();
  613. });
  614. });
  615. it('should work with custom functions that return data asynchronously', function(done) {
  616. sass.render({
  617. data: 'div { color: foo(42px); }',
  618. functions: {
  619. 'foo($a)': function(size, done) {
  620. setTimeout(function() {
  621. done(new sass.types.Number(66, 'em'));
  622. }, 50);
  623. }
  624. }
  625. }, function(error, result) {
  626. assert.equal(result.css.toString().trim(), 'div {\n color: 66em; }');
  627. done();
  628. });
  629. });
  630. it('should let custom functions call setter methods on wrapped sass values (number)', function(done) {
  631. sass.render({
  632. data: 'div { width: foo(42px); height: bar(42px); }',
  633. functions: {
  634. 'foo($a)': function(size) {
  635. size.setUnit('rem');
  636. return size;
  637. },
  638. 'bar($a)': function(size) {
  639. size.setValue(size.getValue() * 2);
  640. return size;
  641. }
  642. }
  643. }, function(error, result) {
  644. assert.equal(result.css.toString().trim(), 'div {\n width: 42rem;\n height: 84px; }');
  645. done();
  646. });
  647. });
  648. it('should properly convert strings when calling custom functions', function(done) {
  649. sass.render({
  650. data: 'div { color: foo("bar"); }',
  651. functions: {
  652. 'foo($a)': function(str) {
  653. str = str.getValue().replace(/['"]/g, '');
  654. return new sass.types.String('"' + str + str + '"');
  655. }
  656. }
  657. }, function(error, result) {
  658. assert.equal(result.css.toString().trim(), 'div {\n color: "barbar"; }');
  659. done();
  660. });
  661. });
  662. it('should let custom functions call setter methods on wrapped sass values (string)', function(done) {
  663. sass.render({
  664. data: 'div { width: foo("bar"); }',
  665. functions: {
  666. 'foo($a)': function(str) {
  667. var unquoted = str.getValue().replace(/['"]/g, '');
  668. str.setValue('"' + unquoted + unquoted + '"');
  669. return str;
  670. }
  671. }
  672. }, function(error, result) {
  673. assert.equal(result.css.toString().trim(), 'div {\n width: "barbar"; }');
  674. done();
  675. });
  676. });
  677. it('should properly convert colors when calling custom functions', function(done) {
  678. sass.render({
  679. data: 'div { color: foo(#f00); background-color: bar(); border-color: baz(); }',
  680. functions: {
  681. 'foo($a)': function(color) {
  682. assert.equal(color.getR(), 255);
  683. assert.equal(color.getG(), 0);
  684. assert.equal(color.getB(), 0);
  685. assert.equal(color.getA(), 1.0);
  686. return new sass.types.Color(255, 255, 0, 0.5);
  687. },
  688. 'bar()': function() {
  689. return new sass.types.Color(0x33ff00ff);
  690. },
  691. 'baz()': function() {
  692. return new sass.types.Color(0xffff0000);
  693. }
  694. }
  695. }, function(error, result) {
  696. assert.equal(
  697. result.css.toString().trim(),
  698. 'div {\n color: rgba(255, 255, 0, 0.5);' +
  699. '\n background-color: rgba(255, 0, 255, 0.2);' +
  700. '\n border-color: red; }'
  701. );
  702. done();
  703. });
  704. });
  705. it('should properly convert boolean when calling custom functions', function(done) {
  706. sass.render({
  707. data: 'div { color: if(foo(true, false), #fff, #000);' +
  708. '\n background-color: if(foo(true, true), #fff, #000); }',
  709. functions: {
  710. 'foo($a, $b)': function(a, b) {
  711. return sass.types.Boolean(a.getValue() && b.getValue());
  712. }
  713. }
  714. }, function(error, result) {
  715. assert.equal(result.css.toString().trim(), 'div {\n color: #000;\n background-color: #fff; }');
  716. done();
  717. });
  718. });
  719. it('should let custom functions call setter methods on wrapped sass values (boolean)', function(done) {
  720. sass.render({
  721. data: 'div { color: if(foo(false), #fff, #000); background-color: if(foo(true), #fff, #000); }',
  722. functions: {
  723. 'foo($a)': function(a) {
  724. return a.getValue() ? sass.types.Boolean.FALSE : sass.types.Boolean.TRUE;
  725. }
  726. }
  727. }, function(error, result) {
  728. assert.equal(result.css.toString().trim(), 'div {\n color: #fff;\n background-color: #000; }');
  729. done();
  730. });
  731. });
  732. it('should properly convert lists when calling custom functions', function(done) {
  733. sass.render({
  734. data: '$test-list: (bar, #f00, 123em); @each $item in foo($test-list) { .#{$item} { color: #fff; } }',
  735. functions: {
  736. 'foo($l)': function(list) {
  737. assert.equal(list.getLength(), 3);
  738. assert.ok(list.getValue(0) instanceof sass.types.String);
  739. assert.equal(list.getValue(0).getValue(), 'bar');
  740. assert.ok(list.getValue(1) instanceof sass.types.Color);
  741. assert.equal(list.getValue(1).getR(), 0xff);
  742. assert.equal(list.getValue(1).getG(), 0);
  743. assert.equal(list.getValue(1).getB(), 0);
  744. assert.ok(list.getValue(2) instanceof sass.types.Number);
  745. assert.equal(list.getValue(2).getValue(), 123);
  746. assert.equal(list.getValue(2).getUnit(), 'em');
  747. var out = new sass.types.List(3);
  748. out.setValue(0, new sass.types.String('foo'));
  749. out.setValue(1, new sass.types.String('bar'));
  750. out.setValue(2, new sass.types.String('baz'));
  751. return out;
  752. }
  753. }
  754. }, function(error, result) {
  755. assert.equal(
  756. result.css.toString().trim(),
  757. '.foo {\n color: #fff; }\n\n.bar {\n color: #fff; }\n\n.baz {\n color: #fff; }'
  758. );
  759. done();
  760. });
  761. });
  762. it('should properly convert maps when calling custom functions', function(done) {
  763. sass.render({
  764. data: '$test-map: foo((abc: 123, #def: true)); div { color: if(map-has-key($test-map, hello), #fff, #000); }' +
  765. 'span { color: map-get($test-map, baz); }',
  766. functions: {
  767. 'foo($m)': function(map) {
  768. assert.equal(map.getLength(), 2);
  769. assert.ok(map.getKey(0) instanceof sass.types.String);
  770. assert.ok(map.getKey(1) instanceof sass.types.Color);
  771. assert.ok(map.getValue(0) instanceof sass.types.Number);
  772. assert.ok(map.getValue(1) instanceof sass.types.Boolean);
  773. assert.equal(map.getKey(0).getValue(), 'abc');
  774. assert.equal(map.getValue(0).getValue(), 123);
  775. assert.equal(map.getKey(1).getR(), 0xdd);
  776. assert.equal(map.getValue(1).getValue(), true);
  777. var out = new sass.types.Map(3);
  778. out.setKey(0, new sass.types.String('hello'));
  779. out.setValue(0, new sass.types.String('world'));
  780. out.setKey(1, new sass.types.String('foo'));
  781. out.setValue(1, new sass.types.String('bar'));
  782. out.setKey(2, new sass.types.String('baz'));
  783. out.setValue(2, new sass.types.String('qux'));
  784. return out;
  785. }
  786. }
  787. }, function(error, result) {
  788. assert.equal(result.css.toString().trim(), 'div {\n color: #fff; }\n\nspan {\n color: qux; }');
  789. done();
  790. });
  791. });
  792. it('should properly convert null when calling custom functions', function(done) {
  793. sass.render({
  794. data: 'div { color: if(foo("bar"), #fff, #000); } ' +
  795. 'span { color: if(foo(null), #fff, #000); }' +
  796. 'table { color: if(bar() == null, #fff, #000); }',
  797. functions: {
  798. 'foo($a)': function(a) {
  799. return sass.types.Boolean(a instanceof sass.types.Null);
  800. },
  801. 'bar()': function() {
  802. return sass.NULL;
  803. }
  804. }
  805. }, function(error, result) {
  806. assert.equal(
  807. result.css.toString().trim(),
  808. 'div {\n color: #000; }\n\nspan {\n color: #fff; }\n\ntable {\n color: #fff; }'
  809. );
  810. done();
  811. });
  812. });
  813. it('should be possible to carry sass values across different renders', function(done) {
  814. var persistentMap;
  815. sass.render({
  816. data: 'div { color: foo((abc: #112233, #ddeeff: true)); }',
  817. functions: {
  818. foo: function(m) {
  819. persistentMap = m;
  820. return sass.types.Color(0, 0, 0);
  821. }
  822. }
  823. }, function() {
  824. sass.render({
  825. data: 'div { color: map-get(bar(), abc); background-color: baz(); }',
  826. functions: {
  827. bar: function() {
  828. return persistentMap;
  829. },
  830. baz: function() {
  831. return persistentMap.getKey(1);
  832. }
  833. }
  834. }, function(errror, result) {
  835. assert.equal(result.css.toString().trim(), 'div {\n color: #112233;\n background-color: #ddeeff; }');
  836. done();
  837. });
  838. });
  839. });
  840. it('should let us register custom functions without signatures', function(done) {
  841. sass.render({
  842. data: 'div { color: foo(20, 22); }',
  843. functions: {
  844. foo: function(a, b) {
  845. return new sass.types.Number(a.getValue() + b.getValue(), 'em');
  846. }
  847. }
  848. }, function(error, result) {
  849. assert.equal(result.css.toString().trim(), 'div {\n color: 42em; }');
  850. done();
  851. });
  852. });
  853. it('should fail when returning anything other than a sass value from a custom function', function(done) {
  854. sass.render({
  855. data: 'div { color: foo(); }',
  856. functions: {
  857. 'foo()': function() {
  858. return {};
  859. }
  860. }
  861. }, function(error) {
  862. assert.ok(/A SassValue object was expected/.test(error.message));
  863. done();
  864. });
  865. });
  866. it('should properly bubble up standard JS errors thrown by custom functions', function(done) {
  867. sass.render({
  868. data: 'div { color: foo(); }',
  869. functions: {
  870. 'foo()': function() {
  871. throw new RangeError('This is a test error');
  872. }
  873. }
  874. }, function(error) {
  875. assert.ok(/This is a test error/.test(error.message));
  876. done();
  877. });
  878. });
  879. it('should properly bubble up unknown errors thrown by custom functions', function(done) {
  880. sass.render({
  881. data: 'div { color: foo(); }',
  882. functions: {
  883. 'foo()': function() {
  884. throw {};
  885. }
  886. }
  887. }, function(error) {
  888. assert.ok(/unexpected error/.test(error.message));
  889. done();
  890. });
  891. });
  892. it('should call custom functions with correct context', function(done) {
  893. function assertExpected(result) {
  894. assert.equal(result.css.toString().trim(), 'div {\n foo1: 1;\n foo2: 2; }');
  895. }
  896. var options = {
  897. data: 'div { foo1: foo(); foo2: foo(); }',
  898. functions: {
  899. // foo() is stateful and will persist an incrementing counter
  900. 'foo()': function() {
  901. assert(this);
  902. this.fooCounter = (this.fooCounter || 0) + 1;
  903. return new sass.types.Number(this.fooCounter);
  904. }
  905. }
  906. };
  907. sass.render(options, function(error, result) {
  908. assertExpected(result);
  909. done();
  910. });
  911. });
  912. describe('should properly bubble up errors from sass color constructor', function() {
  913. it('four booleans', function(done) {
  914. sass.render({
  915. data: 'div { color: foo(); }',
  916. functions: {
  917. 'foo()': function() {
  918. return new sass.types.Color(false, false, false, false);
  919. }
  920. }
  921. }, function(error) {
  922. assert.ok(/Constructor arguments should be numbers exclusively/.test(error.message));
  923. done();
  924. });
  925. });
  926. it('two arguments', function(done) {
  927. sass.render({
  928. data: 'div { color: foo(); }',
  929. functions: {
  930. 'foo()': function() {
  931. return sass.types.Color(2,3);
  932. }
  933. }
  934. }, function(error) {
  935. assert.ok(/Constructor should be invoked with either 0, 1, 3 or 4 arguments/.test(error.message));
  936. done();
  937. });
  938. });
  939. it('single string argument', function(done) {
  940. sass.render({
  941. data: 'div { color: foo(); }',
  942. functions: {
  943. 'foo()': function() {
  944. return sass.types.Color('foo');
  945. }
  946. }
  947. }, function(error) {
  948. assert.ok(/Only argument should be an integer/.test(error.message));
  949. done();
  950. });
  951. });
  952. });
  953. it('should properly bubble up errors from sass value constructors', function(done) {
  954. sass.render({
  955. data: 'div { color: foo(); }',
  956. functions: {
  957. 'foo()': function() {
  958. return sass.types.Boolean('foo');
  959. }
  960. }
  961. }, function(error) {
  962. assert.ok(/Expected one boolean argument/.test(error.message));
  963. done();
  964. });
  965. });
  966. it('should properly bubble up errors from sass value setters', function(done) {
  967. sass.render({
  968. data: 'div { color: foo(); }',
  969. functions: {
  970. 'foo()': function() {
  971. var ret = new sass.types.Number(42);
  972. ret.setUnit(123);
  973. return ret;
  974. }
  975. }
  976. }, function(error) {
  977. assert.ok(/Supplied value should be a string/.test(error.message));
  978. done();
  979. });
  980. });
  981. it('should fail when trying to set a bare number as the List item', function(done) {
  982. sass.render({
  983. data: 'div { color: foo(); }',
  984. functions: {
  985. 'foo()': function() {
  986. var out = new sass.types.List(1);
  987. out.setValue(0, 2);
  988. return out;
  989. }
  990. }
  991. }, function(error) {
  992. assert.ok(/Supplied value should be a SassValue object/.test(error.message));
  993. done();
  994. });
  995. });
  996. it('should fail when trying to set a bare Object as the List item', function(done) {
  997. sass.render({
  998. data: 'div { color: foo(); }',
  999. functions: {
  1000. 'foo()': function() {
  1001. var out = new sass.types.List(1);
  1002. out.setValue(0, {});
  1003. return out;
  1004. }
  1005. }
  1006. }, function(error) {
  1007. assert.ok(/A SassValue is expected as the list item/.test(error.message));
  1008. done();
  1009. });
  1010. });
  1011. it('should fail when trying to set a bare Object as the Map key', function(done) {
  1012. sass.render({
  1013. data: 'div { color: foo(); }',
  1014. functions: {
  1015. 'foo()': function() {
  1016. var out = new sass.types.Map(1);
  1017. out.setKey(0, {});
  1018. out.setValue(0, new sass.types.String('aaa'));
  1019. return out;
  1020. }
  1021. }
  1022. }, function(error) {
  1023. assert.ok(/A SassValue is expected as a map key/.test(error.message));
  1024. done();
  1025. });
  1026. });
  1027. it('should fail when trying to set a bare Object as the Map value', function(done) {
  1028. sass.render({
  1029. data: 'div { color: foo(); }',
  1030. functions: {
  1031. 'foo()': function() {
  1032. var out = new sass.types.Map(1);
  1033. out.setKey(0, new sass.types.String('aaa'));
  1034. out.setValue(0, {});
  1035. return out;
  1036. }
  1037. }
  1038. }, function(error) {
  1039. assert.ok(/A SassValue is expected as a map value/.test(error.message));
  1040. done();
  1041. });
  1042. });
  1043. it('should always map null, true and false to the same (immutable) object', function(done) {
  1044. var counter = 0;
  1045. sass.render({
  1046. data: 'div { color: foo(bar(null)); background-color: baz("foo" == "bar"); }',
  1047. functions: {
  1048. foo: function(a) {
  1049. assert.strictEqual(a, sass.TRUE,
  1050. 'Supplied value should be the same instance as sass.TRUE'
  1051. );
  1052. assert.strictEqual(
  1053. sass.types.Boolean(true), sass.types.Boolean(true),
  1054. 'sass.types.Boolean(true) should return a singleton');
  1055. assert.strictEqual(
  1056. sass.types.Boolean(true), sass.TRUE,
  1057. 'sass.types.Boolean(true) should be the same instance as sass.TRUE');
  1058. counter++;
  1059. return sass.types.String('foo');
  1060. },
  1061. bar: function(a) {
  1062. assert.strictEqual(a, sass.NULL,
  1063. 'Supplied value should be the same instance as sass.NULL');
  1064. assert.throws(function() {
  1065. return new sass.types.Null();
  1066. }, /Cannot instantiate SassNull/);
  1067. counter++;
  1068. return sass.TRUE;
  1069. },
  1070. baz: function(a) {
  1071. assert.strictEqual(a, sass.FALSE,
  1072. 'Supplied value should be the same instance as sass.FALSE');
  1073. assert.throws(function() {
  1074. return new sass.types.Boolean(false);
  1075. }, /Cannot instantiate SassBoolean/);
  1076. assert.strictEqual(
  1077. sass.types.Boolean(false), sass.types.Boolean(false),
  1078. 'sass.types.Boolean(false) should return a singleton');
  1079. assert.strictEqual(
  1080. sass.types.Boolean(false), sass.FALSE,
  1081. 'sass.types.Boolean(false) should return singleton identical to sass.FALSE');
  1082. counter++;
  1083. return sass.types.String('baz');
  1084. }
  1085. }
  1086. }, function() {
  1087. assert.strictEqual(counter, 3);
  1088. done();
  1089. });
  1090. });
  1091. });
  1092. describe('.render({stats: {}})', function() {
  1093. var start = Date.now();
  1094. it('should provide a start timestamp', function(done) {
  1095. sass.render({
  1096. file: fixture('include-files/index.scss')
  1097. }, function(error, result) {
  1098. assert(!error);
  1099. assert.strictEqual(typeof result.stats.start, 'number');
  1100. assert(result.stats.start >= start);
  1101. done();
  1102. });
  1103. });
  1104. it('should provide an end timestamp', function(done) {
  1105. sass.render({
  1106. file: fixture('include-files/index.scss')
  1107. }, function(error, result) {
  1108. assert(!error);
  1109. assert.strictEqual(typeof result.stats.end, 'number');
  1110. assert(result.stats.end >= result.stats.start);
  1111. done();
  1112. });
  1113. });
  1114. it('should provide a duration', function(done) {
  1115. sass.render({
  1116. file: fixture('include-files/index.scss')
  1117. }, function(error, result) {
  1118. assert(!error);
  1119. assert.strictEqual(typeof result.stats.duration, 'number');
  1120. assert.equal(result.stats.end - result.stats.start, result.stats.duration);
  1121. done();
  1122. });
  1123. });
  1124. it('should contain the given entry file', function(done) {
  1125. sass.render({
  1126. file: fixture('include-files/index.scss')
  1127. }, function(error, result) {
  1128. assert(!error);
  1129. assert.equal(result.stats.entry, fixture('include-files/index.scss'));
  1130. done();
  1131. });
  1132. });
  1133. it('should contain an array of all included files', function(done) {
  1134. var expected = [
  1135. fixture('include-files/bar.scss').replace(/\\/g, '/'),
  1136. fixture('include-files/foo.scss').replace(/\\/g, '/'),
  1137. fixture('include-files/index.scss').replace(/\\/g, '/')
  1138. ];
  1139. sass.render({
  1140. file: fixture('include-files/index.scss')
  1141. }, function(error, result) {
  1142. assert(!error);
  1143. assert.deepEqual(result.stats.includedFiles.sort(), expected.sort());
  1144. done();
  1145. });
  1146. });
  1147. it('should contain array with the entry if there are no import statements', function(done) {
  1148. var expected = fixture('simple/index.scss').replace(/\\/g, '/');
  1149. sass.render({
  1150. file: fixture('simple/index.scss')
  1151. }, function(error, result) {
  1152. assert.deepEqual(result.stats.includedFiles, [expected]);
  1153. done();
  1154. });
  1155. });
  1156. it('should state `data` as entry file', function(done) {
  1157. sass.render({
  1158. data: read(fixture('simple/index.scss'), 'utf8')
  1159. }, function(error, result) {
  1160. assert.equal(result.stats.entry, 'data');
  1161. done();
  1162. });
  1163. });
  1164. it('should contain an empty array as includedFiles', function(done) {
  1165. sass.render({
  1166. data: read(fixture('simple/index.scss'), 'utf8')
  1167. }, function(error, result) {
  1168. assert.deepEqual(result.stats.includedFiles, []);
  1169. done();
  1170. });
  1171. });
  1172. });
  1173. describe('.renderSync(options)', function() {
  1174. it('should compile sass to css with file', function(done) {
  1175. var expected = read(fixture('simple/expected.css'), 'utf8').trim();
  1176. var result = sass.renderSync({ file: fixture('simple/index.scss') });
  1177. assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
  1178. done();
  1179. });
  1180. it('should compile sass to css with outFile set to absolute url', function(done) {
  1181. var result = sass.renderSync({
  1182. file: fixture('simple/index.scss'),
  1183. sourceMap: true,
  1184. outFile: fixture('simple/index-test.css')
  1185. });
  1186. assert.equal(JSON.parse(result.map).file, 'index-test.css');
  1187. done();
  1188. });
  1189. it('should compile sass to css with outFile set to relative url', function(done) {
  1190. var result = sass.renderSync({
  1191. file: fixture('simple/index.scss'),
  1192. sourceMap: true,
  1193. outFile: './index-test.css'
  1194. });
  1195. assert.equal(JSON.parse(result.map).file, 'index-test.css');
  1196. done();
  1197. });
  1198. it('should compile sass to css with outFile and sourceMap set to relative url', function(done) {
  1199. var result = sass.renderSync({
  1200. file: fixture('simple/index.scss'),
  1201. sourceMap: './deep/nested/index.map',
  1202. outFile: './index-test.css'
  1203. });
  1204. assert.equal(JSON.parse(result.map).file, '../../index-test.css');
  1205. done();
  1206. });
  1207. it('should compile generate map with sourceMapRoot pass-through option', function(done) {
  1208. var result = sass.renderSync({
  1209. file: fixture('simple/index.scss'),
  1210. sourceMap: './deep/nested/index.map',
  1211. sourceMapRoot: 'http://test.com/',
  1212. outFile: './index-test.css'
  1213. });
  1214. assert.equal(JSON.parse(result.map).sourceRoot, 'http://test.com/');
  1215. done();
  1216. });
  1217. it('should compile sass to css with data', function(done) {
  1218. var src = read(fixture('simple/index.scss'), 'utf8');
  1219. var expected = read(fixture('simple/expected.css'), 'utf8').trim();
  1220. var result = sass.renderSync({ data: src });
  1221. assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
  1222. done();
  1223. });
  1224. it('should compile sass to css using indented syntax', function(done) {
  1225. var src = read(fixture('indent/index.sass'), 'utf8');
  1226. var expected = read(fixture('indent/expected.css'), 'utf8').trim();
  1227. var result = sass.renderSync({
  1228. data: src,
  1229. indentedSyntax: true
  1230. });
  1231. assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
  1232. done();
  1233. });
  1234. it('should NOT compile empty data string', function(done) {
  1235. assert.throws(function() {
  1236. sass.renderSync({ data: '' });
  1237. }, /No input specified: provide a file name or a source string to process/ );
  1238. done();
  1239. });
  1240. it('should NOT compile without any input', function(done) {
  1241. assert.throws(function() {
  1242. sass.renderSync({});
  1243. }, /No input specified: provide a file name or a source string to process/);
  1244. done();
  1245. });
  1246. it('should throw error for bad input', function(done) {
  1247. assert.throws(function() {
  1248. sass.renderSync('somestring');
  1249. });
  1250. assert.throws(function() {
  1251. sass.renderSync({ data: '#navbar width 80%;' });
  1252. });
  1253. done();
  1254. });
  1255. });
  1256. describe('.renderSync(importer)', function() {
  1257. var src = read(fixture('include-files/index.scss'), 'utf8');
  1258. it('should override imports with "data" as input and returns file and contents', function(done) {
  1259. var result = sass.renderSync({
  1260. data: src,
  1261. importer: function(url, prev) {
  1262. return {
  1263. file: prev + url,
  1264. contents: 'div {color: yellow;}'
  1265. };
  1266. }
  1267. });
  1268. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  1269. done();
  1270. });
  1271. it('should override imports with "file" as input and returns file and contents', function(done) {
  1272. var result = sass.renderSync({
  1273. file: fixture('include-files/index.scss'),
  1274. importer: function(url, prev) {
  1275. return {
  1276. file: prev + url,
  1277. contents: 'div {color: yellow;}'
  1278. };
  1279. }
  1280. });
  1281. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  1282. done();
  1283. });
  1284. it('should override imports with "data" as input and returns file', function(done) {
  1285. var result = sass.renderSync({
  1286. data: src,
  1287. importer: function(url) {
  1288. return {
  1289. file: path.resolve(path.dirname(fixture('include-files/index.scss')), url + (path.extname(url) ? '' : '.scss'))
  1290. };
  1291. }
  1292. });
  1293. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  1294. done();
  1295. });
  1296. it('should override imports with "file" as input and returns file', function(done) {
  1297. var result = sass.renderSync({
  1298. file: fixture('include-files/index.scss'),
  1299. importer: function(url, prev) {
  1300. return {
  1301. file: path.resolve(path.dirname(prev), url + (path.extname(url) ? '' : '.scss'))
  1302. };
  1303. }
  1304. });
  1305. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  1306. done();
  1307. });
  1308. it('should override imports with "data" as input and returns contents', function(done) {
  1309. var result = sass.renderSync({
  1310. data: src,
  1311. importer: function() {
  1312. return {
  1313. contents: 'div {color: yellow;}'
  1314. };
  1315. }
  1316. });
  1317. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  1318. done();
  1319. });
  1320. it('should override imports with "file" as input and returns contents', function(done) {
  1321. var result = sass.renderSync({
  1322. file: fixture('include-files/index.scss'),
  1323. importer: function() {
  1324. return {
  1325. contents: 'div {color: yellow;}'
  1326. };
  1327. }
  1328. });
  1329. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  1330. done();
  1331. });
  1332. it('should fallback to default import behaviour if importer returns sass.NULL', function(done) {
  1333. var result = sass.renderSync({
  1334. file: fixture('include-files/index.scss'),
  1335. importer: function() {
  1336. return sass.NULL;
  1337. }
  1338. });
  1339. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  1340. done();
  1341. });
  1342. it('should fallback to default import behaviour if importer returns null for backwards compatibility', function(done) {
  1343. var result = sass.renderSync({
  1344. file: fixture('include-files/index.scss'),
  1345. importer: function() {
  1346. return null;
  1347. }
  1348. });
  1349. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  1350. done();
  1351. });
  1352. it('should fallback to default import behaviour if importer returns undefined for backwards compatibility', function(done) {
  1353. var result = sass.renderSync({
  1354. file: fixture('include-files/index.scss'),
  1355. importer: function() {
  1356. return undefined;
  1357. }
  1358. });
  1359. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  1360. done();
  1361. });
  1362. it('should fallback to default import behaviour if importer returns false for backwards compatibility', function(done) {
  1363. var result = sass.renderSync({
  1364. file: fixture('include-files/index.scss'),
  1365. importer: function() {
  1366. return false;
  1367. }
  1368. });
  1369. assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
  1370. done();
  1371. });
  1372. it('should accept arrays of importers and return respect the order', function(done) {
  1373. var result = sass.renderSync({
  1374. file: fixture('include-files/index.scss'),
  1375. importer: [
  1376. function() {
  1377. return sass.NULL;
  1378. },
  1379. function() {
  1380. return {
  1381. contents: 'div {color: yellow;}'
  1382. };
  1383. }
  1384. ]
  1385. });
  1386. assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
  1387. done();
  1388. });
  1389. it('should be able to see its options in this.options', function(done) {
  1390. var fxt = fixture('include-files/index.scss');
  1391. var sync = false;
  1392. sass.renderSync({
  1393. file: fixture('include-files/index.scss'),
  1394. importer: function() {
  1395. assert.equal(fxt, this.options.file);
  1396. sync = true;
  1397. return {};
  1398. }
  1399. });
  1400. assert.equal(sync, true);
  1401. done();
  1402. });
  1403. it('should throw user-defined error', function(done) {
  1404. assert.throws(function() {
  1405. sass.renderSync({
  1406. data: src,
  1407. importer: function() {
  1408. return new Error('doesn\'t exist!');
  1409. }
  1410. });
  1411. }, /doesn\'t exist!/);
  1412. done();
  1413. });
  1414. it('should throw exception when importer returns an invalid value', function(done) {
  1415. assert.throws(function() {
  1416. sass.renderSync({
  1417. data: src,
  1418. importer: function() {
  1419. return { contents: new Buffer('i am not a string!') };
  1420. }
  1421. });
  1422. }, /returned value of `contents` must be a string/);
  1423. done();
  1424. });
  1425. });
  1426. describe('.renderSync(functions)', function() {
  1427. it('should call custom function in sync mode', function(done) {
  1428. var result = sass.renderSync({
  1429. data: 'div { width: cos(0) * 50px; }',
  1430. functions: {
  1431. 'cos($a)': function(angle) {
  1432. if (!(angle instanceof sass.types.Number)) {
  1433. throw new TypeError('Unexpected type for "angle"');
  1434. }
  1435. return new sass.types.Number(Math.cos(angle.getValue()));
  1436. }
  1437. }
  1438. });
  1439. assert.equal(result.css.toString().trim(), 'div {\n width: 50px; }');
  1440. done();
  1441. });
  1442. it('should return a list of selectors after calling the headings custom function', function(done) {
  1443. var result = sass.renderSync({
  1444. data: '#{headings(2,5)} { color: #08c; }',
  1445. functions: {
  1446. 'headings($from: 0, $to: 6)': function(from, to) {
  1447. var i, f = from.getValue(), t = to.getValue(),
  1448. list = new sass.types.List(t - f + 1);
  1449. for (i = f; i <= t; i++) {
  1450. list.setValue(i - f, new sass.types.String('h' + i));
  1451. }
  1452. return list;
  1453. }
  1454. }
  1455. });
  1456. assert.equal(result.css.toString().trim(), 'h2, h3, h4, h5 {\n color: #08c; }');
  1457. done();
  1458. });
  1459. it('should let custom function invoke sass types constructors without the `new` keyword', function(done) {
  1460. var result = sass.renderSync({
  1461. data: 'div { color: foo(); }',
  1462. functions: {
  1463. 'foo()': function() {
  1464. return sass.types.Number(42, 'em');
  1465. }
  1466. }
  1467. });
  1468. assert.equal(result.css.toString().trim(), 'div {\n color: 42em; }');
  1469. done();
  1470. });
  1471. it('should let us register custom functions without signatures', function(done) {
  1472. var result = sass.renderSync({
  1473. data: 'div { color: foo(20, 22); }',
  1474. functions: {
  1475. foo: function(a, b) {
  1476. return new sass.types.Number(a.getValue() + b.getValue(), 'em');
  1477. }
  1478. }
  1479. });
  1480. assert.equal(result.css.toString().trim(), 'div {\n color: 42em; }');
  1481. done();
  1482. });
  1483. it('should fail when returning anything other than a sass value from a custom function', function(done) {
  1484. assert.throws(function() {
  1485. sass.renderSync({
  1486. data: 'div { color: foo(); }',
  1487. functions: {
  1488. 'foo()': function() {
  1489. return {};
  1490. }
  1491. }
  1492. });
  1493. }, /A SassValue object was expected/);
  1494. done();
  1495. });
  1496. it('should properly bubble up standard JS errors thrown by custom functions', function(done) {
  1497. assert.throws(function() {
  1498. sass.renderSync({
  1499. data: 'div { color: foo(); }',
  1500. functions: {
  1501. 'foo()': function() {
  1502. throw new RangeError('This is a test error');
  1503. }
  1504. }
  1505. });
  1506. }, /This is a test error/);
  1507. done();
  1508. });
  1509. it('should properly bubble up unknown errors thrown by custom functions', function(done) {
  1510. assert.throws(function() {
  1511. sass.renderSync({
  1512. data: 'div { color: foo(); }',
  1513. functions: {
  1514. 'foo()': function() {
  1515. throw {};
  1516. }
  1517. }
  1518. });
  1519. }, /unexpected error/);
  1520. done();
  1521. });
  1522. it('should properly bubble up errors from sass value getters/setters/constructors', function(done) {
  1523. assert.throws(function() {
  1524. sass.renderSync({
  1525. data: 'div { color: foo(); }',
  1526. functions: {
  1527. 'foo()': function() {
  1528. return sass.types.Boolean('foo');
  1529. }
  1530. }
  1531. });
  1532. }, /Expected one boolean argument/);
  1533. assert.throws(function() {
  1534. sass.renderSync({
  1535. data: 'div { color: foo(); }',
  1536. functions: {
  1537. 'foo()': function() {
  1538. var ret = new sass.types.Number(42);
  1539. ret.setUnit(123);
  1540. return ret;
  1541. }
  1542. }
  1543. });
  1544. }, /Supplied value should be a string/);
  1545. done();
  1546. });
  1547. it('should call custom functions with correct context', function(done) {
  1548. function assertExpected(result) {
  1549. assert.equal(result.css.toString().trim(), 'div {\n foo1: 1;\n foo2: 2; }');
  1550. }
  1551. var options = {
  1552. data: 'div { foo1: foo(); foo2: foo(); }',
  1553. functions: {
  1554. // foo() is stateful and will persist an incrementing counter
  1555. 'foo()': function() {
  1556. assert(this);
  1557. this.fooCounter = (this.fooCounter || 0) + 1;
  1558. return new sass.types.Number(this.fooCounter);
  1559. }
  1560. }
  1561. };
  1562. assertExpected(sass.renderSync(options));
  1563. done();
  1564. });
  1565. });
  1566. describe('.renderSync({stats: {}})', function() {
  1567. var start = Date.now();
  1568. var result = sass.renderSync({
  1569. file: fixture('include-files/index.scss')
  1570. });
  1571. it('should provide a start timestamp', function(done) {
  1572. assert.strictEqual(typeof result.stats.start, 'number');
  1573. assert(result.stats.start >= start);
  1574. done();
  1575. });
  1576. it('should provide an end timestamp', function(done) {
  1577. assert.strictEqual(typeof result.stats.end, 'number');
  1578. assert(result.stats.end >= result.stats.start);
  1579. done();
  1580. });
  1581. it('should provide a duration', function(done) {
  1582. assert.strictEqual(typeof result.stats.duration, 'number');
  1583. assert.equal(result.stats.end - result.stats.start, result.stats.duration);
  1584. done();
  1585. });
  1586. it('should contain the given entry file', function(done) {
  1587. assert.equal(result.stats.entry, resolveFixture('include-files/index.scss'));
  1588. done();
  1589. });
  1590. it('should contain an array of all included files', function(done) {
  1591. var expected = [
  1592. fixture('include-files/bar.scss').replace(/\\/g, '/'),
  1593. fixture('include-files/foo.scss').replace(/\\/g, '/'),
  1594. fixture('include-files/index.scss').replace(/\\/g, '/')
  1595. ].sort();
  1596. var actual = result.stats.includedFiles.sort();
  1597. assert.equal(actual[0], expected[0]);
  1598. assert.equal(actual[1], expected[1]);
  1599. assert.equal(actual[2], expected[2]);
  1600. done();
  1601. });
  1602. it('should contain array with the entry if there are no import statements', function(done) {
  1603. var expected = fixture('simple/index.scss').replace(/\\/g, '/');
  1604. var result = sass.renderSync({
  1605. file: fixture('simple/index.scss')
  1606. });
  1607. assert.deepEqual(result.stats.includedFiles, [expected]);
  1608. done();
  1609. });
  1610. it('should state `data` as entry file', function(done) {
  1611. var result = sass.renderSync({
  1612. data: read(fixture('simple/index.scss'), 'utf8')
  1613. });
  1614. assert.equal(result.stats.entry, 'data');
  1615. done();
  1616. });
  1617. it('should contain an empty array as includedFiles', function(done) {
  1618. var result = sass.renderSync({
  1619. data: read(fixture('simple/index.scss'), 'utf8')
  1620. });
  1621. assert.deepEqual(result.stats.includedFiles, []);
  1622. done();
  1623. });
  1624. });
  1625. describe('.info', function() {
  1626. var package = require('../package.json'),
  1627. info = sass.info;
  1628. it('should return a correct version info', function(done) {
  1629. assert(info.indexOf(package.version) > 0);
  1630. assert(info.indexOf('(Wrapper)') > 0);
  1631. assert(info.indexOf('[JavaScript]') > 0);
  1632. assert(info.indexOf('[NA]') < 0);
  1633. assert(info.indexOf('(Sass Compiler)') > 0);
  1634. assert(info.indexOf('[C/C++]') > 0);
  1635. done();
  1636. });
  1637. });
  1638. });