diff --git a/.status b/.status index 709e4f48362d06d17a8bc17688646da38d3f9d3a..c5d34c34c8403597990069c5c62b0e94f033e60d 100644 --- a/.status +++ b/.status @@ -23,6 +23,10 @@ lib/*/*: SkipByDesign lib/*/*/*: SkipByDesign lib/*/*/*/*: SkipByDesign +# PhantomJS isn't supported on the bots (issue 23195). +test/runner/browser/phantom_js: Skip +test/runner/browser/runner_test: Skip + test/runner/browser/loader_test: Pass, Slow # The test harness for browser tests doesn't play nicely with the new way of diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a84739db34583e865134456172771ed0a02982..99ade249ffecf0325d0367d51b2f8fcc1c0ae411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * Add support for running tests on Dartium and the Dartium content shell. +* Add support for running tests on [PhantomJS](http://phantomjs.org/). + ### 0.12.0-beta.6 * Add the ability to run multiple test suites concurrently. By default a number diff --git a/README.md b/README.md index cb04c1ac6dc10b855a471e63b559307179672e01..9e951906fc2a12217247a0f42fa6496172ead231 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,9 @@ valid identifiers are: * `chrome`: Whether the test is running on Google Chrome. +* `phantomjs`: Whether the test is running on + [PhantomJS](http://phantomjs.org/). + * `dart-vm`: Whether the test is running on the Dart VM in any context, including Dartium. It's identical to `!js`. diff --git a/lib/src/backend/test_platform.dart b/lib/src/backend/test_platform.dart index 881dcf4aa27e52b176181671947f259e9799faa7..f2577d486f7b30921bbecd85cc6141e5859ca369 100644 --- a/lib/src/backend/test_platform.dart +++ b/lib/src/backend/test_platform.dart @@ -27,13 +27,18 @@ class TestPlatform { static const TestPlatform chrome = const TestPlatform._("Chrome", "chrome", isBrowser: true, isJS: true, isBlink: true); + /// PhantomJS. + static const TestPlatform phantomJS = const TestPlatform._( + "PhantomJS", "phantomjs", + isBrowser: true, isJS: true, isBlink: true); + /// Mozilla Firefox. static const TestPlatform firefox = const TestPlatform._("Firefox", "firefox", isBrowser: true, isJS: true); /// A list of all instances of [TestPlatform]. static const List<TestPlatform> all = - const [vm, dartium, contentShell, chrome, firefox]; + const [vm, dartium, contentShell, chrome, phantomJS, firefox]; /// Finds a platform by its identifier string. /// diff --git a/lib/src/runner/browser/phantom_js.dart b/lib/src/runner/browser/phantom_js.dart new file mode 100644 index 0000000000000000000000000000000000000000..b4e2f4a40e44ce8326e3be1f6f7a5ffb08d53e9d --- /dev/null +++ b/lib/src/runner/browser/phantom_js.dart @@ -0,0 +1,87 @@ +// 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.runner.browser.phantom_js; + +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import '../../util/io.dart'; +import 'browser.dart'; + +/// The PhantomJS script that opens the host page. +final _script = """ +var system = require('system'); +var page = require('webpage').create(); + +// Pipe browser messages to the process's stdout. This isn't used by default, +// but it can be useful for debugging. +page.onConsoleMessage = function(message) { + console.log(message); +} + +page.open(system.args[1], function(status) { + if (status !== "success") phantom.exit(1); +}); +"""; + +/// A class for running an instance of PhantomJS. +/// +/// Any errors starting or running the process are reported through [onExit]. +class PhantomJS implements Browser { + /// The underlying process. + Process _process; + + 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 PhantomJS open to the given [url], which may be a + /// [Uri] or a [String]. + /// + /// If [executable] is passed, it's used as the PhantomJS executable. + /// Otherwise the default executable name for the current OS will be used. + PhantomJS(url, {String executable}) { + if (executable == null) { + executable = Platform.isWindows ? "phantomjs.exe" : "phantomjs"; + } + + // 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) { + var script = p.join(dir, "script.js"); + new File(script).writeAsStringSync(_script); + + return Process.start(executable, [script, url.toString()]) + .then((process) { + // PhantomJS synchronously emits standard output, which means that if we + // don't drain its stdout stream it can deadlock. + process.stdout.listen((_) {}); + + _process = process; + _onProcessStartedCompleter.complete(); + + return _process.exitCode; + }); + }).then((exitCode) { + if (exitCode != 0) throw "PhantomJS failed with exit code $exitCode."; + }).then(_onExitCompleter.complete) + .catchError(_onExitCompleter.completeError); + } + + Future close() { + _onProcessStarted.then((_) => _process.kill()); + + // Swallow exceptions. The user should explicitly use [onExit] for these. + return onExit.catchError((_) {}); + } +} diff --git a/lib/src/runner/browser/server.dart b/lib/src/runner/browser/server.dart index 67cead97be81560c4933e10d4fb0a827a70209e4..eacec516f7e273e5b1fbe3ea82429c6f393fdc8a 100644 --- a/lib/src/runner/browser/server.dart +++ b/lib/src/runner/browser/server.dart @@ -29,6 +29,7 @@ import 'chrome.dart'; import 'dartium.dart'; import 'content_shell.dart'; import 'firefox.dart'; +import 'phantom_js.dart'; /// A server that serves JS-compiled tests to browsers. /// @@ -352,6 +353,7 @@ void main() { case TestPlatform.dartium: return new Dartium(url); case TestPlatform.contentShell: return new ContentShell(url); case TestPlatform.chrome: return new Chrome(url); + case TestPlatform.phantomJS: return new PhantomJS(url); case TestPlatform.firefox: return new Firefox(url); default: throw new ArgumentError("$browser is not a browser."); diff --git a/test/runner/browser/phantom_js_test.dart b/test/runner/browser/phantom_js_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..09a5c6b60e93b0c20c16378c6ba93c66cbc00a3f --- /dev/null +++ b/test/runner/browser/phantom_js_test.dart @@ -0,0 +1,144 @@ +// 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. + +@TestOn("vm") + +import 'dart:async'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:test/src/runner/browser/phantom_js.dart'; +import 'package:test/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) { + var path = request.url.path; + + // We support both shelf 0.5.x and 0.6.x. The former has a leading "/" + // here, the latter does not. + if (path.startsWith("/")) path = path.substring(1); + + if (path.isEmpty) { + return new shelf.Response.ok(""" +<!doctype html> +<html> +<head> + <script src="index.js"></script> +</head> +</html> +""", headers: {'content-type': 'text/html'}); + } else if (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 PhantomJs with the given URL", () { + javaScript = ''' +var webSocket = new WebSocket(window.location.href.replace("http://", "ws://")); +webSocket.addEventListener("open", function() { + webSocket.send("loaded!"); +}); +'''; + var phantomJS = new PhantomJS( + baseUrlForAddress(server.address, server.port)); + + return webSockets.first.then((webSocket) { + return webSocket.first.then( + (message) => expect(message, equals("loaded!"))); + }).whenComplete(phantomJS.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 phantomJS = new PhantomJS( + 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 PhantomJS and start a new one until we're sure that that has + // finished. + webSocket.first.then((_) { + phantomJS.close(); + + javaScript = ''' +var webSocket = new WebSocket(window.location.href.replace("http://", "ws://")); +webSocket.addEventListener("open", function() { + webSocket.send(localStorage.getItem("data")); +}); +'''; + phantomJS = new PhantomJS( + 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(phantomJS.close), + completes); + } + }, count: 2)); + }); + }); + + test("a process can be killed synchronously after it's started", () { + return shelf_io.serve(expectAsync((_) {}, count: 0), 'localhost', 0) + .then((server) { + var phantomJS = new PhantomJS( + baseUrlForAddress(server.address, server.port)); + return phantomJS.close().whenComplete(server.close); + }); + }); + + test("reports an error in onExit", () { + var phantomJS = new PhantomJS("http://dart-lang.org", + executable: "_does_not_exist"); + expect(phantomJS.onExit, throwsA(new isInstanceOf<ProcessException>())); + }); +} diff --git a/test/runner/browser/runner_test.dart b/test/runner/browser/runner_test.dart index e2c4072f2aa8b0c3fb62ecb20291ca834e98fc2c..80f9774eefc82df5a18511c07a3ff6138c2514f3 100644 --- a/test/runner/browser/runner_test.dart +++ b/test/runner/browser/runner_test.dart @@ -126,6 +126,12 @@ void main() { expect(result.exitCode, equals(0)); }); + test("on PhantomJS", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(_success); + var result = _runUnittest(["-p", "phantomjs", "test.dart"]); + expect(result.exitCode, equals(0)); + }); + test("on Firefox", () { new File(p.join(_sandbox, "test.dart")).writeAsStringSync(_success); var result = _runUnittest(["-p", "firefox", "test.dart"]); @@ -175,6 +181,12 @@ void main() { expect(result.exitCode, equals(1)); }); + test("on PhantomJS", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(_failure); + var result = _runUnittest(["-p", "phantomjs", "test.dart"]); + expect(result.exitCode, equals(1)); + }); + test("on Firefox", () { new File(p.join(_sandbox, "test.dart")).writeAsStringSync(_failure); var result = _runUnittest(["-p", "firefox", "test.dart"]); diff --git a/test/runner/runner_test.dart b/test/runner/runner_test.dart index 1da0be216cf45325c64589e6646fd90a392fce00..6b38a4d3be3e65d395d3d7fa4ad139d77e7e155d 100644 --- a/test/runner/runner_test.dart +++ b/test/runner/runner_test.dart @@ -47,7 +47,7 @@ Usage: pub run test:test [files or directories...] -N, --plain-name A plain-text substring of the name of the test to run. -p, --platform The platform(s) on which to run the tests. - [vm (default), dartium, content-shell, chrome, firefox] + [vm (default), dartium, content-shell, chrome, phantomjs, firefox] -j, --concurrency=<threads> The number of concurrent test suites run. (defaults to $_defaultConcurrency)