api.js 63 KB

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