Test the Node.js API.

Closes #150
This commit is contained in:
Natalie Weizenbaum 2017-07-06 17:36:15 -07:00
parent 2dd7601ba6
commit 6ed530c895
14 changed files with 362 additions and 41 deletions

View File

@ -6,10 +6,11 @@ import 'package:js/js.dart';
import 'exception.dart'; import 'exception.dart';
import 'executable.dart' as executable; import 'executable.dart' as executable;
import 'node/error.dart';
import 'node/exports.dart'; import 'node/exports.dart';
import 'node/options.dart'; import 'node/render_error.dart';
import 'node/result.dart'; import 'node/render_options.dart';
import 'node/render_result.dart';
import 'node/utils.dart';
import 'render.dart'; import 'render.dart';
import 'visitor/serialize.dart'; import 'visitor/serialize.dart';
@ -35,22 +36,17 @@ void main() {
/// possible. /// possible.
/// ///
/// [render]: https://github.com/sass/node-sass#options /// [render]: https://github.com/sass/node-sass#options
void _render( void _render(RenderOptions options,
NodeOptions options, void callback(NodeError error, NodeResult result)) { void callback(RenderError error, RenderResult result)) {
try { try {
var indentWidthValue = options.indentWidth; var result = newRenderResult(render(options.file,
var indentWidth = indentWidthValue is int useSpaces: options.indentType != 'tab',
? indentWidthValue indentWidth: _parseIndentWidth(options.indentWidth),
: int.parse(indentWidthValue.toString()); lineFeed: _parseLineFeed(options.linefeed)));
var lineFeed = _parseLineFeed(options.linefeed);
var result = newNodeResult(render(options.file,
useSpaces: options.indentType == 'space',
indentWidth: indentWidth,
lineFeed: lineFeed));
callback(null, result); callback(null, result);
} on SassException catch (error) { } on SassException catch (error) {
// TODO: populate the error more thoroughly if possible. // TODO: populate the error more thoroughly if possible.
callback(new NodeError(message: error.message), null); callback(newRenderError(message: error.message), null);
} }
} }
@ -60,23 +56,25 @@ void _render(
/// as possible. /// as possible.
/// ///
/// [render]: https://github.com/sass/node-sass#options /// [render]: https://github.com/sass/node-sass#options
NodeResult _renderSync(NodeOptions options) { RenderResult _renderSync(RenderOptions options) {
try { try {
var indentWidthValue = options.indentWidth; return newRenderResult(render(options.file,
var indentWidth = indentWidthValue is int useSpaces: options.indentType != 'tab',
? indentWidthValue indentWidth: _parseIndentWidth(options.indentWidth),
: int.parse(indentWidthValue.toString()); lineFeed: _parseLineFeed(options.linefeed)));
var lineFeed = _parseLineFeed(options.linefeed);
return newNodeResult(render(options.file,
useSpaces: options.indentType == 'space',
indentWidth: indentWidth,
lineFeed: lineFeed));
} on SassException catch (error) { } on SassException catch (error) {
// TODO: populate the error more thoroughly if possible. // TODO: populate the error more thoroughly if possible.
throw new NodeError(message: error.message); jsThrow(newRenderError(message: error.message));
throw "unreachable";
} }
} }
/// Parses the indentation width into an [int].
int _parseIndentWidth(width) {
if (width == null) return null;
return width is int ? width : int.parse(width.toString());
}
/// Parses the name of a line feed type into a [LineFeed]. /// Parses the name of a line feed type into a [LineFeed].
LineFeed _parseLineFeed(String str) { LineFeed _parseLineFeed(String str) {
switch (str) { switch (str) {

View File

@ -0,0 +1,13 @@
// Copyright 2017 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 'package:js/js.dart';
@JS("Function")
class JSFunction {
@JS("Function")
external JSFunction(String arg1, [String arg2, String arg3]);
external call(thisArg, [arg1, arg2]);
}

View File

@ -4,15 +4,25 @@
import 'package:js/js.dart'; import 'package:js/js.dart';
import 'utils.dart';
@JS() @JS()
@anonymous @anonymous
class NodeError { class RenderError {
external String get message; external String get message;
external int get line; external int get line;
external int get column; external int get column;
external int get status; external int get status;
external String get file; external String get file;
external factory NodeError( external factory RenderError._(
{String message, int line, int column, int status, String file}); {String message, int line, int column, int status, String file});
} }
RenderError newRenderError(
{String message, int line, int column, int status, String file}) {
var error = new RenderError._(
message: message, line: line, column: column, status: status, file: file);
setToString(error, () => "Error: $message");
return error;
}

View File

@ -5,9 +5,13 @@
import 'package:js/js.dart'; import 'package:js/js.dart';
@JS() @JS()
class NodeOptions { @anonymous
class RenderOptions {
external String get file; external String get file;
external String get indentType; external String get indentType;
external dynamic get indentWidth; external dynamic get indentWidth;
external String get linefeed; external String get linefeed;
external factory RenderOptions(
{String file, String indentType, indentWidth, String linefeed});
} }

View File

@ -2,6 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at // MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
import 'dart:typed_data';
import 'package:js/js.dart'; import 'package:js/js.dart';
@JS('Buffer.from') @JS('Buffer.from')
@ -9,11 +11,11 @@ external _buffer(String source, String encoding);
@JS() @JS()
@anonymous @anonymous
class NodeResult { class RenderResult {
external get buffer; external Uint8List get css;
external factory NodeResult._({buffer}); external factory RenderResult._({css});
} }
NodeResult newNodeResult(String css) => RenderResult newRenderResult(String css) =>
new NodeResult._(buffer: _buffer(css, 'utf8')); new RenderResult._(css: _buffer(css, 'utf8'));

23
lib/src/node/utils.dart Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2017 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 'package:js/js.dart';
import 'package:meta/meta.dart';
import 'function.dart';
/// Sets the `toString()` function for [object] to [body].
///
/// Dart's JS interop doesn't currently let us set toString() for custom
/// classes, so we use this as a workaround.
void setToString(object, String body()) =>
_setToString.call(object, allowInterop(body));
final _setToString =
new JSFunction("object", "body", "object.toString = body;");
/// Throws [error] like JS would, without any Dart wrappers.
void jsThrow(error) => _jsThrow.call(error);
final _jsThrow = new JSFunction("error", "throw error;");

View File

@ -28,8 +28,15 @@ dev_dependencies:
http: "^0.11.0" http: "^0.11.0"
js: "^0.6.0" js: "^0.6.0"
node_preamble: "^1.1.0" node_preamble: "^1.1.0"
stream_channel: "^1.0.0"
test_descriptor: "^1.0.0" test_descriptor: "^1.0.0"
test_process: "^1.0.0-rc.1" test_process: "^1.0.0-rc.1"
test: "^0.12.5" test: "^0.12.24"
xml: "^2.4.0" xml: "^2.4.0"
yaml: "^2.0.0" yaml: "^2.0.0"
dependency_overrides:
test:
git:
url: git://github.com/dart-lang/test.git
ref: node

View File

@ -2,6 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at // MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
@TestOn('vm')
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';

View File

@ -2,6 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at // MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
@TestOn('vm')
import 'package:package_resolver/package_resolver.dart'; import 'package:package_resolver/package_resolver.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; import 'package:test/test.dart';

View File

@ -6,22 +6,25 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
void hybridMain(StreamChannel channel) async { hybridMain(StreamChannel channel) async {
if (!new Directory("build/npm").existsSync()) { if (!new Directory("build/npm").existsSync()) {
throw "NPM package is not build. Run pub run grinder npm_package."; throw "NPM package is not build. Run pub run grinder npm_package.";
} }
var lastModified = new DateTime(0); var lastModified = new DateTime(0);
var entriesToCheck = new Directory("lib").listSync(recursive: true).toList() var entriesToCheck = new Directory("lib").listSync(recursive: true).toList()
..add("pubspec.lock"); ..add(new File("pubspec.lock"));
for (var entry in entriesToCheck) { for (var entry in entriesToCheck) {
if (entry is! File) continue; if (entry is File) {
var entryLastModified = entry.lastModifiedSync(); var entryLastModified = entry.lastModifiedSync();
if (lastModified.isBefore(entryLastModified)) if (lastModified.isBefore(entryLastModified)) {
lastModified = entryLastModified; lastModified = entryLastModified;
}
}
} }
if (lastModified if (lastModified

56
test/hybrid.dart Normal file
View File

@ -0,0 +1,56 @@
// Copyright 2017 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 'dart:convert';
import 'package:test/test.dart';
/// Creates a directory in the system temp directory and returns its path.
Future<String> createTempDir() async => (await runHybridExpression(
'(await Directory.systemTemp.createTemp("dart_sass_")).path')) as String;
/// Writes [text] to [path].
Future writeTextFile(String path, String text) => runHybridExpression(
'new File(message[0]).writeAsString(message[1])', [path, text]);
/// Recursively deletes the directoy at [path].
Future deleteDirectory(String path) =>
runHybridExpression('new Directory(message).delete(recursive: true)', path);
/// Runs [expression], which may be asynchronous, in a hybrid isolate.
///
/// Returns the result of [expression] if it's JSON-serializable.
Future runHybridExpression(String expression, [message]) async {
var channel = spawnHybridCode(
'''
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:stream_channel/stream_channel.dart';
hybridMain(StreamChannel channel, message) async {
var result = await ${expression};
channel.sink.add(_isJsonSafe(result) ? JSON.encode(result) : 'null');
channel.sink.close();
}
bool _isJsonSafe(object) {
if (object == null) return true;
if (object is String) return true;
if (object is num) return true;
if (object is bool) return true;
if (object is List) return object.every(_isJsonSafe);
if (object is Map) {
return object.keys.every(_isJsonSafe) &&
object.values.every(_isJsonSafe);
}
return false;
}
''',
message: message);
return JSON.decode((await channel.stream.first) as String);
}

33
test/node_api.dart Normal file
View File

@ -0,0 +1,33 @@
// Copyright 2017 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.
/// This library exposes Dart Sass's Node.js API, imported as JavaScript, back
/// to Dart. This is kind of convoluted, but it allows us to test the API as it
/// will be used in the real world without having to manually write any JS.
import 'dart:typed_data';
import 'package:sass/src/node/render_error.dart';
import 'package:sass/src/node/render_options.dart';
import 'package:sass/src/node/render_result.dart';
import 'package:js/js.dart';
import 'package:path/path.dart' as p;
export 'package:sass/src/node/render_error.dart';
export 'package:sass/src/node/render_options.dart';
export 'package:sass/src/node/render_result.dart';
/// The Sass module.
final sass = _require(p.absolute("build/npm/sass.dart"));
@JS("require")
external Sass _require(String path);
@JS()
class Sass {
external RenderResult renderSync(RenderOptions args);
external void render(RenderOptions args,
void callback(RenderError error, RenderResult result));
}

166
test/node_api_test.dart Normal file
View File

@ -0,0 +1,166 @@
// Copyright 2017 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(const ['node'])
import 'dart:async';
import 'dart:convert';
import 'package:js/js.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'ensure_npm_package.dart';
import 'hybrid.dart';
import 'node_api.dart';
String sandbox;
String sassPath;
void main() {
setUpAll(ensureNpmPackage);
setUp(() async {
sandbox = await createTempDir();
sassPath = p.join(sandbox, 'test.scss');
await writeTextFile(sassPath, 'a {b: c}');
});
tearDown(() async {
if (sandbox != null) await deleteDirectory(sandbox);
});
group("renderSync()", () {
test("renders a file", () {
expect(_renderSync(new RenderOptions(file: sassPath)), equals('''
a {
b: c;
}'''));
});
test("allows tab indentation", () {
expect(_renderSync(new RenderOptions(file: sassPath, indentType: 'tab')),
equals('''
a {
\t\tb: c;
}'''));
});
test("allows unknown indentation names", () {
expect(_renderSync(new RenderOptions(file: sassPath, indentType: 'asdf')),
equals('''
a {
b: c;
}'''));
});
group("linefeed allows", () {
test("cr", () {
expect(_renderSync(new RenderOptions(file: sassPath, linefeed: 'cr')),
equals('a {\r b: c;\r}'));
});
test("crlf", () {
expect(_renderSync(new RenderOptions(file: sassPath, linefeed: 'crlf')),
equals('a {\r\n b: c;\r\n}'));
});
test("lfcr", () {
expect(_renderSync(new RenderOptions(file: sassPath, linefeed: 'lfcr')),
equals('a {\n\r b: c;\n\r}'));
});
test("unknown names", () {
expect(_renderSync(new RenderOptions(file: sassPath, linefeed: 'asdf')),
equals('a {\n b: c;\n}'));
});
});
group("indentWidth allows", () {
test("a number", () {
expect(_renderSync(new RenderOptions(file: sassPath, indentWidth: 10)),
equals('''
a {
b: c;
}'''));
});
test("a string", () {
expect(_renderSync(new RenderOptions(file: sassPath, indentWidth: '1')),
equals('''
a {
b: c;
}'''));
});
});
group("throws an error that", () {
setUp(() => writeTextFile(sassPath, 'a {b: }'));
test("has a useful toString", () {
var error = _renderSyncError(new RenderOptions(file: sassPath));
expect(error.toString(), equals("Error: Expected expression."));
});
test("has a useful message", () {
var error = _renderSyncError(new RenderOptions(file: sassPath));
expect(error.message, equals("Expected expression."));
});
});
});
group("render()", () {
test("renders a file", () async {
expect(await _render(new RenderOptions(file: sassPath)), equals('''
a {
b: c;
}'''));
});
test("throws an error that has a useful toString", () async {
await writeTextFile(sassPath, 'a {b: }');
var error = await _renderError(new RenderOptions(file: sassPath));
expect(error.toString(), equals("Error: Expected expression."));
});
});
}
/// Returns the result of rendering via [options] as a string.
Future<String> _render(RenderOptions options) {
var completer = new Completer<String>();
sass.render(options, allowInterop((error, result) {
expect(error, isNull);
completer.complete(UTF8.decode(result.css));
}));
return completer.future;
}
/// Asserts that rendering via [options] produces an error, and returns that
/// error.
Future<RenderError> _renderError(RenderOptions options) {
var completer = new Completer<RenderError>();
sass.render(options, allowInterop((error, result) {
expect(result, isNull);
completer.complete(error as RenderError);
}));
return completer.future;
}
/// Returns the result of rendering via [options] as a string.
String _renderSync(RenderOptions options) =>
UTF8.decode(sass.renderSync(options).css);
/// Asserts that rendering via [options] produces an error, and returns that
/// error.
RenderError _renderSyncError(RenderOptions options) {
try {
sass.renderSync(options);
} catch (error) {
return error as RenderError;
}
throw "Expected renderSync() to throw an error.";
}

View File

@ -2,7 +2,9 @@
// MIT-style license that can be found in the LICENSE file or at // MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
@TestOn('vm')
@Tags(const ['node']) @Tags(const ['node'])
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';