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