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