Index: mojo/public/dart/third_party/test/lib/src/runner/browser/iframe_listener.dart |
diff --git a/mojo/public/dart/third_party/test/lib/src/runner/browser/iframe_listener.dart b/mojo/public/dart/third_party/test/lib/src/runner/browser/iframe_listener.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..9da65771bfd5f7e96f57cf460e821a6db4112af9 |
--- /dev/null |
+++ b/mojo/public/dart/third_party/test/lib/src/runner/browser/iframe_listener.dart |
@@ -0,0 +1,182 @@ |
+// 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.iframe_listener; |
+ |
+import 'dart:async'; |
+import 'dart:convert'; |
+import 'dart:html' hide Metadata; |
+ |
+import '../../backend/declarer.dart'; |
+import '../../backend/metadata.dart'; |
+import '../../backend/suite.dart'; |
+import '../../backend/test.dart'; |
+import '../../backend/test_platform.dart'; |
+import '../../util/multi_channel.dart'; |
+import '../../util/remote_exception.dart'; |
+import '../../utils.dart'; |
+ |
+/// A class that runs tests in a separate iframe. |
+/// |
+/// This indirectly communicates with the test server. It uses `postMessage` to |
+/// relay communication through the host page, which has a WebSocket connection |
+/// to the test server. |
+class IframeListener { |
+ /// The test suite to run. |
+ final Suite _suite; |
+ |
+ /// Extracts metadata about all the tests in the function returned by |
+ /// [getMain] and sends information about them over the `postMessage` |
+ /// connection. |
+ /// |
+ /// The main function is wrapped in a closure so that we can handle it being |
+ /// undefined here rather than in the generated code. |
+ /// |
+ /// Once that's done, this starts listening for commands about which tests to |
+ /// run. |
+ static Future start(Function getMain()) async { |
+ var channel = _postMessageChannel(); |
+ |
+ var main; |
+ try { |
+ main = getMain(); |
+ } on NoSuchMethodError catch (_) { |
+ _sendLoadException(channel, "No top-level main() function defined."); |
+ return; |
+ } |
+ |
+ if (main is! Function) { |
+ _sendLoadException(channel, "Top-level main getter is not a function."); |
+ return; |
+ } else if (main is! AsyncFunction) { |
+ _sendLoadException(channel, "Top-level main() function takes arguments."); |
+ return; |
+ } |
+ |
+ var declarer; |
+ try { |
+ declarer = new Declarer(); |
+ await runZoned(() => new Future.sync(main), zoneValues: { |
+ #test.declarer: declarer |
+ }, zoneSpecification: new ZoneSpecification(print: (_, __, ___, line) { |
+ channel.sink.add({"type": "print", "line": line}); |
+ })); |
+ } catch (error, stackTrace) { |
+ channel.sink.add({ |
+ "type": "error", |
+ "error": RemoteException.serialize(error, stackTrace) |
+ }); |
+ return; |
+ } |
+ |
+ var url = Uri.parse(window.location.href); |
+ var message = JSON.decode(Uri.decodeFull(url.fragment)); |
+ var metadata = new Metadata.deserialize(message['metadata']); |
+ var browser = TestPlatform.find(message['browser']); |
+ |
+ var suite = new Suite( |
+ declarer.tests, platform: browser, metadata: metadata); |
+ new IframeListener._(suite)._listen(channel); |
+ |
+ return; |
+ } |
+ |
+ /// Constructs a [MultiChannel] wrapping the `postMessage` communication with |
+ /// the host page. |
+ /// |
+ /// This [MultiChannel] corresponds to a [MultiChannel] in the server's |
+ /// [IframeTest] class. |
+ static MultiChannel _postMessageChannel() { |
+ var inputController = new StreamController(sync: true); |
+ var outputController = new StreamController(sync: true); |
+ |
+ window.onMessage.listen((message) { |
+ // A message on the Window can theoretically come from any website. It's |
+ // very unlikely that a malicious site would care about hacking someone's |
+ // unit tests, let alone be able to find the test server while it's |
+ // running, but it's good practice to check the origin anyway. |
+ if (message.origin != window.location.origin) return; |
+ message.stopPropagation(); |
+ inputController.add(message.data); |
+ }); |
+ |
+ outputController.stream.listen((data) { |
+ // TODO(nweiz): Stop manually adding href here once issue 22554 is |
+ // fixed. |
+ window.parent.postMessage({ |
+ "href": window.location.href, |
+ "data": data |
+ }, window.location.origin); |
+ }); |
+ |
+ return new MultiChannel(inputController.stream, outputController.sink); |
+ } |
+ |
+ /// Sends a message over [channel] indicating that the tests failed to load. |
+ /// |
+ /// [message] should describe the failure. |
+ static void _sendLoadException(MultiChannel channel, String message) { |
+ channel.sink.add({"type": "loadException", "message": message}); |
+ } |
+ |
+ IframeListener._(this._suite); |
+ |
+ /// Send information about [_suite] across [channel] and start listening for |
+ /// commands to run the tests. |
+ void _listen(MultiChannel channel) { |
+ var tests = []; |
+ for (var i = 0; i < _suite.tests.length; i++) { |
+ var test = _suite.tests[i]; |
+ var testChannel = channel.virtualChannel(); |
+ tests.add({ |
+ "name": test.name, |
+ "metadata": test.metadata.serialize(), |
+ "channel": testChannel.id |
+ }); |
+ |
+ testChannel.stream.listen((message) { |
+ assert(message['command'] == 'run'); |
+ _runTest(test, channel.virtualChannel(message['channel'])); |
+ }); |
+ } |
+ |
+ channel.sink.add({ |
+ "type": "success", |
+ "tests": tests |
+ }); |
+ } |
+ |
+ /// Runs [test] and sends the results across [sendPort]. |
+ void _runTest(Test test, MultiChannel channel) { |
+ var liveTest = test.load(_suite); |
+ |
+ channel.stream.listen((message) { |
+ assert(message['command'] == 'close'); |
+ liveTest.close(); |
+ }); |
+ |
+ liveTest.onStateChange.listen((state) { |
+ channel.sink.add({ |
+ "type": "state-change", |
+ "status": state.status.name, |
+ "result": state.result.name |
+ }); |
+ }); |
+ |
+ liveTest.onError.listen((asyncError) { |
+ channel.sink.add({ |
+ "type": "error", |
+ "error": RemoteException.serialize( |
+ asyncError.error, asyncError.stackTrace) |
+ }); |
+ }); |
+ |
+ liveTest.onPrint.listen((line) { |
+ print(line); |
+ channel.sink.add({"type": "print", "line": line}); |
+ }); |
+ |
+ liveTest.run().then((_) => channel.sink.add({"type": "complete"})); |
+ } |
+} |