mirror of
https://github.com/danog/dart-sass.git
synced 2024-12-12 09:09:39 +01:00
561 lines
18 KiB
Dart
561 lines
18 KiB
Dart
// Copyright 2018 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.
|
|
|
|
@TestOn('node')
|
|
@Tags(['node'])
|
|
|
|
import 'dart:async';
|
|
import 'dart:js_util';
|
|
|
|
import 'package:js/js.dart';
|
|
import 'package:node_interop/js.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import 'package:sass/src/io.dart';
|
|
import 'package:sass/src/value/number.dart';
|
|
|
|
import '../ensure_npm_package.dart';
|
|
import '../hybrid.dart';
|
|
import 'api.dart';
|
|
import 'utils.dart';
|
|
|
|
// TODO: no dynamic
|
|
|
|
void main() {
|
|
setUpAll(ensureNpmPackage);
|
|
useSandbox();
|
|
|
|
group("rejects a signature", () {
|
|
test("with an invalid argument list", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: "", functions: jsify({"foo(": allowInterop(neverCalled)})));
|
|
expect(error.toString(), contains('Invalid signature'));
|
|
});
|
|
|
|
test("that's an empty string", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: "", functions: jsify({"": allowInterop(neverCalled)})));
|
|
expect(error.toString(), contains('Invalid signature'));
|
|
});
|
|
|
|
test("that's just an argument list", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: "", functions: jsify({r"($var)": allowInterop(neverCalled)})));
|
|
expect(error.toString(), contains('Invalid signature'));
|
|
});
|
|
|
|
test("with an invalid identifier", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: "", functions: jsify({"~~~": allowInterop(neverCalled)})));
|
|
expect(error.toString(), contains('Invalid signature'));
|
|
});
|
|
});
|
|
|
|
group("allows a signature", () {
|
|
test("with no argument list", () {
|
|
expect(
|
|
renderSync(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop(expectAsync0(
|
|
() => callConstructor(sass.types.Number, [12])))
|
|
}))),
|
|
equalsIgnoringWhitespace("a { b: 12; }"));
|
|
});
|
|
|
|
test("with an empty argument list", () {
|
|
expect(
|
|
renderSync(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo()": allowInterop(expectAsync0(
|
|
() => callConstructor(sass.types.Number, [12])))
|
|
}))),
|
|
equalsIgnoringWhitespace("a { b: 12; }"));
|
|
});
|
|
});
|
|
|
|
group("are dash-normalized", () {
|
|
test("when defined with dashes", () {
|
|
expect(
|
|
renderSync(RenderOptions(
|
|
data: "a {b: foo_bar()}",
|
|
functions: jsify({
|
|
"foo-bar": allowInterop(expectAsync0(
|
|
() => callConstructor(sass.types.Number, [12])))
|
|
}))),
|
|
equalsIgnoringWhitespace("a { b: 12; }"));
|
|
});
|
|
|
|
test("when defined with underscores", () {
|
|
expect(
|
|
renderSync(RenderOptions(
|
|
data: "a {b: foo-bar()}",
|
|
functions: jsify({
|
|
"foo_bar": allowInterop(expectAsync0(
|
|
() => callConstructor(sass.types.Number, [12])))
|
|
}))),
|
|
equalsIgnoringWhitespace("a { b: 12; }"));
|
|
});
|
|
});
|
|
|
|
group("rejects function calls that", () {
|
|
test("have too few arguments", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({r"foo($var)": allowInterop(neverCalled)})));
|
|
expect(error.toString(), contains(r'Missing argument $var'));
|
|
});
|
|
|
|
test("have too many arguments", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: "a {b: foo(1, 2)}",
|
|
functions: jsify({r"foo($var)": allowInterop(neverCalled)})));
|
|
expect(error.toString(),
|
|
contains('Only 1 argument allowed, but 2 were passed.'));
|
|
});
|
|
|
|
test("passes a non-existent named argument", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: r"a {b: foo($val: 1)}",
|
|
functions: jsify({r"foo()": allowInterop(neverCalled)})));
|
|
expect(error.toString(), contains(r'No argument named $val.'));
|
|
});
|
|
});
|
|
|
|
group("passes arguments", () {
|
|
test("by position", () {
|
|
expect(
|
|
renderSync(RenderOptions(
|
|
data: "a {b: last(1px, 2em)}",
|
|
functions: jsify({
|
|
r"last($value1, $value2)":
|
|
allowInterop(expectAsync2((value1, value2) => value2))
|
|
}))),
|
|
equalsIgnoringWhitespace("a { b: 2em; }"));
|
|
});
|
|
|
|
test("by name", () {
|
|
expect(
|
|
renderSync(RenderOptions(
|
|
data: r"a {b: last($value2: 1px, $value1: 2em)}",
|
|
functions: jsify({
|
|
r"last($value1, $value2)":
|
|
allowInterop(expectAsync2((value1, value2) => value2))
|
|
}))),
|
|
equalsIgnoringWhitespace("a { b: 1px; }"));
|
|
});
|
|
|
|
test("by splat", () {
|
|
expect(
|
|
renderSync(RenderOptions(
|
|
data: "a {b: last((1px 2em)...)}",
|
|
functions: jsify({
|
|
r"last($value1, $value2)":
|
|
allowInterop(expectAsync2((value1, value2) => value2))
|
|
}))),
|
|
equalsIgnoringWhitespace("a { b: 2em; }"));
|
|
});
|
|
|
|
test("by arglist", () {
|
|
expect(
|
|
renderSync(RenderOptions(
|
|
data: "a {b: last(1px, 2em)}",
|
|
functions: jsify({
|
|
r"last($args...)": allowInterop(expectAsync1(
|
|
(NodeSassList args) => args.getValue(args.getLength() - 1)))
|
|
}))),
|
|
equalsIgnoringWhitespace("a { b: 2em; }"));
|
|
});
|
|
});
|
|
|
|
group("rejects a return value that", () {
|
|
test("isn't a Sass value", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({"foo": allowInterop(expectAsync0(() => 10))})));
|
|
expect(error.toString(), contains('must be a Sass value type'));
|
|
});
|
|
|
|
test("is null", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({"foo": allowInterop(expectAsync0(() => null))})));
|
|
expect(error.toString(), contains('must be a Sass value type'));
|
|
});
|
|
});
|
|
|
|
group('this', () {
|
|
String sassPath;
|
|
setUp(() async {
|
|
sassPath = p.join(sandbox, 'test.scss');
|
|
});
|
|
|
|
test('includes default option values', () {
|
|
renderSync(RenderOptions(
|
|
data: 'a {b: foo()}',
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
var options = this_.options;
|
|
expect(options.includePaths, equals(p.current));
|
|
expect(options.precision, equals(SassNumber.precision));
|
|
expect(options.style, equals(1));
|
|
expect(options.indentType, equals(0));
|
|
expect(options.indentWidth, equals(2));
|
|
expect(options.linefeed, equals('\n'));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
|
|
test('includes the data when rendering via data', () {
|
|
renderSync(RenderOptions(
|
|
data: 'a {b: foo()}',
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.data, equals('a {b: foo()}'));
|
|
expect(this_.options.file, isNull);
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
|
|
test('includes the filename when rendering via file', () async {
|
|
await writeTextFile(sassPath, 'a {b: foo()}');
|
|
renderSync(RenderOptions(
|
|
file: sassPath,
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.data, isNull);
|
|
expect(this_.options.file, equals(sassPath));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
|
|
test('includes other include paths', () {
|
|
renderSync(RenderOptions(
|
|
data: 'a {b: foo()}',
|
|
includePaths: [sandbox],
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.includePaths,
|
|
equals('${p.current}${isWindows ? ';' : ':'}$sandbox'));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
|
|
group('can override', () {
|
|
test('indentWidth', () {
|
|
renderSync(RenderOptions(
|
|
data: 'a {b: foo()}',
|
|
indentWidth: 5,
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.indentWidth, equals(5));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
|
|
test('indentType', () {
|
|
renderSync(RenderOptions(
|
|
data: 'a {b: foo()}',
|
|
indentType: 'tab',
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.indentType, equals(1));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
|
|
test('linefeed', () {
|
|
renderSync(RenderOptions(
|
|
data: 'a {b: foo()}',
|
|
linefeed: 'cr',
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.linefeed, equals('\r'));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
});
|
|
|
|
test('has a circular reference', () {
|
|
renderSync(RenderOptions(
|
|
data: 'a {b: foo()}',
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.context, same(this_));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
|
|
group('includes render stats with', () {
|
|
test('a start time', () {
|
|
var start = DateTime.now();
|
|
renderSync(RenderOptions(
|
|
data: 'a {b: foo()}',
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.result.stats.start,
|
|
greaterThanOrEqualTo(start.millisecondsSinceEpoch));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
|
|
test('a data entry', () {
|
|
renderSync(RenderOptions(
|
|
data: 'a {b: foo()}',
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.result.stats.entry, equals('data'));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
|
|
test('a file entry', () async {
|
|
await writeTextFile(sassPath, 'a {b: foo()}');
|
|
renderSync(RenderOptions(
|
|
file: sassPath,
|
|
functions: jsify({
|
|
'foo': allowInteropCaptureThis(expectAsync1((RenderContext this_) {
|
|
expect(this_.options.result.stats.entry, equals(sassPath));
|
|
return callConstructor(sass.types.Number, [12]);
|
|
}))
|
|
}),
|
|
));
|
|
});
|
|
});
|
|
});
|
|
|
|
test("gracefully handles an error from the function", () {
|
|
var error = renderSyncError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({"foo": allowInterop(() => throw "aw beans")})));
|
|
expect(error.toString(), contains('aw beans'));
|
|
});
|
|
|
|
group("render()", () {
|
|
test("runs a synchronous function", () {
|
|
expect(
|
|
render(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop(
|
|
(void _) => callConstructor(sass.types.Number, [1]))
|
|
}))),
|
|
completion(equalsIgnoringWhitespace("a { b: 1; }")));
|
|
});
|
|
|
|
test("runs an asynchronous function", () {
|
|
expect(
|
|
render(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop((void done(Object result)) {
|
|
Timer(Duration.zero, () {
|
|
done(callConstructor(sass.types.Number, [1]));
|
|
});
|
|
})
|
|
}))),
|
|
completion(equalsIgnoringWhitespace("a { b: 1; }")));
|
|
});
|
|
|
|
test("reports a synchronous error", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions:
|
|
jsify({"foo": allowInterop((void _) => throw "aw beans")})));
|
|
expect(error.toString(), contains('aw beans'));
|
|
});
|
|
|
|
test("reports a synchronous sass.types.Error", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop(
|
|
(void _) => callConstructor(sass.types.Error, ["aw beans"]))
|
|
})));
|
|
expect(error.toString(), contains('aw beans'));
|
|
});
|
|
|
|
test("reports an asynchronous error", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop((void done(Object result)) {
|
|
Timer(Duration.zero, () {
|
|
done(JsError("aw beans"));
|
|
});
|
|
})
|
|
})));
|
|
expect(error.toString(), contains('aw beans'));
|
|
});
|
|
|
|
test("reports an asynchronous sass.types.Error", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop((void done(Object result)) {
|
|
Timer(Duration.zero, () {
|
|
done(callConstructor(sass.types.Error, ["aw beans"]));
|
|
});
|
|
})
|
|
})));
|
|
expect(error.toString(), contains('aw beans'));
|
|
});
|
|
|
|
test("reports a null return", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop((void done(Object result)) {
|
|
Timer(Duration.zero, () {
|
|
done(null);
|
|
});
|
|
})
|
|
})));
|
|
expect(error.toString(), contains('must be a Sass value type'));
|
|
});
|
|
|
|
test("reports a call to done without arguments", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop((void done()) {
|
|
Timer(Duration.zero, () {
|
|
done();
|
|
});
|
|
})
|
|
})));
|
|
expect(error.toString(), contains('must be a Sass value type'));
|
|
});
|
|
|
|
test("reports an invalid signature", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "", functions: jsify({"foo(": allowInterop(neverCalled)})));
|
|
expect(error.toString(), contains('Invalid signature'));
|
|
});
|
|
|
|
group("with fibers", () {
|
|
setUpAll(() {
|
|
try {
|
|
fiber;
|
|
} catch (_) {
|
|
throw "Can't load fibers package.\n"
|
|
"Run pub run grinder before-test.";
|
|
}
|
|
});
|
|
|
|
test("runs a synchronous function", () {
|
|
expect(
|
|
render(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop(
|
|
(void _) => callConstructor(sass.types.Number, [1]))
|
|
}),
|
|
fiber: fiber)),
|
|
completion(equalsIgnoringWhitespace("a { b: 1; }")));
|
|
});
|
|
|
|
test("runs an asynchronous function", () {
|
|
expect(
|
|
render(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop((void done(Object result)) {
|
|
Timer(Duration.zero, () {
|
|
done(callConstructor(sass.types.Number, [1]));
|
|
});
|
|
})
|
|
}),
|
|
fiber: fiber)),
|
|
completion(equalsIgnoringWhitespace("a { b: 1; }")));
|
|
});
|
|
|
|
test("reports a synchronous error", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions:
|
|
jsify({"foo": allowInterop((void _) => throw "aw beans")}),
|
|
fiber: fiber));
|
|
expect(error.toString(), contains('aw beans'));
|
|
});
|
|
|
|
test("reports an asynchronous error", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop((void done(Object result)) {
|
|
Timer(Duration.zero, () {
|
|
done(JsError("aw beans"));
|
|
});
|
|
})
|
|
}),
|
|
fiber: fiber));
|
|
expect(error.toString(), contains('aw beans'));
|
|
});
|
|
|
|
test("reports a null return", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop((void done(Object result)) {
|
|
Timer(Duration.zero, () {
|
|
done(null);
|
|
});
|
|
})
|
|
}),
|
|
fiber: fiber));
|
|
expect(error.toString(), contains('must be a Sass value type'));
|
|
});
|
|
|
|
test("reports a call to done without arguments", () async {
|
|
var error = await renderError(RenderOptions(
|
|
data: "a {b: foo()}",
|
|
functions: jsify({
|
|
"foo": allowInterop((void done()) {
|
|
Timer(Duration.zero, () {
|
|
done();
|
|
});
|
|
})
|
|
}),
|
|
fiber: fiber));
|
|
expect(error.toString(), contains('must be a Sass value type'));
|
|
});
|
|
});
|
|
});
|
|
|
|
// Node Sass currently doesn't provide any representation of first-class
|
|
// functions, but they shouldn't crash or be corrupted.
|
|
test("a function is passed through as-is", () {
|
|
expect(
|
|
renderSync(RenderOptions(
|
|
data: "a {b: call(id(get-function('str-length')), 'foo')}",
|
|
functions: jsify({
|
|
r"id($value)": allowInterop(expectAsync1((value) => value))
|
|
}))),
|
|
equalsIgnoringWhitespace("a { b: 3; }"));
|
|
});
|
|
}
|