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

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

Issue 10918240: Make unittest follow the new package layout. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 8 years, 3 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 | « pkg/unittest/html_print.dart ('k') | pkg/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 '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 """;
OLDNEW
« no previous file with comments | « pkg/unittest/html_print.dart ('k') | pkg/unittest/interfaces.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698