OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library unittest.runner.browser.host; |
| 6 |
| 7 import 'dart:async'; |
| 8 import 'dart:convert'; |
| 9 import 'dart:html'; |
| 10 |
| 11 import 'package:stack_trace/stack_trace.dart'; |
| 12 import 'package:unittest/src/util/multi_channel.dart'; |
| 13 import 'package:unittest/src/util/stream_channel.dart'; |
| 14 |
| 15 // TODO(nweiz): test this once we can run browser tests. |
| 16 /// Code that runs in the browser and loads test suites at the server's behest. |
| 17 /// |
| 18 /// One instance of this runs for each browser. When the server tells it to load |
| 19 /// a test, it starts an iframe pointing at that test's code; from then on, it |
| 20 /// just relays messages between the two. |
| 21 /// |
| 22 /// The browser uses two layers of [MultiChannel]s when communicating with the |
| 23 /// server: |
| 24 /// |
| 25 /// server |
| 26 /// │ |
| 27 /// (WebSocket) |
| 28 /// │ |
| 29 /// ┏━ host.html ━━━━━━━━┿━━━━━━━━━━━━━━━━━┓ |
| 30 /// ┃ │ ┃ |
| 31 /// ┃ ┌──────┬───MultiChannel─────┐ ┃ |
| 32 /// ┃ │ │ │ │ │ ┃ |
| 33 /// ┃ host suite suite suite suite ┃ |
| 34 /// ┃ │ │ │ │ ┃ |
| 35 /// ┗━━━━━━━━━━━┿━━━━━━┿━━━━━━┿━━━━━━┿━━━━━┛ |
| 36 /// │ │ │ │ |
| 37 /// │ ... ... ... |
| 38 /// │ |
| 39 /// (postMessage) |
| 40 /// │ |
| 41 /// ┏━ suite.html (in iframe) ┿━━━━━━━━━━━━━━━━━━━━━━━━━━┓ |
| 42 /// ┃ │ ┃ |
| 43 /// ┃ ┌──────────MultiChannel┬─────────┐ ┃ |
| 44 /// ┃ │ │ │ │ │ ┃ |
| 45 /// ┃ IframeListener test test test running test ┃ |
| 46 /// ┃ ┃ |
| 47 /// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ |
| 48 /// |
| 49 /// The host (this code) has a [MultiChannel] that splits the WebSocket |
| 50 /// connection with the server. One connection is used for the host itself to |
| 51 /// receive messages like "load a suite at this URL", and the rest are connected |
| 52 /// to each test suite's iframe via `postMessage`. |
| 53 /// |
| 54 /// Each iframe then has its own [MultiChannel] which takes its `postMessage` |
| 55 /// connection and splits it again. One connection is used for the |
| 56 /// [IframeListener], which sends messages like "here are all the tests in this |
| 57 /// suite". The rest are used for each test, receiving messages like "start |
| 58 /// running". A new connection is also created whenever a test begins running to |
| 59 /// send status messages about its progress. |
| 60 /// |
| 61 /// It's of particular note that the suite's [MultiChannel] connection uses the |
| 62 /// host's purely as a transport layer; neither is aware that the other is also |
| 63 /// using [MultiChannel]. This is necessary, since the host doesn't share memory |
| 64 /// with the suites and thus can't share its [MultiChannel] with them, but it |
| 65 /// does mean that the server needs to be sure to nest its [MultiChannel]s at |
| 66 /// the same place the client does. |
| 67 void main() { |
| 68 runZoned(() { |
| 69 var serverChannel = _connectToServer(); |
| 70 serverChannel.stream.listen((message) { |
| 71 assert(message['command'] == 'loadSuite'); |
| 72 var suiteChannel = serverChannel.virtualChannel(message['channel']); |
| 73 var iframeChannel = _connectToIframe(message['url']); |
| 74 suiteChannel.pipe(iframeChannel); |
| 75 }); |
| 76 }, onError: (error, stackTrace) { |
| 77 print("$error\n${new Trace.from(stackTrace).terse}"); |
| 78 }); |
| 79 } |
| 80 |
| 81 /// Creates a [MultiChannel] connection to the server, using a [WebSocket] as |
| 82 /// the underlying protocol. |
| 83 MultiChannel _connectToServer() { |
| 84 // The `managerUrl` query parameter contains the WebSocket URL of the remote |
| 85 // [BrowserManager] with which this communicates. |
| 86 var currentUrl = Uri.parse(window.location.href); |
| 87 var webSocketUrl = currentUrl |
| 88 .resolve(currentUrl.queryParameters['managerUrl']) |
| 89 .replace(scheme: 'ws'); |
| 90 var webSocket = new WebSocket(webSocketUrl.toString()); |
| 91 |
| 92 var inputController = new StreamController(sync: true); |
| 93 webSocket.onMessage.listen( |
| 94 (message) => inputController.add(JSON.decode(message.data))); |
| 95 |
| 96 var outputController = new StreamController(sync: true); |
| 97 outputController.stream.listen( |
| 98 (message) => webSocket.send(JSON.encode(message))); |
| 99 |
| 100 return new MultiChannel(inputController.stream, outputController.sink); |
| 101 } |
| 102 |
| 103 /// Creates an iframe with `src` [url] and establishes a connection to it using |
| 104 /// `postMessage`. |
| 105 StreamChannel _connectToIframe(String url) { |
| 106 var iframe = new IFrameElement(); |
| 107 iframe.src = url; |
| 108 document.body.children.add(iframe); |
| 109 |
| 110 var inputController = new StreamController(sync: true); |
| 111 var outputController = new StreamController(sync: true); |
| 112 iframe.onLoad.first.then((_) { |
| 113 // TODO(nweiz): use MessageChannel once Firefox supports it |
| 114 // (http://caniuse.com/#search=MessageChannel). |
| 115 |
| 116 // Send an initial command to give the iframe something to reply to. |
| 117 iframe.contentWindow.postMessage( |
| 118 {"command": "connect"}, |
| 119 window.location.origin); |
| 120 |
| 121 window.onMessage.listen((message) { |
| 122 // A message on the Window can theoretically come from any website. It's |
| 123 // very unlikely that a malicious site would care about hacking someone's |
| 124 // unit tests, let alone be able to find the unittest server while it's |
| 125 // running, but it's good practice to check the origin anyway. |
| 126 if (message.origin != window.location.origin) return; |
| 127 |
| 128 // TODO(nweiz): Stop manually checking href here once issue 22554 is |
| 129 // fixed. |
| 130 if (message.data["href"] != iframe.src) return; |
| 131 |
| 132 message.stopPropagation(); |
| 133 inputController.add(message.data["data"]); |
| 134 }); |
| 135 |
| 136 outputController.stream.listen((message) => |
| 137 iframe.contentWindow.postMessage(message, window.location.origin)); |
| 138 }); |
| 139 |
| 140 return new StreamChannel(inputController.stream, outputController.sink); |
| 141 } |
OLD | NEW |