Index: lib/src/runner/browser/browser_manager.dart |
diff --git a/lib/src/runner/browser/browser_manager.dart b/lib/src/runner/browser/browser_manager.dart |
index f002f329cd3af47be4cf3dbacfba2180c5c5aad1..d602be814f68d32d644f5d159b8a37e171ad24ba 100644 |
--- a/lib/src/runner/browser/browser_manager.dart |
+++ b/lib/src/runner/browser/browser_manager.dart |
@@ -7,6 +7,7 @@ library test.runner.browser.browser_manager; |
import 'dart:async'; |
import 'dart:convert'; |
+import 'package:async/async.dart'; |
import 'package:http_parser/http_parser.dart'; |
import 'package:pool/pool.dart'; |
@@ -17,18 +18,32 @@ import '../../util/multi_channel.dart'; |
import '../../util/remote_exception.dart'; |
import '../../util/stack_trace_mapper.dart'; |
import '../../utils.dart'; |
+import '../application_exception.dart'; |
import '../environment.dart'; |
import '../load_exception.dart'; |
import '../runner_suite.dart'; |
+import 'browser.dart'; |
+import 'chrome.dart'; |
+import 'content_shell.dart'; |
+import 'dartium.dart'; |
+import 'firefox.dart'; |
import 'iframe_test.dart'; |
+import 'internet_explorer.dart'; |
+import 'phantom_js.dart'; |
+import 'safari.dart'; |
/// A class that manages the connection to a single running browser. |
/// |
/// This is in charge of telling the browser which test suites to load and |
/// converting its responses into [Suite] objects. |
class BrowserManager { |
- /// The browser that this is managing. |
- final TestPlatform browser; |
+ /// The browser instance that this is connected to via [_channel]. |
+ final Browser _browser; |
+ |
+ // TODO(nweiz): Consider removing the duplication between this and |
+ // [_browser.name]. |
+ /// The [TestPlatform] for [_browser]. |
+ final TestPlatform _platform; |
/// The channel used to communicate with the browser. |
/// |
@@ -60,16 +75,78 @@ class BrowserManager { |
CancelableCompleter _pauseCompleter; |
/// The environment to attach to each suite. |
- _BrowserEnvironment _environment; |
+ Future<_BrowserEnvironment> _environment; |
+ |
+ /// Starts the browser identified by [platform] and has it connect to [url]. |
+ /// |
+ /// [url] should serve a page that establishes a WebSocket connection with |
+ /// this process. That connection, once established, should be emitted via |
+ /// [future]. |
+ /// |
+ /// Returns the browser manager, or throws an [ApplicationException] if a |
+ /// connection fails to be established. |
+ static Future<BrowserManager> start(TestPlatform platform, Uri url, |
+ Future<CompatibleWebSocket> future) { |
+ var browser = _newBrowser(url, platform); |
+ |
+ var completer = new Completer(); |
+ |
+ // TODO(nweiz): Gracefully handle the browser being killed before the |
+ // tests complete. |
+ browser.onExit.then((_) { |
+ throw new ApplicationException( |
+ "${platform.name} exited before connecting."); |
+ }).catchError((error, stackTrace) { |
+ if (completer.isCompleted) return; |
+ completer.completeError(error, stackTrace); |
+ }); |
+ |
+ future.then((webSocket) { |
+ if (completer.isCompleted) return; |
+ completer.complete(new BrowserManager._(browser, platform, webSocket)); |
+ }).catchError((error, stackTrace) { |
+ browser.close(); |
+ if (completer.isCompleted) return; |
+ completer.completeError(error, stackTrace); |
+ }); |
+ |
+ return completer.future.timeout(new Duration(seconds: 30), onTimeout: () { |
+ browser.close(); |
+ throw new ApplicationException( |
+ "Timed out waiting for ${platform.name} to connect."); |
+ }); |
+ } |
+ |
+ /// Starts the browser identified by [browser] and has it load [url]. |
+ static Browser _newBrowser(Uri url, TestPlatform browser) { |
+ switch (browser) { |
+ 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); |
+ case TestPlatform.safari: return new Safari(url); |
+ case TestPlatform.internetExplorer: return new InternetExplorer(url); |
+ default: |
+ throw new ArgumentError("$browser is not a browser."); |
+ } |
+ } |
/// Creates a new BrowserManager that communicates with [browser] over |
/// [webSocket]. |
- BrowserManager(this.browser, CompatibleWebSocket webSocket) |
+ BrowserManager._(this._browser, this._platform, CompatibleWebSocket webSocket) |
: _channel = new MultiChannel( |
webSocket.map(JSON.decode), |
mapSink(webSocket, JSON.encode)) { |
- _environment = new _BrowserEnvironment(this); |
- _channel.stream.listen(_onMessage, onDone: _onDone); |
+ _environment = _loadBrowserEnvironment(); |
+ _channel.stream.listen(_onMessage, onDone: close); |
+ } |
+ |
+ /// Loads [_BrowserEnvironment]. |
+ Future<_BrowserEnvironment> _loadBrowserEnvironment() async { |
+ var observatoryUrl; |
+ if (_platform.isDartVM) observatoryUrl = await _browser.observatoryUrl; |
+ return new _BrowserEnvironment(this, observatoryUrl); |
} |
/// Tells the browser the load a test suite from the URL [url]. |
@@ -84,7 +161,7 @@ class BrowserManager { |
{StackTraceMapper mapper}) async { |
url = url.replace(fragment: Uri.encodeFull(JSON.encode({ |
"metadata": metadata.serialize(), |
- "browser": browser.identifier |
+ "browser": _platform.identifier |
}))); |
// The stream may close before emitting a value if the browser is killed |
@@ -130,13 +207,14 @@ class BrowserManager { |
throw new LoadException( |
path, |
"Timed out waiting for the test suite to connect on " |
- "${browser.name}."); |
+ "${_platform.name}."); |
}); |
}); |
if (response == null) { |
closeIframe(); |
- return null; |
+ throw new LoadException( |
+ path, "Connection closed before test suite loaded."); |
} |
if (response["type"] == "loadException") { |
@@ -152,12 +230,12 @@ class BrowserManager { |
asyncError.stackTrace); |
} |
- return new RunnerSuite(_environment, response["tests"].map((test) { |
+ return new RunnerSuite(await _environment, response["tests"].map((test) { |
var testMetadata = new Metadata.deserialize(test['metadata']); |
var testChannel = suiteChannel.virtualChannel(test['channel']); |
return new IframeTest(test['name'], testMetadata, testChannel, |
mapper: mapper); |
- }), platform: browser, metadata: metadata, path: path, |
+ }), platform: _platform, metadata: metadata, path: path, |
onClose: () => closeIframe()); |
} |
@@ -183,12 +261,15 @@ class BrowserManager { |
_pauseCompleter.complete(); |
} |
- /// The callback called when the WebSocket is closed. |
- void _onDone() { |
+ /// Closes the manager and releases any resources it owns, including closing |
+ /// the browser. |
+ Future close() => _closeMemoizer.runOnce(() { |
_closed = true; |
if (_pauseCompleter != null) _pauseCompleter.complete(); |
_pauseCompleter = null; |
- } |
+ return _browser.close(); |
+ }); |
+ final _closeMemoizer = new AsyncMemoizer(); |
} |
/// An implementation of [Environment] for the browser. |
@@ -197,7 +278,9 @@ class BrowserManager { |
class _BrowserEnvironment implements Environment { |
final BrowserManager _manager; |
- _BrowserEnvironment(this._manager); |
+ final Uri observatoryUrl; |
+ |
+ _BrowserEnvironment(this._manager, this.observatoryUrl); |
CancelableFuture displayPause() => _manager._displayPause(); |
} |