Chromium Code Reviews| 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 ━━━━━━━━┿━━━━━━━━━━━━━━━━━┓ | |
|
Bob Nystrom
2015/03/02 20:31:31
Box drawing characters aren't reliably monospace-s
nweiz
2015/03/02 22:39:42
It's too cool to kill! It looks good with GitHub's
| |
| 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 fixed . | |
|
Bob Nystrom
2015/03/02 20:31:30
Long line is long.
nweiz
2015/03/02 22:39:42
Done.
nweiz
2015/03/02 22:39:42
Done.
| |
| 129 if (message.data["href"] != iframe.src) return; | |
| 130 | |
| 131 message.stopPropagation(); | |
| 132 inputController.add(message.data["data"]); | |
| 133 }); | |
| 134 | |
| 135 outputController.stream.listen((message) => | |
| 136 iframe.contentWindow.postMessage(message, window.location.origin)); | |
| 137 }); | |
| 138 | |
| 139 return new StreamChannel(inputController.stream, outputController.sink); | |
| 140 } | |
| OLD | NEW |