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 |