dart-sass/test/importer_test.dart
2021-12-29 16:01:03 -08:00

523 lines
19 KiB
Dart

// Copyright 2019 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:source_maps/source_maps.dart' as source_maps;
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:sass_embedded/src/embedded_sass.pb.dart';
import 'package:sass_embedded/src/utils.dart';
import 'embedded_process.dart';
import 'utils.dart';
void main() {
late EmbeddedProcess process;
setUp(() async {
process = await EmbeddedProcess.start();
});
group("emits a protocol error", () {
test("for a response without a corresponding request ID", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
var request = getCanonicalizeRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..canonicalizeResponse =
(InboundMessage_CanonicalizeResponse()..id = request.id + 1));
await expectParamsError(
process,
errorId,
"Response ID ${request.id + 1} doesn't match any outstanding "
"requests.");
await process.kill();
});
test("for a response that doesn't match the request type", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
var request = getCanonicalizeRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()..id = request.id));
await expectParamsError(
process,
errorId,
"Request ID ${request.id} doesn't match response type "
"InboundMessage_ImportResponse.");
await process.kill();
});
test("for an unset importer", () async {
process.inbound.add(compileString("a {b: c}",
importers: [InboundMessage_CompileRequest_Importer()]));
await expectParamsError(
process, 0, "Missing mandatory field Importer.importer");
await process.kill();
});
});
group("canonicalization", () {
group("emits a protocol error", () {
test("for a canonicalize response with an empty URL", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
var request = getCanonicalizeRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..canonicalizeResponse = (InboundMessage_CanonicalizeResponse()
..id = request.id
..url = ""));
await _expectImportParamsError(
process, 'CanonicalizeResponse.url must be absolute, was ""');
await process.kill();
});
test("for a canonicalize response with a relative URL", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
var request = getCanonicalizeRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..canonicalizeResponse = (InboundMessage_CanonicalizeResponse()
..id = request.id
..url = "relative"));
await _expectImportParamsError(process,
'CanonicalizeResponse.url must be absolute, was "relative"');
await process.kill();
});
});
group("includes in CanonicalizeRequest", () {
var compilationId = 1234;
var importerId = 5679;
late OutboundMessage_CanonicalizeRequest request;
setUp(() async {
process.inbound.add(compileString("@import 'other'",
id: compilationId,
importers: [
InboundMessage_CompileRequest_Importer()..importerId = importerId
]));
request = getCanonicalizeRequest(await process.outbound.next);
});
test("the same compilationId as the compilation", () async {
expect(request.compilationId, equals(compilationId));
await process.kill();
});
test("a known importerId", () async {
expect(request.importerId, equals(importerId));
await process.kill();
});
test("the imported URL", () async {
expect(request.url, equals("other"));
await process.kill();
});
});
test("errors cause compilation to fail", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
var request = getCanonicalizeRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..canonicalizeResponse = (InboundMessage_CanonicalizeResponse()
..id = request.id
..error = "oh no"));
var failure = getCompileFailure(await process.outbound.next);
expect(failure.message, equals('oh no'));
expect(failure.span.text, equals("'other'"));
expect(failure.stackTrace, equals('- 1:9 root stylesheet\n'));
await process.kill();
});
test("null results count as not found", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
var request = getCanonicalizeRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..canonicalizeResponse =
(InboundMessage_CanonicalizeResponse()..id = request.id));
var failure = getCompileFailure(await process.outbound.next);
expect(failure.message, equals("Can't find stylesheet to import."));
expect(failure.span.text, equals("'other'"));
await process.kill();
});
test("attempts importers in order", () async {
process.inbound.add(compileString("@import 'other'", importers: [
for (var i = 0; i < 10; i++)
InboundMessage_CompileRequest_Importer()..importerId = i
]));
for (var i = 0; i < 10; i++) {
var request = getCanonicalizeRequest(await process.outbound.next);
expect(request.importerId, equals(i));
process.inbound.add(InboundMessage()
..canonicalizeResponse =
(InboundMessage_CanonicalizeResponse()..id = request.id));
}
await process.kill();
});
test("tries resolved URL using the original importer first", () async {
process.inbound.add(compileString("@import 'midstream'", importers: [
for (var i = 0; i < 10; i++)
InboundMessage_CompileRequest_Importer()..importerId = i
]));
for (var i = 0; i < 5; i++) {
var request = getCanonicalizeRequest(await process.outbound.next);
expect(request.url, equals("midstream"));
expect(request.importerId, equals(i));
process.inbound.add(InboundMessage()
..canonicalizeResponse =
(InboundMessage_CanonicalizeResponse()..id = request.id));
}
var canonicalize = getCanonicalizeRequest(await process.outbound.next);
expect(canonicalize.importerId, equals(5));
process.inbound.add(InboundMessage()
..canonicalizeResponse = (InboundMessage_CanonicalizeResponse()
..id = canonicalize.id
..url = "custom:foo/bar"));
var import = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = import.id
..success = (InboundMessage_ImportResponse_ImportSuccess()
..contents = "@import 'upstream'")));
canonicalize = getCanonicalizeRequest(await process.outbound.next);
expect(canonicalize.importerId, equals(5));
expect(canonicalize.url, equals("custom:foo/upstream"));
await process.kill();
});
});
group("importing", () {
group("emits a protocol error", () {
test("for an import result with a relative sourceMapUrl", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
await _canonicalize(process);
var import = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = import.id
..success = (InboundMessage_ImportResponse_ImportSuccess()
..sourceMapUrl = "relative")));
await _expectImportParamsError(
process,
'ImportResponse.success.source_map_url must be absolute, was '
'"relative"');
await process.kill();
});
});
group("includes in ImportRequest", () {
var compilationId = 1234;
var importerId = 5678;
late OutboundMessage_ImportRequest request;
setUp(() async {
process.inbound.add(compileString("@import 'other'",
id: compilationId,
importers: [
InboundMessage_CompileRequest_Importer()..importerId = importerId
]));
var canonicalize = getCanonicalizeRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..canonicalizeResponse = (InboundMessage_CanonicalizeResponse()
..id = canonicalize.id
..url = "custom:foo"));
request = getImportRequest(await process.outbound.next);
});
test("the same compilationId as the compilation", () async {
expect(request.compilationId, equals(compilationId));
await process.kill();
});
test("a known importerId", () async {
expect(request.importerId, equals(importerId));
await process.kill();
});
test("the canonical URL", () async {
expect(request.url, equals("custom:foo"));
await process.kill();
});
});
test("null results count as not found", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
var canonicalizeRequest =
getCanonicalizeRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..canonicalizeResponse = (InboundMessage_CanonicalizeResponse()
..id = canonicalizeRequest.id
..url = "o:other"));
var importRequest = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse =
(InboundMessage_ImportResponse()..id = importRequest.id));
var failure = getCompileFailure(await process.outbound.next);
expect(failure.message, equals("Can't find stylesheet to import."));
expect(failure.span.text, equals("'other'"));
await process.kill();
});
test("errors cause compilation to fail", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
await _canonicalize(process);
var request = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = request.id
..error = "oh no"));
var failure = getCompileFailure(await process.outbound.next);
expect(failure.message, equals('oh no'));
expect(failure.span.text, equals("'other'"));
expect(failure.stackTrace, equals('- 1:9 root stylesheet\n'));
await process.kill();
});
test("can return an SCSS file", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
await _canonicalize(process);
var request = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = request.id
..success = (InboundMessage_ImportResponse_ImportSuccess()
..contents = "a {b: 1px + 2px}")));
await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }")));
await process.kill();
});
test("can return an indented syntax file", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
await _canonicalize(process);
var request = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = request.id
..success = (InboundMessage_ImportResponse_ImportSuccess()
..contents = "a\n b: 1px + 2px"
..syntax = Syntax.INDENTED)));
await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }")));
await process.kill();
});
test("can return a plain CSS file", () async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
await _canonicalize(process);
var request = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = request.id
..success = (InboundMessage_ImportResponse_ImportSuccess()
..contents = "a {b: c}"
..syntax = Syntax.CSS)));
await expectLater(process.outbound, emits(isSuccess("a { b: c; }")));
await process.kill();
});
test("uses a data: URL rather than an empty source map URL", () async {
process.inbound.add(compileString("@import 'other'",
sourceMap: true,
importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
await _canonicalize(process);
var request = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = request.id
..success = (InboundMessage_ImportResponse_ImportSuccess()
..contents = "a {b: c}"
..sourceMapUrl = "")));
await expectLater(
process.outbound,
emits(isSuccess("a { b: c; }", sourceMap: (String map) {
var mapping = source_maps.parse(map) as source_maps.SingleMapping;
expect(mapping.urls, [startsWith("data:")]);
})));
await process.kill();
});
test("uses a non-empty source map URL", () async {
process.inbound.add(compileString("@import 'other'",
sourceMap: true,
importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
await _canonicalize(process);
var request = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = request.id
..success = (InboundMessage_ImportResponse_ImportSuccess()
..contents = "a {b: c}"
..sourceMapUrl = "file:///asdf")));
await expectLater(
process.outbound,
emits(isSuccess("a { b: c; }", sourceMap: (String map) {
var mapping = source_maps.parse(map) as source_maps.SingleMapping;
expect(mapping.urls, equals(["file:///asdf"]));
})));
await process.kill();
});
});
test("handles an importer for a string compile request", () async {
process.inbound.add(compileString("@import 'other'",
importer: InboundMessage_CompileRequest_Importer()..importerId = 1));
await _canonicalize(process);
var request = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = request.id
..success = (InboundMessage_ImportResponse_ImportSuccess()
..contents = "a {b: 1px + 2px}")));
await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }")));
await process.kill();
});
group("load paths", () {
test("are used to load imports", () async {
await d.dir("dir", [d.file("other.scss", "a {b: c}")]).create();
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..path = d.path("dir")
]));
await expectLater(process.outbound, emits(isSuccess("a { b: c; }")));
await process.kill();
});
test("are accessed in order", () async {
for (var i = 0; i < 3; i++) {
await d.dir("dir$i", [d.file("other$i.scss", "a {b: $i}")]).create();
}
process.inbound.add(compileString("@import 'other2'", importers: [
for (var i = 0; i < 3; i++)
InboundMessage_CompileRequest_Importer()..path = d.path("dir$i")
]));
await expectLater(process.outbound, emits(isSuccess("a { b: 2; }")));
await process.kill();
});
test("take precedence over later importers", () async {
await d.dir("dir", [d.file("other.scss", "a {b: c}")]).create();
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..path = d.path("dir"),
InboundMessage_CompileRequest_Importer()..importerId = 1
]));
await expectLater(process.outbound, emits(isSuccess("a { b: c; }")));
await process.kill();
});
test("yield precedence to earlier importers", () async {
await d.dir("dir", [d.file("other.scss", "a {b: c}")]).create();
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..importerId = 1,
InboundMessage_CompileRequest_Importer()..path = d.path("dir")
]));
await _canonicalize(process);
var request = getImportRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..importResponse = (InboundMessage_ImportResponse()
..id = request.id
..success = (InboundMessage_ImportResponse_ImportSuccess()
..contents = "x {y: z}")));
await expectLater(process.outbound, emits(isSuccess("x { y: z; }")));
await process.kill();
});
});
}
/// Handles a `CanonicalizeRequest` and returns a response with a generic
/// canonical URL.
///
/// This is used when testing import requests, to avoid duplicating a bunch of
/// generic code for canonicalization. It shouldn't be used for testing
/// canonicalization itself.
Future<void> _canonicalize(EmbeddedProcess process) async {
var request = getCanonicalizeRequest(await process.outbound.next);
process.inbound.add(InboundMessage()
..canonicalizeResponse = (InboundMessage_CanonicalizeResponse()
..id = request.id
..url = "custom:other"));
}
/// Asserts that [process] emits a [ProtocolError] params error with the given
/// [message] on its protobuf stream and causes the compilation to fail.
Future<void> _expectImportParamsError(EmbeddedProcess process, message) async {
await expectLater(process.outbound,
emits(isProtocolError(errorId, ProtocolErrorType.PARAMS, message)));
var failure = getCompileFailure(await process.outbound.next);
expect(failure.message, equals('Protocol error: $message'));
expect(failure.span.text, equals("'other'"));
}