// Copyright 2016 Google Inc. Use of this source code is governed by an // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. import 'dart:async'; import 'package:dart2_constant/convert.dart' as convert; import 'package:source_maps/source_maps.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test_process/test_process.dart'; import 'package:sass/sass.dart' as sass; import 'package:sass/src/io.dart'; import 'package:sass/src/util/path.dart'; import 'utils.dart'; /// Defines test that are shared between the Dart and Node.js CLI test suites. void sharedTests(Future runSass(Iterable arguments)) { /// Runs the executable on [arguments] plus an output file, then verifies that /// the contents of the output file match [expected]. Future expectCompiles(List arguments, expected) async { var sass = await runSass( arguments.toList()..add("out.css")..add("--no-source-map")); await sass.shouldExit(0); await d.file("out.css", expected).validate(); } test("--help prints the usage documentation", () async { // Checking the entire output is brittle, so just do a sanity check to make // sure it's not totally busted. var sass = await runSass(["--help"]); expect(sass.stdout, emits("Compile Sass to CSS.")); expect( sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test("compiles a Sass file to CSS", () async { await d.file("test.scss", "a {b: 1 + 2}").create(); var sass = await runSass(["test.scss"]); expect( sass.stdout, emitsInOrder([ "a {", " b: 3;", "}", ])); await sass.shouldExit(0); }); test("writes a CSS file to disk", () async { await d.file("test.scss", "a {b: 1 + 2}").create(); var sass = await runSass(["--no-source-map", "test.scss", "out.css"]); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d.file("out.css", equalsIgnoringWhitespace("a { b: 3; }")).validate(); }); test("creates directories if necessary", () async { await d.file("test.scss", "a {b: 1 + 2}").create(); var sass = await runSass(["--no-source-map", "test.scss", "some/new/dir/out.css"]); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d .file("some/new/dir/out.css", equalsIgnoringWhitespace("a { b: 3; }")) .validate(); }); test("compiles from stdin with the magic path -", () async { var sass = await runSass(["-"]); sass.stdin.writeln("a {b: 1 + 2}"); sass.stdin.close(); expect( sass.stdout, emitsInOrder([ "a {", " b: 3;", "}", ])); await sass.shouldExit(0); }); group("can import files", () { test("relative to the entrypoint", () async { await d.file("test.scss", "@import 'dir/test'").create(); await d.dir("dir", [d.file("test.scss", "a {b: 1 + 2}")]).create(); await expectCompiles( ["test.scss"], equalsIgnoringWhitespace("a { b: 3; }")); }); test("from the load path", () async { await d.file("test.scss", "@import 'test2'").create(); await d.dir("dir", [d.file("test2.scss", "a {b: c}")]).create(); await expectCompiles(["--load-path", "dir", "test.scss"], equalsIgnoringWhitespace("a { b: c; }")); }); test("relative in preference to from the load path", () async { await d.file("test.scss", "@import 'test2'").create(); await d.file("test2.scss", "x {y: z}").create(); await d.dir("dir", [d.file("test2.scss", "a {b: c}")]).create(); await expectCompiles(["--load-path", "dir", "test.scss"], equalsIgnoringWhitespace("x { y: z; }")); }); test("in load path order", () async { await d.file("test.scss", "@import 'test2'").create(); await d.dir("dir1", [d.file("test2.scss", "a {b: c}")]).create(); await d.dir("dir2", [d.file("test2.scss", "x {y: z}")]).create(); await expectCompiles( ["--load-path", "dir2", "--load-path", "dir1", "test.scss"], equalsIgnoringWhitespace("x { y: z; }")); }); }); group("with --stdin", () { test("compiles from stdin", () async { var sass = await runSass(["--stdin"]); sass.stdin.writeln("a {b: 1 + 2}"); sass.stdin.close(); expect( sass.stdout, emitsInOrder([ "a {", " b: 3;", "}", ])); await sass.shouldExit(0); }); test("writes a CSS file to disk", () async { var sass = await runSass(["--no-source-map", "--stdin", "out.css"]); sass.stdin.writeln("a {b: 1 + 2}"); sass.stdin.close(); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d .file("out.css", equalsIgnoringWhitespace("a { b: 3; }")) .validate(); }); test("uses the indented syntax with --indented", () async { var sass = await runSass(["--no-source-map", "--stdin", "--indented"]); sass.stdin.writeln("a\n b: 1 + 2"); sass.stdin.close(); expect( sass.stdout, emitsInOrder([ "a {", " b: 3;", "}", ])); await sass.shouldExit(0); }); }); group("with colon arguments", () { test("compiles multiple sources to multiple destinations", () async { await d.file("test1.scss", "a {b: c}").create(); await d.file("test2.scss", "x {y: z}").create(); var sass = await runSass( ["--no-source-map", "test1.scss:out1.css", "test2.scss:out2.css"]); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d .file("out1.css", equalsIgnoringWhitespace("a { b: c; }")) .validate(); await d .file("out2.css", equalsIgnoringWhitespace("x { y: z; }")) .validate(); }); test("creates destination directories", () async { await d.file("test.scss", "a {b: c}").create(); var sass = await runSass(["--no-source-map", "test.scss:dir/out.css"]); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d.dir("dir", [ d.file("out.css", equalsIgnoringWhitespace("a { b: c; }")) ]).validate(); }); test("creates source maps for each compilation", () async { await d.file("test1.scss", "a {b: c}").create(); await d.file("test2.scss", "x {y: z}").create(); var sass = await runSass(["test1.scss:out1.css", "test2.scss:out2.css"]); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d.file("out1.css", contains("out1.css.map")).validate(); await d.file("out1.css.map", contains("test1.scss")).validate(); await d.file("out2.css", contains("out2.css.map")).validate(); await d.file("out2.css.map", contains("test2.scss")).validate(); }); group("with a directory argument", () { test("compiles all the stylesheets in the directory", () async { await d.dir("in", [ d.file("test1.scss", "a {b: c}"), d.file("test2.sass", "x\n y: z") ]).create(); var sass = await runSass(["--no-source-map", "in:out"]); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d.dir("out", [ d.file("test1.css", equalsIgnoringWhitespace("a { b: c; }")), d.file("test2.css", equalsIgnoringWhitespace("x { y: z; }")) ]).validate(); }); test("creates subdirectories in the destination", () async { await d.dir("in", [ d.dir("sub", [d.file("test.scss", "a {b: c}")]) ]).create(); var sass = await runSass(["--no-source-map", "in:out"]); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d.dir("out", [ d.dir("sub", [d.file("test.css", equalsIgnoringWhitespace("a { b: c; }"))]) ]).validate(); }); test("ignores partials", () async { await d.dir("in", [ d.file("_fake.scss", "a {b:"), d.file("real.scss", "x {y: z}") ]).create(); var sass = await runSass(["--no-source-map", "in:out"]); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d.dir("out", [ d.file("real.css", equalsIgnoringWhitespace("x { y: z; }")), d.nothing("fake.css"), d.nothing("_fake.css") ]).validate(); }); test("ignores files without a Sass extension", () async { await d.dir("in", [ d.file("fake.szss", "a {b:"), d.file("real.scss", "x {y: z}") ]).create(); var sass = await runSass(["--no-source-map", "in:out"]); expect(sass.stdout, emitsDone); await sass.shouldExit(0); await d.dir("out", [ d.file("real.css", equalsIgnoringWhitespace("x { y: z; }")), d.nothing("fake.css") ]).validate(); }); }); group("reports all", () { test("file-not-found errors", () async { var sass = await runSass(["test1.scss:out1.css", "test2.scss:out2.css"]); expect( sass.stderr, emitsInOrder([ startsWith("Error reading test1.scss: "), "", startsWith("Error reading test2.scss: ") ])); await sass.shouldExit(66); }); test("compilation errors", () async { await d.file("test1.scss", "a {b: }").create(); await d.file("test2.scss", "x {y: }").create(); var sass = await runSass(["test1.scss:out1.css", "test2.scss:out2.css"]); expect( sass.stderr, emitsInOrder([ "Error: Expected expression.", "a {b: }", " ^", " test1.scss 1:7 root stylesheet", "", "Error: Expected expression.", "x {y: }", " ^", " test2.scss 1:7 root stylesheet" ])); await sass.shouldExit(65); }); test("runtime errors", () async { await d.file("test1.scss", "a {b: 1 + #abc}").create(); await d.file("test2.scss", "x {y: 1 + #abc}").create(); var sass = await runSass(["test1.scss:out1.css", "test2.scss:out2.css"]); expect( sass.stderr, emitsInOrder([ 'Error: Undefined operation "1 + #abc".', "a {b: 1 + #abc}", " ^^^^^^^^", " test1.scss 1:7 root stylesheet", "", 'Error: Undefined operation "1 + #abc".', "x {y: 1 + #abc}", " ^^^^^^^^", " test2.scss 1:7 root stylesheet" ])); await sass.shouldExit(65); }); }); group("doesn't allow", () { group("positional arguments", () { test("before", () async { var sass = await runSass(["positional", "test.scss:out.css"]); expect(sass.stdout, emits('Positional and ":" arguments may not both be used.')); await sass.shouldExit(64); }); test("after", () async { var sass = await runSass(["test.scss:out.css", "positional"]); expect(sass.stdout, emits('Positional and ":" arguments may not both be used.')); await sass.shouldExit(64); }); }); test("--stdin", () async { var sass = await runSass(["--stdin", "test.scss:out.css"]); expect( sass.stdout, emits('--stdin may not be used with ":" arguments.')); await sass.shouldExit(64); }); test("multiple colons", () async { var sass = await runSass(["test.scss:out.css:wut"]); expect(sass.stdout, emits('"test.scss:out.css:wut" may only contain one ":".')); await sass.shouldExit(64); }); test("duplicate sources", () async { var sass = await runSass(["test.scss:out1.css", "test.scss:out2.css"]); expect(sass.stdout, emits('Duplicate source "test.scss".')); await sass.shouldExit(64); }); }); }); test("gracefully reports errors from stdin", () async { var sass = await runSass(["-"]); sass.stdin.writeln("a {b: 1 + }"); sass.stdin.close(); expect( sass.stderr, emitsInOrder([ "Error: Expected expression.", "a {b: 1 + }", " ^", " - 1:11 root stylesheet", ])); await sass.shouldExit(65); }); test("supports relative imports", () async { await d.file("test.scss", "@import 'dir/test'").create(); await d.dir("dir", [d.file("test.scss", "a {b: 1 + 2}")]).create(); var sass = await runSass(["test.scss"]); expect( sass.stdout, emitsInOrder([ "a {", " b: 3;", "}", ])); await sass.shouldExit(0); }); test("emits warnings on standard error", () async { await d.file("test.scss", "@warn 'aw beans'").create(); var sass = await runSass(["test.scss"]); expect(sass.stdout, emitsDone); expect( sass.stderr, emitsInOrder([ "WARNING: aw beans", " test.scss 1:1 root stylesheet", ])); await sass.shouldExit(0); }); test("emits debug messages on standard error", () async { await d.file("test.scss", "@debug 'what the heck'").create(); var sass = await runSass(["test.scss"]); expect(sass.stdout, emitsDone); expect(sass.stderr, emits("test.scss:1 DEBUG: what the heck")); await sass.shouldExit(0); }); group("with --quiet", () { test("doesn't emit @warn", () async { await d.file("test.scss", "@warn heck").create(); var sass = await runSass(["--quiet", "test.scss"]); expect(sass.stderr, emitsDone); await sass.shouldExit(0); }); test("doesn't emit @debug", () async { await d.file("test.scss", "@debug heck").create(); var sass = await runSass(["--quiet", "test.scss"]); expect(sass.stderr, emitsDone); await sass.shouldExit(0); }); test("doesn't emit parser warnings", () async { await d.file("test.scss", "a {b: c && d}").create(); var sass = await runSass(["--quiet", "test.scss"]); expect(sass.stderr, emitsDone); await sass.shouldExit(0); }); test("doesn't emit runner warnings", () async { await d.file("test.scss", "#{blue} {x: y}").create(); var sass = await runSass(["--quiet", "test.scss"]); expect(sass.stderr, emitsDone); await sass.shouldExit(0); }); }); group("source maps:", () { group("for a simple compilation", () { Map map; setUp(() async { await d.file("test.scss", "a {b: 1 + 2}").create(); await (await runSass(["test.scss", "out.css"])).shouldExit(0); map = _readJson("out.css.map"); }); test("refers to the source file", () { expect(map, containsPair("sources", ["test.scss"])); }); test("refers to the target file", () { expect(map, containsPair("file", "out.css")); }); test("contains mappings", () { SingleMapping sourceMap; sass.compileString("a {b: 1 + 2}", sourceMap: (map) => sourceMap = map); expect(map, containsPair("mappings", sourceMap.toJson()["mappings"])); }); }); group("with multiple sources", () { setUp(() async { await d.file("test.scss", """ @import 'dir/other'; x {y: z} """).create(); await d.dir("dir", [d.file("other.scss", "a {b: 1 + 2}")]).create(); }); test("refers to them using relative URLs by default", () async { await (await runSass(["test.scss", "out.css"])).shouldExit(0); expect(_readJson("out.css.map"), containsPair("sources", ["dir/other.scss", "test.scss"])); }); test("refers to them using relative URLs with --source-map-urls=relative", () async { await (await runSass( ["--source-map-urls=relative", "test.scss", "out.css"])) .shouldExit(0); expect(_readJson("out.css.map"), containsPair("sources", ["dir/other.scss", "test.scss"])); }); test("refers to them using absolute URLs with --source-map-urls=absolute", () async { await (await runSass( ["--source-map-urls=absolute", "test.scss", "out.css"])) .shouldExit(0); expect( _readJson("out.css.map"), containsPair("sources", [ p .toUri(p.canonicalize(p.join(d.sandbox, "dir/other.scss"))) .toString(), p.toUri(p.canonicalize(p.join(d.sandbox, "test.scss"))).toString() ])); }); test("includes source contents with --embed-sources", () async { await (await runSass(["--embed-sources", "test.scss", "out.css"])) .shouldExit(0); expect( _readJson("out.css.map"), containsPair("sourcesContent", ["a {b: 1 + 2}", readFile(p.join(d.sandbox, "test.scss"))])); }); }); test("refers to a source in another directory", () async { await d.dir("in", [d.file("test.scss", "x {y: z}")]).create(); await (await runSass(["in/test.scss", "out/test.css"])).shouldExit(0); expect(_readJson("out/test.css.map"), containsPair("sources", ["../in/test.scss"])); }); test("includes a source map comment", () async { await d.file("test.scss", "a {b: c}").create(); await (await runSass(["test.scss", "out.css"])).shouldExit(0); await d .file( "out.css", endsWith("\n\n/*# sourceMappingURL=out.css.map */\n")) .validate(); }); test("with --stdin uses an empty string", () async { var sass = await runSass(["--stdin", "out.css"]); sass.stdin.writeln("a {b: c}"); sass.stdin.close(); await sass.shouldExit(0); expect(_readJson("out.css.map"), containsPair("sources", [""])); }); group("with --no-source-map,", () { setUp(() async { await d.file("test.scss", "a {b: c}").create(); }); test("no source map is generated", () async { await (await runSass(["--no-source-map", "test.scss", "out.css"])) .shouldExit(0); await d.file("out.css", isNot(contains("/*#"))).validate(); await d.nothing("out.css.map").validate(); }); test("--source-map-urls is disallowed", () async { var sass = await runSass([ "--no-source-map", "--source-map-urls=absolute", "test.scss", "out.css" ]); expect(sass.stdout, emits("--source-map-urls isn't allowed with --no-source-map.")); expect(sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test("--embed-sources is disallowed", () async { var sass = await runSass( ["--no-source-map", "--embed-sources", "test.scss", "out.css"]); expect(sass.stdout, emits("--embed-sources isn't allowed with --no-source-map.")); expect(sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test("--embed-source-map is disallowed", () async { var sass = await runSass( ["--no-source-map", "--embed-source-map", "test.scss", "out.css"]); expect(sass.stdout, emits("--embed-source-map isn't allowed with --no-source-map.")); expect(sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); }); group("when emitting to stdout", () { test("--source-map isn't allowed", () async { await d.file("test.scss", "a {b: c}").create(); var sass = await runSass(["--source-map", "test.scss"]); expect( sass.stdout, emits("When printing to stdout, --source-map requires " "--embed-source-map.")); expect(sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test("--source-map-urls is disallowed", () async { await d.file("test.scss", "a {b: c}").create(); var sass = await runSass(["--source-map-urls=absolute", "test.scss"]); expect( sass.stdout, emits("When printing to stdout, --source-map-urls requires " "--embed-source-map.")); expect(sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test("--embed-sources is disallowed", () async { await d.file("test.scss", "a {b: c}").create(); var sass = await runSass(["--embed-sources", "test.scss"]); expect( sass.stdout, emits("When printing to stdout, --embed-sources requires " "--embed-source-map.")); expect(sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test( "--source-map-urls=relative is disallowed even with " "--embed-source-map", () async { await d.file("test.scss", "a {b: c}").create(); var sass = await runSass( ["--source-map-urls=relative", "--embed-source-map", "test.scss"]); expect( sass.stdout, emits("--source-map-urls=relative isn't allowed when printing to " "stdout.")); expect(sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test("everything is allowed with --embed-source-map", () async { await d.file("test.scss", "a {b: c}").create(); var sass = await runSass([ "--source-map", "--source-map-urls=absolute", "--embed-sources", "--embed-source-map", "test.scss" ]); var css = (await sass.stdout.rest.toList()).join("\n"); await sass.shouldExit(0); var map = embeddedSourceMap(css); expect(map, isNotEmpty); expect(map, isNot(contains("file"))); }); }); group("with --embed-source-map", () { setUp(() async { await d.file("test.scss", "a {b: 1 + 2}").create(); }); Map map; group("with the target in the same directory", () { setUp(() async { await (await runSass(["--embed-source-map", "test.scss", "out.css"])) .shouldExit(0); var css = readFile(p.join(d.sandbox, "out.css")); map = embeddedSourceMap(css); }); test("contains mappings in the generated CSS", () { SingleMapping sourceMap; sass.compileString("a {b: 1 + 2}", sourceMap: (map) => sourceMap = map); expect(map, containsPair("mappings", sourceMap.toJson()["mappings"])); }); test("refers to the source file", () { expect(map, containsPair("sources", ["test.scss"])); }); test("refers to the target file", () { expect(map, containsPair("file", "out.css")); }); test("doesn't generate a source map file", () async { await d.nothing("out.css.map").validate(); }); }); group("with the target in a different directory", () { setUp(() async { await ensureDir(p.join(d.sandbox, "dir")); await (await runSass( ["--embed-source-map", "test.scss", "dir/out.css"])) .shouldExit(0); var css = readFile(p.join(d.sandbox, "dir/out.css")); map = embeddedSourceMap(css); }); test("refers to the source file", () { expect(map, containsPair("sources", ["../test.scss"])); }); test("refers to the target file", () { expect(map, containsPair("file", "out.css")); }); }); }); }); group("reports errors", () { test("from invalid arguments", () async { var sass = await runSass(["--asdf"]); expect( sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test("from too many positional arguments", () async { var sass = await runSass(["abc", "def", "ghi"]); expect( sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test("from too many positional arguments with --stdin", () async { var sass = await runSass(["--stdin", "abc", "def"]); expect( sass.stdout, emitsThrough(contains("Print this usage information."))); await sass.shouldExit(64); }); test("from a file that doesn't exist", () async { var sass = await runSass(["asdf"]); expect(sass.stderr, emits(startsWith("Error reading asdf:"))); expect(sass.stderr, emitsDone); await sass.shouldExit(66); }); test("from invalid syntax", () async { await d.file("test.scss", "a {b: }").create(); var sass = await runSass(["test.scss"]); expect( sass.stderr, emitsInOrder([ "Error: Expected expression.", "a {b: }", " ^", " test.scss 1:7 root stylesheet", ])); await sass.shouldExit(65); }); test("from the runtime", () async { await d.file("test.scss", "a {b: 1px + 1deg}").create(); var sass = await runSass(["test.scss"]); expect( sass.stderr, emitsInOrder([ "Error: Incompatible units deg and px.", "a {b: 1px + 1deg}", " ^^^^^^^^^^", " test.scss 1:7 root stylesheet", ])); await sass.shouldExit(65); }); test("with colors with --color", () async { await d.file("test.scss", "a {b: }").create(); var sass = await runSass(["--color", "test.scss"]); expect( sass.stderr, emitsInOrder([ "Error: Expected expression.", "a {b: \u001b[31m\u001b[0m}", " \u001b[31m^\u001b[0m", " test.scss 1:7 root stylesheet", ])); await sass.shouldExit(65); }); test("with full stack traces with --trace", () async { await d.file("test.scss", "a {b: }").create(); var sass = await runSass(["--trace", "test.scss"]); expect(sass.stderr, emitsThrough(contains("\.dart"))); await sass.shouldExit(65); }); test("for package urls", () async { await d.file("test.scss", "@import 'package:nope/test';").create(); var sass = await runSass(["test.scss"]); expect( sass.stderr, emitsInOrder([ "Error: \"package:\" URLs aren't supported on this platform.", "@import 'package:nope/test';", " ^^^^^^^^^^^^^^^^^^^", " test.scss 1:9 root stylesheet" ])); await sass.shouldExit(65); }); }); } /// Reads the file at [path] within [d.sandbox] and JSON-decodes it. Map _readJson(String path) => convert.json.decode(readFile(p.join(d.sandbox, path))) as Map;