diff --git a/CHANGELOG.md b/CHANGELOG.md index f0faab10980ca1498d1754ad1a5e90090f8fe266..b37d10a696b19ffe217965fec1b8406028f81caf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 0.12.0-beta.7 + +* Browser tests can now load assets by making HTTP requests to the corresponding + relative URLs. + ### 0.12.0-beta.6 * Add the ability to run multiple test suites concurrently. By default a number diff --git a/lib/pub_serve.dart b/lib/pub_serve.dart index f7cf373224829a29cb3e78ad8c2785eae9674032..71fa1cffd516dcbf100a655fd519381b757a9366 100644 --- a/lib/pub_serve.dart +++ b/lib/pub_serve.dart @@ -19,16 +19,16 @@ class PubServeTransformer extends Transformer implements DeclaringTransformer { void declareOutputs(DeclaringTransform transform) { var id = transform.primaryId; - transform.declareOutput(id.changeExtension('.vm_test.dart')); - transform.declareOutput(id.changeExtension('.browser_test.dart')); - transform.declareOutput(id.changeExtension('.browser_test.html')); + transform.declareOutput(id.addExtension('.vm_test.dart')); + transform.declareOutput(id.addExtension('.browser_test.dart')); + transform.declareOutput(id.addExtension('.browser_test.html')); } void apply(Transform transform) { var id = transform.primaryInput.id; transform.addOutput( - new Asset.fromString(id.changeExtension('.vm_test.dart'), ''' + new Asset.fromString(id.addExtension('.vm_test.dart'), ''' import "package:test/src/runner/vm/isolate_listener.dart"; import "${p.url.basename(id.path)}" as test; @@ -39,7 +39,7 @@ void main(_, Map message) { } ''')); - var browserId = id.changeExtension('.browser_test.dart'); + var browserId = id.addExtension('.browser_test.dart'); transform.addOutput(new Asset.fromString(browserId, ''' import "package:test/src/runner/browser/iframe_listener.dart"; @@ -51,7 +51,7 @@ void main(_) { ''')); transform.addOutput( - new Asset.fromString(browserId.changeExtension('.html'), ''' + new Asset.fromString(id.addExtension('.browser_test.html'), ''' <!DOCTYPE html> <html> <head> diff --git a/lib/src/runner/browser/server.dart b/lib/src/runner/browser/server.dart index a98bcb08dc55c96864391ad7faba476f6b07299e..1c2ee38ead6e8f418b1373894b3b58dde7b9e145 100644 --- a/lib/src/runner/browser/server.dart +++ b/lib/src/runner/browser/server.dart @@ -18,6 +18,7 @@ import 'package:shelf_web_socket/shelf_web_socket.dart'; import '../../backend/suite.dart'; import '../../backend/test_platform.dart'; import '../../util/io.dart'; +import '../../util/path_handler.dart'; import '../../util/one_off_handler.dart'; import '../../utils.dart'; import '../load_exception.dart'; @@ -33,6 +34,9 @@ import 'firefox.dart'; class BrowserServer { /// Starts the server. /// + /// [root] is the root directory that the server should serve. It defaults to + /// the working directory. + /// /// If [packageRoot] is passed, it's used for all package imports when /// compiling tests to JS. Otherwise, the package root is inferred from the /// location of the source file. @@ -41,25 +45,35 @@ class BrowserServer { /// 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, Uri pubServeUrl, - bool color: false}) { - var server = new BrowserServer._(packageRoot, pubServeUrl, color); + static Future<BrowserServer> start({String root, String packageRoot, + Uri pubServeUrl, bool color: false}) { + var server = new BrowserServer._(root, packageRoot, pubServeUrl, color); return server._load().then((_) => server); } /// The underlying HTTP server. HttpServer _server; + /// A randomly-generated secret. + /// + /// This is used to ensure that other users on the same system can't snoop + /// on data being served through this server. + final _secret = randomBase64(24, urlSafe: true); + /// The URL for this server. - Uri get url => baseUrlForAddress(_server.address, _server.port); + Uri get url => baseUrlForAddress(_server.address, _server.port) + .resolve(_secret + "/"); - /// a [OneOffHandler] for servicing WebSocket connections for + /// A [OneOffHandler] for servicing WebSocket connections for /// [BrowserManager]s. /// /// This is one-off because each [BrowserManager] can only connect to a single /// WebSocket, final _webSocketHandler = new OneOffHandler(); + /// A [PathHandler] used to serve compiled JS. + final _jsHandler = new PathHandler(); + /// The [CompilerPool] managing active instances of `dart2js`. /// /// This is `null` if tests are loaded from `pub serve`. @@ -68,6 +82,9 @@ class BrowserServer { /// The temporary directory in which compiled JS is emitted. final String _compiledDir; + /// The root directory served statically by this server. + final String _root; + /// The package root which is passed to `dart2js`. final String _packageRoot; @@ -109,8 +126,9 @@ class BrowserServer { /// per run, rather than one per browser per run. final _compileFutures = new Map<String, Future>(); - BrowserServer._(this._packageRoot, Uri pubServeUrl, bool color) - : _pubServeUrl = pubServeUrl, + BrowserServer._(String root, this._packageRoot, Uri pubServeUrl, bool color) + : _root = root == null ? p.current : root, + _pubServeUrl = pubServeUrl, _compiledDir = pubServeUrl == null ? createTempDir() : null, _http = pubServeUrl == null ? null : new HttpClient(), _compilers = new CompilerPool(color: color); @@ -121,19 +139,70 @@ class BrowserServer { .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')); + .add(_createPackagesHandler()) + .add(_jsHandler.handler) + .add(_wrapperHandler) + .add(createStaticHandler(_root)); } - return shelf_io.serve(cascade.handler, 'localhost', 0).then((server) { + var pipeline = new shelf.Pipeline() + .addMiddleware(nestingMiddleware(_secret)) + .addHandler(cascade.handler); + + return shelf_io.serve(pipeline, 'localhost', 0).then((server) { _server = server; }); } + /// Returns a handler that serves the contents of the "packages/" directory + /// for any URL that contains "packages/". + /// + /// This is a factory so it can wrap a static handler. + shelf.Handler _createPackagesHandler() { + var packageRoot = _packageRoot == null + ? p.join(_root, 'packages') + : _packageRoot; + var staticHandler = + createStaticHandler(packageRoot, serveFilesOutsidePath: true); + + return (request) { + var segments = p.url.split(shelfUrl(request).path); + + for (var i = 0; i < segments.length; i++) { + if (segments[i] != "packages") continue; + return staticHandler( + shelfChange(request, path: p.url.joinAll(segments.take(i + 1)))); + } + + return new shelf.Response.notFound("Not found."); + }; + } + + /// A handler that serves wrapper HTML to bootstrap tests. + shelf.Response _wrapperHandler(shelf.Request request) { + var path = p.fromUri(shelfUrl(request)); + var withoutExtensions = p.withoutExtension(p.withoutExtension(path)); + var base = p.basename(withoutExtensions); + + if (path.endsWith(".browser_test.html")) { + // TODO(nweiz): support user-authored HTML files. + return new shelf.Response.ok(''' +<!DOCTYPE html> +<html> +<head> + <title>${HTML_ESCAPE.convert(base)}.dart Test</title> + <script type="application/javascript" + src="${HTML_ESCAPE.convert(base)}.browser_test.dart.js"> + </script> +</head> +</html> +''', headers: {'Content-Type': 'text/html'}); + } + + return new shelf.Response.notFound('Not found.'); + } + /// Loads the test suite at [path] on the browser [browser]. /// /// This will start a browser to load the suite if one isn't already running. @@ -145,22 +214,18 @@ class BrowserServer { return new Future.sync(() { if (_pubServeUrl != null) { - var suitePrefix = p.withoutExtension(p.relative(path, from: 'test')) + + var suitePrefix = p.relative(path, from: p.join(_root, '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) { - if (_closed) return null; - - // 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 + "/"); - }); + return _pubServeSuite(path, jsUrl).then((_) => + _pubServeUrl.resolve('$suitePrefix.html')); } + + return _compileSuite(path).then((_) { + if (_closed) return null; + return url.resolveUri( + p.toUri(p.relative(path, from: _root) + ".browser_test.html")); + }); }).then((suiteUrl) { if (_closed) return null; @@ -210,27 +275,24 @@ class BrowserServer { /// Compile the test suite at [dartPath] to JavaScript. /// - /// Returns a [Future] that completes to the path to the JavaScript. - Future<String> _compileSuite(String dartPath) { + /// Once the suite has been compiled, it's added to [_jsHandler] so it can be + /// served. + Future _compileSuite(String dartPath) { return _compileFutures.putIfAbsent(dartPath, () { var dir = new Directory(_compiledDir).createTempSync('test_').path; var jsPath = p.join(dir, p.basename(dartPath) + ".js"); + return _compilers.compile(dartPath, jsPath, packageRoot: packageRootFor(dartPath, _packageRoot)) .then((_) { - if (_closed) return null; + if (_closed) return; - // TODO(nweiz): support user-authored HTML files. - new File(p.join(dir, "index.html")).writeAsStringSync(''' -<!DOCTYPE html> -<html> -<head> - <title>${HTML_ESCAPE.convert(dartPath)} Test</title> - <script src="${HTML_ESCAPE.convert(p.basename(jsPath))}"></script> -</head> -</html> -'''); - return dir; + _jsHandler.add( + p.relative(dartPath, from: _root) + '.browser_test.dart.js', + (request) { + return new shelf.Response.ok(new File(jsPath).readAsStringSync(), + headers: {'Content-Type': 'application/javascript'}); + }); }); }); } @@ -248,13 +310,10 @@ class BrowserServer { completer.complete(new BrowserManager(webSocket)); })); - var webSocketUrl = url.replace(scheme: 'ws', path: '/$path'); + var webSocketUrl = url.replace(scheme: 'ws').resolve(path); - var hostUrl = url; - if (_pubServeUrl != null) { - hostUrl = _pubServeUrl.resolve( - '/packages/test/src/runner/browser/static/'); - } + var hostUrl = (_pubServeUrl == null ? url : _pubServeUrl) + .resolve('packages/test/src/runner/browser/static/index.html'); var browser = _newBrowser(hostUrl.replace(queryParameters: { 'managerUrl': webSocketUrl.toString() diff --git a/lib/src/runner/loader.dart b/lib/src/runner/loader.dart index afff40a5544d3fd99a3228a7d783c15ee01628e4..475e10d5f277024a6c4a9c281b9928362d033ae5 100644 --- a/lib/src/runner/loader.dart +++ b/lib/src/runner/loader.dart @@ -32,6 +32,9 @@ class Loader { /// Whether to enable colors for Dart compilation. final bool _color; + /// The root directory that will be served for browser tests. + final String _root; + /// The package root to use for loading tests, or `null` to use the automatic /// root. final String _packageRoot; @@ -51,6 +54,7 @@ class Loader { if (_browserServerCompleter == null) { _browserServerCompleter = new Completer(); BrowserServer.start( + root: _root, packageRoot: _packageRoot, pubServeUrl: _pubServeUrl, color: _color) @@ -63,6 +67,9 @@ class Loader { /// Creates a new loader. /// + /// [root] is the root directory that will be served for browser tests. It + /// defaults to the working directory. + /// /// If [packageRoot] is passed, it's used as the package root for all loaded /// tests. Otherwise, the `packages/` directories next to the test entrypoints /// will be used. @@ -71,10 +78,11 @@ class Loader { /// instance at that URL rather than from the filesystem. /// /// If [color] is true, console colors will be used when compiling Dart. - Loader(Iterable<TestPlatform> platforms, {String packageRoot, + Loader(Iterable<TestPlatform> platforms, {String root, String packageRoot, Uri pubServeUrl, bool color: false}) : _platforms = platforms.toList(), _pubServeUrl = pubServeUrl, + _root = root == null ? p.current : root, _packageRoot = packageRoot, _color = color; @@ -154,8 +162,7 @@ class Loader { return new Future.sync(() { if (_pubServeUrl != null) { var url = _pubServeUrl.resolve( - p.withoutExtension(p.relative(path, from: 'test')) + - '.vm_test.dart'); + p.relative(path, from: 'test') + '.vm_test.dart'); return Isolate.spawnUri(url, [], {'reply': receivePort.sendPort}) .then((isolate) => new IsolateWrapper(isolate, () {})) .catchError((error, stackTrace) { diff --git a/lib/src/util/one_off_handler.dart b/lib/src/util/one_off_handler.dart index 069d6818875d0d4028f022be2d5060cd2537d61a..442a76486590504b3c606904899641d2be525290 100644 --- a/lib/src/util/one_off_handler.dart +++ b/lib/src/util/one_off_handler.dart @@ -2,11 +2,13 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library test.one_off_handler; +library test.util.one_off_handler; import 'package:path/path.dart' as p; import 'package:shelf/shelf.dart' as shelf; +import '../utils.dart'; + /// A Shelf handler that provides support for one-time handlers. /// /// This is useful for handlers that only expect to be hit once before becoming @@ -36,18 +38,12 @@ class OneOffHandler { /// Dispatches [request] to the appropriate handler. _onRequest(shelf.Request request) { - var components = p.url.split(request.url.path); - - // For shelf < 0.6.0, the first component of the path is always "/". We can - // safely skip it. - if (components.isNotEmpty && components.first == "/") { - components.removeAt(0); - } - + var components = p.url.split(shelfUrl(request).path); if (components.isEmpty) return new shelf.Response.notFound(null); - var handler = _handlers.remove(components.removeAt(0)); + var path = components.removeAt(0); + var handler = _handlers.remove(path); if (handler == null) return new shelf.Response.notFound(null); - return handler(request); + return handler(shelfChange(request, path: path)); } } diff --git a/lib/src/util/path_handler.dart b/lib/src/util/path_handler.dart new file mode 100644 index 0000000000000000000000000000000000000000..45ce45a22b7567876cd2cd37f2db9671cea169b1 --- /dev/null +++ b/lib/src/util/path_handler.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library test.util.path_handler; + +import 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart' as shelf; + +import '../utils.dart'; + +/// A handler that routes to sub-handlers based on exact path prefixes. +class PathHandler { + /// A trie of path components to handlers. + final _paths = new _Node(); + + /// The shelf handler. + shelf.Handler get handler => _onRequest; + + PathHandler(); + + /// Routes requests at or under [path] to [handler]. + /// + /// If [path] is a parent or child directory of another path in this handler, + /// the longest matching prefix wins. + void add(String path, shelf.Handler handler) { + var node = _paths; + for (var component in p.url.split(path)) { + node = node.children.putIfAbsent(component, () => new _Node()); + } + node.handler = handler; + } + + _onRequest(shelf.Request request) { + var handler; + var handlerIndex; + var node = _paths; + var components = p.url.split(shelfUrl(request).path); + for (var i = 0; i < components.length; i++ ) { + node = node.children[components[i]]; + if (node == null) break; + if (node.handler == null) continue; + handler = node.handler; + handlerIndex = i; + } + + if (handler == null) return new shelf.Response.notFound("Not found."); + + return handler(shelfChange(request, + path: p.joinAll(components.take(handlerIndex + 1)))); + } +} + +/// A trie node. +class _Node { + shelf.Handler handler; + final children = new Map<String, _Node>(); +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 7d1094edd970080d7e04b8690353efb78633cad2..37f2e6639248a30d20dd15e6d7293f54bd11b286 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -5,11 +5,15 @@ library test.utils; import 'dart:async'; +import 'dart:math' as math; +import 'package:crypto/crypto.dart'; import 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart' as shelf; import 'package:stack_trace/stack_trace.dart'; import 'backend/operating_system.dart'; +import 'util/path_handler.dart'; /// A typedef for a possibly-asynchronous function. /// @@ -179,3 +183,62 @@ Stream mergeStreams(Iterable<Stream> streamIter) { return controller.stream; } + +/// Returns a random base64 string containing [bytes] bytes of data. +/// +/// [seed] is passed to [math.Random]; [urlSafe] and [addLineSeparator] are +/// passed to [CryptoUtils.bytesToBase64]. +String randomBase64(int bytes, {int seed, bool urlSafe: false, + bool addLineSeparator: false}) { + var random = new math.Random(seed); + var data = []; + for (var i = 0; i < bytes; i++) { + data.add(random.nextInt(256)); + } + return CryptoUtils.bytesToBase64(data, + urlSafe: urlSafe, addLineSeparator: addLineSeparator); +} + +// TODO(nweiz): Remove this and [shelfChange] once Shelf 0.6.0 has been out for +// six months or so. +/// Returns `request.url` in a cross-version way. +/// +/// This follows the semantics of Shelf 0.6.x, even when using Shelf 0.5.x: the +/// returned URL never starts with "/". +Uri shelfUrl(shelf.Request request) { + var url = request.url; + if (!url.path.startsWith("/")) return url; + return url.replace(path: url.path.replaceFirst("/", "")); +} + +/// Like [shelf.Request.change], but cross-version. +/// +/// This follows the semantics of Shelf 0.6.x, even when using Shelf 0.5.x. +shelf.Request shelfChange(shelf.Request typedRequest, {String path}) { + // Explicitly make the request dynamic since we're calling methods here that + // aren't defined in all support Shelf versions, and we don't want the + // analyzer to complain. + var request = typedRequest as dynamic; + + try { + return request.change(path: path); + } on NoSuchMethodError catch (_) { + var newScriptName = p.url.join(request.scriptName, path); + if (request.scriptName.isEmpty) newScriptName = "/" + newScriptName; + + var newUrlPath = p.url.relative(request.url.path.replaceFirst("/", ""), + from: path); + newUrlPath = newUrlPath == "." ? "" : "/" + newUrlPath; + + return request.change( + scriptName: newScriptName, url: request.url.replace(path: newUrlPath)); + } +} + +/// Returns middleware that nests all requests beneath the URL prefix [beneath]. +shelf.Middleware nestingMiddleware(String beneath) { + return (handler) { + var pathHandler = new PathHandler()..add(beneath, handler); + return pathHandler.handler; + }; +} diff --git a/pubspec.yaml b/pubspec.yaml index f28b7cb5cb1b0b9798737de7c2cc3753d7f51e7d..3aedd5852f94b379bcdefe6bab9370b87189caae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: test -version: 0.12.0-beta.6 +version: 0.12.0-dev author: Dart Team <misc@dartlang.org> description: A library for writing dart unit tests. homepage: https://github.com/dart-lang/test @@ -9,6 +9,7 @@ dependencies: analyzer: '>=0.23.0 <0.25.0' args: '>=0.12.1 <0.14.0' barback: '>=0.14.0 <0.16.0' + crypto: '^0.9.0' http_parser: '^0.0.2' pool: '^1.0.0' pub_semver: '^1.0.0' diff --git a/test/runner/browser/loader_test.dart b/test/runner/browser/loader_test.dart index a3e93c5fe2c20cf6c957a3bdfd187a174a38bf39..322de04cd04951fa23aae36b8a98860f71eda259 100644 --- a/test/runner/browser/loader_test.dart +++ b/test/runner/browser/loader_test.dart @@ -33,9 +33,10 @@ void main() { void main() { setUp(() { + _sandbox = createTempDir(); _loader = new Loader([TestPlatform.chrome], + root: _sandbox, packageRoot: p.join(packageDir, 'packages')); - _sandbox = createTempDir(); /// TODO(nweiz): Use scheduled_test for this once it's compatible with this /// version of test. new File(p.join(_sandbox, 'a_test.dart')).writeAsStringSync(_tests); @@ -88,7 +89,7 @@ void main() { }); test("throws a nice error if the package root doesn't exist", () { - var loader = new Loader([TestPlatform.chrome]); + var loader = new Loader([TestPlatform.chrome], root: _sandbox); expect( loader.loadFile(p.join(_sandbox, 'a_test.dart')).first .whenComplete(loader.close), @@ -98,6 +99,7 @@ void main() { test("loads a suite both in the browser and the VM", () { var loader = new Loader([TestPlatform.vm, TestPlatform.chrome], + root: _sandbox, packageRoot: p.join(packageDir, 'packages')); var path = p.join(_sandbox, 'a_test.dart'); return loader.loadFile(path).toList().then((suites) { diff --git a/test/runner/loader_test.dart b/test/runner/loader_test.dart index f5e8160848097826367189249ce2d49dfb228345..8474cbedbc4ebfa713af46b3077bce88babbd349 100644 --- a/test/runner/loader_test.dart +++ b/test/runner/loader_test.dart @@ -33,9 +33,10 @@ void main() { void main() { setUp(() { + _sandbox = createTempDir(); _loader = new Loader([TestPlatform.vm], + root: _sandbox, packageRoot: p.join(packageDir, 'packages')); - _sandbox = createTempDir(); }); tearDown(() { @@ -87,7 +88,7 @@ void main() { }); test("throws a nice error if the package root doesn't exist", () { - var loader = new Loader([TestPlatform.vm]); + var loader = new Loader([TestPlatform.vm], root: _sandbox); expect( loader.loadFile(p.join(_sandbox, 'a_test.dart')).first .whenComplete(loader.close), diff --git a/test/runner/pub_serve_test.dart b/test/runner/pub_serve_test.dart index 6304b8cfe68add1d6863dfaf0aedef9ba6fd5ec2..d15370f5dd9ee2ad2e3cb3330f4676d7898082af 100644 --- a/test/runner/pub_serve_test.dart +++ b/test/runner/pub_serve_test.dart @@ -223,7 +223,7 @@ transformers: contains('-1: load error'), contains(''' Failed to load "test/my_test.dart": - Error getting http://localhost:54321/my_test.vm_test.dart: Connection refused + Error getting http://localhost:54321/my_test.dart.vm_test.dart: Connection refused Make sure "pub serve" is running.''') ])); expect(result.exitCode, equals(1)); @@ -235,8 +235,8 @@ transformers: expect(result.stdout, allOf([ contains('-1: load error'), contains('Failed to load "test/my_test.dart":'), - contains('Error getting http://localhost:54321/my_test.browser_test.dart' - '.js: Connection refused (errno '), + contains('Error getting http://localhost:54321/my_test.dart.browser_test' + '.dart.js: Connection refused (errno '), contains('Make sure "pub serve" is running.') ])); expect(result.exitCode, equals(1)); diff --git a/test/util/path_handler_test.dart b/test/util/path_handler_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..d6ea38f12f84b92140906c57b2de9bf36063e259 --- /dev/null +++ b/test/util/path_handler_test.dart @@ -0,0 +1,77 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:shelf/shelf.dart' as shelf; +import 'package:test/src/util/path_handler.dart'; +import 'package:test/test.dart'; + +void main() { + var handler; + setUp(() => handler = new PathHandler()); + + _handle(request) => new Future.sync(() => handler.handler(request)); + + test("returns a 404 for a root URL", () { + var request = new shelf.Request("GET", Uri.parse("http://localhost/")); + return _handle(request).then((response) { + expect(response.statusCode, equals(404)); + }); + }); + + test("returns a 404 for an unregistered URL", () { + var request = new shelf.Request("GET", Uri.parse("http://localhost/foo")); + return _handle(request).then((response) { + expect(response.statusCode, equals(404)); + }); + }); + + test("runs a handler for an exact URL", () { + var request = new shelf.Request("GET", Uri.parse("http://localhost/foo")); + handler.add("foo", expectAsync((request) { + expect(request.handlerPath, equals('/foo')); + expect(request.url.path, isEmpty); + return new shelf.Response.ok("good job!"); + })); + + return _handle(request).then((response) { + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals("good job!"))); + }); + }); + + test("runs a handler for a suffix", () { + var request = new shelf.Request( + "GET", Uri.parse("http://localhost/foo/bar")); + handler.add("foo", expectAsync((request) { + expect(request.handlerPath, equals('/foo/')); + expect(request.url.path, 'bar'); + return new shelf.Response.ok("good job!"); + })); + + return _handle(request).then((response) { + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals("good job!"))); + }); + }); + + test("runs the longest matching handler", () { + var request = new shelf.Request( + "GET", Uri.parse("http://localhost/foo/bar/baz")); + + handler.add("foo", expectAsync((_) {}, count: 0)); + handler.add("foo/bar", expectAsync((request) { + expect(request.handlerPath, equals('/foo/bar/')); + expect(request.url.path, 'baz'); + return new shelf.Response.ok("good job!"); + })); + handler.add("foo/bar/baz/bang", expectAsync((_) {}, count: 0)); + + return _handle(request).then((response) { + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals("good job!"))); + }); + }); +}