// 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 unset import result", () 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)); await _expectImportParamsError( process, "Missing mandatory field ImportResponse.result"); await process.kill(); }); 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("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 _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 _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'")); }