Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * This configuration can be used to rerun selected tests, as well | 6 * This configuration can be used to rerun selected tests, as well |
| 7 * as see diagnostic output from tests. It runs each test in its own | 7 * as see diagnostic output from tests. It runs each test in its own |
| 8 * IFrame, so the configuration consists of two parts - a 'parent' | 8 * IFrame, so the configuration consists of two parts - a 'parent' |
| 9 * config that manages all the tests, and a 'child' config for the | 9 * config that manages all the tests, and a 'child' config for the |
| 10 * IFrame that runs the individual tests. | 10 * IFrame that runs the individual tests. |
| 11 */ | 11 */ |
| 12 library unittest_interactive_html_config; | 12 library unittest_interactive_html_config; |
| 13 | 13 |
| 14 // TODO(gram) - add options for: remove IFrame on done/keep | 14 // TODO(gram) - add options for: remove IFrame on done/keep |
| 15 // IFrame for failed tests/keep IFrame for all tests. | 15 // IFrame for failed tests/keep IFrame for all tests. |
| 16 | 16 |
| 17 import 'dart:html'; | 17 import 'dart:html'; |
| 18 import 'dart:async'; | |
| 18 import 'dart:math'; | 19 import 'dart:math'; |
| 19 import 'unittest.dart'; | 20 import 'unittest.dart'; |
| 20 | 21 |
| 21 /** The messages exchanged between parent and child. */ | 22 /** The messages exchanged between parent and child. */ |
| 22 | 23 |
| 23 class _Message { | 24 class _Message { |
| 24 static const START = 'start'; | 25 static const START = 'start'; |
| 25 static const LOG = 'log'; | 26 static const LOG = 'log'; |
| 26 static const STACK = 'stack'; | 27 static const STACK = 'stack'; |
| 27 static const PASS = 'pass'; | 28 static const PASS = 'pass'; |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 46 elapsed = int.parse(msg.substring(idx, idx2)); | 47 elapsed = int.parse(msg.substring(idx, idx2)); |
| 47 ++idx2; | 48 ++idx2; |
| 48 body = msg.substring(idx2); | 49 body = msg.substring(idx2); |
| 49 } | 50 } |
| 50 | 51 |
| 51 String toString() => text(messageType, elapsed, body); | 52 String toString() => text(messageType, elapsed, body); |
| 52 } | 53 } |
| 53 | 54 |
| 54 | 55 |
| 55 class HtmlConfiguration extends Configuration { | 56 class HtmlConfiguration extends Configuration { |
| 56 // TODO(rnystrom): Get rid of this if we get canonical closures for methods. | 57 StreamSubscription _errorSubscription; |
| 57 EventListener _onErrorClosure; | |
| 58 | 58 |
| 59 void _installErrorHandler() { | 59 void _installErrorHandler() { |
| 60 if (_onErrorClosure == null) { | 60 if (_errorSubscription == null) { |
| 61 _onErrorClosure = | 61 assert(_errorSubscription == null); |
|
gram
2013/03/12 22:44:46
This assert seems superfluous, given the if guard.
| |
| 62 (e) => handleExternalError(e, '(DOM callback has errors)'); | 62 |
| 63 // Listen for uncaught errors. | 63 // Listen for uncaught errors. |
| 64 window.on.error.add(_onErrorClosure); | 64 _errorSubscription = window.onError.listen((e) { |
| 65 handleExternalError(e, '(DOM callback has errors)'); | |
| 66 }); | |
| 65 } | 67 } |
| 66 } | 68 } |
| 67 | 69 |
| 68 void _uninstallErrorHandler() { | 70 void _uninstallErrorHandler() { |
| 69 if (_onErrorClosure != null) { | 71 if (_errorSubscription != null) { |
| 70 window.on.error.remove(_onErrorClosure); | 72 _errorSubscription.cancel(); |
| 71 _onErrorClosure = null; | 73 _errorSubscription = null; |
| 72 } | 74 } |
| 73 } | 75 } |
| 74 } | 76 } |
| 75 | 77 |
| 76 /** | 78 /** |
| 77 * The child configuration that is used to run individual tests in | 79 * The child configuration that is used to run individual tests in |
| 78 * an IFrame and post the results back to the parent. In principle | 80 * an IFrame and post the results back to the parent. In principle |
| 79 * this can run more than one test in the IFrame but currently only | 81 * this can run more than one test in the IFrame but currently only |
| 80 * one is used. | 82 * one is used. |
| 81 */ | 83 */ |
| 82 class ChildInteractiveHtmlConfiguration extends HtmlConfiguration { | 84 class ChildInteractiveHtmlConfiguration extends HtmlConfiguration { |
| 83 | 85 |
| 84 /** The window to which results must be posted. */ | 86 /** The window to which results must be posted. */ |
| 85 Window parentWindow; | 87 WindowBase parentWindow; |
| 86 | 88 |
| 87 /** The time at which tests start. */ | 89 /** The time at which tests start. */ |
| 88 Map<int,DateTime> _testStarts; | 90 Map<int,DateTime> _testStarts; |
| 89 | 91 |
| 90 ChildInteractiveHtmlConfiguration() : | 92 ChildInteractiveHtmlConfiguration() : |
| 91 _testStarts = new Map<int,DateTime>(); | 93 _testStarts = new Map<int,DateTime>(); |
| 92 | 94 |
| 93 /** Don't start running tests automatically. */ | 95 /** Don't start running tests automatically. */ |
| 94 get autoStart => false; | 96 get autoStart => false; |
| 95 | 97 |
| 96 void onInit() { | 98 void onInit() { |
| 97 _installErrorHandler(); | 99 _installErrorHandler(); |
| 98 | 100 |
| 99 /** | 101 /** |
| 100 * The parent posts a 'start' message to kick things off, | 102 * The parent posts a 'start' message to kick things off, |
| 101 * which is handled by this handler. It saves the parent | 103 * which is handled by this handler. It saves the parent |
| 102 * window, gets the test ID from the query parameter in the | 104 * window, gets the test ID from the query parameter in the |
| 103 * IFrame URL, sets that as a solo test and starts test execution. | 105 * IFrame URL, sets that as a solo test and starts test execution. |
| 104 */ | 106 */ |
| 105 window.on.message.add((MessageEvent e) { | 107 window.onMessage.listen((MessageEvent e) { |
| 106 // Get the result, do any logging, then do a pass/fail. | 108 // Get the result, do any logging, then do a pass/fail. |
| 107 var m = new _Message.fromString(e.data); | 109 var m = new _Message.fromString(e.data); |
| 108 if (m.messageType == _Message.START) { | 110 if (m.messageType == _Message.START) { |
| 109 parentWindow = e.source; | 111 parentWindow = e.source; |
| 110 String search = window.location.search; | 112 String search = window.location.search; |
| 111 int pos = search.indexOf('t='); | 113 int pos = search.indexOf('t='); |
| 112 String ids = search.substring(pos+2); | 114 String ids = search.substring(pos+2); |
| 113 int id = int.parse(ids); | 115 int id = int.parse(ids); |
| 114 setSoloTest(id); | 116 setSoloTest(id); |
| 115 runTests(); | 117 runTests(); |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 181 String _stack; | 183 String _stack; |
| 182 | 184 |
| 183 int _testTime; | 185 int _testTime; |
| 184 /** | 186 /** |
| 185 * Whether or not we have already wrapped the TestCase test functions | 187 * Whether or not we have already wrapped the TestCase test functions |
| 186 * in new closures that instead create an IFrame and get it to run the | 188 * in new closures that instead create an IFrame and get it to run the |
| 187 * test. | 189 * test. |
| 188 */ | 190 */ |
| 189 bool _doneWrap = false; | 191 bool _doneWrap = false; |
| 190 | 192 |
| 191 /** | 193 StreamSubscription _messageSubscription; |
| 192 * We use this to make a single closure from _handleMessage so we | |
| 193 * can remove the handler later. | |
| 194 */ | |
| 195 Function _messageHandler; | |
| 196 | 194 |
| 197 ParentInteractiveHtmlConfiguration() : | 195 ParentInteractiveHtmlConfiguration() : |
| 198 _testStarts = new Map<int,DateTime>(); | 196 _testStarts = new Map<int,DateTime>(); |
| 199 | 197 |
| 200 // We need to block until the test is done, so we make a | 198 // We need to block until the test is done, so we make a |
| 201 // dummy async callback that we will use to flag completion. | 199 // dummy async callback that we will use to flag completion. |
| 202 Function completeTest = null; | 200 Function completeTest = null; |
| 203 | 201 |
| 204 wrapTest(TestCase testCase) { | 202 wrapTest(TestCase testCase) { |
| 205 String baseUrl = window.location.toString(); | 203 String baseUrl = window.location.toString(); |
| 206 String url = '${baseUrl}?t=${testCase.id}'; | 204 String url = '${baseUrl}?t=${testCase.id}'; |
| 207 return () { | 205 return () { |
| 208 // Rebuild the child IFrame. | 206 // Rebuild the child IFrame. |
| 209 Element childDiv = document.query('#child'); | 207 Element childDiv = document.query('#child'); |
| 210 childDiv.nodes.clear(); | 208 childDiv.nodes.clear(); |
| 211 IFrameElement child = new Element.html(""" | 209 IFrameElement child = new Element.html(""" |
| 212 <iframe id='childFrame${testCase.id}' src='$url' style='display:none'> | 210 <iframe id='childFrame${testCase.id}' src='$url' style='display:none'> |
| 213 </iframe>"""); | 211 </iframe>"""); |
| 214 childDiv.nodes.add(child); | 212 childDiv.nodes.add(child); |
| 215 completeTest = expectAsync0((){ }); | 213 completeTest = expectAsync0((){ }); |
| 216 // Kick off the test when the IFrame is loaded. | 214 // Kick off the test when the IFrame is loaded. |
| 217 child.on.load.add((e) { | 215 child.onLoad.listen((e) { |
| 218 child.contentWindow.postMessage(_Message.text(_Message.START), '*'); | 216 child.contentWindow.postMessage(_Message.text(_Message.START), '*'); |
| 219 }); | 217 }); |
| 220 }; | 218 }; |
| 221 } | 219 } |
| 222 | 220 |
| 223 void _handleMessage(MessageEvent e) { | 221 void _handleMessage(MessageEvent e) { |
| 224 // Get the result, do any logging, then do a pass/fail. | 222 // Get the result, do any logging, then do a pass/fail. |
| 225 var msg = new _Message.fromString(e.data); | 223 var msg = new _Message.fromString(e.data); |
| 226 if (msg.messageType == _Message.LOG) { | 224 if (msg.messageType == _Message.LOG) { |
| 227 logMessage(e.data); | 225 logMessage(e.data); |
| 228 } else if (msg.messageType == _Message.STACK) { | 226 } else if (msg.messageType == _Message.STACK) { |
| 229 _stack = msg.body; | 227 _stack = msg.body; |
| 230 } else { | 228 } else { |
| 231 _testTime = msg.elapsed; | 229 _testTime = msg.elapsed; |
| 232 logMessage(_Message.text(_Message.LOG, _testTime, 'Complete')); | 230 logMessage(_Message.text(_Message.LOG, _testTime, 'Complete')); |
| 233 if (msg.messageType == _Message.PASS) { | 231 if (msg.messageType == _Message.PASS) { |
| 234 currentTestCase.pass(); | 232 currentTestCase.pass(); |
| 235 } else if (msg.messageType == _Message.FAIL) { | 233 } else if (msg.messageType == _Message.FAIL) { |
| 236 currentTestCase.fail(msg.body, _stack); | 234 currentTestCase.fail(msg.body, _stack); |
| 237 } else if (msg.messageType == _Message.ERROR) { | 235 } else if (msg.messageType == _Message.ERROR) { |
| 238 currentTestCase.error(msg.body, _stack); | 236 currentTestCase.error(msg.body, _stack); |
| 239 } | 237 } |
| 240 completeTest(); | 238 completeTest(); |
| 241 } | 239 } |
| 242 } | 240 } |
| 243 | 241 |
| 244 void onInit() { | 242 void onInit() { |
| 245 _installErrorHandler(); | 243 _installErrorHandler(); |
| 246 _messageHandler = _handleMessage; // We need to make just one closure. | |
| 247 document.query('#group-divs').innerHtml = ""; | 244 document.query('#group-divs').innerHtml = ""; |
| 248 } | 245 } |
| 249 | 246 |
| 250 void onStart() { | 247 void onStart() { |
| 251 _installErrorHandler(); | 248 _installErrorHandler(); |
| 252 if (!_doneWrap) { | 249 if (!_doneWrap) { |
| 253 _doneWrap = true; | 250 _doneWrap = true; |
| 254 for (int i = 0; i < testCases.length; i++) { | 251 for (int i = 0; i < testCases.length; i++) { |
| 255 testCases[i].test = wrapTest(testCases[i]); | 252 testCases[i].test = wrapTest(testCases[i]); |
| 256 testCases[i].setUp = null; | 253 testCases[i].setUp = null; |
| 257 testCases[i].tearDown = null; | 254 testCases[i].tearDown = null; |
| 258 } | 255 } |
| 259 } | 256 } |
| 260 window.on.message.add(_messageHandler); | 257 assert(_messageSubscription == null); |
| 258 _messageSubscription = window.onMessage.listen(_handleMessage); | |
| 261 } | 259 } |
| 262 | 260 |
| 263 static final _notAlphaNumeric = new RegExp('[^a-z0-9A-Z]'); | 261 static final _notAlphaNumeric = new RegExp('[^a-z0-9A-Z]'); |
| 264 | 262 |
| 265 String _stringToDomId(String s) { | 263 String _stringToDomId(String s) { |
| 266 if (s.length == 0) { | 264 if (s.length == 0) { |
| 267 return '-None-'; | 265 return '-None-'; |
| 268 } | 266 } |
| 269 return s.trim().replaceAll(_notAlphaNumeric, '-'); | 267 return s.trim().replaceAll(_notAlphaNumeric, '-'); |
| 270 } | 268 } |
| (...skipping 19 matching lines...) Expand all Loading... | |
| 290 groupDiv = new Element.html(""" | 288 groupDiv = new Element.html(""" |
| 291 <div class='test-describe' id='$groupId'> | 289 <div class='test-describe' id='$groupId'> |
| 292 <h2> | 290 <h2> |
| 293 <input type='checkbox' checked='true' class='groupselect'> | 291 <input type='checkbox' checked='true' class='groupselect'> |
| 294 Group: ${testCase.currentGroup} | 292 Group: ${testCase.currentGroup} |
| 295 </h2> | 293 </h2> |
| 296 <ul class='tests'> | 294 <ul class='tests'> |
| 297 </ul> | 295 </ul> |
| 298 </div>"""); | 296 </div>"""); |
| 299 document.query('#group-divs').nodes.add(groupDiv); | 297 document.query('#group-divs').nodes.add(groupDiv); |
| 300 groupDiv.query('.groupselect').on.click.add((e) { | 298 groupDiv.query('.groupselect').onClick.listen((e) { |
| 301 var parent = document.query('#$groupId'); | 299 var parent = document.query('#$groupId'); |
| 302 InputElement cb = parent.query('.groupselect'); | 300 InputElement cb = parent.query('.groupselect'); |
| 303 var state = cb.checked; | 301 var state = cb.checked; |
| 304 var tests = parent.query('.tests'); | 302 var tests = parent.query('.tests'); |
| 305 for (Element t in tests.elements) { | 303 for (Element t in tests.children) { |
| 306 cb = t.query('.testselect') as InputElement; | 304 cb = t.query('.testselect') as InputElement; |
| 307 cb.checked = state; | 305 cb.checked = state; |
| 308 var testId = int.parse(t.id.substring(_testIdPrefix.length)); | 306 var testId = int.parse(t.id.substring(_testIdPrefix.length)); |
| 309 if (state) { | 307 if (state) { |
| 310 enableTest(testId); | 308 enableTest(testId); |
| 311 } else { | 309 } else { |
| 312 disableTest(testId); | 310 disableTest(testId); |
| 313 } | 311 } |
| 314 } | 312 } |
| 315 }); | 313 }); |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 328 <span class='timer-result test-timer-result'></span> | 326 <span class='timer-result test-timer-result'></span> |
| 329 <span class='test-name closed'>${testCase.description}</span> | 327 <span class='test-name closed'>${testCase.description}</span> |
| 330 </span> | 328 </span> |
| 331 </p> | 329 </p> |
| 332 </div> | 330 </div> |
| 333 <div class='scrollpane'> | 331 <div class='scrollpane'> |
| 334 <ol class='test-actions' id='$_actionIdPrefix$id'></ol> | 332 <ol class='test-actions' id='$_actionIdPrefix$id'></ol> |
| 335 </div> | 333 </div> |
| 336 </li>"""); | 334 </li>"""); |
| 337 list.nodes.add(testItem); | 335 list.nodes.add(testItem); |
| 338 testItem.query('#$_selectedIdPrefix$id').on.change.add((e) { | 336 testItem.query('#$_selectedIdPrefix$id').onChange.listen((e) { |
| 339 InputElement cb = testItem.query('#$_selectedIdPrefix$id'); | 337 InputElement cb = testItem.query('#$_selectedIdPrefix$id'); |
| 340 testCase.enabled = cb.checked; | 338 testCase.enabled = cb.checked; |
| 341 }); | 339 }); |
| 342 testItem.query('.test-label').on.click.add((e) { | 340 testItem.query('.test-label').onClick.listen((e) { |
| 343 var _testItem = document.query('#$_testIdPrefix$id'); | 341 var _testItem = document.query('#$_testIdPrefix$id'); |
| 344 var _actions = _testItem.query('#$_actionIdPrefix$id'); | 342 var _actions = _testItem.query('#$_actionIdPrefix$id'); |
| 345 var _label = _testItem.query('.test-name'); | 343 var _label = _testItem.query('.test-name'); |
| 346 if (_actions.style.display == 'none') { | 344 if (_actions.style.display == 'none') { |
| 347 _actions.style.display = 'table'; | 345 _actions.style.display = 'table'; |
| 348 _label.classes.remove('closed'); | 346 _label.classes.remove('closed'); |
| 349 _label.classes.add('open'); | 347 _label.classes.add('open'); |
| 350 } else { | 348 } else { |
| 351 _actions.style.display = 'none'; | 349 _actions.style.display = 'none'; |
| 352 _label.classes.remove('open'); | 350 _label.classes.remove('open'); |
| (...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 407 e.classes.add(result); | 405 e.classes.add(result); |
| 408 } | 406 } |
| 409 actions.style.display = 'none'; | 407 actions.style.display = 'none'; |
| 410 } | 408 } |
| 411 | 409 |
| 412 void onSummary(int passed, int failed, int errors, List<TestCase> results, | 410 void onSummary(int passed, int failed, int errors, List<TestCase> results, |
| 413 String uncaughtError) { | 411 String uncaughtError) { |
| 414 } | 412 } |
| 415 | 413 |
| 416 void onDone(bool success) { | 414 void onDone(bool success) { |
| 417 window.on.message.remove(_messageHandler); | 415 assert(_messageSubscription != null); |
| 416 _messageSubscription.cancel(); | |
| 417 _messageSubscription = null; | |
| 418 _uninstallErrorHandler(); | 418 _uninstallErrorHandler(); |
| 419 document.query('#busy').style.display = 'none'; | 419 document.query('#busy').style.display = 'none'; |
| 420 InputElement startButton = document.query('#start'); | 420 InputElement startButton = document.query('#start'); |
| 421 startButton.disabled = false; | 421 startButton.disabled = false; |
| 422 } | 422 } |
| 423 } | 423 } |
| 424 | 424 |
| 425 /** | 425 /** |
| 426 * Add the divs to the DOM if they are not present. We have a 'controls' | 426 * Add the divs to the DOM if they are not present. We have a 'controls' |
| 427 * div for control, 'specs' div with test results, a 'busy' div for the | 427 * div for control, 'specs' div with test results, a 'busy' div for the |
| 428 * animated GIF used to indicate tests are running, and a 'child' div to | 428 * animated GIF used to indicate tests are running, and a 'child' div to |
| 429 * hold the iframe for the test. | 429 * hold the iframe for the test. |
| 430 */ | 430 */ |
| 431 void _prepareDom() { | 431 void _prepareDom() { |
| 432 if (document.query('#control') == null) { | 432 if (document.query('#control') == null) { |
| 433 // Use this as an opportunity for adding the CSS too. | 433 // Use this as an opportunity for adding the CSS too. |
| 434 // I wanted to avoid having to include a css element explicitly | 434 // I wanted to avoid having to include a css element explicitly |
| 435 // in the main html file. I considered moving all the styles | 435 // in the main html file. I considered moving all the styles |
| 436 // inline as attributes but that started getting very messy, | 436 // inline as attributes but that started getting very messy, |
| 437 // so we do it this way. | 437 // so we do it this way. |
| 438 document.body.nodes.add(new Element.html("<style>$_CSS</style>")); | 438 document.body.nodes.add(new Element.html("<style>$_CSS</style>")); |
| 439 document.body.nodes.add(new Element.html( | 439 document.body.nodes.add(new Element.html( |
| 440 "<div id='control'>" | 440 "<div id='control'>" |
| 441 "<input id='start' disabled='true' type='button' value='Run'>" | 441 "<input id='start' disabled='true' type='button' value='Run'>" |
| 442 "</div>")); | 442 "</div>")); |
| 443 document.query('#start').on.click.add((e) { | 443 document.query('#start').onClick.listen((e) { |
| 444 InputElement startButton = document.query('#start'); | 444 InputElement startButton = document.query('#start'); |
| 445 startButton.disabled = true; | 445 startButton.disabled = true; |
| 446 rerunTests(); | 446 rerunTests(); |
| 447 }); | 447 }); |
| 448 } | 448 } |
| 449 if (document.query('#otherlogs') == null) { | 449 if (document.query('#otherlogs') == null) { |
| 450 document.body.nodes.add(new Element.html( | 450 document.body.nodes.add(new Element.html( |
| 451 "<div id='otherlogs'></div>")); | 451 "<div id='otherlogs'></div>")); |
| 452 } | 452 } |
| 453 if (document.query('#specs') == null) { | 453 if (document.query('#specs') == null) { |
| (...skipping 216 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 670 display: block; | 670 display: block; |
| 671 list-style-type: disc; | 671 list-style-type: disc; |
| 672 -webkit-margin-before: 1em; | 672 -webkit-margin-before: 1em; |
| 673 -webkit-margin-after: 1em; | 673 -webkit-margin-after: 1em; |
| 674 -webkit-margin-start: 0px; | 674 -webkit-margin-start: 0px; |
| 675 -webkit-margin-end: 0px; | 675 -webkit-margin-end: 0px; |
| 676 -webkit-padding-start: 40px; | 676 -webkit-padding-start: 40px; |
| 677 } | 677 } |
| 678 | 678 |
| 679 """; | 679 """; |
| OLD | NEW |