Skip to content
Snippets Groups Projects
Commit c45a2016 authored by Natalie Weizenbaum's avatar Natalie Weizenbaum
Browse files

Add a class to start and stop a Chrome process.

See #5

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//958423002
parent 3bd1e9d3
No related branches found
No related tags found
No related merge requests found
// 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 unittest.runner.browser.chrome;
import 'dart:async';
import 'dart:io';
import '../../util/io.dart';
// TODO(nweiz): move this into its own package?
// TODO(nweiz): support other browsers.
/// A class for running an instance of Chrome.
///
/// Most of the communication with the browser is expected to happen via HTTP,
/// so this exposes a bare-bones API. The browser starts as soon as the class is
/// constructed, and is killed when [close] is called.
///
/// Any errors starting or running the process are reported through [onExit].
class Chrome {
/// The underlying process.
Process _process;
/// The temporary directory used as the browser's user data dir.
///
/// A new data dir is created for each run to ensure that they're
/// well-isolated.
String _dir;
/// A future that completes when the browser exits.
///
/// If there's a problem starting or running the browser, this will complete
/// with an error.
Future get onExit => _onExitCompleter.future;
final _onExitCompleter = new Completer();
/// A future that completes when the browser process has started.
///
/// This is used to ensure that [close] works regardless of when it's called.
Future get _onProcessStarted => _onProcessStartedCompleter.future;
final _onProcessStartedCompleter = new Completer();
/// Starts a new instance of Chrome open to the given [url], which may be a
/// [Uri] or a [String].
///
/// If [executable] is passed, it's used as the Chrome executable. Otherwise
/// `"google-chrome"` will be looked up on the system PATH.
Chrome(url, {String executable}) {
if (executable == null) executable = "google-chrome";
// Don't return a Future here because there's no need for the caller to wait
// for the process to actually start. They should just wait for the HTTP
// request instead.
withTempDir((dir) {
_dir = dir;
return Process.start(executable, [
"--user-data-dir=$_dir",
url.toString(),
"--disable-extensions",
"--disable-popup-blocking",
"--bwsi",
"--no-first-run"
]).then((process) {
_process = process;
_onProcessStartedCompleter.complete();
// TODO(nweiz): the browser's standard output is almost always useless
// noise, but we should allow the user to opt in to seeing it.
return _process.exitCode;
});
}).then((exitCode) {
if (exitCode != 0) throw "Chrome failed with exit code $exitCode.";
}).then(_onExitCompleter.complete)
.catchError(_onExitCompleter.completeError);
}
/// Kills the browser process.
///
/// Returns the same [Future] as [onExit], except that it won't emit
/// exceptions.
Future close() {
_onProcessStarted.then((_) => _process.kill());
// Swallow exceptions. The user should explicitly use [onExit] for these.
return onExit.catchError((_) {});
}
}
......@@ -57,3 +57,20 @@ Future withTempDir(Future fn(String path)) {
.whenComplete(() => tempDir.deleteSync(recursive: true));
});
}
/// Creates a URL string for [address]:[port].
///
/// Handles properly formatting IPv6 addresses.
Uri baseUrlForAddress(InternetAddress address, int port) {
if (address.isLoopback) {
return new Uri(scheme: "http", host: "localhost", port: port);
}
// IPv6 addresses in URLs need to be enclosed in square brackets to avoid
// URL ambiguity with the ":" in the address.
if (address.type == InternetAddressType.IP_V6) {
return new Uri(scheme: "http", host: "[${address.address}]", port: port);
}
return new Uri(scheme: "http", host: address.address, port: port);
}
......@@ -8,6 +8,8 @@ environment:
dependencies:
args: '^0.12.1'
matcher: '^0.12.0-alpha.0'
shelf: '^0.5.3'
shelf_web_socket: '^0.0.1'
stack_trace: '^1.2.0'
dev_dependencies:
fake_async: '^0.1.2'
// 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:unittest/unittest.dart';
import 'package:unittest/src/runner/browser/chrome.dart';
import 'package:unittest/src/util/io.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_web_socket/shelf_web_socket.dart';
void main() {
group("running JavaScript", () {
// The JavaScript to serve in the server. We use actual JavaScript here to
// avoid the pain of compiling to JS in a test
var javaScript;
var servePage = (request) {
if (request.url.path == "/") {
return new shelf.Response.ok("""
<!doctype html>
<html>
<head>
<script src="index.js"></script>
</head>
</html>
""", headers: {'content-type': 'text/html'});
} else if (request.url.path == "/index.js") {
return new shelf.Response.ok(javaScript,
headers: {'content-type': 'application/javascript'});
} else {
return new shelf.Response.notFound(null);
}
};
var server;
var webSockets;
setUp(() {
var webSocketsController = new StreamController();
webSockets = webSocketsController.stream;
return shelf_io.serve(
new shelf.Cascade()
.add(webSocketHandler(webSocketsController.add))
.add(servePage).handler,
'localhost', 0).then((server_) {
server = server_;
});
});
tearDown(() {
if (server != null) server.close();
javaScript = null;
server = null;
webSockets = null;
});
test("starts Chrome with the given URL", () {
javaScript = '''
var webSocket = new WebSocket(window.location.href.replace("http://", "ws://"));
webSocket.addEventListener("open", function() {
webSocket.send("loaded!");
});
''';
var chrome = new Chrome(baseUrlForAddress(server.address, server.port));
return webSockets.first.then((webSocket) {
return webSocket.first.then(
(message) => expect(message, equals("loaded!")));
}).whenComplete(chrome.close);
});
test("doesn't preserve state across runs", () {
javaScript = '''
localStorage.setItem("data", "value");
var webSocket = new WebSocket(window.location.href.replace("http://", "ws://"));
webSocket.addEventListener("open", function() {
webSocket.send("done");
});
''';
var chrome = new Chrome(baseUrlForAddress(server.address, server.port));
var first = true;
webSockets.listen(expectAsync((webSocket) {
if (first) {
// The first request will set local storage data. We can't kill the
// old chrome and start a new one until we're sure that that has
// finished.
webSocket.first.then((_) {
chrome.close();
javaScript = '''
var webSocket = new WebSocket(window.location.href.replace("http://", "ws://"));
webSocket.addEventListener("open", function() {
webSocket.send(localStorage.getItem("data"));
});
''';
chrome = new Chrome(baseUrlForAddress(server.address, server.port));
first = false;
});
} else {
// The second request will return the local storage data. This should
// be null, indicating that no data was saved between runs.
expect(
webSocket.first
.then((message) => expect(message, equals('null')))
.whenComplete(chrome.close),
completes);
}
}, count: 2));
});
});
test("a process can be killed synchronously after it's started", () {
return shelf_io.serve(expectAsync((_) {}, count: 0), 'localhost', 8080)
.then((server) {
var chrome = new Chrome(baseUrlForAddress(server.address, server.port));
return chrome.close().whenComplete(server.close);
});
});
test("reports an error in onExit", () {
var chrome = new Chrome("http://dart-lang.org",
executable: "_does_not_exist");
expect(chrome.onExit, throwsA(new isInstanceOf<ProcessException>()));
});
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment