Fix spawnHybridUri() in the browser (#689)

Closes #688
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22aa888..a3427cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.12.24+8
+
+* `spawnHybridUri()` now interprets relative URIs correctly in browser tests.
+
 ## 0.12.24+7
 
 * Declare support for `async` 2.0.0.
diff --git a/lib/src/frontend/spawn_hybrid.dart b/lib/src/frontend/spawn_hybrid.dart
index 37f9517..2008935 100644
--- a/lib/src/frontend/spawn_hybrid.dart
+++ b/lib/src/frontend/spawn_hybrid.dart
@@ -98,10 +98,17 @@
 
   String absoluteUri;
   if (parsedUrl.scheme.isEmpty) {
-    var suitePath = Invoker.current.liveTest.suite.path;
-    absoluteUri = p.url.join(
-        p.url.dirname(p.toUri(p.absolute(suitePath)).toString()),
-        parsedUrl.toString());
+    // If we're running in a browser context, the working directory is already
+    // relative to the test file, whereas on the VM the working directory is the
+    // root of the package.
+    if (p.style == p.Style.url) {
+      absoluteUri = p.absolute(parsedUrl.toString());
+    } else {
+      var suitePath = Invoker.current.liveTest.suite.path;
+      absoluteUri = p.url.join(
+          p.url.dirname(p.toUri(p.absolute(suitePath)).toString()),
+          parsedUrl.toString());
+    }
   } else {
     absoluteUri = uri.toString();
   }
diff --git a/pubspec.yaml b/pubspec.yaml
index d60c2d3..86f72f7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 0.12.24+7
+version: 0.12.24+8
 author: Dart Team <misc@dartlang.org>
 description: A library for writing dart unit tests.
 homepage: https://github.com/dart-lang/test
diff --git a/test/runner/hybrid_test.dart b/test/runner/hybrid_test.dart
index 242b0ca..cec8762 100644
--- a/test/runner/hybrid_test.dart
+++ b/test/runner/hybrid_test.dart
@@ -22,165 +22,13 @@
   });
 
   group("spawnHybridUri():", () {
-    test("loads a file in a separate isolate connected via StreamChannel",
-        () async {
-      await d.file("test.dart", """
-        import "package:test/test.dart";
-
-        void main() {
-          test("hybrid emits numbers", () {
-            expect(spawnHybridUri("hybrid.dart").stream.toList(),
-                completion(equals([1, 2, 3])));
-          });
-        }
-      """).create();
-
-      await d.file("hybrid.dart", """
-        import "package:stream_channel/stream_channel.dart";
-
-        void hybridMain(StreamChannel channel) {
-          channel.sink..add(1)..add(2)..add(3)..close();
-        }
-      """).create();
-
-      var test = await runTest(["test.dart"]);
-      expect(
-          test.stdout,
-          containsInOrder(
-              ["+0: hybrid emits numbers", "+1: All tests passed!"]));
-      await test.shouldExit(0);
+    group("in the VM", () {
+      _spawnHybridUriTests();
     });
 
-    test("resolves URIs relative to the test file", () async {
-      await d.dir("test/dir/subdir", [
-        d.file("test.dart", """
-          import "package:test/test.dart";
-
-          void main() {
-            test("hybrid emits numbers", () {
-              expect(spawnHybridUri("hybrid.dart").stream.toList(),
-                  completion(equals([1, 2, 3])));
-            });
-          }
-        """),
-        d.file("hybrid.dart", """
-          import "package:stream_channel/stream_channel.dart";
-
-          void hybridMain(StreamChannel channel) {
-            channel.sink..add(1)..add(2)..add(3)..close();
-          }
-        """),
-      ]).create();
-
-      var test = await runTest(["test/dir/subdir/test.dart"]);
-      expect(
-          test.stdout,
-          containsInOrder(
-              ["+0: hybrid emits numbers", "+1: All tests passed!"]));
-      await test.shouldExit(0);
-    });
-
-    test("supports absolute file: URIs", () async {
-      var url = p.toUri(p.absolute(p.join(d.sandbox, 'hybrid.dart')));
-      await d.file("test.dart", """
-        import "package:test/test.dart";
-
-        void main() {
-          test("hybrid emits numbers", () {
-            expect(spawnHybridUri("$url").stream.toList(),
-                completion(equals([1, 2, 3])));
-          });
-        }
-      """).create();
-
-      await d.file("hybrid.dart", """
-        import "package:stream_channel/stream_channel.dart";
-
-        void hybridMain(StreamChannel channel) {
-          channel.sink..add(1)..add(2)..add(3)..close();
-        }
-      """).create();
-
-      var test = await runTest(["test.dart"]);
-      expect(
-          test.stdout,
-          containsInOrder(
-              ["+0: hybrid emits numbers", "+1: All tests passed!"]));
-      await test.shouldExit(0);
-    });
-
-    test("supports Uri objects", () async {
-      await d.file("test.dart", """
-        import "package:test/test.dart";
-
-        void main() {
-          test("hybrid emits numbers", () {
-            expect(spawnHybridUri(Uri.parse("hybrid.dart")).stream.toList(),
-                completion(equals([1, 2, 3])));
-          });
-        }
-      """).create();
-
-      await d.file("hybrid.dart", """
-        import "package:stream_channel/stream_channel.dart";
-
-        void hybridMain(StreamChannel channel) {
-          channel.sink..add(1)..add(2)..add(3)..close();
-        }
-      """).create();
-
-      var test = await runTest(["test.dart"]);
-      expect(
-          test.stdout,
-          containsInOrder(
-              ["+0: hybrid emits numbers", "+1: All tests passed!"]));
-      await test.shouldExit(0);
-    });
-
-    test("rejects non-String, non-Uri objects", () {
-      expect(() => spawnHybridUri(123), throwsArgumentError);
-    });
-
-    test("passes a message to the hybrid isolate", () async {
-      await d.file("test.dart", """
-        import "package:test/test.dart";
-
-        void main() {
-          test("hybrid echoes message", () {
-            expect(
-                spawnHybridUri(Uri.parse("hybrid.dart"), message: 123)
-                    .stream.first,
-                completion(equals(123)));
-
-            expect(
-                spawnHybridUri(Uri.parse("hybrid.dart"), message: "wow")
-                    .stream.first,
-                completion(equals("wow")));
-          });
-        }
-      """).create();
-
-      await d.file("hybrid.dart", """
-        import "package:stream_channel/stream_channel.dart";
-
-        void hybridMain(StreamChannel channel, Object message) {
-          channel.sink..add(message)..close();
-        }
-      """).create();
-
-      var test = await runTest(["test.dart"]);
-      expect(
-          test.stdout,
-          containsInOrder(
-              ["+0: hybrid echoes message", "+1: All tests passed!"]));
-      await test.shouldExit(0);
-    });
-
-    test("emits an error from the stream channel if the isolate fails to load",
-        () {
-      expect(spawnHybridUri("non existent file").stream.first,
-          throwsA(new isInstanceOf<IsolateSpawnException>()));
-    });
+    group("in the browser", () {
+      _spawnHybridUriTests(["-p", "chrome"]);
+    }, tags: "browser");
   });
 
   group("spawnHybridCode()", () {
@@ -544,3 +392,163 @@
     });
   });
 }
+
+/// Defines tests for `spawnHybridUri()`.
+///
+/// If [arguments] is given, it's passed on to the invocation of the test
+/// runner.
+void _spawnHybridUriTests([Iterable<String> arguments]) {
+  arguments ??= [];
+
+  test("loads a file in a separate isolate connected via StreamChannel",
+      () async {
+    await d.file("test.dart", """
+        import "package:test/test.dart";
+
+        void main() {
+          test("hybrid emits numbers", () {
+            expect(spawnHybridUri("hybrid.dart").stream.toList(),
+                completion(equals([1, 2, 3])));
+          });
+        }
+      """).create();
+
+    await d.file("hybrid.dart", """
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          channel.sink..add(1)..add(2)..add(3)..close();
+        }
+      """).create();
+
+    var test = await runTest(["test.dart"]..addAll(arguments));
+    expect(test.stdout,
+        containsInOrder(["+0: hybrid emits numbers", "+1: All tests passed!"]));
+    await test.shouldExit(0);
+  });
+
+  test("resolves URIs relative to the test file", () async {
+    await d.dir("test/dir/subdir", [
+      d.file("test.dart", """
+          import "package:test/test.dart";
+
+          void main() {
+            test("hybrid emits numbers", () {
+              expect(spawnHybridUri("hybrid.dart").stream.toList(),
+                  completion(equals([1, 2, 3])));
+            });
+          }
+        """),
+      d.file("hybrid.dart", """
+          import "package:stream_channel/stream_channel.dart";
+
+          void hybridMain(StreamChannel channel) {
+            channel.sink..add(1)..add(2)..add(3)..close();
+          }
+        """),
+    ]).create();
+
+    var test = await runTest(["test/dir/subdir/test.dart"]..addAll(arguments));
+    expect(test.stdout,
+        containsInOrder(["+0: hybrid emits numbers", "+1: All tests passed!"]));
+    await test.shouldExit(0);
+  });
+
+  test("supports absolute file: URIs", () async {
+    var url = p.toUri(p.absolute(p.join(d.sandbox, 'hybrid.dart')));
+    await d.file("test.dart", """
+        import "package:test/test.dart";
+
+        void main() {
+          test("hybrid emits numbers", () {
+            expect(spawnHybridUri("$url").stream.toList(),
+                completion(equals([1, 2, 3])));
+          });
+        }
+      """).create();
+
+    await d.file("hybrid.dart", """
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          channel.sink..add(1)..add(2)..add(3)..close();
+        }
+      """).create();
+
+    var test = await runTest(["test.dart"]..addAll(arguments));
+    expect(test.stdout,
+        containsInOrder(["+0: hybrid emits numbers", "+1: All tests passed!"]));
+    await test.shouldExit(0);
+  });
+
+  test("supports Uri objects", () async {
+    await d.file("test.dart", """
+        import "package:test/test.dart";
+
+        void main() {
+          test("hybrid emits numbers", () {
+            expect(spawnHybridUri(Uri.parse("hybrid.dart")).stream.toList(),
+                completion(equals([1, 2, 3])));
+          });
+        }
+      """).create();
+
+    await d.file("hybrid.dart", """
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          channel.sink..add(1)..add(2)..add(3)..close();
+        }
+      """).create();
+
+    var test = await runTest(["test.dart"]..addAll(arguments));
+    expect(test.stdout,
+        containsInOrder(["+0: hybrid emits numbers", "+1: All tests passed!"]));
+    await test.shouldExit(0);
+  });
+
+  test("rejects non-String, non-Uri objects", () {
+    expect(() => spawnHybridUri(123), throwsArgumentError);
+  });
+
+  test("passes a message to the hybrid isolate", () async {
+    await d.file("test.dart", """
+        import "package:test/test.dart";
+
+        void main() {
+          test("hybrid echoes message", () {
+            expect(
+                spawnHybridUri(Uri.parse("hybrid.dart"), message: 123)
+                    .stream.first,
+                completion(equals(123)));
+
+            expect(
+                spawnHybridUri(Uri.parse("hybrid.dart"), message: "wow")
+                    .stream.first,
+                completion(equals("wow")));
+          });
+        }
+      """).create();
+
+    await d.file("hybrid.dart", """
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel, Object message) {
+          channel.sink..add(message)..close();
+        }
+      """).create();
+
+    var test = await runTest(["test.dart"]..addAll(arguments));
+    expect(
+        test.stdout,
+        containsInOrder(
+            ["+0: hybrid echoes message", "+1: All tests passed!"]));
+    await test.shouldExit(0);
+  });
+
+  test("emits an error from the stream channel if the isolate fails to load",
+      () {
+    expect(spawnHybridUri("non existent file").stream.first,
+        throwsA(new isInstanceOf<IsolateSpawnException>()));
+  });
+}