Allow spawnHybridUri() to take root-relative URLs (#708)

This makes it possible to use the same spawnHybridUri() call from
multiple test files that are in different places in the filesystem.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f9a687..9478222 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.12.26
+
+* The `spawnHybridUri()` function now allows root-relative URLs, which are
+  interpreted as relative to the root of the package.
+
 ## 0.12.25
 
 * Add a `override_platforms` configuration field which allows test platforms'
diff --git a/lib/src/frontend/spawn_hybrid.dart b/lib/src/frontend/spawn_hybrid.dart
index 2008935..001a00f 100644
--- a/lib/src/frontend/spawn_hybrid.dart
+++ b/lib/src/frontend/spawn_hybrid.dart
@@ -63,8 +63,11 @@
 /// ```
 ///
 /// If [uri] is relative, it will be interpreted relative to the `file:` URL for
-/// the test suite being executed. If it's a `package:` URL, it will be resolved
-/// using the current package's dependency constellation.
+/// the test suite being executed. If it's root-relative (that is, if it begins
+/// with `/`) it will be interpreted relative to the root of the package (the
+/// directory that contains `pubspec.yaml`, *not* the `test/` directory). If
+/// it's a `package:` URL, it will be resolved using the current package's
+/// dependency constellation.
 ///
 /// Returns a [StreamChannel] that's connected to the channel passed to
 /// `hybridMain()`. Only JSON-encodable objects may be sent through this
@@ -98,16 +101,35 @@
 
   String absoluteUri;
   if (parsedUrl.scheme.isEmpty) {
+    var isRootRelative = parsedUrl.path.startsWith("/");
+
     // 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());
+      if (isRootRelative) {
+        // A root-relative URL is interpreted as relative to the package root,
+        // which means placing it beneath the URL secret.
+        var secret = Uri.encodeComponent(Uri.base.pathSegments[0]);
+        absoluteUri = p.absolute("/$secret$parsedUrl");
+        print("Uri.base: ${Uri.base}");
+        print("absoluteUri: ${absoluteUri}");
+      } else {
+        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());
+      if (isRootRelative) {
+        // We assume that the current path is the package root. `pub run`
+        // enforces this currently, but at some point it would probably be good
+        // to pass in an explicit root.
+        absoluteUri = p.url
+            .join(p.toUri(p.current).toString(), parsedUrl.path.substring(1));
+      } 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 c94e820..2a8c344 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 0.12.25
+version: 0.12.26
 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 cec8762..ec5d568 100644
--- a/test/runner/hybrid_test.dart
+++ b/test/runner/hybrid_test.dart
@@ -28,7 +28,11 @@
 
     group("in the browser", () {
       _spawnHybridUriTests(["-p", "chrome"]);
-    }, tags: "browser");
+    }, tags: "chrome");
+
+    group("in Node.js", () {
+      _spawnHybridUriTests(["-p", "node"]);
+    }, tags: "node");
   });
 
   group("spawnHybridCode()", () {
@@ -454,6 +458,35 @@
     await test.shouldExit(0);
   });
 
+  test("resolves root-relative URIs relative to the package root", () async {
+    await d.dir("test/dir/subdir", [
+      d.file("test.dart", """
+          import "package:test/test.dart";
+
+          void main() {
+            test("hybrid emits numbers", () {
+              expect(
+                  spawnHybridUri("/test/dir/subdir/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", """