From c5e657750aac9cd2e344858c679c23d693cc7b39 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum <nweiz@google.com> Date: Tue, 3 Mar 2015 13:06:57 -0800 Subject: [PATCH] Add BrowserManager and IframeTest for communicating with browser tests. See #5 R=kevmoo@google.com Review URL: https://codereview.chromium.org//971103002 --- lib/src/runner/browser/browser_manager.dart | 68 +++++++++++++++++++++ lib/src/runner/browser/iframe_test.dart | 52 ++++++++++++++++ lib/src/utils.dart | 10 +++ 3 files changed, 130 insertions(+) create mode 100644 lib/src/runner/browser/browser_manager.dart create mode 100644 lib/src/runner/browser/iframe_test.dart diff --git a/lib/src/runner/browser/browser_manager.dart b/lib/src/runner/browser/browser_manager.dart new file mode 100644 index 00000000..44851f0b --- /dev/null +++ b/lib/src/runner/browser/browser_manager.dart @@ -0,0 +1,68 @@ +// 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.browser_manager; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:http_parser/http_parser.dart'; + +import '../../backend/suite.dart'; +import '../../util/multi_channel.dart'; +import '../../util/remote_exception.dart'; +import '../../utils.dart'; +import '../load_exception.dart'; +import 'iframe_test.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 channel used to communicate with the browser. + /// + /// This is connected to a page running `static/host.dart`. + final MultiChannel _channel; + + /// Creates a new BrowserManager that communicates with a browser over + /// [webSocket]. + BrowserManager(CompatibleWebSocket webSocket) + : _channel = new MultiChannel( + webSocket.map(JSON.decode), + mapSink(webSocket, JSON.encode)); + + /// 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. + Future<Suite> loadSuite(String path, Uri url) { + var suiteChannel = _channel.virtualChannel(); + _channel.sink.add({ + "command": "loadSuite", + "url": url.toString(), + "channel": suiteChannel.id + }); + + // Create a nested MultiChannel because the iframe will be using a channel + // wrapped within the host's channel. + suiteChannel = new MultiChannel(suiteChannel.stream, suiteChannel.sink); + return suiteChannel.stream.first.then((response) { + if (response["type"] == "loadException") { + return new Future.error(new LoadException(path, response["message"])); + } else if (response["type"] == "error") { + var asyncError = RemoteException.deserialize(response["error"]); + return new Future.error( + new LoadException(path, asyncError.error), + asyncError.stackTrace); + } + + return new Suite(path, response["tests"].map((test) { + var testChannel = suiteChannel.virtualChannel(test['channel']); + return new IframeTest(test['name'], testChannel); + })); + }); + } +} diff --git a/lib/src/runner/browser/iframe_test.dart b/lib/src/runner/browser/iframe_test.dart new file mode 100644 index 00000000..e4aee528 --- /dev/null +++ b/lib/src/runner/browser/iframe_test.dart @@ -0,0 +1,52 @@ +// 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.iframe_test; + +import '../../backend/live_test.dart'; +import '../../backend/live_test_controller.dart'; +import '../../backend/state.dart'; +import '../../backend/suite.dart'; +import '../../backend/test.dart'; +import '../../util/multi_channel.dart'; +import '../../util/remote_exception.dart'; + +/// A test in a running iframe. +class IframeTest implements Test { + final String name; + + /// The channel used to communicate with the test's [IframeListener]. + final MultiChannel _channel; + + IframeTest(this.name, this._channel); + + LiveTest load(Suite suite) { + var controller; + controller = new LiveTestController(suite, this, () { + controller.setState(const State(Status.running, Result.success)); + + var testChannel = _channel.virtualChannel(); + _channel.sink.add({ + 'command': 'run', + 'channel': testChannel.id + }); + + testChannel.stream.listen((message) { + if (message['type'] == 'error') { + var asyncError = RemoteException.deserialize(message['error']); + controller.addError(asyncError.error, asyncError.stackTrace); + } else if (message['type'] == 'state-change') { + controller.setState( + new State( + new Status.parse(message['status']), + new Result.parse(message['result']))); + } else { + assert(message['type'] == 'complete'); + controller.completer.complete(); + } + }); + }); + return controller.liveTest; + } +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 06bd8dff..6b2b21ef 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -64,6 +64,16 @@ List flatten(Iterable nested) { return result; } +/// Returns a sink that maps events sent to [original] using [fn]. +StreamSink mapSink(StreamSink original, fn(event)) { + var controller = new StreamController(sync: true); + controller.stream.listen( + (event) => original.add(fn(event)), + onError: (error, stackTrace) => original.addError(error, stackTrace), + onDone: () => original.close()); + return controller.sink; +} + /// Truncates [text] to fit within [maxLength]. /// /// This will try to truncate along word boundaries and preserve words both at -- GitLab