1 /*eslint new-cap: ["error", {"capIsNewExceptions": ["Color"]}]*/
3 var assert = require('assert'),
5 path = require('path'),
6 read = fs.readFileSync,
7 sassPath = process.env.NODESASS_COV
8 ? require.resolve('../lib-cov')
9 : require.resolve('../lib'),
10 sass = require(sassPath),
11 fixture = path.join.bind(null, __dirname, 'fixtures'),
12 resolveFixture = path.resolve.bind(null, __dirname, 'fixtures');
14 describe('api', function() {
16 describe('.render(options, callback)', function() {
18 beforeEach(function() {
19 delete process.env.SASS_PATH;
22 it('should compile sass to css with file', function(done) {
23 var expected = read(fixture('simple/expected.css'), 'utf8').trim();
26 file: fixture('simple/index.scss')
27 }, function(error, result) {
28 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
33 it('should compile sass to css with outFile set to absolute url', function(done) {
35 file: fixture('simple/index.scss'),
37 outFile: fixture('simple/index-test.css')
38 }, function(error, result) {
39 assert.equal(JSON.parse(result.map).file, 'index-test.css');
44 it('should compile sass to css with outFile set to relative url', function(done) {
46 file: fixture('simple/index.scss'),
48 outFile: './index-test.css'
49 }, function(error, result) {
50 assert.equal(JSON.parse(result.map).file, 'index-test.css');
55 it('should compile sass to css with outFile and sourceMap set to relative url', function(done) {
57 file: fixture('simple/index.scss'),
58 sourceMap: './deep/nested/index.map',
59 outFile: './index-test.css'
60 }, function(error, result) {
61 assert.equal(JSON.parse(result.map).file, '../../index-test.css');
66 it('should compile generate map with sourceMapRoot pass-through option', function(done) {
68 file: fixture('simple/index.scss'),
69 sourceMap: './deep/nested/index.map',
70 sourceMapRoot: 'http://test.com/',
71 outFile: './index-test.css'
72 }, function(error, result) {
73 assert.equal(JSON.parse(result.map).sourceRoot, 'http://test.com/');
78 it('should compile sass to css with data', function(done) {
79 var src = read(fixture('simple/index.scss'), 'utf8');
80 var expected = read(fixture('simple/expected.css'), 'utf8').trim();
84 }, function(error, result) {
85 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
90 it('should compile sass to css using indented syntax', function(done) {
91 var src = read(fixture('indent/index.sass'), 'utf8');
92 var expected = read(fixture('indent/expected.css'), 'utf8').trim();
97 }, function(error, result) {
98 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
103 it('should NOT compile empty data string', function(done) {
107 assert.equal(error.message, 'No input specified: provide a file name or a source string to process');
112 it('should NOT compile without any input', function(done) {
113 sass.render({ }, function(error) {
114 assert.equal(error.message, 'No input specified: provide a file name or a source string to process');
119 it('should returnn error status 1 for bad input', function(done) {
121 data: '#navbar width 80%;'
123 assert(error.message);
124 assert.equal(error.status, 1);
129 it('should compile with include paths', function(done) {
130 var src = read(fixture('include-path/index.scss'), 'utf8');
131 var expected = read(fixture('include-path/expected.css'), 'utf8').trim();
136 fixture('include-path/functions'),
137 fixture('include-path/lib')
139 }, function(error, result) {
140 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
145 it('should add cwd to the front on include paths', function(done) {
146 var src = fixture('cwd-include-path/root/index.scss');
147 var expected = read(fixture('cwd-include-path/expected.css'), 'utf8').trim();
148 var cwd = process.cwd();
150 process.chdir(fixture('cwd-include-path'));
154 }, function(error, result) {
155 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
162 it('should check SASS_PATH in the specified order', function(done) {
163 var src = read(fixture('sass-path/index.scss'), 'utf8');
164 var expectedRed = read(fixture('sass-path/expected-red.css'), 'utf8').trim();
165 var expectedOrange = read(fixture('sass-path/expected-orange.css'), 'utf8').trim();
168 fixture('sass-path/red'),
169 fixture('sass-path/orange')
172 process.env.SASS_PATH = envIncludes.join(path.delimiter);
176 }, function(error, result) {
177 assert.equal(result.css.toString().trim(), expectedRed.replace(/\r\n/g, '\n'));
180 process.env.SASS_PATH = envIncludes.reverse().join(path.delimiter);
184 }, function(error, result) {
185 assert.equal(result.css.toString().trim(), expectedOrange.replace(/\r\n/g, '\n'));
190 it('should prefer include path over SASS_PATH', function(done) {
191 var src = read(fixture('sass-path/index.scss'), 'utf8');
192 var expectedRed = read(fixture('sass-path/expected-red.css'), 'utf8').trim();
193 var expectedOrange = read(fixture('sass-path/expected-orange.css'), 'utf8').trim();
196 fixture('sass-path/red')
198 process.env.SASS_PATH = envIncludes.join(path.delimiter);
203 }, function(error, result) {
204 assert.equal(result.css.toString().trim(), expectedRed.replace(/\r\n/g, '\n'));
208 includePaths: [fixture('sass-path/orange')]
209 }, function(error, result) {
210 assert.equal(result.css.toString().trim(), expectedOrange.replace(/\r\n/g, '\n'));
215 it('should render with precision option', function(done) {
216 var src = read(fixture('precision/index.scss'), 'utf8');
217 var expected = read(fixture('precision/expected.css'), 'utf8').trim();
222 }, function(error, result) {
223 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
228 it('should contain all included files in stats when data is passed', function(done) {
229 var src = read(fixture('include-files/index.scss'), 'utf8');
231 fixture('include-files/bar.scss').replace(/\\/g, '/'),
232 fixture('include-files/foo.scss').replace(/\\/g, '/')
237 includePaths: [fixture('include-files')]
238 }, function(error, result) {
239 assert.deepEqual(result.stats.includedFiles, expected);
244 it('should render with indentWidth and indentType options', function(done) {
246 data: 'div { color: transparent; }',
249 }, function(error, result) {
250 assert.equal(result.css.toString().trim(), 'div {\n\t\t\t\t\t\t\tcolor: transparent; }');
255 it('should render with linefeed option', function(done) {
257 data: 'div { color: transparent; }',
259 }, function(error, result) {
260 assert.equal(result.css.toString().trim(), 'div {\n\r color: transparent; }');
266 describe('.render(importer)', function() {
267 var src = read(fixture('include-files/index.scss'), 'utf8');
269 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) {
271 file: fixture('include-files/chained-imports-with-custom-importer.scss'),
272 importer: function(url, prev, done) {
273 // NOTE: to see that this test failure is only due to the stated
274 // issue do each of the following and see that the tests pass.
276 // a) add `return sass.NULL;` as the first line in this function to
277 // cause non-custom importers to always be used.
278 // b) comment out the conditional below to force our custom
279 // importer to always be used.
281 // You will notice that the tests pass when either all native, or
282 // all custom importers are used, but not when a native + custom
283 // import chain is used.
284 if (url !== 'file-processed-by-loader') {
288 file: fixture('include-files/' + url + '.scss')
291 }, function(err, data) {
292 assert.equal(err, null);
295 data.css.toString().trim(),
296 'body {\n color: "red"; }'
303 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) {
305 data: '@import "a";',
306 importer: function(url, prev, done) {
309 file: '/Users/me/sass/lib/a.scss',
310 contents: '@import "b"'
314 assert.equal(prev, '/Users/me/sass/lib/a.scss');
316 file: '/Users/me/sass/lib/b.scss',
317 contents: 'div {color: yellow;}'
326 it('should override imports with "data" as input and fires callback with file and contents', function(done) {
329 importer: function(url, prev, done) {
331 file: '/some/other/path.scss',
332 contents: 'div {color: yellow;}'
335 }, function(error, result) {
336 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
341 it('should should resolve imports depth first', function (done) {
342 var actualImportOrder = [];
343 var expectedImportOrder = [
344 'a', '_common', 'vars', 'struct', 'a1', 'common', 'vars', 'struct', 'b', 'b1'
346 var expected = read(fixture('depth-first/expected.css'));
349 file: fixture('depth-first/index.scss'),
350 importer: function (url, prev, done) {
351 actualImportOrder.push(url);
354 }, function(error, result) {
355 assert.equal(result.css.toString().trim(), expected);
356 assert.deepEqual(actualImportOrder, expectedImportOrder);
361 it('should override imports with "file" as input and fires callback with file and contents', function(done) {
363 file: fixture('include-files/index.scss'),
364 importer: function(url, prev, done) {
366 file: '/some/other/path.scss',
367 contents: 'div {color: yellow;}'
370 }, function(error, result) {
371 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
376 it('should override imports with "data" as input and returns file and contents', function(done) {
379 importer: function(url, prev) {
382 contents: 'div {color: yellow;}'
385 }, function(error, result) {
386 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
391 it('should override imports with "file" as input and returns file and contents', function(done) {
393 file: fixture('include-files/index.scss'),
394 importer: function(url, prev) {
397 contents: 'div {color: yellow;}'
400 }, function(error, result) {
401 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
406 it('should override imports with "data" as input and fires callback with file', function(done) {
409 importer: function(url, /* jshint unused:false */ prev, done) {
411 file: path.resolve(path.dirname(fixture('include-files/index.scss')), url + (path.extname(url) ? '' : '.scss'))
414 }, function(error, result) {
415 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
420 it('should override imports with "file" as input and fires callback with file', function(done) {
422 file: fixture('include-files/index.scss'),
423 importer: function(url, prev, done) {
425 file: path.resolve(path.dirname(prev), url + (path.extname(url) ? '' : '.scss'))
428 }, function(error, result) {
429 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
434 it('should override imports with "data" as input and returns file', function(done) {
437 importer: function(url) {
439 file: path.resolve(path.dirname(fixture('include-files/index.scss')), url + (path.extname(url) ? '' : '.scss'))
442 }, function(error, result) {
443 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
448 it('should override imports with "file" as input and returns file', function(done) {
450 file: fixture('include-files/index.scss'),
451 importer: function(url, prev) {
453 file: path.resolve(path.dirname(prev), url + (path.extname(url) ? '' : '.scss'))
456 }, function(error, result) {
457 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
462 it('should fallback to default import behaviour if importer returns sass.NULL', function(done) {
464 file: fixture('include-files/index.scss'),
465 importer: function(url, prev, done) {
468 }, function(error, result) {
469 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
474 it('should fallback to default import behaviour if importer returns null for backwards compatibility', function(done) {
476 file: fixture('include-files/index.scss'),
477 importer: function(url, prev, done) {
480 }, function(error, result) {
481 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
486 it('should fallback to default import behaviour if importer returns undefined for backwards compatibility', function(done) {
488 file: fixture('include-files/index.scss'),
489 importer: function(url, prev, done) {
492 }, function(error, result) {
493 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
498 it('should fallback to default import behaviour if importer returns false for backwards compatibility', function(done) {
500 file: fixture('include-files/index.scss'),
501 importer: function(url, prev, done) {
504 }, function(error, result) {
505 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
510 it('should override imports with "data" as input and fires callback with contents', function(done) {
513 importer: function(url, prev, done) {
515 contents: 'div {color: yellow;}'
518 }, function(error, result) {
519 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
524 it('should override imports with "file" as input and fires callback with contents', function(done) {
526 file: fixture('include-files/index.scss'),
527 importer: function(url, prev, done) {
529 contents: 'div {color: yellow;}'
532 }, function(error, result) {
533 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
538 it('should override imports with "data" as input and returns contents', function(done) {
541 importer: function() {
543 contents: 'div {color: yellow;}'
546 }, function(error, result) {
547 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
552 it('should override imports with "file" as input and returns contents', function(done) {
554 file: fixture('include-files/index.scss'),
555 importer: function() {
557 contents: 'div {color: yellow;}'
560 }, function(error, result) {
561 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
566 it('should accept arrays of importers and return respect the order', function(done) {
568 file: fixture('include-files/index.scss'),
575 contents: 'div {color: yellow;}'
579 }, function(error, result) {
580 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
585 it('should be able to see its options in this.options', function(done) {
586 var fxt = fixture('include-files/index.scss');
589 importer: function() {
590 assert.equal(fxt, this.options.file);
594 assert.equal(fxt, this.options.file);
599 it('should be able to access a persistent options object', function(done) {
602 importer: function() {
603 this.state = this.state || 0;
606 contents: 'div {color: yellow;}'
610 assert.equal(this.state, 2);
615 it('should wrap importer options', function(done) {
619 importer: function() {
620 assert.notStrictEqual(this.options.importer, options.importer);
622 contents: 'div {color: yellow;}'
626 sass.render(options, function() {
631 it('should reflect user-defined error when returned as callback', function(done) {
634 importer: function(url, prev, done) {
635 done(new Error('doesn\'t exist!'));
638 assert(/doesn\'t exist!/.test(error.message));
643 it('should reflect user-defined error with return', function(done) {
646 importer: function() {
647 return new Error('doesn\'t exist!');
650 assert(/doesn\'t exist!/.test(error.message));
655 it('should throw exception when importer returns an invalid value', function(done) {
658 importer: function() {
659 return { contents: new Buffer('i am not a string!') };
662 assert(/returned value of `contents` must be a string/.test(error.message));
668 describe('.render(functions)', function() {
669 it('should call custom defined nullary function', function(done) {
671 data: 'div { color: foo(); }',
673 'foo()': function() {
674 return new sass.types.Number(42, 'px');
677 }, function(error, result) {
678 assert.equal(result.css.toString().trim(), 'div {\n color: 42px; }');
683 it('should call custom function with multiple args', function(done) {
685 data: 'div { color: foo(3, 42px); }',
687 'foo($a, $b)': function(factor, size) {
688 return new sass.types.Number(factor.getValue() * size.getValue(), size.getUnit());
691 }, function(error, result) {
692 assert.equal(result.css.toString().trim(), 'div {\n color: 126px; }');
697 it('should work with custom functions that return data asynchronously', function(done) {
699 data: 'div { color: foo(42px); }',
701 'foo($a)': function(size, done) {
702 setTimeout(function() {
703 done(new sass.types.Number(66, 'em'));
707 }, function(error, result) {
708 assert.equal(result.css.toString().trim(), 'div {\n color: 66em; }');
713 it('should let custom functions call setter methods on wrapped sass values (number)', function(done) {
715 data: 'div { width: foo(42px); height: bar(42px); }',
717 'foo($a)': function(size) {
721 'bar($a)': function(size) {
722 size.setValue(size.getValue() * 2);
726 }, function(error, result) {
727 assert.equal(result.css.toString().trim(), 'div {\n width: 42rem;\n height: 84px; }');
732 it('should properly convert strings when calling custom functions', function(done) {
734 data: 'div { color: foo("bar"); }',
736 'foo($a)': function(str) {
737 str = str.getValue().replace(/['"]/g, '');
738 return new sass.types.String('"' + str + str + '"');
741 }, function(error, result) {
742 assert.equal(result.css.toString().trim(), 'div {\n color: "barbar"; }');
747 it('should let custom functions call setter methods on wrapped sass values (string)', function(done) {
749 data: 'div { width: foo("bar"); }',
751 'foo($a)': function(str) {
752 var unquoted = str.getValue().replace(/['"]/g, '');
753 str.setValue('"' + unquoted + unquoted + '"');
757 }, function(error, result) {
758 assert.equal(result.css.toString().trim(), 'div {\n width: "barbar"; }');
763 it('should properly convert colors when calling custom functions', function(done) {
765 data: 'div { color: foo(#f00); background-color: bar(); border-color: baz(); }',
767 'foo($a)': function(color) {
768 assert.equal(color.getR(), 255);
769 assert.equal(color.getG(), 0);
770 assert.equal(color.getB(), 0);
771 assert.equal(color.getA(), 1.0);
773 return new sass.types.Color(255, 255, 0, 0.5);
775 'bar()': function() {
776 return new sass.types.Color(0x33ff00ff);
778 'baz()': function() {
779 return new sass.types.Color(0xffff0000);
782 }, function(error, result) {
784 result.css.toString().trim(),
785 'div {\n color: rgba(255, 255, 0, 0.5);' +
786 '\n background-color: rgba(255, 0, 255, 0.2);' +
787 '\n border-color: red; }'
793 it('should properly convert boolean when calling custom functions', function(done) {
795 data: 'div { color: if(foo(true, false), #fff, #000);' +
796 '\n background-color: if(foo(true, true), #fff, #000); }',
798 'foo($a, $b)': function(a, b) {
799 return sass.types.Boolean(a.getValue() && b.getValue());
802 }, function(error, result) {
803 assert.equal(result.css.toString().trim(), 'div {\n color: #000;\n background-color: #fff; }');
808 it('should let custom functions call setter methods on wrapped sass values (boolean)', function(done) {
810 data: 'div { color: if(foo(false), #fff, #000); background-color: if(foo(true), #fff, #000); }',
812 'foo($a)': function(a) {
813 return a.getValue() ? sass.types.Boolean.FALSE : sass.types.Boolean.TRUE;
816 }, function(error, result) {
817 assert.equal(result.css.toString().trim(), 'div {\n color: #fff;\n background-color: #000; }');
822 it('should properly convert lists when calling custom functions', function(done) {
824 data: '$test-list: (bar, #f00, 123em); @each $item in foo($test-list) { .#{$item} { color: #fff; } }',
826 'foo($l)': function(list) {
827 assert.equal(list.getLength(), 3);
828 assert.ok(list.getValue(0) instanceof sass.types.String);
829 assert.equal(list.getValue(0).getValue(), 'bar');
830 assert.ok(list.getValue(1) instanceof sass.types.Color);
831 assert.equal(list.getValue(1).getR(), 0xff);
832 assert.equal(list.getValue(1).getG(), 0);
833 assert.equal(list.getValue(1).getB(), 0);
834 assert.ok(list.getValue(2) instanceof sass.types.Number);
835 assert.equal(list.getValue(2).getValue(), 123);
836 assert.equal(list.getValue(2).getUnit(), 'em');
838 var out = new sass.types.List(3);
839 out.setValue(0, new sass.types.String('foo'));
840 out.setValue(1, new sass.types.String('bar'));
841 out.setValue(2, new sass.types.String('baz'));
845 }, function(error, result) {
847 result.css.toString().trim(),
848 '.foo {\n color: #fff; }\n\n.bar {\n color: #fff; }\n\n.baz {\n color: #fff; }'
854 it('should properly convert maps when calling custom functions', function(done) {
856 data: '$test-map: foo((abc: 123, #def: true)); div { color: if(map-has-key($test-map, hello), #fff, #000); }' +
857 'span { color: map-get($test-map, baz); }',
859 'foo($m)': function(map) {
860 assert.equal(map.getLength(), 2);
861 assert.ok(map.getKey(0) instanceof sass.types.String);
862 assert.ok(map.getKey(1) instanceof sass.types.Color);
863 assert.ok(map.getValue(0) instanceof sass.types.Number);
864 assert.ok(map.getValue(1) instanceof sass.types.Boolean);
865 assert.equal(map.getKey(0).getValue(), 'abc');
866 assert.equal(map.getValue(0).getValue(), 123);
867 assert.equal(map.getKey(1).getR(), 0xdd);
868 assert.equal(map.getValue(1).getValue(), true);
870 var out = new sass.types.Map(3);
871 out.setKey(0, new sass.types.String('hello'));
872 out.setValue(0, new sass.types.String('world'));
873 out.setKey(1, new sass.types.String('foo'));
874 out.setValue(1, new sass.types.String('bar'));
875 out.setKey(2, new sass.types.String('baz'));
876 out.setValue(2, new sass.types.String('qux'));
880 }, function(error, result) {
881 assert.equal(result.css.toString().trim(), 'div {\n color: #fff; }\n\nspan {\n color: qux; }');
886 it('should properly convert null when calling custom functions', function(done) {
888 data: 'div { color: if(foo("bar"), #fff, #000); } ' +
889 'span { color: if(foo(null), #fff, #000); }' +
890 'table { color: if(bar() == null, #fff, #000); }',
892 'foo($a)': function(a) {
893 return sass.types.Boolean(a instanceof sass.types.Null);
895 'bar()': function() {
899 }, function(error, result) {
901 result.css.toString().trim(),
902 'div {\n color: #000; }\n\nspan {\n color: #fff; }\n\ntable {\n color: #fff; }'
908 it('should be possible to carry sass values across different renders', function(done) {
912 data: 'div { color: foo((abc: #112233, #ddeeff: true)); }',
916 return sass.types.Color(0, 0, 0);
921 data: 'div { color: map-get(bar(), abc); background-color: baz(); }',
924 return persistentMap;
927 return persistentMap.getKey(1);
930 }, function(errror, result) {
931 assert.equal(result.css.toString().trim(), 'div {\n color: #112233;\n background-color: #ddeeff; }');
937 it('should let us register custom functions without signatures', function(done) {
939 data: 'div { color: foo(20, 22); }',
941 foo: function(a, b) {
942 return new sass.types.Number(a.getValue() + b.getValue(), 'em');
945 }, function(error, result) {
946 assert.equal(result.css.toString().trim(), 'div {\n color: 42em; }');
951 it('should fail when returning anything other than a sass value from a custom function', function(done) {
953 data: 'div { color: foo(); }',
955 'foo()': function() {
960 assert.ok(/A SassValue object was expected/.test(error.message));
965 it('should properly bubble up standard JS errors thrown by custom functions', function(done) {
967 data: 'div { color: foo(); }',
969 'foo()': function() {
970 throw new RangeError('This is a test error');
974 assert.ok(/This is a test error/.test(error.message));
979 it('should properly bubble up unknown errors thrown by custom functions', function(done) {
981 data: 'div { color: foo(); }',
983 'foo()': function() {
988 assert.ok(/unexpected error/.test(error.message));
993 it('should call custom functions with correct context', function(done) {
994 function assertExpected(result) {
995 assert.equal(result.css.toString().trim(), 'div {\n foo1: 1;\n foo2: 2; }');
998 data: 'div { foo1: foo(); foo2: foo(); }',
1000 // foo() is stateful and will persist an incrementing counter
1001 'foo()': function() {
1003 this.fooCounter = (this.fooCounter || 0) + 1;
1004 return new sass.types.Number(this.fooCounter);
1009 sass.render(options, function(error, result) {
1010 assertExpected(result);
1015 describe('should properly bubble up errors from sass color constructor', function() {
1016 it('four booleans', function(done) {
1018 data: 'div { color: foo(); }',
1020 'foo()': function() {
1021 return new sass.types.Color(false, false, false, false);
1024 }, function(error) {
1025 assert.ok(/Constructor arguments should be numbers exclusively/.test(error.message));
1030 it('two arguments', function(done) {
1032 data: 'div { color: foo(); }',
1034 'foo()': function() {
1035 return sass.types.Color(2,3);
1038 }, function(error) {
1039 assert.ok(/Constructor should be invoked with either 0, 1, 3 or 4 arguments/.test(error.message));
1044 it('single string argument', function(done) {
1046 data: 'div { color: foo(); }',
1048 'foo()': function() {
1049 return sass.types.Color('foo');
1052 }, function(error) {
1053 assert.ok(/Only argument should be an integer/.test(error.message));
1059 it('should properly bubble up errors from sass value constructors', function(done) {
1061 data: 'div { color: foo(); }',
1063 'foo()': function() {
1064 return sass.types.Boolean('foo');
1067 }, function(error) {
1068 assert.ok(/Expected one boolean argument/.test(error.message));
1073 it('should properly bubble up errors from sass value setters', function(done) {
1075 data: 'div { color: foo(); }',
1077 'foo()': function() {
1078 var ret = new sass.types.Number(42);
1083 }, function(error) {
1084 assert.ok(/Supplied value should be a string/.test(error.message));
1089 it('should fail when trying to set a bare number as the List item', function(done) {
1091 data: 'div { color: foo(); }',
1093 'foo()': function() {
1094 var out = new sass.types.List(1);
1099 }, function(error) {
1100 assert.ok(/Supplied value should be a SassValue object/.test(error.message));
1105 it('should fail when trying to set a bare Object as the List item', function(done) {
1107 data: 'div { color: foo(); }',
1109 'foo()': function() {
1110 var out = new sass.types.List(1);
1111 out.setValue(0, {});
1115 }, function(error) {
1116 assert.ok(/A SassValue is expected as the list item/.test(error.message));
1121 it('should fail when trying to set a bare Object as the Map key', function(done) {
1123 data: 'div { color: foo(); }',
1125 'foo()': function() {
1126 var out = new sass.types.Map(1);
1128 out.setValue(0, new sass.types.String('aaa'));
1132 }, function(error) {
1133 assert.ok(/A SassValue is expected as a map key/.test(error.message));
1138 it('should fail when trying to set a bare Object as the Map value', function(done) {
1140 data: 'div { color: foo(); }',
1142 'foo()': function() {
1143 var out = new sass.types.Map(1);
1144 out.setKey(0, new sass.types.String('aaa'));
1145 out.setValue(0, {});
1149 }, function(error) {
1150 assert.ok(/A SassValue is expected as a map value/.test(error.message));
1155 it('should always map null, true and false to the same (immutable) object', function(done) {
1159 data: 'div { color: foo(bar(null)); background-color: baz("foo" == "bar"); }',
1162 assert.strictEqual(a, sass.TRUE,
1163 'Supplied value should be the same instance as sass.TRUE'
1167 sass.types.Boolean(true), sass.types.Boolean(true),
1168 'sass.types.Boolean(true) should return a singleton');
1171 sass.types.Boolean(true), sass.TRUE,
1172 'sass.types.Boolean(true) should be the same instance as sass.TRUE');
1176 return sass.types.String('foo');
1179 assert.strictEqual(a, sass.NULL,
1180 'Supplied value should be the same instance as sass.NULL');
1182 assert.throws(function() {
1183 return new sass.types.Null();
1184 }, /Cannot instantiate SassNull/);
1191 assert.strictEqual(a, sass.FALSE,
1192 'Supplied value should be the same instance as sass.FALSE');
1194 assert.throws(function() {
1195 return new sass.types.Boolean(false);
1196 }, /Cannot instantiate SassBoolean/);
1199 sass.types.Boolean(false), sass.types.Boolean(false),
1200 'sass.types.Boolean(false) should return a singleton');
1203 sass.types.Boolean(false), sass.FALSE,
1204 'sass.types.Boolean(false) should return singleton identical to sass.FALSE');
1208 return sass.types.String('baz');
1212 assert.strictEqual(counter, 3);
1218 describe('.render({stats: {}})', function() {
1219 var start = Date.now();
1221 it('should provide a start timestamp', function(done) {
1223 file: fixture('include-files/index.scss')
1224 }, function(error, result) {
1226 assert.strictEqual(typeof result.stats.start, 'number');
1227 assert(result.stats.start >= start);
1232 it('should provide an end timestamp', function(done) {
1234 file: fixture('include-files/index.scss')
1235 }, function(error, result) {
1237 assert.strictEqual(typeof result.stats.end, 'number');
1238 assert(result.stats.end >= result.stats.start);
1243 it('should provide a duration', function(done) {
1245 file: fixture('include-files/index.scss')
1246 }, function(error, result) {
1248 assert.strictEqual(typeof result.stats.duration, 'number');
1249 assert.equal(result.stats.end - result.stats.start, result.stats.duration);
1254 it('should contain the given entry file', function(done) {
1256 file: fixture('include-files/index.scss')
1257 }, function(error, result) {
1259 assert.equal(result.stats.entry, fixture('include-files/index.scss'));
1264 it('should contain an array of all included files', function(done) {
1266 fixture('include-files/bar.scss').replace(/\\/g, '/'),
1267 fixture('include-files/foo.scss').replace(/\\/g, '/'),
1268 fixture('include-files/index.scss').replace(/\\/g, '/')
1272 file: fixture('include-files/index.scss')
1273 }, function(error, result) {
1275 assert.deepEqual(result.stats.includedFiles.sort(), expected.sort());
1280 it('should contain array with the entry if there are no import statements', function(done) {
1281 var expected = fixture('simple/index.scss').replace(/\\/g, '/');
1284 file: fixture('simple/index.scss')
1285 }, function(error, result) {
1286 assert.deepEqual(result.stats.includedFiles, [expected]);
1291 it('should state `data` as entry file', function(done) {
1293 data: read(fixture('simple/index.scss'), 'utf8')
1294 }, function(error, result) {
1295 assert.equal(result.stats.entry, 'data');
1300 it('should contain an empty array as includedFiles', function(done) {
1302 data: read(fixture('simple/index.scss'), 'utf8')
1303 }, function(error, result) {
1304 assert.deepEqual(result.stats.includedFiles, []);
1310 describe('.renderSync(options)', function() {
1311 it('should compile sass to css with file', function(done) {
1312 var expected = read(fixture('simple/expected.css'), 'utf8').trim();
1313 var result = sass.renderSync({ file: fixture('simple/index.scss') });
1315 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
1319 it('should compile sass to css with outFile set to absolute url', function(done) {
1320 var result = sass.renderSync({
1321 file: fixture('simple/index.scss'),
1323 outFile: fixture('simple/index-test.css')
1326 assert.equal(JSON.parse(result.map).file, 'index-test.css');
1330 it('should compile sass to css with outFile set to relative url', function(done) {
1331 var result = sass.renderSync({
1332 file: fixture('simple/index.scss'),
1334 outFile: './index-test.css'
1337 assert.equal(JSON.parse(result.map).file, 'index-test.css');
1341 it('should compile sass to css with outFile and sourceMap set to relative url', function(done) {
1342 var result = sass.renderSync({
1343 file: fixture('simple/index.scss'),
1344 sourceMap: './deep/nested/index.map',
1345 outFile: './index-test.css'
1348 assert.equal(JSON.parse(result.map).file, '../../index-test.css');
1352 it('should compile generate map with sourceMapRoot pass-through option', function(done) {
1353 var result = sass.renderSync({
1354 file: fixture('simple/index.scss'),
1355 sourceMap: './deep/nested/index.map',
1356 sourceMapRoot: 'http://test.com/',
1357 outFile: './index-test.css'
1360 assert.equal(JSON.parse(result.map).sourceRoot, 'http://test.com/');
1364 it('should compile sass to css with data', function(done) {
1365 var src = read(fixture('simple/index.scss'), 'utf8');
1366 var expected = read(fixture('simple/expected.css'), 'utf8').trim();
1367 var result = sass.renderSync({ data: src });
1369 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
1373 it('should compile sass to css using indented syntax', function(done) {
1374 var src = read(fixture('indent/index.sass'), 'utf8');
1375 var expected = read(fixture('indent/expected.css'), 'utf8').trim();
1376 var result = sass.renderSync({
1378 indentedSyntax: true
1381 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
1385 it('should NOT compile empty data string', function(done) {
1386 assert.throws(function() {
1387 sass.renderSync({ data: '' });
1388 }, /No input specified: provide a file name or a source string to process/ );
1392 it('should NOT compile without any input', function(done) {
1393 assert.throws(function() {
1394 sass.renderSync({});
1395 }, /No input specified: provide a file name or a source string to process/);
1399 it('should throw error for bad input', function(done) {
1400 assert.throws(function() {
1401 sass.renderSync('somestring');
1403 assert.throws(function() {
1404 sass.renderSync({ data: '#navbar width 80%;' });
1410 it('should compile with include paths', function(done) {
1411 var src = read(fixture('include-path/index.scss'), 'utf8');
1412 var expected = read(fixture('include-path/expected.css'), 'utf8').trim();
1413 var result = sass.renderSync({
1416 fixture('include-path/functions'),
1417 fixture('include-path/lib')
1421 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
1425 it('should add cwd to the front on include paths', function(done) {
1426 var src = fixture('cwd-include-path/root/index.scss');
1427 var expected = read(fixture('cwd-include-path/expected.css'), 'utf8').trim();
1428 var cwd = process.cwd();
1430 process.chdir(fixture('cwd-include-path'));
1431 var result = sass.renderSync({
1434 fixture('include-path/functions'),
1435 fixture('include-path/lib')
1440 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
1444 it('should check SASS_PATH in the specified order', function(done) {
1445 var src = read(fixture('sass-path/index.scss'), 'utf8');
1446 var expectedRed = read(fixture('sass-path/expected-red.css'), 'utf8').trim();
1447 var expectedOrange = read(fixture('sass-path/expected-orange.css'), 'utf8').trim();
1450 fixture('sass-path/red'),
1451 fixture('sass-path/orange')
1454 process.env.SASS_PATH = envIncludes.join(path.delimiter);
1455 var result = sass.renderSync({
1460 assert.equal(result.css.toString().trim(), expectedRed.replace(/\r\n/g, '\n'));
1462 process.env.SASS_PATH = envIncludes.reverse().join(path.delimiter);
1463 result = sass.renderSync({
1468 assert.equal(result.css.toString().trim(), expectedOrange.replace(/\r\n/g, '\n'));
1472 it('should prefer include path over SASS_PATH', function(done) {
1473 var src = read(fixture('sass-path/index.scss'), 'utf8');
1474 var expectedRed = read(fixture('sass-path/expected-red.css'), 'utf8').trim();
1475 var expectedOrange = read(fixture('sass-path/expected-orange.css'), 'utf8').trim();
1478 fixture('sass-path/red')
1480 process.env.SASS_PATH = envIncludes.join(path.delimiter);
1482 var result = sass.renderSync({
1487 assert.equal(result.css.toString().trim(), expectedRed.replace(/\r\n/g, '\n'));
1489 result = sass.renderSync({
1491 includePaths: [fixture('sass-path/orange')]
1494 assert.equal(result.css.toString().trim(), expectedOrange.replace(/\r\n/g, '\n'));
1498 it('should render with precision option', function(done) {
1499 var src = read(fixture('precision/index.scss'), 'utf8');
1500 var expected = read(fixture('precision/expected.css'), 'utf8').trim();
1501 var result = sass.renderSync({
1506 assert.equal(result.css.toString().trim(), expected.replace(/\r\n/g, '\n'));
1510 it('should contain all included files in stats when data is passed', function(done) {
1511 var src = read(fixture('include-files/index.scss'), 'utf8');
1513 fixture('include-files/bar.scss').replace(/\\/g, '/'),
1514 fixture('include-files/foo.scss').replace(/\\/g, '/')
1517 var result = sass.renderSync({
1519 includePaths: [fixture('include-files')]
1522 assert.deepEqual(result.stats.includedFiles, expected);
1526 it('should render with indentWidth and indentType options', function(done) {
1527 var result = sass.renderSync({
1528 data: 'div { color: transparent; }',
1533 assert.equal(result.css.toString().trim(), 'div {\n\t\t\t\t\t\t\tcolor: transparent; }');
1537 it('should render with linefeed option', function(done) {
1538 var result = sass.renderSync({
1539 data: 'div { color: transparent; }',
1543 assert.equal(result.css.toString().trim(), 'div {\n\r color: transparent; }');
1548 describe('.renderSync(importer)', function() {
1549 var src = read(fixture('include-files/index.scss'), 'utf8');
1551 it('should override imports with "data" as input and returns file and contents', function(done) {
1552 var result = sass.renderSync({
1554 importer: function(url, prev) {
1557 contents: 'div {color: yellow;}'
1562 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
1566 it('should override imports with "file" as input and returns file and contents', function(done) {
1567 var result = sass.renderSync({
1568 file: fixture('include-files/index.scss'),
1569 importer: function(url, prev) {
1572 contents: 'div {color: yellow;}'
1577 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
1581 it('should override imports with "data" as input and returns file', function(done) {
1582 var result = sass.renderSync({
1584 importer: function(url) {
1586 file: path.resolve(path.dirname(fixture('include-files/index.scss')), url + (path.extname(url) ? '' : '.scss'))
1591 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
1595 it('should override imports with "file" as input and returns file', function(done) {
1596 var result = sass.renderSync({
1597 file: fixture('include-files/index.scss'),
1598 importer: function(url, prev) {
1600 file: path.resolve(path.dirname(prev), url + (path.extname(url) ? '' : '.scss'))
1605 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
1609 it('should override imports with "data" as input and returns contents', function(done) {
1610 var result = sass.renderSync({
1612 importer: function() {
1614 contents: 'div {color: yellow;}'
1619 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
1623 it('should override imports with "file" as input and returns contents', function(done) {
1624 var result = sass.renderSync({
1625 file: fixture('include-files/index.scss'),
1626 importer: function() {
1628 contents: 'div {color: yellow;}'
1633 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
1639 it('should fallback to default import behaviour if importer returns sass.NULL', function(done) {
1640 var result = sass.renderSync({
1641 file: fixture('include-files/index.scss'),
1642 importer: function() {
1647 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
1651 it('should fallback to default import behaviour if importer returns null for backwards compatibility', function(done) {
1652 var result = sass.renderSync({
1653 file: fixture('include-files/index.scss'),
1654 importer: function() {
1659 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
1663 it('should fallback to default import behaviour if importer returns undefined for backwards compatibility', function(done) {
1664 var result = sass.renderSync({
1665 file: fixture('include-files/index.scss'),
1666 importer: function() {
1671 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
1675 it('should fallback to default import behaviour if importer returns false for backwards compatibility', function(done) {
1676 var result = sass.renderSync({
1677 file: fixture('include-files/index.scss'),
1678 importer: function() {
1683 assert.equal(result.css.toString().trim(), '/* foo.scss */\n/* bar.scss */');
1687 it('should accept arrays of importers and return respect the order', function(done) {
1688 var result = sass.renderSync({
1689 file: fixture('include-files/index.scss'),
1696 contents: 'div {color: yellow;}'
1702 assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }');
1706 it('should be able to see its options in this.options', function(done) {
1707 var fxt = fixture('include-files/index.scss');
1710 file: fixture('include-files/index.scss'),
1711 importer: function() {
1712 assert.equal(fxt, this.options.file);
1717 assert.equal(sync, true);
1721 it('should throw user-defined error', function(done) {
1722 assert.throws(function() {
1725 importer: function() {
1726 return new Error('doesn\'t exist!');
1729 }, /doesn\'t exist!/);
1734 it('should throw exception when importer returns an invalid value', function(done) {
1735 assert.throws(function() {
1738 importer: function() {
1739 return { contents: new Buffer('i am not a string!') };
1742 }, /returned value of `contents` must be a string/);
1748 describe('.renderSync(functions)', function() {
1749 it('should call custom function in sync mode', function(done) {
1750 var result = sass.renderSync({
1751 data: 'div { width: cos(0) * 50px; }',
1753 'cos($a)': function(angle) {
1754 if (!(angle instanceof sass.types.Number)) {
1755 throw new TypeError('Unexpected type for "angle"');
1757 return new sass.types.Number(Math.cos(angle.getValue()));
1762 assert.equal(result.css.toString().trim(), 'div {\n width: 50px; }');
1766 it('should return a list of selectors after calling the headings custom function', function(done) {
1767 var result = sass.renderSync({
1768 data: '#{headings(2,5)} { color: #08c; }',
1770 'headings($from: 0, $to: 6)': function(from, to) {
1771 var i, f = from.getValue(), t = to.getValue(),
1772 list = new sass.types.List(t - f + 1);
1774 for (i = f; i <= t; i++) {
1775 list.setValue(i - f, new sass.types.String('h' + i));
1783 assert.equal(result.css.toString().trim(), 'h2, h3, h4, h5 {\n color: #08c; }');
1787 it('should let custom function invoke sass types constructors without the `new` keyword', function(done) {
1788 var result = sass.renderSync({
1789 data: 'div { color: foo(); }',
1791 'foo()': function() {
1792 return sass.types.Number(42, 'em');
1797 assert.equal(result.css.toString().trim(), 'div {\n color: 42em; }');
1801 it('should let us register custom functions without signatures', function(done) {
1802 var result = sass.renderSync({
1803 data: 'div { color: foo(20, 22); }',
1805 foo: function(a, b) {
1806 return new sass.types.Number(a.getValue() + b.getValue(), 'em');
1811 assert.equal(result.css.toString().trim(), 'div {\n color: 42em; }');
1815 it('should fail when returning anything other than a sass value from a custom function', function(done) {
1816 assert.throws(function() {
1818 data: 'div { color: foo(); }',
1820 'foo()': function() {
1825 }, /A SassValue object was expected/);
1830 it('should properly bubble up standard JS errors thrown by custom functions', function(done) {
1831 assert.throws(function() {
1833 data: 'div { color: foo(); }',
1835 'foo()': function() {
1836 throw new RangeError('This is a test error');
1840 }, /This is a test error/);
1845 it('should properly bubble up unknown errors thrown by custom functions', function(done) {
1846 assert.throws(function() {
1848 data: 'div { color: foo(); }',
1850 'foo()': function() {
1855 }, /unexpected error/);
1860 it('should properly bubble up errors from sass value getters/setters/constructors', function(done) {
1861 assert.throws(function() {
1863 data: 'div { color: foo(); }',
1865 'foo()': function() {
1866 return sass.types.Boolean('foo');
1870 }, /Expected one boolean argument/);
1872 assert.throws(function() {
1874 data: 'div { color: foo(); }',
1876 'foo()': function() {
1877 var ret = new sass.types.Number(42);
1883 }, /Supplied value should be a string/);
1888 it('should call custom functions with correct context', function(done) {
1889 function assertExpected(result) {
1890 assert.equal(result.css.toString().trim(), 'div {\n foo1: 1;\n foo2: 2; }');
1893 data: 'div { foo1: foo(); foo2: foo(); }',
1895 // foo() is stateful and will persist an incrementing counter
1896 'foo()': function() {
1898 this.fooCounter = (this.fooCounter || 0) + 1;
1899 return new sass.types.Number(this.fooCounter);
1903 assertExpected(sass.renderSync(options));
1908 describe('.renderSync({stats: {}})', function() {
1909 var start = Date.now();
1910 var result = sass.renderSync({
1911 file: fixture('include-files/index.scss')
1914 it('should provide a start timestamp', function(done) {
1915 assert.strictEqual(typeof result.stats.start, 'number');
1916 assert(result.stats.start >= start);
1920 it('should provide an end timestamp', function(done) {
1921 assert.strictEqual(typeof result.stats.end, 'number');
1922 assert(result.stats.end >= result.stats.start);
1926 it('should provide a duration', function(done) {
1927 assert.strictEqual(typeof result.stats.duration, 'number');
1928 assert.equal(result.stats.end - result.stats.start, result.stats.duration);
1932 it('should contain the given entry file', function(done) {
1933 assert.equal(result.stats.entry, resolveFixture('include-files/index.scss'));
1937 it('should contain an array of all included files', function(done) {
1939 fixture('include-files/bar.scss').replace(/\\/g, '/'),
1940 fixture('include-files/foo.scss').replace(/\\/g, '/'),
1941 fixture('include-files/index.scss').replace(/\\/g, '/')
1943 var actual = result.stats.includedFiles.sort();
1945 assert.equal(actual[0], expected[0]);
1946 assert.equal(actual[1], expected[1]);
1947 assert.equal(actual[2], expected[2]);
1951 it('should contain array with the entry if there are no import statements', function(done) {
1952 var expected = fixture('simple/index.scss').replace(/\\/g, '/');
1954 var result = sass.renderSync({
1955 file: fixture('simple/index.scss')
1958 assert.deepEqual(result.stats.includedFiles, [expected]);
1962 it('should state `data` as entry file', function(done) {
1963 var result = sass.renderSync({
1964 data: read(fixture('simple/index.scss'), 'utf8')
1967 assert.equal(result.stats.entry, 'data');
1971 it('should contain an empty array as includedFiles', function(done) {
1972 var result = sass.renderSync({
1973 data: read(fixture('simple/index.scss'), 'utf8')
1976 assert.deepEqual(result.stats.includedFiles, []);
1981 describe('.info', function() {
1982 var package = require('../package.json'),
1985 it('should return a correct version info', function(done) {
1986 assert(info.indexOf(package.version) > 0);
1987 assert(info.indexOf('(Wrapper)') > 0);
1988 assert(info.indexOf('[JavaScript]') > 0);
1989 assert(info.indexOf('[NA]') < 0);
1990 assert(info.indexOf('(Sass Compiler)') > 0);
1991 assert(info.indexOf('[C/C++]') > 0);