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 5b4e669c05c7cb07cedd40b74403507751c23ae6..513c2a7cb149b17468a654b6ae67af363f158067 100644 |
--- a/lib/src/runner/browser/browser_manager.dart |
+++ b/lib/src/runner/browser/browser_manager.dart |
@@ -15,6 +15,7 @@ import '../../backend/test_platform.dart'; |
import '../../util/stack_trace_mapper.dart'; |
import '../application_exception.dart'; |
import '../environment.dart'; |
+import '../plugin/platform_helpers.dart'; |
import '../runner_suite.dart'; |
import 'browser.dart'; |
import 'chrome.dart'; |
@@ -24,7 +25,6 @@ import 'firefox.dart'; |
import 'internet_explorer.dart'; |
import 'phantom_js.dart'; |
import 'safari.dart'; |
-import 'suite.dart'; |
/// A class that manages the connection to a single running browser. |
/// |
@@ -42,7 +42,7 @@ class BrowserManager { |
/// The channel used to communicate with the browser. |
/// |
/// This is connected to a page running `static/host.dart`. |
- final MultiChannel _channel; |
+ MultiChannel _channel; |
/// A pool that ensures that limits the number of initial connections the |
/// manager will wait for at once. |
@@ -57,7 +57,7 @@ class BrowserManager { |
/// |
/// This is used to ensure that the suites can be referred to consistently |
/// across the client and server. |
- int _suiteId = 0; |
+ int _suiteID = 0; |
/// Whether the channel to the browser has closed. |
bool _closed = false; |
@@ -71,6 +71,18 @@ class BrowserManager { |
/// The environment to attach to each suite. |
Future<_BrowserEnvironment> _environment; |
+ /// Controllers for every suite in this browser. |
+ /// |
+ /// These are used to mark suites as debugging or not based on the browser's |
+ /// pings. |
+ final _controllers = new Set<RunnerSuiteController>(); |
+ |
+ // A timer that's reset whenever we receive a message from the browser. |
+ // |
+ // Because the browser stops running code when the user is actively debugging, |
+ // this lets us detect whether they're debugging reasonably accurately. |
+ RestartableTimer _timer; |
+ |
/// Starts the browser identified by [platform] and has it connect to [url]. |
/// |
/// [url] should serve a page that establishes a WebSocket connection with |
@@ -133,8 +145,33 @@ class BrowserManager { |
/// Creates a new BrowserManager that communicates with [browser] over |
/// [webSocket]. |
- BrowserManager._(this._browser, this._platform, WebSocketChannel webSocket) |
- : _channel = new MultiChannel(webSocket.transform(jsonDocument)) { |
+ BrowserManager._(this._browser, this._platform, WebSocketChannel webSocket) { |
+ // The duration should be short enough that the debugging console is open as |
+ // soon as the user is done setting breakpoints, but long enough that a test |
+ // doing a lot of synchronous work doesn't trigger a false positive. |
+ // |
+ // Start this canceled because we don't want it to start ticking until we |
+ // get some response from the iframe. |
+ _timer = new RestartableTimer(new Duration(seconds: 3), () { |
kevmoo
2016/02/17 16:22:48
const Duration?
nweiz
2016/02/17 21:52:04
I really don't think const adds any value in most
|
+ for (var controller in _controllers) { |
+ controller.setDebugging(true); |
+ } |
+ })..cancel(); |
+ |
+ // Whenever we get a message, no matter which child channel it's for, we the |
+ // know browser is still running code which means the user isn't debugging. |
+ _channel = new MultiChannel(webSocket.transform(jsonDocument) |
+ .changeStream((stream) { |
+ return stream.map((message) { |
+ _timer.reset(); |
+ for (var controller in _controllers) { |
+ controller.setDebugging(false); |
+ } |
+ |
+ return message; |
+ }); |
+ })); |
+ |
_environment = _loadBrowserEnvironment(); |
_channel.stream.listen(_onMessage, onDone: close); |
} |
@@ -160,38 +197,48 @@ class BrowserManager { |
/// |
/// If [mapper] is passed, it's used to map stack traces for errors coming |
/// from this test suite. |
- Future<RunnerSuite> loadSuite(String path, Uri url, Metadata metadata, |
+ Future<RunnerSuite> load(String path, Uri url, Metadata metadata, |
{StackTraceMapper mapper}) async { |
url = url.replace(fragment: Uri.encodeFull(JSON.encode({ |
"metadata": metadata.serialize(), |
"browser": _platform.identifier |
}))); |
- // The stream may close before emitting a value if the browser is killed |
- // prematurely (e.g. via Control-C). |
- var suiteVirtualChannel = _channel.virtualChannel(); |
- var suiteId = _suiteId++; |
- |
+ var suiteID = _suiteID++; |
+ var controller; |
closeIframe() { |
if (_closed) return; |
+ _controllers.remove(controller); |
_channel.sink.add({ |
"command": "closeSuite", |
- "id": suiteId |
+ "id": suiteID |
}); |
} |
+ // The virtual channel will be closed when the suite is closed, in which |
+ // case we should unload the iframe. |
+ var suiteChannel = _channel.virtualChannel(); |
+ var suiteChannelID = suiteChannel.id; |
+ suiteChannel = suiteChannel.transformStream( |
+ new StreamTransformer.fromHandlers(handleDone: (sink) { |
+ closeIframe(); |
+ sink.close(); |
+ })); |
+ |
return await _pool.withResource(() async { |
_channel.sink.add({ |
"command": "loadSuite", |
"url": url.toString(), |
- "id": suiteId, |
- "channel": suiteVirtualChannel.id |
+ "id": suiteID, |
+ "channel": suiteChannelID |
}); |
try { |
- return await loadBrowserSuite( |
- suiteVirtualChannel, await _environment, path, |
- mapper: mapper, platform: _platform, onClose: () => closeIframe()); |
+ controller = await deserializeSuite( |
+ path, _platform, metadata, await _environment, suiteChannel, |
+ mapTrace: mapper?.mapStackTrace); |
+ _controllers.add(controller); |
+ return controller.suite; |
} catch (_) { |
closeIframe(); |
rethrow; |
@@ -219,6 +266,8 @@ class BrowserManager { |
/// The callback for handling messages received from the host page. |
void _onMessage(Map message) { |
+ if (message["command"] == "ping") return; |
+ |
assert(message["command"] == "resume"); |
if (_pauseCompleter == null) return; |
_pauseCompleter.complete(); |
@@ -228,8 +277,10 @@ class BrowserManager { |
/// the browser. |
Future close() => _closeMemoizer.runOnce(() { |
_closed = true; |
+ _timer.cancel(); |
if (_pauseCompleter != null) _pauseCompleter.complete(); |
_pauseCompleter = null; |
+ _controllers.clear(); |
return _browser.close(); |
}); |
final _closeMemoizer = new AsyncMemoizer(); |