| Index: lib/src/runner/browser/server.dart | 
| diff --git a/lib/src/runner/browser/server.dart b/lib/src/runner/browser/server.dart | 
| index 9730e3c2a276ca36808cccd00ca5caabc535146f..87b6ef38edb402a82d4b775dc5b36922bf1fa15b 100644 | 
| --- a/lib/src/runner/browser/server.dart | 
| +++ b/lib/src/runner/browser/server.dart | 
| @@ -9,6 +9,7 @@ import 'dart:convert'; | 
| import 'dart:io'; | 
|  | 
| import 'package:path/path.dart' as p; | 
| +import 'package:pool/pool.dart'; | 
| import 'package:shelf/shelf.dart' as shelf; | 
| import 'package:shelf/shelf_io.dart' as shelf_io; | 
| import 'package:shelf_static/shelf_static.dart'; | 
| @@ -17,6 +18,8 @@ import 'package:shelf_web_socket/shelf_web_socket.dart'; | 
| import '../../backend/suite.dart'; | 
| import '../../util/io.dart'; | 
| import '../../util/one_off_handler.dart'; | 
| +import '../../utils.dart'; | 
| +import '../load_exception.dart'; | 
| import 'browser_manager.dart'; | 
| import 'compiler_pool.dart'; | 
| import 'chrome.dart'; | 
| @@ -31,9 +34,13 @@ class BrowserServer { | 
| /// compiling tests to JS. Otherwise, the package root is inferred from the | 
| /// location of the source file. | 
| /// | 
| +  /// If [pubServeUrl] is passed, tests will be loaded from the `pub serve` | 
| +  /// instance at that URL rather than from the filesystem. | 
| +  /// | 
| /// If [color] is true, console colors will be used when compiling Dart. | 
| -  static Future<BrowserServer> start({String packageRoot, bool color: false}) { | 
| -    var server = new BrowserServer._(packageRoot, color); | 
| +  static Future<BrowserServer> start({String packageRoot, Uri pubServeUrl, | 
| +      bool color: false}) { | 
| +    var server = new BrowserServer._(packageRoot, pubServeUrl, color); | 
| return server._load().then((_) => server); | 
| } | 
|  | 
| @@ -51,6 +58,8 @@ class BrowserServer { | 
| final _webSocketHandler = new OneOffHandler(); | 
|  | 
| /// The [CompilerPool] managing active instances of `dart2js`. | 
| +  /// | 
| +  /// This is `null` if tests are loaded from `pub serve`. | 
| final CompilerPool _compilers; | 
|  | 
| /// The temporary directory in which compiled JS is emitted. | 
| @@ -59,6 +68,20 @@ class BrowserServer { | 
| /// The package root which is passed to `dart2js`. | 
| final String _packageRoot; | 
|  | 
| +  /// The URL for the `pub serve` instance to use to load tests. | 
| +  /// | 
| +  /// This is `null` if tests should be compiled manually. | 
| +  final Uri _pubServeUrl; | 
| + | 
| +  /// The pool of active `pub serve` compilations. | 
| +  /// | 
| +  /// Pub itself ensures that only one compilation runs at a time; we just use | 
| +  /// this pool to make sure that the output is nice and linear. | 
| +  final _pubServePool = new Pool(1); | 
| + | 
| +  /// The HTTP client to use when caching JS files in `pub serve`. | 
| +  final HttpClient _http; | 
| + | 
| /// The browser in which test suites are loaded and run. | 
| /// | 
| /// This is `null` until a suite is loaded. | 
| @@ -76,7 +99,14 @@ class BrowserServer { | 
| })); | 
|  | 
| var webSocketUrl = url.replace(scheme: 'ws', path: '/$path'); | 
| -      _browser = new Chrome(url.replace(queryParameters: { | 
| + | 
| +      var hostUrl = url; | 
| +      if (_pubServeUrl != null) { | 
| +        hostUrl = _pubServeUrl.resolve( | 
| +            '/packages/test/src/runner/browser/static/'); | 
| +      } | 
| + | 
| +      _browser = new Chrome(hostUrl.replace(queryParameters: { | 
| 'managerUrl': webSocketUrl.toString() | 
| })); | 
|  | 
| @@ -91,18 +121,27 @@ class BrowserServer { | 
| } | 
| Completer<BrowserManager> _browserManagerCompleter; | 
|  | 
| -  BrowserServer._(this._packageRoot, bool color) | 
| -      : _compiledDir = Directory.systemTemp.createTempSync('test_').path, | 
| +  BrowserServer._(this._packageRoot, Uri pubServeUrl, bool color) | 
| +      : _pubServeUrl = pubServeUrl, | 
| +        _compiledDir = pubServeUrl == null | 
| +            ? Directory.systemTemp.createTempSync('test_').path | 
| +            : null, | 
| +        _http = pubServeUrl == null ? null : new HttpClient(), | 
| _compilers = new CompilerPool(color: color); | 
|  | 
| /// Starts the underlying server. | 
| Future _load() { | 
| -    var staticPath = p.join(libDir(packageRoot: _packageRoot), | 
| -        'src/runner/browser/static'); | 
| var cascade = new shelf.Cascade() | 
| -        .add(_webSocketHandler.handler) | 
| -        .add(createStaticHandler(staticPath, defaultDocument: 'index.html')) | 
| -        .add(createStaticHandler(_compiledDir, defaultDocument: 'index.html')); | 
| +        .add(_webSocketHandler.handler); | 
| + | 
| +    if (_pubServeUrl == null) { | 
| +      var staticPath = p.join(libDir(packageRoot: _packageRoot), | 
| +          'src/runner/browser/static'); | 
| +      cascade = cascade | 
| +          .add(createStaticHandler(staticPath, defaultDocument: 'index.html')) | 
| +          .add(createStaticHandler(_compiledDir, | 
| +              defaultDocument: 'index.html')); | 
| +    } | 
|  | 
| return shelf_io.serve(cascade.handler, 'localhost', 0).then((server) { | 
| _server = server; | 
| @@ -113,19 +152,67 @@ class BrowserServer { | 
| /// | 
| /// This will start a browser to load the suite if one isn't already running. | 
| Future<Suite> loadSuite(String path) { | 
| -    return _compileSuite(path).then((dir) { | 
| +    return new Future.sync(() { | 
| +      if (_pubServeUrl != null) { | 
| + | 
| +        var suitePrefix = p.withoutExtension(p.relative(path, from: 'test')) + | 
| +            '.browser_test'; | 
| +        var jsUrl = _pubServeUrl.resolve('$suitePrefix.dart.js'); | 
| +        return _pubServeSuite(path, jsUrl) | 
| +            .then((_) => _pubServeUrl.resolve('$suitePrefix.html')); | 
| +      } else { | 
| +        return _compileSuite(path).then((dir) { | 
| +          // Add a trailing slash because at least on Chrome, the iframe's | 
| +          // window.location.href will do so automatically, and if that differs | 
| +          // from the original URL communication will fail. | 
| +          return url.resolve( | 
| +              "/" + p.toUri(p.relative(dir, from: _compiledDir)).path + "/"); | 
| +        }); | 
| +      } | 
| +    }).then((suiteUrl) { | 
| // TODO(nweiz): Don't start the browser until all the suites are compiled. | 
| return _browserManager.then((browserManager) { | 
| -        // Add a trailing slash because at least on Chrome, the iframe's | 
| -        // window.location.href will do so automatically, and if that differs | 
| -        // from the original URL communication will fail. | 
| -        var suiteUrl = url.resolve( | 
| -            "/" + p.toUri(p.relative(dir, from: _compiledDir)).path + "/"); | 
| return browserManager.loadSuite(path, suiteUrl); | 
| }); | 
| }); | 
| } | 
|  | 
| +  /// Loads a test suite at [path] from the `pub serve` URL [jsUrl]. | 
| +  /// | 
| +  /// This ensures that only one suite is loaded at a time, and that any errors | 
| +  /// are exposed as [LoadException]s. | 
| +  Future _pubServeSuite(String path, Uri jsUrl) { | 
| +    return _pubServePool.withResource(() { | 
| +      var timer = new Timer(new Duration(seconds: 1), () { | 
| +        print('"pub serve" is compiling $path...'); | 
| +      }); | 
| + | 
| +      return _http.headUrl(jsUrl) | 
| +          .then((request) => request.close()) | 
| +          .whenComplete(timer.cancel) | 
| +          .catchError((error, stackTrace) { | 
| +        if (error is! IOException) throw error; | 
| + | 
| +        var message = getErrorMessage(error); | 
| +        if (error is SocketException) { | 
| +          message = "${error.osError.message} " | 
| +              "(errno ${error.osError.errorCode})"; | 
| +        } | 
| + | 
| +        throw new LoadException(path, | 
| +            "Error getting $jsUrl: $message\n" | 
| +            'Make sure "pub serve" is running.'); | 
| +      }).then((response) { | 
| +        if (response.statusCode == 200) return; | 
| + | 
| +        throw new LoadException(path, | 
| +            "Error getting $jsUrl: ${response.statusCode} " | 
| +                "${response.reasonPhrase}\n" | 
| +            'Make sure "pub serve" is serving the test/ directory.'); | 
| +      }); | 
| +    }); | 
| +  } | 
| + | 
| /// Compile the test suite at [dartPath] to JavaScript. | 
| /// | 
| /// Returns a [Future] that completes to the path to the JavaScript. | 
| @@ -154,7 +241,12 @@ class BrowserServer { | 
| /// Returns a [Future] that completes once the server is closed and its | 
| /// resources have been fully released. | 
| Future close() { | 
| -    new Directory(_compiledDir).deleteSync(recursive: true); | 
| +    if (_pubServeUrl == null) { | 
| +      new Directory(_compiledDir).deleteSync(recursive: true); | 
| +    } else { | 
| +      _http.close(); | 
| +    } | 
| + | 
| return _server.close().then((_) { | 
| if (_browserManagerCompleter == null) return null; | 
| return _browserManager.then((_) => _browser.close()); | 
|  |