Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(265)

Side by Side Diff: lib/unittest/interactive_html_config.dart

Issue 10836241: Move unittest from lib to pkg. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 8 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « lib/unittest/html_print.dart ('k') | lib/unittest/interfaces.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 """;
OLDNEW
« no previous file with comments | « lib/unittest/html_print.dart ('k') | lib/unittest/interfaces.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698