OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
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 | |
8 * IFrame, so the configuration consists of two parts - a 'parent' | |
9 * config that manages all the tests, and a 'child' config for the | |
10 * IFrame that runs the individual tests. | |
11 */ | |
12 #library('interactive_config'); | |
13 | |
14 // TODO(gram) - add options for: remove IFrame on done/keep | |
15 // IFrame for failed tests/keep IFrame for all tests. | |
16 | |
17 #import('dart:html'); | |
18 #import('dart:math'); | |
19 #import('unittest.dart'); | |
20 | |
21 /** The messages exchanged between parent and child. */ | |
22 | |
23 class _Message { | |
24 static const START = 'start'; | |
25 static const LOG = 'log'; | |
26 static const STACK = 'stack'; | |
27 static const PASS = 'pass'; | |
28 static const FAIL = 'fail'; | |
29 static const 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 int idx = msg.indexOf(' '); | |
43 messageType = msg.substring(0, idx); | |
44 ++idx; | |
45 int idx2 = msg.indexOf(' ', idx); | |
46 elapsed = parseInt(msg.substring(idx, idx2)); | |
47 ++idx2; | |
48 body = msg.substring(idx2); | |
49 } | |
50 | |
51 String toString() => text(messageType, elapsed, body); | |
52 } | |
53 | |
54 /** | |
55 * The child configuration that is used to run individual tests in | |
56 * an IFrame and post the results back to the parent. In principle | |
57 * this can run more than one test in the IFrame but currently only | |
58 * one is used. | |
59 */ | |
60 class ChildInteractiveHtmlConfiguration extends Configuration { | |
61 // TODO(rnystrom): Get rid of this if we get canonical closures for methods. | |
62 EventListener _onErrorClosure; | |
63 | |
64 /** The window to which results must be posted. */ | |
65 Window parentWindow; | |
66 | |
67 /** The time at which tests start. */ | |
68 Map<int,Date> _testStarts; | |
69 | |
70 ChildInteractiveHtmlConfiguration() : | |
71 _testStarts = new Map<int,Date>(); | |
72 | |
73 /** Don't start running tests automatically. */ | |
74 get autoStart => false; | |
75 | |
76 void onInit() { | |
77 _onErrorClosure = | |
78 (e) => handleExternalError(e, '(DOM callback has errors)'); | |
79 | |
80 /** | |
81 * The parent posts a 'start' message to kick things off, | |
82 * which is handled by this handler. It saves the parent | |
83 * window, gets the test ID from the query parameter in the | |
84 * IFrame URL, sets that as a solo test and starts test execution. | |
85 */ | |
86 window.on.message.add((MessageEvent e) { | |
87 // Get the result, do any logging, then do a pass/fail. | |
88 var m = new _Message.fromString(e.data); | |
89 if (m.messageType == _Message.START) { | |
90 parentWindow = e.source; | |
91 String search = window.location.search; | |
92 int pos = search.indexOf('t='); | |
93 String ids = search.substring(pos+2); | |
94 int id = parseInt(ids); | |
95 setSoloTest(id); | |
96 runTests(); | |
97 } | |
98 }); | |
99 } | |
100 | |
101 void onStart() { | |
102 // Listen for uncaught errors. | |
103 window.on.error.add(_onErrorClosure); | |
104 } | |
105 | |
106 /** Record the start time of the test. */ | |
107 void onTestStart(TestCase testCase) { | |
108 super.onTestStart(testCase); | |
109 _testStarts[testCase.id]= new Date.now(); | |
110 } | |
111 | |
112 /** | |
113 * Tests can call [logMessage] for diagnostic output. These log | |
114 * messages in turn get passed to this method, which adds | |
115 * a timestamp and posts them back to the parent window. | |
116 */ | |
117 void logTestCaseMessage(TestCase testCase, String message) { | |
118 int elapsed; | |
119 if (testCase == null) { | |
120 elapsed = -1; | |
121 } else { | |
122 Date end = new Date.now(); | |
123 elapsed = end.difference(_testStarts[testCase.id]).inMilliseconds; | |
124 } | |
125 parentWindow.postMessage( | |
126 _Message.text(_Message.LOG, elapsed, message).toString(), '*'); | |
127 } | |
128 | |
129 /** | |
130 * Get the elapsed time for the test, anbd post the test result | |
131 * back to the parent window. If the test failed due to an exception | |
132 * the stack is posted back too (before the test result). | |
133 */ | |
134 void onTestResult(TestCase testCase) { | |
135 super.onTestResult(testCase); | |
136 Date end = new Date.now(); | |
137 int elapsed = end.difference(_testStarts[testCase.id]).inMilliseconds; | |
138 if (testCase.stackTrace != null) { | |
139 parentWindow.postMessage( | |
140 _Message.text(_Message.STACK, elapsed, testCase.stackTrace), '*'); | |
141 } | |
142 parentWindow.postMessage( | |
143 _Message.text(testCase.result, elapsed, testCase.message), '*'); | |
144 } | |
145 | |
146 void onDone(int passed, int failed, int errors, List<TestCase> results, | |
147 String uncaughtError) { | |
148 window.on.error.remove(_onErrorClosure); | |
149 } | |
150 } | |
151 | |
152 /** | |
153 * The parent configuration runs in the top-level window; it wraps the tests | |
154 * in new functions that create child IFrames and run the real tests. | |
155 */ | |
156 class ParentInteractiveHtmlConfiguration extends Configuration { | |
157 Map<int,Date> _testStarts; | |
158 // TODO(rnystrom): Get rid of this if we get canonical closures for methods. | |
159 EventListener _onErrorClosure; | |
160 | |
161 /** The stack that was posted back from the child, if any. */ | |
162 String _stack; | |
163 | |
164 int _testTime; | |
165 /** | |
166 * Whether or not we have already wrapped the TestCase test functions | |
167 * in new closures that instead create an IFrame and get it to run the | |
168 * test. | |
169 */ | |
170 bool _doneWrap = false; | |
171 | |
172 /** | |
173 * We use this to make a single closure from _handleMessage so we | |
174 * can remove the handler later. | |
175 */ | |
176 Function _messageHandler; | |
177 | |
178 ParentInteractiveHtmlConfiguration() : | |
179 _testStarts = new Map<int,Date>(); | |
180 | |
181 // We need to block until the test is done, so we make a | |
182 // dummy async callback that we will use to flag completion. | |
183 Function completeTest = null; | |
184 | |
185 wrapTest(TestCase testCase) { | |
186 String baseUrl = window.location.toString(); | |
187 String url = '${baseUrl}?t=${testCase.id}'; | |
188 return () { | |
189 // Rebuild the child IFrame. | |
190 Element childDiv = document.query('#child'); | |
191 childDiv.nodes.clear(); | |
192 IFrameElement child = new Element.html(""" | |
193 <iframe id='childFrame${testCase.id}' src='$url' style='display:none'> | |
194 </iframe>"""); | |
195 childDiv.nodes.add(child); | |
196 completeTest = expectAsync0((){ }); | |
197 // Kick off the test when the IFrame is loaded. | |
198 child.on.load.add((e) { | |
199 child.contentWindow.postMessage(_Message.text(_Message.START), '*'); | |
200 }); | |
201 }; | |
202 } | |
203 | |
204 void _handleMessage(MessageEvent e) { | |
205 // Get the result, do any logging, then do a pass/fail. | |
206 var msg = new _Message.fromString(e.data); | |
207 if (msg.messageType == _Message.LOG) { | |
208 logMessage(e.data); | |
209 } else if (msg.messageType == _Message.STACK) { | |
210 _stack = msg.body; | |
211 } else { | |
212 _testTime = msg.elapsed; | |
213 logMessage(_Message.text(_Message.LOG, _testTime, 'Complete')); | |
214 if (msg.messageType == _Message.PASS) { | |
215 currentTestCase.pass(); | |
216 } else if (msg.messageType == _Message.FAIL) { | |
217 currentTestCase.fail(msg.body, _stack); | |
218 } else if (msg.messageType == _Message.ERROR) { | |
219 currentTestCase.error(msg.body, _stack); | |
220 } | |
221 completeTest(); | |
222 } | |
223 } | |
224 | |
225 void onInit() { | |
226 _messageHandler = _handleMessage; // We need to make just one closure. | |
227 _onErrorClosure = | |
228 (e) => handleExternalError(e, '(DOM callback has errors)'); | |
229 document.query('#group-divs').innerHTML = ""; | |
230 } | |
231 | |
232 void onStart() { | |
233 // Listen for uncaught errors. | |
234 window.on.error.add(_onErrorClosure); | |
235 if (!_doneWrap) { | |
236 _doneWrap = true; | |
237 for (int i = 0; i < testCases.length; i++) { | |
238 testCases[i].test = wrapTest(testCases[i]); | |
239 testCases[i].setUp = null; | |
240 testCases[i].tearDown = null; | |
241 } | |
242 } | |
243 window.on.message.add(_messageHandler); | |
244 } | |
245 | |
246 static const _notAlphaNumeric = const RegExp('[^a-z0-9A-Z]'); | |
247 | |
248 String _stringToDomId(String s) { | |
249 if (s.length == 0) { | |
250 return '-None-'; | |
251 } | |
252 return s.trim().replaceAll(_notAlphaNumeric, '-'); | |
253 } | |
254 | |
255 // Used for DOM element IDs for tests result list entries. | |
256 static const _testIdPrefix = 'test-'; | |
257 // Used for DOM element IDs for test log message lists. | |
258 static const _actionIdPrefix = 'act-'; | |
259 // Used for DOM element IDs for test checkboxes. | |
260 static const _selectedIdPrefix = 'selected-'; | |
261 | |
262 void onTestStart(TestCase testCase) { | |
263 var id = testCase.id; | |
264 _testStarts[testCase.id]= new Date.now(); | |
265 super.onTestStart(testCase); | |
266 _stack = null; | |
267 // Convert the group name to a DOM id. | |
268 String groupId = _stringToDomId(testCase.currentGroup); | |
269 // Get the div for the group. If it doesn't exist, | |
270 // create it. | |
271 var groupDiv = document.query('#$groupId'); | |
272 if (groupDiv == null) { | |
273 groupDiv = new Element.html(""" | |
274 <div class='test-describe' id='$groupId'> | |
275 <h2> | |
276 <input type='checkbox' checked='true' class='groupselect'> | |
277 Group: ${testCase.currentGroup} | |
278 </h2> | |
279 <ul class='tests'> | |
280 </ul> | |
281 </div>"""); | |
282 document.query('#group-divs').nodes.add(groupDiv); | |
283 groupDiv.query('.groupselect').on.click.add((e) { | |
284 var parent = document.query('#$groupId'); | |
285 InputElement cb = parent.query('.groupselect'); | |
286 var state = cb.checked; | |
287 var tests = parent.query('.tests'); | |
288 for (Element t in tests.elements) { | |
289 cb = t.query('.testselect') as InputElement; | |
290 cb.checked = state; | |
291 var testId = parseInt(t.id.substring(_testIdPrefix.length)); | |
292 if (state) { | |
293 enableTest(testId); | |
294 } else { | |
295 disableTest(testId); | |
296 } | |
297 } | |
298 }); | |
299 } | |
300 var list = groupDiv.query('.tests'); | |
301 var testItem = list.query('#$_testIdPrefix$id'); | |
302 if (testItem == null) { | |
303 // Create the li element for the test. | |
304 testItem = new Element.html(""" | |
305 <li id='$_testIdPrefix$id' class='test-it status-pending'> | |
306 <div class='test-info'> | |
307 <p class='test-title'> | |
308 <input type='checkbox' checked='true' class='testselect' | |
309 id='$_selectedIdPrefix$id'> | |
310 <span class='test-label'> | |
311 <span class='timer-result test-timer-result'></span> | |
312 <span class='test-name closed'>${testCase.description}</span> | |
313 </span> | |
314 </p> | |
315 </div> | |
316 <div class='scrollpane'> | |
317 <ol class='test-actions' id='$_actionIdPrefix$id'></ol> | |
318 </div> | |
319 </li>"""); | |
320 list.nodes.add(testItem); | |
321 testItem.query('#$_selectedIdPrefix$id').on.change.add((e) { | |
322 InputElement cb = testItem.query('#$_selectedIdPrefix$id'); | |
323 testCase.enabled = cb.checked; | |
324 }); | |
325 testItem.query('.test-label').on.click.add((e) { | |
326 var _testItem = document.query('#$_testIdPrefix$id'); | |
327 var _actions = _testItem.query('#$_actionIdPrefix$id'); | |
328 var _label = _testItem.query('.test-name'); | |
329 if (_actions.style.display == 'none') { | |
330 _actions.style.display = 'table'; | |
331 _label.classes.remove('closed'); | |
332 _label.classes.add('open'); | |
333 } else { | |
334 _actions.style.display = 'none'; | |
335 _label.classes.remove('open'); | |
336 _label.classes.add('closed'); | |
337 } | |
338 }); | |
339 } else { // Reset the test element. | |
340 testItem.classes.clear(); | |
341 testItem.classes.add('test-it'); | |
342 testItem.classes.add('status-pending'); | |
343 testItem.query('#$_actionIdPrefix$id').innerHTML = ''; | |
344 } | |
345 } | |
346 | |
347 // Actually test logging is handled by the child, then posted | |
348 // back to the parent. So here we know that the [message] argument | |
349 // is in the format used by [_Message]. | |
350 void logTestCaseMessage(TestCase testCase, String message) { | |
351 var msg = new _Message.fromString(message); | |
352 if (msg.elapsed < 0) { // No associated test case. | |
353 document.query('#otherlogs').nodes.add( | |
354 new Element.html('<p>${msg.body}</p>')); | |
355 } else { | |
356 var actions = document.query('#$_testIdPrefix${testCase.id}'). | |
357 query('.test-actions'); | |
358 String elapsedText = msg.elapsed >= 0 ? "${msg.elapsed}ms" : ""; | |
359 actions.nodes.add(new Element.html( | |
360 "<li style='list-style-stype:none>" | |
361 "<div class='timer-result'>${elapsedText}</div>" | |
362 "<div class='test-title'>${msg.body}</div>" | |
363 "</li>")); | |
364 } | |
365 } | |
366 | |
367 void onTestResult(TestCase testCase) { | |
368 if (!testCase.enabled) return; | |
369 super.onTestResult(testCase); | |
370 if (testCase.message != '') { | |
371 logTestCaseMessage(testCase, | |
372 _Message.text(_Message.LOG, -1, testCase.message)); | |
373 } | |
374 int id = testCase.id; | |
375 var testItem = document.query('#$_testIdPrefix$id'); | |
376 var timeSpan = testItem.query('.test-timer-result'); | |
377 timeSpan.text = '${_testTime}ms'; | |
378 // Convert status into what we need for our CSS. | |
379 String result = 'status-error'; | |
380 if (testCase.result == 'pass') { | |
381 result = 'status-success'; | |
382 } else if (testCase.result == 'fail') { | |
383 result = 'status-failure'; | |
384 } | |
385 testItem.classes.remove('status-pending'); | |
386 testItem.classes.add(result); | |
387 // hide the actions | |
388 var actions = testItem.query('.test-actions'); | |
389 for (Element e in actions.nodes) { | |
390 e.classes.add(result); | |
391 } | |
392 actions.style.display = 'none'; | |
393 } | |
394 | |
395 void onDone(int passed, int failed, int errors, List<TestCase> results, | |
396 String uncaughtError) { | |
397 window.on.message.remove(_messageHandler); | |
398 window.on.error.remove(_onErrorClosure); | |
399 document.query('#busy').style.display = 'none'; | |
400 InputElement startButton = document.query('#start'); | |
401 startButton.disabled = false; | |
402 } | |
403 } | |
404 | |
405 /** | |
406 * Add the divs to the DOM if they are not present. We have a 'controls' | |
407 * div for control, 'specs' div with test results, a 'busy' div for the | |
408 * animated GIF used to indicate tests are running, and a 'child' div to | |
409 * hold the iframe for the test. | |
410 */ | |
411 void _prepareDom() { | |
412 if (document.query('#control') == null) { | |
413 // Use this as an opportunity for adding the CSS too. | |
414 // I wanted to avoid having to include a css element explicitly | |
415 // in the main html file. I considered moving all the styles | |
416 // inline as attributes but that started getting very messy, | |
417 // so we do it this way. | |
418 document.body.nodes.add(new Element.html("<style>$_CSS</style>")); | |
419 document.body.nodes.add(new Element.html( | |
420 "<div id='control'>" | |
421 "<input id='start' disabled='true' type='button' value='Run'>" | |
422 "</div>")); | |
423 document.query('#start').on.click.add((e) { | |
424 InputElement startButton = document.query('#start'); | |
425 startButton.disabled = true; | |
426 rerunTests(); | |
427 }); | |
428 } | |
429 if (document.query('#otherlogs') == null) { | |
430 document.body.nodes.add(new Element.html( | |
431 "<div id='otherlogs'></div>")); | |
432 } | |
433 if (document.query('#specs') == null) { | |
434 document.body.nodes.add(new Element.html( | |
435 "<div id='specs'><div id='group-divs'></div></div>")); | |
436 } | |
437 if (document.query('#busy') == null) { | |
438 document.body.nodes.add(new Element.html( | |
439 "<div id='busy' style='display:none'><img src='googleballs.gif'>" | |
440 "</img></div>")); | |
441 } | |
442 if (document.query('#child') == null) { | |
443 document.body.nodes.add(new Element.html("<div id='child'></div>")); | |
444 } | |
445 } | |
446 | |
447 /** | |
448 * Allocate a Configuration. We allocate either a parent or | |
449 * child, depedning on whether the URL has a search part. | |
450 */ | |
451 void useInteractiveHtmlConfiguration() { | |
452 if (window.location.search == '') { // This is the parent. | |
453 _prepareDom(); | |
454 configure(new ParentInteractiveHtmlConfiguration()); | |
455 } else { | |
456 configure(new ChildInteractiveHtmlConfiguration()); | |
457 } | |
458 } | |
459 | |
460 String _CSS = """ | |
461 body { | |
462 font-family: Arial, sans-serif; | |
463 margin: 0; | |
464 font-size: 14px; | |
465 } | |
466 | |
467 #application h2, | |
468 #specs h2 { | |
469 margin: 0; | |
470 padding: 0.5em; | |
471 font-size: 1.1em; | |
472 } | |
473 | |
474 #header, | |
475 #application, | |
476 .test-info, | |
477 .test-actions li { | |
478 overflow: hidden; | |
479 } | |
480 | |
481 #application { | |
482 margin: 10px; | |
483 } | |
484 | |
485 #application iframe { | |
486 width: 100%; | |
487 height: 758px; | |
488 } | |
489 | |
490 #application iframe { | |
491 border: none; | |
492 } | |
493 | |
494 #specs { | |
495 padding-top: 50px | |
496 } | |
497 | |
498 .test-describe h2 { | |
499 border-top: 2px solid #BABAD1; | |
500 background-color: #efefef; | |
501 } | |
502 | |
503 .tests, | |
504 .test-it ol, | |
505 .status-display { | |
506 margin: 0; | |
507 padding: 0; | |
508 } | |
509 | |
510 .test-info { | |
511 margin-left: 1em; | |
512 margin-top: 0.5em; | |
513 border-radius: 8px 0 0 8px; | |
514 -webkit-border-radius: 8px 0 0 8px; | |
515 -moz-border-radius: 8px 0 0 8px; | |
516 cursor: pointer; | |
517 } | |
518 | |
519 .test-info:hover .test-name { | |
520 text-decoration: underline; | |
521 } | |
522 | |
523 .test-info .closed:before { | |
524 content: '\\25b8\\00A0'; | |
525 } | |
526 | |
527 .test-info .open:before { | |
528 content: '\\25be\\00A0'; | |
529 font-weight: bold; | |
530 } | |
531 | |
532 .test-it ol { | |
533 margin-left: 2.5em; | |
534 } | |
535 | |
536 .status-display, | |
537 .status-display li { | |
538 float: right; | |
539 } | |
540 | |
541 .status-display li { | |
542 padding: 5px 10px; | |
543 } | |
544 | |
545 .timer-result, | |
546 .test-title { | |
547 display: inline-block; | |
548 margin: 0; | |
549 padding: 4px; | |
550 } | |
551 | |
552 .test-actions .test-title, | |
553 .test-actions .test-result { | |
554 display: table-cell; | |
555 padding-left: 0.5em; | |
556 padding-right: 0.5em; | |
557 } | |
558 | |
559 .test-it { | |
560 list-style-type: none; | |
561 } | |
562 | |
563 .test-actions { | |
564 display: table; | |
565 } | |
566 | |
567 .test-actions li { | |
568 display: table-row; | |
569 } | |
570 | |
571 .timer-result { | |
572 width: 4em; | |
573 padding: 0 10px; | |
574 text-align: right; | |
575 font-family: monospace; | |
576 } | |
577 | |
578 .test-it pre, | |
579 .test-actions pre { | |
580 clear: left; | |
581 color: black; | |
582 margin-left: 6em; | |
583 } | |
584 | |
585 .test-describe { | |
586 margin: 5px 5px 10px 2em; | |
587 border-left: 1px solid #BABAD1; | |
588 border-right: 1px solid #BABAD1; | |
589 border-bottom: 1px solid #BABAD1; | |
590 padding-bottom: 0.5em; | |
591 } | |
592 | |
593 .test-actions .status-pending .test-title:before { | |
594 content: \\'\\\\00bb\\\\00A0\\'; | |
595 } | |
596 | |
597 .scrollpane { | |
598 max-height: 20em; | |
599 overflow: auto; | |
600 } | |
601 | |
602 #busy { | |
603 display: block; | |
604 } | |
605 /** Colors */ | |
606 | |
607 #header { | |
608 background-color: #F2C200; | |
609 } | |
610 | |
611 #application { | |
612 border: 1px solid #BABAD1; | |
613 } | |
614 | |
615 .status-pending .test-info { | |
616 background-color: #F9EEBC; | |
617 } | |
618 | |
619 .status-success .test-info { | |
620 background-color: #B1D7A1; | |
621 } | |
622 | |
623 .status-failure .test-info { | |
624 background-color: #FF8286; | |
625 } | |
626 | |
627 .status-error .test-info { | |
628 background-color: black; | |
629 color: white; | |
630 } | |
631 | |
632 .test-actions .status-success .test-title { | |
633 color: #30B30A; | |
634 } | |
635 | |
636 .test-actions .status-failure .test-title { | |
637 color: #DF0000; | |
638 } | |
639 | |
640 .test-actions .status-error .test-title { | |
641 color: black; | |
642 } | |
643 | |
644 .test-actions .timer-result { | |
645 color: #888; | |
646 } | |
647 | |
648 ul, menu, dir { | |
649 display: block; | |
650 list-style-type: disc; | |
651 -webkit-margin-before: 1em; | |
652 -webkit-margin-after: 1em; | |
653 -webkit-margin-start: 0px; | |
654 -webkit-margin-end: 0px; | |
655 -webkit-padding-start: 40px; | |
656 } | |
657 | |
658 """; | |
OLD | NEW |