/
browser_manager.dart
305 lines (260 loc) · 10.4 KB
/
browser_manager.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
// 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 'dart:convert';
import 'package:async/async.dart';
import 'package:pool/pool.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../backend/test_platform.dart';
import '../../util/stack_trace_mapper.dart';
import '../application_exception.dart';
import '../configuration/suite.dart';
import '../environment.dart';
import '../plugin/platform_helpers.dart';
import '../runner_suite.dart';
import 'browser.dart';
import 'chrome.dart';
import 'content_shell.dart';
import 'dartium.dart';
import 'firefox.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 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.
///
/// This is connected to a page running `static/host.dart`.
MultiChannel _channel;
/// A pool that ensures that limits the number of initial connections the
/// manager will wait for at once.
///
/// This isn't the *total* number of connections; any number of iframes may be
/// loaded in the same browser. However, the browser can only load so many at
/// once, and we want a timeout in case they fail so we only wait for so many
/// at once.
final _pool = new Pool(8);
/// The ID of the next suite to be loaded.
///
/// This is used to ensure that the suites can be referred to consistently
/// across the client and server.
int _suiteID = 0;
/// Whether the channel to the browser has closed.
bool _closed = false;
/// The completer for [_BrowserEnvironment.displayPause].
///
/// This will be `null` as long as the browser isn't displaying a pause
/// screen.
CancelableCompleter _pauseCompleter;
/// 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
/// this process. That connection, once established, should be emitted via
/// [future]. If [debug] is true, starts the browser in debug mode, with its
/// debugger interfaces on and detected.
///
/// Returns the browser manager, or throws an [ApplicationException] if a
/// connection fails to be established.
static Future<BrowserManager> start(TestPlatform platform, Uri url,
Future<WebSocketChannel> future, {bool debug: false}) {
var browser = _newBrowser(url, platform, debug: debug);
var completer = new Completer<BrowserManager>();
// 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].
///
/// If [debug] is true, starts the browser in debug mode.
static Browser _newBrowser(Uri url, TestPlatform browser,
{bool debug: false}) {
switch (browser) {
case TestPlatform.dartium: return new Dartium(url, debug: debug);
case TestPlatform.contentShell:
return new ContentShell(url, debug: debug);
case TestPlatform.chrome: return new Chrome(url);
case TestPlatform.phantomJS: return new PhantomJS(url, debug: debug);
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, 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), () {
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) {
if (!_closed) _timer.reset();
for (var controller in _controllers) {
controller.setDebugging(false);
}
return message;
});
}));
_environment = _loadBrowserEnvironment();
_channel.stream.listen(_onMessage, onDone: close);
}
/// Loads [_BrowserEnvironment].
Future<_BrowserEnvironment> _loadBrowserEnvironment() async {
var observatoryUrl;
if (_platform.isDartVM) observatoryUrl = await _browser.observatoryUrl;
var remoteDebuggerUrl;
if (_platform.isHeadless) {
remoteDebuggerUrl = await _browser.remoteDebuggerUrl;
}
return new _BrowserEnvironment(this, observatoryUrl, remoteDebuggerUrl);
}
/// Tells the browser the load a test suite from the URL [url].
///
/// [url] should be an HTML page with a reference to the JS-compiled test
/// suite. [path] is the path of the original test suite file, which is used
/// for reporting. [suiteConfig] is the configuration for the test suite.
///
/// If [mapper] is passed, it's used to map stack traces for errors coming
/// from this test suite.
Future<RunnerSuite> load(String path, Uri url, SuiteConfiguration suiteConfig,
{StackTraceMapper mapper}) async {
url = url.replace(fragment: Uri.encodeFull(JSON.encode({
"metadata": suiteConfig.metadata.serialize(),
"browser": _platform.identifier
})));
var suiteID = _suiteID++;
var controller;
closeIframe() {
if (_closed) return;
_controllers.remove(controller);
_channel.sink.add({
"command": "closeSuite",
"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/*<Future<RunnerSuite>>*/(() async {
_channel.sink.add({
"command": "loadSuite",
"url": url.toString(),
"id": suiteID,
"channel": suiteChannelID
});
try {
controller = await deserializeSuite(
path, _platform, suiteConfig, await _environment, suiteChannel,
mapTrace: mapper?.mapStackTrace);
_controllers.add(controller);
return controller.suite;
} catch (_) {
closeIframe();
rethrow;
}
});
}
/// An implementation of [Environment.displayPause].
CancelableOperation _displayPause() {
if (_pauseCompleter != null) return _pauseCompleter.operation;
_pauseCompleter = new CancelableCompleter(onCancel: () {
_channel.sink.add({"command": "resume"});
_pauseCompleter = null;
});
_pauseCompleter.operation.value.whenComplete(() {
_pauseCompleter = null;
});
_channel.sink.add({"command": "displayPause"});
return _pauseCompleter.operation;
}
/// 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();
}
/// Closes the manager and releases any resources it owns, including closing
/// 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();
}
/// An implementation of [Environment] for the browser.
///
/// All methods forward directly to [BrowserManager].
class _BrowserEnvironment implements Environment {
final BrowserManager _manager;
final supportsDebugging = true;
final Uri observatoryUrl;
final Uri remoteDebuggerUrl;
_BrowserEnvironment(this._manager, this.observatoryUrl,
this.remoteDebuggerUrl);
CancelableOperation displayPause() => _manager._displayPause();
}