Index: lib/unittest/html_enhanced_config.dart |
diff --git a/lib/unittest/html_enhanced_config.dart b/lib/unittest/html_enhanced_config.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..1bdf924ea492f2e66c8db842e2f09142edc2761c |
--- /dev/null |
+++ b/lib/unittest/html_enhanced_config.dart |
@@ -0,0 +1,363 @@ |
+// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+/** |
+ * A simple unit test library for running tests in a browser. |
+ * |
+ * Provides enhanced HTML output with collapsible group headers |
+ * and other at-a-glance information about the test results. |
+ */ |
+#library('unittest'); |
+ |
+#import('dart:html'); |
+#import('unittest.dart'); |
+ |
+ |
+class HtmlEnhancedConfiguration extends Configuration { |
+ /** Whether this is run within dartium layout tests. */ |
+ final bool _isLayoutTest; |
+ HtmlEnhancedConfiguration(this._isLayoutTest); |
+ |
+ // TODO(rnystrom): Get rid of this if we get canonical closures for methods. |
+ EventListener _onErrorClosure; |
+ |
+ void onInit() { |
+ //initialize and load CSS |
+ final String _CSSID = '_unittestcss_'; |
+ |
+ var cssElement = document.head.query('#${_CSSID}'); |
+ if (cssElement == null){ |
+ document.head.elements.add(new Element.html( |
+ '<style id="${_CSSID}"></style>')); |
+ cssElement = document.head.query('#${_CSSID}'); |
+ } |
+ |
+ cssElement.innerHTML = _htmlTestCSS; |
+ |
+ _onErrorClosure = (e) { |
+ // TODO(vsm): figure out how to expose the stack trace here |
+ // Currently e.message works in dartium, but not in dartc. |
+ notifyError('(DOM callback has errors) Caught ${e}', ''); |
+ }; |
+ } |
+ |
+ void onStart() { |
+ window.postMessage('unittest-suite-wait-for-done', '*'); |
+ // Listen for uncaught errors. |
+ window.on.error.add(_onErrorClosure); |
+ } |
+ |
+ void onTestResult(TestCase testCase) {} |
+ |
+ void onDone(int passed, int failed, int errors, List<TestCase> results) { |
+ window.on.error.remove(_onErrorClosure); |
+ |
+ _showInteractiveResultsInPage(passed, failed, errors, results, |
+ _isLayoutTest); |
+ |
+ window.postMessage('unittest-suite-done', '*'); |
+ } |
+ |
+ void _showInteractiveResultsInPage(int passed, int failed, int errors, |
+ List<TestCase> results, bool isLayoutTest){ |
+ if (isLayoutTest && passed == results.length) { |
+ document.body.innerHTML = "PASS"; |
+ } else { |
+ // changed the StringBuffer to an Element fragment |
+ Element te = new Element.html('<div class="unittest-table"></div>'); |
+ |
+ te.elements.add(new Element.html(passed == results.length |
+ ? "<div class='unittest-overall unittest-pass'>PASS</div>" |
+ : "<div class='unittest-overall unittest-fail'>FAIL</div>")); |
+ |
+ // moved summary to the top since web browsers |
+ // don't auto-scroll to the bottom like consoles typically do. |
+ if (passed == results.length) { |
+ te.elements.add(new Element.html(""" |
+ <div class='unittest-pass'>All ${passed} tests passed</div>""")); |
+ } else { |
+ |
+ te.elements.add(new Element.html(""" |
+ <div class='unittest-summary'> |
+ <span class='unittest-pass'>Total ${passed} passed</span>, |
+ <span class='unittest-fail'>${failed} failed</span>, |
+ <span class='unittest-error'>${errors} errors</span> |
+ </div>""")); |
+ } |
+ |
+ te.elements.add(new Element.html(""" |
+ <div><button id='btnCollapseAll'>Collapse All</button></div> |
+ """)); |
+ |
+ // handle the click event for the collapse all button |
+ te.query('#btnCollapseAll').on.click.add((_){ |
+ document |
+ .queryAll('.unittest-row') |
+ .forEach((el) => el.attributes['class'] = el.attributes['class'] |
+ .replaceAll('unittest-row ', 'unittest-row-hidden ')); |
+ }); |
+ |
+ var previousGroup = ''; |
+ var groupPassFail = true; |
+ final indentAmount = 50; |
+ |
+ // order by group and sort numerically within each group |
+ var groupedBy = new LinkedHashMap<String, List<TestCase>>(); |
+ |
+ for (final t in results){ |
+ if (!groupedBy.containsKey(t.currentGroup)){ |
+ groupedBy[t.currentGroup] = new List<TestCase>(); |
+ } |
+ |
+ groupedBy[t.currentGroup].add(t); |
+ } |
+ |
+ // flatten the list again with tests ordered |
+ List<TestCase> flattened = new List<TestCase>(); |
+ |
+ groupedBy |
+ .getValues() |
+ .forEach((tList){ |
+ tList.sort((tcA, tcB) => tcA.id - tcB.id); |
+ flattened.addAll(tList); |
+ } |
+ ); |
+ |
+ // output group headers and test rows |
+ for (final test_ in flattened) { |
+ |
+ // replace everything but numbers and letters from the group name with |
+ // '_' so we can use in id and class properties. |
+ var safeGroup = test_.currentGroup |
+ .replaceAll("(?:[^a-z0-9 ]|(?<=['\"])s)",'_') |
Siggi Cherem (dart-lang)
2012/04/24 23:26:30
Can you help me understand this expression better.
|
+ .replaceAll(' ','_'); |
+ |
+ if (test_.currentGroup != previousGroup){ |
+ |
+ previousGroup = test_.currentGroup; |
+ |
+ var testsInGroup = results.filter( |
+ (TestCase t) => t.currentGroup == previousGroup); |
+ var groupTotalTestCount = testsInGroup.length; |
+ var groupTestPassedCount = testsInGroup.filter( |
+ (TestCase t) => t.result == 'pass').length; |
+ groupPassFail = groupTotalTestCount == groupTestPassedCount; |
+ |
+ te.elements.add(new Element.html(""" |
+ <div> |
+ <div id='${safeGroup}' |
+ class='unittest-group ${safeGroup} test${safeGroup}'> |
+ <div ${_isIE ? "style='display:inline-block' ": ""} |
+ class='unittest-row-status'> |
+ <div class='unittest-group-status unittest-group-status- |
+ ${groupPassFail ? 'pass' : 'fail'}'></div> |
+ </div> |
+ <div ${_isIE ? "style='display:inline-block' ": ""}> |
+ ${test_.currentGroup}</div> |
+ <div ${_isIE ? "style='display:inline-block' ": ""}> |
+ (${groupTestPassedCount}/${groupTotalTestCount})</div> |
+ </div> |
+ </div>""")); |
+ |
+ var grp = te.query('#${safeGroup}'); |
+ if (grp != null){ |
+ grp.on.click.add((_){ |
+ var row = document.query('.unittest-row-${safeGroup}'); |
+ if (row.attributes['class'].contains('unittest-row ')){ |
+ document.queryAll('.unittest-row-${safeGroup}').forEach( |
+ (e) => e.attributes['class'] = e.attributes['class'] |
+ .replaceAll('unittest-row ', 'unittest-row-hidden ')); |
+ }else{ |
+ document.queryAll('.unittest-row-${safeGroup}').forEach( |
+ (e) => e.attributes['class'] = e.attributes['class'] |
+ .replaceAll('unittest-row-hidden', 'unittest-row')); |
+ } |
+ }); |
+ } |
+ } |
+ |
+ _buildRow(test_, te, safeGroup, !groupPassFail); |
+ } |
+ |
+ document.body.elements.clear(); |
+ document.body.elements.add(te); |
+ } |
+ } |
+ |
+ void _buildRow(TestCase test_, Element te, String groupID, bool isVisible) { |
+ var background = 'unittest-row-${test_.id % 2 == 0 ? "even" : "odd"}'; |
+ var display = '${isVisible ? "unittest-row" : "unittest-row-hidden"}'; |
+ |
+ // TODO (prujohn@gmail.com) I had to borrow this from html_print.dart |
+ // Probably should put it in some more common location. |
+ String _htmlEscape(String string) { |
+ return string.replaceAll('&', '&') |
+ .replaceAll('<','<') |
+ .replaceAll('>','>'); |
+ } |
+ |
+ addRowElement(id, status, description){ |
+ te.elements.add( |
+ new Element.html( |
+ ''' <div> |
+ <div class='$display unittest-row-${groupID} $background'> |
+ <div ${_isIE ? "style='display:inline-block' ": ""} |
+ class='unittest-row-id'>$id</div> |
+ <div ${_isIE ? "style='display:inline-block' ": ""} |
+ class="unittest-row-status unittest-${test_.result}"> |
+ $status</div> |
+ <div ${_isIE ? "style='display:inline-block' ": ""} |
+ class='unittest-row-description'>$description</div> |
+ </div> |
+ </div>''' |
+ ) |
+ ); |
+ } |
+ |
+ if (!test_.isComplete) { |
+ addRowElement('${test_.id}', 'NO STATUS', 'Test did not complete.'); |
+ return; |
+ } |
+ |
+ addRowElement('${test_.id}', '${test_.result.toUpperCase()}', |
+ '${test_.description}. ${_htmlEscape(test_.message)}'); |
+ |
+ if (test_.stackTrace != null) { |
+ addRowElement('', '', '<pre>${_htmlEscape(test_.stackTrace)}</pre>'); |
+ } |
+ } |
+ |
+ |
+ static bool get _isIE() => document.window.navigator.userAgent.contains('MSIE'); |
Siggi Cherem (dart-lang)
2012/04/21 00:02:13
still a few more lines with > 80 (here and below)
|
+ |
+ String get _htmlTestCSS() => |
+ ''' |
+ body{ |
+ font-size: 14px; |
+ font-family: 'Open Sans', 'Lucida Sans Unicode', 'Lucida Grande', sans-serif; |
+ background: WhiteSmoke; |
+ } |
+ |
+ .unittest-group |
+ { |
+ background: rgb(75,75,75); |
+ width:98%; |
+ color: WhiteSmoke; |
+ font-weight: bold; |
+ padding: 6px; |
+ |
+ /* Provide some visual separation between groups for IE */ |
+ ${_isIE ? "border-bottom:solid black 1px;": ""} |
+ ${_isIE ? "border-top:solid #777777 1px;": ""} |
+ |
+ background-image: -webkit-linear-gradient(bottom, rgb(50,50,50) 0%, rgb(100,100,100) 100%); |
+ background-image: -moz-linear-gradient(bottom, rgb(50,50,50) 0%, rgb(100,100,100) 100%); |
+ background-image: -ms-linear-gradient(bottom, rgb(50,50,50) 0%, rgb(100,100,100) 100%); |
+ background-image: linear-gradient(bottom, rgb(50,50,50) 0%, rgb(100,100,100) 100%); |
+ |
+ display: -webkit-box; |
+ display: -moz-box; |
+ display: -ms-box; |
+ display: box; |
+ |
+ -webkit-box-orient: horizontal; |
+ -moz-box-orient: horizontal; |
+ -ms-box-orient: horizontal; |
+ box-orient: horizontal; |
+ |
+ -webkit-box-align: center; |
+ -moz-box-align: center; |
+ -ms-box-align: center; |
+ box-align: center; |
+ } |
+ |
+ .unittest-group-status |
+ { |
+ width: 20px; |
+ height: 20px; |
+ border-radius: 20px; |
+ margin-left: 10px; |
+ } |
+ |
+ .unittest-group-status-pass{ |
+ background: Green; |
+ background: -webkit-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
+ background: -moz-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
+ background: -ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
+ background: radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
+ } |
+ |
+ .unittest-group-status-fail{ |
+ background: Red; |
+ background: -webkit-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); |
+ background: -moz-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); |
+ background: -ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
+ background: radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); |
+ } |
+ |
+ .unittest-overall{ |
+ font-size: 20px; |
+ } |
+ |
+ .unittest-summary{ |
+ font-size: 18px; |
+ } |
+ |
+ .unittest-pass{ |
+ color: Green; |
+ } |
+ |
+ .unittest-fail, .unittest-error |
+ { |
+ color: Red; |
+ } |
+ |
+ .unittest-row |
+ { |
+ display: -webkit-box; |
+ display: -moz-box; |
+ display: -ms-box; |
+ display: box; |
+ -webkit-box-orient: horizontal; |
+ -moz-box-orient: horizontal; |
+ -ms-box-orient: horizontal; |
+ box-orient: horizontal; |
+ width: 100%; |
+ } |
+ |
+ .unittest-row-hidden |
+ { |
+ display: none; |
+ } |
+ |
+ .unittest-row-odd |
+ { |
+ background: WhiteSmoke; |
+ } |
+ |
+ .unittest-row-even |
+ { |
+ background: #E5E5E5; |
+ } |
+ |
+ .unittest-row-id |
+ { |
+ width: 3em; |
+ } |
+ |
+ .unittest-row-status |
+ { |
+ width: 4em; |
+ } |
+ |
+ .unittest-row-description |
+ { |
+ } |
+ |
+ '''; |
+} |
+ |
+void useHtmlEnhancedConfiguration([bool isLayoutTest = false]) { |
+ configure(new HtmlEnhancedConfiguration(isLayoutTest)); |
+} |