OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2013, 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 /// A simple unit test library for running tests in a browser. |
| 6 /// |
| 7 /// Provides enhanced HTML output with collapsible group headers |
| 8 /// and other at-a-glance information about the test results. |
| 9 library unittest.html_enhanced_config; |
| 10 |
| 11 import 'dart:collection' show LinkedHashMap; |
| 12 import 'dart:convert'; |
| 13 import 'dart:html'; |
| 14 import 'unittest.dart'; |
| 15 |
| 16 class HtmlEnhancedConfiguration extends SimpleConfiguration { |
| 17 /// Whether this is run within dartium layout tests. |
| 18 final bool _isLayoutTest; |
| 19 HtmlEnhancedConfiguration(this._isLayoutTest); |
| 20 |
| 21 var _onErrorSubscription = null; |
| 22 var _onMessageSubscription = null; |
| 23 |
| 24 void _installOnErrorHandler() { |
| 25 if (_onErrorSubscription == null) { |
| 26 // Listen for uncaught errors. |
| 27 _onErrorSubscription = window.onError.listen( |
| 28 (e) => handleExternalError(e, '(DOM callback has errors)')); |
| 29 } |
| 30 } |
| 31 |
| 32 void _installOnMessageHandler() { |
| 33 if (_onMessageSubscription == null) { |
| 34 // Listen for errors from JS. |
| 35 _onMessageSubscription = window.onMessage.listen( |
| 36 (e) => processMessage(e)); |
| 37 } |
| 38 } |
| 39 |
| 40 void _installHandlers() { |
| 41 _installOnErrorHandler(); |
| 42 _installOnMessageHandler(); |
| 43 } |
| 44 |
| 45 void _uninstallHandlers() { |
| 46 if (_onErrorSubscription != null) { |
| 47 _onErrorSubscription.cancel(); |
| 48 _onErrorSubscription = null; |
| 49 } |
| 50 if (_onMessageSubscription != null) { |
| 51 _onMessageSubscription.cancel(); |
| 52 _onMessageSubscription = null; |
| 53 } |
| 54 } |
| 55 |
| 56 void processMessage(e) { |
| 57 if ('unittest-suite-external-error' == e.data) { |
| 58 handleExternalError('<unknown>', '(external error detected)'); |
| 59 } |
| 60 } |
| 61 |
| 62 void onInit() { |
| 63 _installHandlers(); |
| 64 //initialize and load CSS |
| 65 final String _CSSID = '_unittestcss_'; |
| 66 |
| 67 var cssElement = document.head.querySelector('#${_CSSID}'); |
| 68 if (cssElement == null) { |
| 69 cssElement = new StyleElement(); |
| 70 cssElement.id = _CSSID; |
| 71 document.head.append(cssElement); |
| 72 } |
| 73 |
| 74 cssElement.text = _htmlTestCSS; |
| 75 window.postMessage('unittest-suite-wait-for-done', '*'); |
| 76 } |
| 77 |
| 78 void onStart() { |
| 79 // Listen for uncaught errors. |
| 80 _installOnErrorHandler(); |
| 81 } |
| 82 |
| 83 void onSummary(int passed, int failed, int errors, List<TestCase> results, |
| 84 String uncaughtError) { |
| 85 _showInteractiveResultsInPage(passed, failed, errors, results, |
| 86 _isLayoutTest, uncaughtError); |
| 87 } |
| 88 |
| 89 void onDone(bool success) { |
| 90 _uninstallHandlers(); |
| 91 window.postMessage('unittest-suite-done', '*'); |
| 92 } |
| 93 |
| 94 void _showInteractiveResultsInPage(int passed, int failed, int errors, |
| 95 List<TestCase> results, bool isLayoutTest, String uncaughtError) { |
| 96 if (isLayoutTest && passed == results.length) { |
| 97 document.body.innerHtml = "PASS"; |
| 98 } else { |
| 99 // changed the StringBuffer to an Element fragment |
| 100 Element te = new Element.html('<div class="unittest-table"></div>'); |
| 101 |
| 102 te.children.add(new Element.html(passed == results.length |
| 103 ? "<div class='unittest-overall unittest-pass'>PASS</div>" |
| 104 : "<div class='unittest-overall unittest-fail'>FAIL</div>")); |
| 105 |
| 106 // moved summary to the top since web browsers |
| 107 // don't auto-scroll to the bottom like consoles typically do. |
| 108 if (passed == results.length && uncaughtError == null) { |
| 109 te.children.add(new Element.html(""" |
| 110 <div class='unittest-pass'>All ${passed} tests passed</div>""")); |
| 111 } else { |
| 112 |
| 113 if (uncaughtError != null) { |
| 114 te.children.add(new Element.html(""" |
| 115 <div class='unittest-summary'> |
| 116 <span class='unittest-error'>Uncaught error: $uncaughtError</span> |
| 117 </div>""")); |
| 118 } |
| 119 |
| 120 te.children.add(new Element.html(""" |
| 121 <div class='unittest-summary'> |
| 122 <span class='unittest-pass'>Total ${passed} passed</span>, |
| 123 <span class='unittest-fail'>${failed} failed</span>, |
| 124 <span class='unittest-error'> |
| 125 ${errors + (uncaughtError == null ? 0 : 1)} errors</span> |
| 126 </div>""")); |
| 127 } |
| 128 |
| 129 te.children.add(new Element.html(""" |
| 130 <div><button id='btnCollapseAll'>Collapse All</button></div> |
| 131 """)); |
| 132 |
| 133 // handle the click event for the collapse all button |
| 134 te.querySelector('#btnCollapseAll').onClick.listen((_){ |
| 135 document |
| 136 .querySelectorAll('.unittest-row') |
| 137 .forEach((el) => el.attributes['class'] = el.attributes['class'] |
| 138 .replaceAll('unittest-row ', 'unittest-row-hidden ')); |
| 139 }); |
| 140 |
| 141 var previousGroup = ''; |
| 142 var groupPassFail = true; |
| 143 final indentAmount = 50; |
| 144 |
| 145 // order by group and sort numerically within each group |
| 146 var groupedBy = new LinkedHashMap<String, List<TestCase>>(); |
| 147 |
| 148 for (final t in results) { |
| 149 if (!groupedBy.containsKey(t.currentGroup)) { |
| 150 groupedBy[t.currentGroup] = new List<TestCase>(); |
| 151 } |
| 152 |
| 153 groupedBy[t.currentGroup].add(t); |
| 154 } |
| 155 |
| 156 // flatten the list again with tests ordered |
| 157 List<TestCase> flattened = new List<TestCase>(); |
| 158 |
| 159 groupedBy |
| 160 .values |
| 161 .forEach((tList){ |
| 162 tList.sort((tcA, tcB) => tcA.id - tcB.id); |
| 163 flattened.addAll(tList); |
| 164 } |
| 165 ); |
| 166 |
| 167 var nonAlphanumeric = new RegExp('[^a-z0-9A-Z]'); |
| 168 |
| 169 // output group headers and test rows |
| 170 for (final test_ in flattened) { |
| 171 |
| 172 // replace everything but numbers and letters from the group name with |
| 173 // '_' so we can use in id and class properties. |
| 174 var safeGroup = test_.currentGroup.replaceAll(nonAlphanumeric, '_'); |
| 175 |
| 176 if (test_.currentGroup != previousGroup) { |
| 177 |
| 178 previousGroup = test_.currentGroup; |
| 179 |
| 180 var testsInGroup = results |
| 181 .where((TestCase t) => t.currentGroup == previousGroup) |
| 182 .toList(); |
| 183 var groupTotalTestCount = testsInGroup.length; |
| 184 var groupTestPassedCount = testsInGroup.where( |
| 185 (TestCase t) => t.result == 'pass').length; |
| 186 groupPassFail = groupTotalTestCount == groupTestPassedCount; |
| 187 var passFailClass = "unittest-group-status unittest-group-" |
| 188 "status-${groupPassFail ? 'pass' : 'fail'}"; |
| 189 |
| 190 te.children.add(new Element.html(""" |
| 191 <div> |
| 192 <div id='${safeGroup}' |
| 193 class='unittest-group ${safeGroup} test${safeGroup}'> |
| 194 <div ${_isIE ? "style='display:inline-block' ": ""} |
| 195 class='unittest-row-status'> |
| 196 <div class='$passFailClass'></div> |
| 197 </div> |
| 198 <div ${_isIE ? "style='display:inline-block' ": ""}> |
| 199 ${test_.currentGroup}</div> |
| 200 |
| 201 <div ${_isIE ? "style='display:inline-block' ": ""}> |
| 202 (${groupTestPassedCount}/${groupTotalTestCount})</div> |
| 203 </div> |
| 204 </div>""")); |
| 205 |
| 206 // 'safeGroup' could be empty |
| 207 var grp = (safeGroup == '') ? |
| 208 null : te.querySelector('#${safeGroup}'); |
| 209 if (grp != null) { |
| 210 grp.onClick.listen((_) { |
| 211 var row = document.querySelector('.unittest-row-${safeGroup}'); |
| 212 if (row.attributes['class'].contains('unittest-row ')){ |
| 213 document.querySelectorAll('.unittest-row-${safeGroup}').forEach( |
| 214 (e) => e.attributes['class'] = e.attributes['class'] |
| 215 .replaceAll('unittest-row ', 'unittest-row-hidden ')); |
| 216 }else{ |
| 217 document.querySelectorAll('.unittest-row-${safeGroup}').forEach( |
| 218 (e) => e.attributes['class'] = e.attributes['class'] |
| 219 .replaceAll('unittest-row-hidden', 'unittest-row')); |
| 220 } |
| 221 }); |
| 222 } |
| 223 } |
| 224 |
| 225 _buildRow(test_, te, safeGroup, !groupPassFail); |
| 226 } |
| 227 |
| 228 document.body.children.clear(); |
| 229 document.body.children.add(te); |
| 230 } |
| 231 } |
| 232 |
| 233 void _buildRow(TestCase test_, Element te, String groupID, bool isVisible) { |
| 234 var background = 'unittest-row-${test_.id % 2 == 0 ? "even" : "odd"}'; |
| 235 var display = '${isVisible ? "unittest-row" : "unittest-row-hidden"}'; |
| 236 |
| 237 addRowElement(id, status, description){ |
| 238 te.children.add( |
| 239 new Element.html( |
| 240 ''' <div> |
| 241 <div class='$display unittest-row-${groupID} $background'> |
| 242 <div ${_isIE ? "style='display:inline-block' ": ""} |
| 243 class='unittest-row-id'>$id</div> |
| 244 <div ${_isIE ? "style='display:inline-block' ": ""} |
| 245 class="unittest-row-status unittest-${test_.result}"> |
| 246 $status</div> |
| 247 <div ${_isIE ? "style='display:inline-block' ": ""} |
| 248 class='unittest-row-description'>$description</div> |
| 249 </div> |
| 250 </div>''' |
| 251 ) |
| 252 ); |
| 253 } |
| 254 |
| 255 if (!test_.isComplete) { |
| 256 addRowElement('${test_.id}', 'NO STATUS', 'Test did not complete.'); |
| 257 return; |
| 258 } |
| 259 |
| 260 addRowElement('${test_.id}', '${test_.result.toUpperCase()}', |
| 261 '${test_.description}. ${HTML_ESCAPE.convert(test_.message)}'); |
| 262 |
| 263 if (test_.stackTrace != null) { |
| 264 addRowElement('', '', |
| 265 '<pre>${HTML_ESCAPE.convert(test_.stackTrace.toString())}</pre>'); |
| 266 } |
| 267 } |
| 268 |
| 269 |
| 270 static bool get _isIE => window.navigator.userAgent.contains('MSIE'); |
| 271 |
| 272 String get _htmlTestCSS => |
| 273 ''' |
| 274 body{ |
| 275 font-size: 14px; |
| 276 font-family: 'Open Sans', 'Lucida Sans Unicode', 'Lucida Grande',''' |
| 277 ''' sans-serif; |
| 278 background: WhiteSmoke; |
| 279 } |
| 280 |
| 281 .unittest-group |
| 282 { |
| 283 background: rgb(75,75,75); |
| 284 width:98%; |
| 285 color: WhiteSmoke; |
| 286 font-weight: bold; |
| 287 padding: 6px; |
| 288 cursor: pointer; |
| 289 |
| 290 /* Provide some visual separation between groups for IE */ |
| 291 ${_isIE ? "border-bottom:solid black 1px;": ""} |
| 292 ${_isIE ? "border-top:solid #777777 1px;": ""} |
| 293 |
| 294 background-image: -webkit-linear-gradient(bottom, rgb(50,50,50) 0%, ''' |
| 295 '''rgb(100,100,100) 100%); |
| 296 background-image: -moz-linear-gradient(bottom, rgb(50,50,50) 0%, ''' |
| 297 '''rgb(100,100,100) 100%); |
| 298 background-image: -ms-linear-gradient(bottom, rgb(50,50,50) 0%, ''' |
| 299 '''rgb(100,100,100) 100%); |
| 300 background-image: linear-gradient(bottom, rgb(50,50,50) 0%, ''' |
| 301 '''rgb(100,100,100) 100%); |
| 302 |
| 303 display: -webkit-box; |
| 304 display: -moz-box; |
| 305 display: -ms-box; |
| 306 display: box; |
| 307 |
| 308 -webkit-box-orient: horizontal; |
| 309 -moz-box-orient: horizontal; |
| 310 -ms-box-orient: horizontal; |
| 311 box-orient: horizontal; |
| 312 |
| 313 -webkit-box-align: center; |
| 314 -moz-box-align: center; |
| 315 -ms-box-align: center; |
| 316 box-align: center; |
| 317 } |
| 318 |
| 319 .unittest-group-status |
| 320 { |
| 321 width: 20px; |
| 322 height: 20px; |
| 323 border-radius: 20px; |
| 324 margin-left: 10px; |
| 325 } |
| 326 |
| 327 .unittest-group-status-pass{ |
| 328 background: Green; |
| 329 background: ''' |
| 330 '''-webkit-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
| 331 background: ''' |
| 332 '''-moz-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
| 333 background: ''' |
| 334 '''-ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
| 335 background: ''' |
| 336 '''radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
| 337 } |
| 338 |
| 339 .unittest-group-status-fail{ |
| 340 background: Red; |
| 341 background: ''' |
| 342 '''-webkit-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); |
| 343 background: ''' |
| 344 '''-moz-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); |
| 345 background: ''' |
| 346 '''-ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); |
| 347 background: radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); |
| 348 } |
| 349 |
| 350 .unittest-overall{ |
| 351 font-size: 20px; |
| 352 } |
| 353 |
| 354 .unittest-summary{ |
| 355 font-size: 18px; |
| 356 } |
| 357 |
| 358 .unittest-pass{ |
| 359 color: Green; |
| 360 } |
| 361 |
| 362 .unittest-fail, .unittest-error |
| 363 { |
| 364 color: Red; |
| 365 } |
| 366 |
| 367 .unittest-row |
| 368 { |
| 369 display: -webkit-box; |
| 370 display: -moz-box; |
| 371 display: -ms-box; |
| 372 display: box; |
| 373 -webkit-box-orient: horizontal; |
| 374 -moz-box-orient: horizontal; |
| 375 -ms-box-orient: horizontal; |
| 376 box-orient: horizontal; |
| 377 width: 100%; |
| 378 } |
| 379 |
| 380 .unittest-row-hidden |
| 381 { |
| 382 display: none; |
| 383 } |
| 384 |
| 385 .unittest-row-odd |
| 386 { |
| 387 background: WhiteSmoke; |
| 388 } |
| 389 |
| 390 .unittest-row-even |
| 391 { |
| 392 background: #E5E5E5; |
| 393 } |
| 394 |
| 395 .unittest-row-id |
| 396 { |
| 397 width: 3em; |
| 398 } |
| 399 |
| 400 .unittest-row-status |
| 401 { |
| 402 width: 4em; |
| 403 } |
| 404 |
| 405 .unittest-row-description |
| 406 { |
| 407 } |
| 408 |
| 409 '''; |
| 410 } |
| 411 |
| 412 void useHtmlEnhancedConfiguration([bool isLayoutTest = false]) { |
| 413 unittestConfiguration = isLayoutTest ? _singletonLayout : _singletonNotLayout; |
| 414 } |
| 415 |
| 416 final _singletonLayout = new HtmlEnhancedConfiguration(true); |
| 417 final _singletonNotLayout = new HtmlEnhancedConfiguration(false); |
OLD | NEW |