| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2013, 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 /** | |
| 6 * A configuration for running layout tests with testrunner. | |
| 7 * This configuration is similar to the interactive_html_config | |
| 8 * as it runs each test in its own IFrame. However, where the former | |
| 9 * recreated the IFrame for each test, here the IFrames are preserved. | |
| 10 * Furthermore we post a message on completion. | |
| 11 */ | |
| 12 library html_layout_config; | |
| 13 | |
| 14 import 'dart:async'; | |
| 15 import 'dart:html'; | |
| 16 import 'dart:math'; | |
| 17 import 'unittest.dart'; | |
| 18 | |
| 19 /** The messages exchanged between parent and child. */ | |
| 20 // TODO(gram) At some point postMessage was supposed to support | |
| 21 // sending arrays and maps. When it does we can get rid of the encoding/ | |
| 22 // decoding of messages as string. | |
| 23 class _Message { | |
| 24 static final START = 'start'; | |
| 25 static final LOG = 'log'; | |
| 26 static final STACK = 'stack'; | |
| 27 static final PASS = 'pass'; | |
| 28 static final FAIL = 'fail'; | |
| 29 static final ERROR = 'error'; | |
| 30 | |
| 31 String messageType; | |
| 32 int elapsed; | |
| 33 String body; | |
| 34 | |
| 35 static String text(String messageType, | |
| 36 [int elapsed = 0, String body = '']) => | |
| 37 '$messageType $elapsed $body'; | |
| 38 | |
| 39 _Message(this.messageType, [this.elapsed = 0, this.body = '']); | |
| 40 | |
| 41 _Message.fromString(String msg) { | |
| 42 // The format of a message is '<type> <elapsedTime> <body>'. | |
| 43 // If we don't get a type we default to a 'log' type. | |
| 44 var messageParser = new RegExp('\([a-z]*\) \([0-9]*\) \(.*\)'); | |
| 45 Match match = messageParser.firstMatch(msg); | |
| 46 if (match == null) { | |
| 47 messageType = 'log'; | |
| 48 elapsed = 0; | |
| 49 body = msg; | |
| 50 } else { | |
| 51 messageType = match.group(1); | |
| 52 elapsed = int.parse(match.group(2)); | |
| 53 body = match.group(3); | |
| 54 } | |
| 55 } | |
| 56 | |
| 57 String toString() => text(messageType, elapsed, body); | |
| 58 } | |
| 59 | |
| 60 /** | |
| 61 * The child configuration that is used to run individual tests in | |
| 62 * an IFrame and post the results back to the parent. In principle | |
| 63 * this can run more than one test in the IFrame but currently only | |
| 64 * one is used. | |
| 65 */ | |
| 66 class ChildHtmlConfiguration extends Configuration { | |
| 67 get name => 'ChildHtmlConfiguration'; | |
| 68 | |
| 69 StreamSubscription _errorSubscription; | |
| 70 | |
| 71 /** The window to which results must be posted. */ | |
| 72 Window parentWindow; | |
| 73 | |
| 74 /** The time at which tests start. */ | |
| 75 Map<int,DateTime> _testStarts; | |
| 76 | |
| 77 ChildHtmlConfiguration() : | |
| 78 _testStarts = new Map<int,DateTime>(); | |
| 79 | |
| 80 /** Don't start running tests automatically. */ | |
| 81 get autoStart => false; | |
| 82 | |
| 83 void onInit() { | |
| 84 /** | |
| 85 * The parent posts a 'start' message to kick things off, | |
| 86 * which is handled by this handler. It saves the parent | |
| 87 * window, gets the test ID from the query parameter in the | |
| 88 * IFrame URL, sets that as a solo test and starts test execution. | |
| 89 */ | |
| 90 window.onMessage.listen((MessageEvent e) { | |
| 91 var m = new _Message.fromString(e.data); | |
| 92 if (m.messageType == _Message.START) { | |
| 93 parentWindow = e.source; | |
| 94 String search = window.location.search; | |
| 95 int pos = search.indexOf('t='); | |
| 96 String ids = search.substring(pos+2); | |
| 97 int id = int.parse(ids); | |
| 98 setSoloTest(id); | |
| 99 runTests(); | |
| 100 } | |
| 101 }); | |
| 102 } | |
| 103 | |
| 104 void onStart() { | |
| 105 _errorSubscription = window.onError.listen((e) { | |
| 106 handleExternalError(e, '(DOM callback has errors)'); | |
| 107 }); | |
| 108 } | |
| 109 | |
| 110 /** Record the start time of the test. */ | |
| 111 void onTestStart(TestCase testCase) { | |
| 112 super.onTestStart(testCase); | |
| 113 _testStarts[testCase.id]= new DateTime.now(); | |
| 114 } | |
| 115 | |
| 116 /** | |
| 117 * Tests can call [log] for diagnostic output. These log | |
| 118 * messages in turn get passed to this method, which adds | |
| 119 * a timestamp and posts them back to the parent window. | |
| 120 */ | |
| 121 void logTestCaseMessage(TestCase testCase, String message) { | |
| 122 int elapsed; | |
| 123 if (testCase == null) { | |
| 124 elapsed = -1; | |
| 125 } else { | |
| 126 DateTime end = new DateTime.now(); | |
| 127 elapsed = end.difference(_testStarts[testCase.id]).inMilliseconds; | |
| 128 } | |
| 129 parentWindow.postMessage( | |
| 130 _Message.text(_Message.LOG, elapsed, message).toString(), '*'); | |
| 131 } | |
| 132 | |
| 133 /** | |
| 134 * Get the elapsed time for the test, and post the test result | |
| 135 * back to the parent window. If the test failed due to an exception | |
| 136 * the stack is posted back too (before the test result). | |
| 137 */ | |
| 138 void onTestResult(TestCase testCase) { | |
| 139 super.onTestResult(testCase); | |
| 140 DateTime end = new DateTime.now(); | |
| 141 int elapsed = end.difference(_testStarts[testCase.id]).inMilliseconds; | |
| 142 if (testCase.stackTrace != null) { | |
| 143 parentWindow.postMessage( | |
| 144 _Message.text(_Message.STACK, elapsed, testCase.stackTrace), '*'); | |
| 145 } | |
| 146 parentWindow.postMessage( | |
| 147 _Message.text(testCase.result, elapsed, testCase.message), '*'); | |
| 148 } | |
| 149 | |
| 150 void onSummary(int passed, int failed, int errors, List<TestCase> results, | |
| 151 String uncaughtError) { | |
| 152 } | |
| 153 | |
| 154 void onDone(bool success) { | |
| 155 assert(_errorSubscription != null); | |
| 156 _errorSubscription.cancel(); | |
| 157 _errorSubscription = null; | |
| 158 } | |
| 159 } | |
| 160 | |
| 161 /** | |
| 162 * The parent configuration runs in the top-level window; it wraps the tests | |
| 163 * in new functions that create child IFrames and run the real tests. | |
| 164 */ | |
| 165 class ParentHtmlConfiguration extends Configuration { | |
| 166 get autoStart => false; | |
| 167 get name => 'ParentHtmlConfiguration'; | |
| 168 Map<int,DateTime> _testStarts; | |
| 169 | |
| 170 /** The stack that was posted back from the child, if any. */ | |
| 171 String _stack; | |
| 172 | |
| 173 int _testTime; | |
| 174 /** | |
| 175 * Whether or not we have already wrapped the TestCase test functions | |
| 176 * in new closures that instead create an IFrame and get it to run the | |
| 177 * test. | |
| 178 */ | |
| 179 bool _doneWrap = false; | |
| 180 | |
| 181 StreamSubscription _messageSubscription, _errorSubscription; | |
| 182 | |
| 183 ParentHtmlConfiguration() : | |
| 184 _testStarts = new Map<int,DateTime>(); | |
| 185 | |
| 186 // We need to block until the test is done, so we make a | |
| 187 // dummy async callback that we will use to flag completion. | |
| 188 Function completeTest = null; | |
| 189 | |
| 190 wrapTest(TestCase testCase) { | |
| 191 String baseUrl = window.location.toString(); | |
| 192 String url = '${baseUrl}?t=${testCase.id}'; | |
| 193 return () { | |
| 194 // Add the child IFrame. | |
| 195 Element childDiv = document.query('#child'); | |
| 196 var label = new Element.html( | |
| 197 "<pre id='result${testCase.id}'>${testCase.description}</pre>"); | |
| 198 IFrameElement child = new Element.html(""" | |
| 199 <iframe id='childFrame${testCase.id}' src='$url'> | |
| 200 </iframe>"""); | |
| 201 childDiv.nodes.add(label); | |
| 202 childDiv.nodes.add(child); | |
| 203 completeTest = expectAsync0((){ }); | |
| 204 // Kick off the test when the IFrame is loaded. | |
| 205 child.onLoad.listen((e) { | |
| 206 child.contentWindow.postMessage(_Message.text(_Message.START), '*'); | |
| 207 }); | |
| 208 }; | |
| 209 } | |
| 210 | |
| 211 void _handleMessage(MessageEvent e) { | |
| 212 // Get the result, do any logging, then do a pass/fail. | |
| 213 var msg = new _Message.fromString(e.data); | |
| 214 if (msg.messageType == _Message.LOG) { | |
| 215 logMessage(e.data); | |
| 216 } else if (msg.messageType == _Message.STACK) { | |
| 217 _stack = msg.body; | |
| 218 } else { | |
| 219 _testTime = msg.elapsed; | |
| 220 if (msg.messageType == _Message.PASS) { | |
| 221 currentTestCase.pass(); | |
| 222 } else if (msg.messageType == _Message.FAIL) { | |
| 223 currentTestCase.fail(msg.body, _stack); | |
| 224 } else if (msg.messageType == _Message.ERROR) { | |
| 225 currentTestCase.error(msg.body, _stack); | |
| 226 } | |
| 227 completeTest(); | |
| 228 } | |
| 229 } | |
| 230 | |
| 231 void onInit() { | |
| 232 } | |
| 233 | |
| 234 void onStart() { | |
| 235 // Listen for uncaught errors. | |
| 236 assert(_errorSubscription == null); | |
| 237 _errorSubscription = window.onError.listen((e) { | |
| 238 handleExternalError(e, '(DOM callback has errors)'); | |
| 239 }); | |
| 240 | |
| 241 if (!_doneWrap) { | |
| 242 _doneWrap = true; | |
| 243 for (int i = 0; i < testCases.length; i++) { | |
| 244 testCases[i].testFunction = wrapTest(testCases[i]); | |
| 245 testCases[i].setUp = null; | |
| 246 testCases[i].tearDown = null; | |
| 247 } | |
| 248 } | |
| 249 | |
| 250 _messageSubscription = window.onMessage.listen(_handleMessage); | |
| 251 } | |
| 252 | |
| 253 void onTestStart(TestCase testCase) { | |
| 254 var id = testCase.id; | |
| 255 _testStarts[testCase.id]= new DateTime.now(); | |
| 256 super.onTestStart(testCase); | |
| 257 _stack = null; | |
| 258 } | |
| 259 | |
| 260 // Actually test logging is handled by the child, then posted | |
| 261 // back to the parent. So here we know that the [message] argument | |
| 262 // is in the format used by [_Message]. | |
| 263 void logTestCaseMessage(TestCase testCase, String message) { | |
| 264 var msg = new _Message.fromString(message); | |
| 265 document.query('#otherlogs').nodes.add( | |
| 266 new Element.html('<p>${msg.body}</p>')); | |
| 267 } | |
| 268 | |
| 269 void onTestResult(TestCase testCase) { | |
| 270 if (!testCase.enabled) return; | |
| 271 super.onTestResult(testCase); | |
| 272 document.query('#result${testCase.id}').text = | |
| 273 '${testCase.result}:${testCase.runningTime.inMilliseconds}:' | |
| 274 '${testCase.description}//${testCase.message}'; | |
| 275 } | |
| 276 | |
| 277 void onSummary(int passed, int failed, int errors, List<TestCase> results, | |
| 278 String uncaughtError) { | |
| 279 } | |
| 280 void onDone(bool success) { | |
| 281 _messageSubscription.cancel(); | |
| 282 _messageSubscription = null; | |
| 283 | |
| 284 _errorSubscription.cancel(); | |
| 285 _errorSubscription = null; | |
| 286 | |
| 287 window.postMessage('done', '*'); // Unblock DRT | |
| 288 } | |
| 289 } | |
| 290 | |
| 291 /** | |
| 292 * Add the divs to the DOM if they are not present. | |
| 293 */ | |
| 294 void _prepareDom() { | |
| 295 if (document.query('#otherlogs') == null) { | |
| 296 document.body.nodes.add(new Element.html( | |
| 297 "<div id='otherlogs'></div>")); | |
| 298 } | |
| 299 if (document.query('#child') == null) { | |
| 300 document.body.nodes.add(new Element.html("<div id='child'></div>")); | |
| 301 } | |
| 302 } | |
| 303 | |
| 304 /** | |
| 305 * Allocate a Configuration. We allocate either a parent or | |
| 306 * child, depending on whether the URL has a search part. | |
| 307 */ | |
| 308 void useHtmlLayoutConfiguration() { | |
| 309 if (config != null) return; | |
| 310 if (window.location.search == '') { // This is the parent. | |
| 311 _prepareDom(); | |
| 312 configure(new ParentHtmlConfiguration()); | |
| 313 } else { | |
| 314 configure(new ChildHtmlConfiguration()); | |
| 315 } | |
| 316 } | |
| 317 | |
| OLD | NEW |