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 #library('dartest'); |
| 6 |
| 7 #import('dart:dom'); |
| 8 #import('../unittest/unittest_dartest.dart'); |
| 9 |
| 10 #source('css.dart'); |
| 11 |
| 12 interface Elements { |
| 13 Element get containerDiv(); |
| 14 Element get mainElem(); |
| 15 Element get testBody(); |
| 16 Element get testsRunElem(); |
| 17 Element get testsFailedElem(); |
| 18 Element get testsErrorsElem(); |
| 19 Element get orange(); |
| 20 Element get red(); |
| 21 Element get green(); |
| 22 Element get coverageBody(); |
| 23 Element get covPreElem(); |
| 24 HTMLElement get covTableBody(); |
| 25 } |
| 26 |
| 27 class AppElements implements Elements { |
| 28 Element containerDiv; |
| 29 Element mainElem; |
| 30 Element testBody; |
| 31 Element testsRunElem; |
| 32 Element testsFailedElem; |
| 33 Element testsErrorsElem; |
| 34 Element orange; |
| 35 Element red; |
| 36 Element green; |
| 37 Element coverageBody; |
| 38 Element covPreElem; |
| 39 HTMLElement covTableBody; |
| 40 } |
| 41 |
| 42 |
| 43 /** DARTest provides a library to run tests in the app. */ |
| 44 class DARTest{ |
| 45 AppElements _inAppElements, _fullAppElements, _appElements; |
| 46 DOMWindow _runnerWindow; |
| 47 |
| 48 DARTest() { |
| 49 _runnerWindow = window; |
| 50 dartestLogger = _log; |
| 51 _inAppElements = new AppElements(); |
| 52 _appElements = _inAppElements; |
| 53 DARTestCss.inject(document, true); |
| 54 } |
| 55 |
| 56 void run() { |
| 57 _renderMain(); |
| 58 _createResultsTable(); |
| 59 } |
| 60 |
| 61 void _log(String message) { |
| 62 _runnerWindow.console.log(message); |
| 63 } |
| 64 |
| 65 /** Create the results table after loading tests. */ |
| 66 void _createResultsTable() { |
| 67 _log('Creating results table'); |
| 68 HTMLTableElement table = _runnerWindow.document.createElement('table'); |
| 69 table.className = 'dt-results'; |
| 70 HTMLTableSectionElement head = _runnerWindow.document.createElement('thead')
; |
| 71 head.innerHTML = '<tr><th>ID <th>Description <th>Result'; |
| 72 table.appendChild(head); |
| 73 |
| 74 HTMLTableSectionElement body = _runnerWindow.document.createElement('tbody')
; |
| 75 body.id = 'dt-results-body'; |
| 76 tests.forEach((final t) { |
| 77 HTMLTableRowElement testDetailRow = |
| 78 _runnerWindow.document.createElement('tr'); |
| 79 testDetailRow.id = 'dt-test-${t.id}'; |
| 80 _addTestDetails(t, testDetailRow); |
| 81 body.appendChild(testDetailRow); |
| 82 |
| 83 HTMLTableRowElement testMessageRow = |
| 84 _runnerWindow.document.createElement('tr'); |
| 85 testMessageRow.id = 'dt-detail-${t.id}'; |
| 86 testMessageRow.className = 'dt-hide'; |
| 87 body.appendChild(testMessageRow); |
| 88 }); |
| 89 |
| 90 table.appendChild(body); |
| 91 |
| 92 if(_appElements.testBody != null) { |
| 93 _appElements.testBody.appendChild(table); |
| 94 } |
| 95 } |
| 96 |
| 97 /** Update the results table for test. */ |
| 98 void _updateResultsTable(TestCase t, DOMWindow domWin) { |
| 99 HTMLTableRowElement row = domWin.document.getElementById('dt-test-${t.id}'); |
| 100 row.className = 'dt-result-row'; |
| 101 row.innerHTML = ''; // Remove all children as we will re-populate them |
| 102 _addTestDetails(t, row); |
| 103 |
| 104 HTMLTableRowElement details = |
| 105 domWin.document.getElementById('dt-detail-${t.id}'); |
| 106 details.appendChild(_getTestStats(t, domWin)); |
| 107 |
| 108 row.addEventListener('click', (Event e) { |
| 109 if(details.className == 'dt-hide') { |
| 110 details.className = ''; |
| 111 } else { |
| 112 details.className = 'dt-hide'; |
| 113 } |
| 114 }, true); |
| 115 } |
| 116 |
| 117 /** Escape HTML special chars. */ |
| 118 String _escape(String str) { |
| 119 str = str.replaceAll('&','&'); |
| 120 str = str.replaceAll('<','<'); |
| 121 str = str.replaceAll('>','>'); |
| 122 str = str.replaceAll('"','"'); |
| 123 str = str.replaceAll("'",'''); |
| 124 str = str.replaceAll('/','/'); |
| 125 return str; |
| 126 } |
| 127 |
| 128 /** Get test results as table cells. */ |
| 129 void _addTestDetails(TestCase t, HTMLTableRowElement row) { |
| 130 HTMLTableCellElement testId = _runnerWindow.document.createElement('td'); |
| 131 testId.textContent = t.id; |
| 132 row.appendChild(testId); |
| 133 |
| 134 HTMLTableCellElement testDesc = _runnerWindow.document.createElement('td'); |
| 135 testDesc.textContent = t.description; |
| 136 row.appendChild(testDesc); |
| 137 |
| 138 HTMLTableCellElement testResult = _runnerWindow.document.createElement('td')
; |
| 139 String result = (t.result == null) ? 'none' : _escape(t.result); |
| 140 testResult.className = 'dt-$result'; |
| 141 testResult.title = '${_escape(t.message)}'; |
| 142 testResult.textContent = result.toUpperCase(); |
| 143 row.appendChild(testResult); |
| 144 } |
| 145 |
| 146 HTMLTableCellElement _getTestStats(TestCase t, DOMWindow domWin) { |
| 147 HTMLTableCellElement tableCell = domWin.document.createElement('td'); |
| 148 tableCell.colSpan = 3; |
| 149 |
| 150 if(t.message != '') { |
| 151 HTMLSpanElement messageSpan = domWin.document.createElement('span'); |
| 152 messageSpan.textContent = t.message; |
| 153 tableCell.appendChild(messageSpan); |
| 154 tableCell.appendChild(domWin.document.createElement('br')); |
| 155 } |
| 156 if(t.stackTrace != null) { |
| 157 HTMLPreElement stackTacePre = domWin.document.createElement('pre'); |
| 158 stackTacePre.textContent = t.stackTrace; |
| 159 } |
| 160 |
| 161 HTMLSpanElement durationSpan = domWin.document.createElement('span'); |
| 162 durationSpan.textContent = 'took ${_printDuration(t.runningTime)}'; |
| 163 tableCell.appendChild(durationSpan); |
| 164 |
| 165 return tableCell; |
| 166 } |
| 167 |
| 168 /** Update the UI after running test. */ |
| 169 void _updateDARTestUI(TestCase test) { |
| 170 _updateResultsTable(test, window); |
| 171 if(_runnerWindow != window) { |
| 172 _updateResultsTable(test, _runnerWindow); |
| 173 } |
| 174 |
| 175 if(test.result != null) { |
| 176 _log(' Result: ${test.result.toUpperCase()} ${test.message}'); |
| 177 } |
| 178 if(test.runningTime != null) { |
| 179 _log(' took ${_printDuration(test.runningTime)}'); |
| 180 } |
| 181 _updateStatusProgress(_appElements); |
| 182 if(_runnerWindow != window) { |
| 183 _updateStatusProgress(_inAppElements); |
| 184 } |
| 185 } |
| 186 |
| 187 void _updateStatusProgress(AppElements elements) { |
| 188 // Update progressbar |
| 189 var pPass = |
| 190 ((testsRun - testsFailed - testsErrors) / tests.length) * 100; |
| 191 elements.green.setAttribute('style', 'width:$pPass%'); |
| 192 var pFailed = pPass + (testsFailed / tests.length) * 100; |
| 193 elements.red.setAttribute('style', 'width:$pFailed%'); |
| 194 var pErrors = pFailed + (testsErrors / tests.length) * 100; |
| 195 elements.orange.setAttribute('style', 'width:$pErrors%'); |
| 196 |
| 197 // Update status |
| 198 elements.testsRunElem.textContent = testsRun.toString(); |
| 199 elements.testsFailedElem.textContent = testsFailed.toString(); |
| 200 elements.testsErrorsElem.textContent = testsErrors.toString(); |
| 201 } |
| 202 |
| 203 String _printDuration(Duration timeDuration) { |
| 204 StringBuffer out = new StringBuffer(); |
| 205 if(timeDuration.inDays > 0) { |
| 206 out.add('${timeDuration.inDays} days '); |
| 207 } |
| 208 if(timeDuration.inHours > 0) { |
| 209 out.add('${timeDuration.inHours} hrs '); |
| 210 } |
| 211 if(timeDuration.inMinutes > 0) { |
| 212 out.add('${timeDuration.inMinutes} mins '); |
| 213 } |
| 214 if(timeDuration.inSeconds > 0) { |
| 215 out.add('${timeDuration.inSeconds} s '); |
| 216 } |
| 217 if(timeDuration.inMilliseconds > 0 || out.length == 0) { |
| 218 out.add('${timeDuration.inMilliseconds} ms'); |
| 219 } |
| 220 return out.toString(); |
| 221 } |
| 222 |
| 223 /** Populates the floating div with controls and toolbar. */ |
| 224 HTMLDivElement _renderMain() { |
| 225 HTMLDivElement containerDiv = _runnerWindow.document.createElement('div'); |
| 226 containerDiv.className = 'dt-container'; |
| 227 _appElements.containerDiv = containerDiv; |
| 228 |
| 229 // Add the test controls |
| 230 HTMLDivElement mainElem = _runnerWindow.document.createElement('div'); |
| 231 mainElem.className = 'dt-main'; |
| 232 _appElements.mainElem = mainElem; |
| 233 |
| 234 _showTestControls(); |
| 235 |
| 236 // Create header to hold window controls |
| 237 if(_runnerWindow == window) { |
| 238 HTMLDivElement headDiv = _runnerWindow.document.createElement('div'); |
| 239 headDiv.className = 'dt-header'; |
| 240 headDiv.innerHTML = 'DARTest: In-App View'; |
| 241 HTMLImageElement close = _runnerWindow.document.createElement('img'); |
| 242 close.className = 'dt-header-close'; |
| 243 close.addEventListener('click', (Event) { |
| 244 containerDiv.className = 'dt-hide'; |
| 245 }, true); |
| 246 HTMLImageElement pop = _runnerWindow.document.createElement('img'); |
| 247 pop.className = 'dt-header-pop'; |
| 248 pop.addEventListener('click', (Event) => _dartestMaximize(), true); |
| 249 HTMLImageElement minMax = _runnerWindow.document.createElement('img'); |
| 250 minMax.className = 'dt-header-min'; |
| 251 minMax.addEventListener('click', (Event) { |
| 252 if (mainElem.classList.contains('dt-hide')) { |
| 253 mainElem.classList.remove('dt-hide'); |
| 254 mainElem.classList.add('dt-show'); |
| 255 minMax.className = 'dt-header-min'; |
| 256 } else { |
| 257 if (mainElem.classList.contains('dt-show')) { |
| 258 mainElem.classList.remove('dt-show'); |
| 259 } |
| 260 mainElem.classList.add('dt-hide'); |
| 261 minMax.className = 'dt-header-max'; |
| 262 } |
| 263 }, true); |
| 264 headDiv.appendChild(close); |
| 265 headDiv.appendChild(pop); |
| 266 headDiv.appendChild(minMax); |
| 267 |
| 268 containerDiv.appendChild(headDiv); |
| 269 } |
| 270 |
| 271 HTMLDivElement tabDiv = _runnerWindow.document.createElement('div'); |
| 272 tabDiv.className = 'dt-tab'; |
| 273 HTMLUListElement tabList = _runnerWindow.document.createElement('ul'); |
| 274 HTMLLIElement testingTab = _runnerWindow.document.createElement('li'); |
| 275 HTMLLIElement coverageTab = _runnerWindow.document.createElement('li'); |
| 276 testingTab.className = 'dt-tab-selected'; |
| 277 testingTab.textContent = 'Testing'; |
| 278 testingTab.addEventListener('click', (Event) { |
| 279 _showTestControls(); |
| 280 _changeTabs(testingTab, coverageTab); |
| 281 }, true); |
| 282 tabList.appendChild(testingTab); |
| 283 coverageTab.textContent = 'Coverage'; |
| 284 coverageTab.addEventListener('click', (Event) { |
| 285 _showCoverageControls(); |
| 286 _changeTabs(coverageTab, testingTab); |
| 287 }, true); |
| 288 tabList.appendChild(coverageTab); |
| 289 tabDiv.appendChild(tabList); |
| 290 containerDiv.appendChild(tabDiv); |
| 291 |
| 292 if(_runnerWindow != window) { |
| 293 HTMLDivElement popIn = _runnerWindow.document.createElement('div'); |
| 294 popIn.className = 'dt-minimize'; |
| 295 popIn.innerHTML = 'Pop In ⇲'; |
| 296 popIn.addEventListener('click', (Event) => _dartestMinimize(), true); |
| 297 containerDiv.appendChild(popIn); |
| 298 } |
| 299 |
| 300 containerDiv.appendChild(mainElem); |
| 301 _runnerWindow.document.body.appendChild(containerDiv); |
| 302 } |
| 303 |
| 304 void _changeTabs(HTMLLIElement clickedTab, HTMLLIElement oldTab) { |
| 305 oldTab.className = ''; |
| 306 clickedTab.className = 'dt-tab-selected'; |
| 307 } |
| 308 |
| 309 void _showTestControls() { |
| 310 HTMLDivElement testBody = _appElements.testBody; |
| 311 if(testBody == null) { |
| 312 testBody = _runnerWindow.document.createElement('div'); |
| 313 _appElements.testBody = testBody; |
| 314 |
| 315 // Create a toolbar to hold action buttons |
| 316 HTMLDivElement toolDiv = _runnerWindow.document.createElement('div'); |
| 317 toolDiv.className = 'dt-toolbar'; |
| 318 HTMLButtonElement runBtn = _runnerWindow.document.createElement('button'); |
| 319 runBtn.innerHTML = '►'; |
| 320 runBtn.title = 'Run Tests'; |
| 321 runBtn.className = 'dt-button dt-run'; |
| 322 runBtn.addEventListener('click', (Event) { |
| 323 _log('Running tests'); |
| 324 updateUI = _updateDARTestUI; |
| 325 runDartests(); |
| 326 }, true); |
| 327 toolDiv.appendChild(runBtn); |
| 328 HTMLButtonElement exportBtn = |
| 329 _runnerWindow.document.createElement('button'); |
| 330 exportBtn.innerHTML = '↷'; |
| 331 exportBtn.title = 'Export Results'; |
| 332 exportBtn.className = 'dt-button dt-run'; |
| 333 exportBtn.addEventListener('click', (Event e) { |
| 334 _log('Exporting results'); |
| 335 _exportTestResults(); |
| 336 }, true); |
| 337 toolDiv.appendChild(exportBtn); |
| 338 testBody.appendChild(toolDiv); |
| 339 |
| 340 // Create a datalist element for showing test status |
| 341 HTMLDListElement statList = _runnerWindow.document.createElement('dl'); |
| 342 statList.className = 'dt-status'; |
| 343 HTMLElement runsDt = _runnerWindow.document.createElement('dt'); |
| 344 runsDt.textContent = 'Runs:'; |
| 345 statList.appendChild(runsDt); |
| 346 HTMLElement testsRunElem = _runnerWindow.document.createElement('dd'); |
| 347 _appElements.testsRunElem = testsRunElem; |
| 348 testsRunElem.textContent = testsRun.toString(); |
| 349 statList.appendChild(testsRunElem); |
| 350 |
| 351 HTMLElement failDt = _runnerWindow.document.createElement('dt'); |
| 352 failDt.textContent = 'Failed:'; |
| 353 statList.appendChild(failDt); |
| 354 HTMLElement testsFailedElem = _runnerWindow.document.createElement('dd'); |
| 355 _appElements.testsFailedElem = testsFailedElem; |
| 356 testsFailedElem.textContent = testsFailed.toString(); |
| 357 statList.appendChild(testsFailedElem); |
| 358 |
| 359 HTMLElement errDt = _runnerWindow.document.createElement('dt'); |
| 360 errDt.textContent = 'Errors:'; |
| 361 statList.appendChild(errDt); |
| 362 HTMLElement testsErrorsElem = _runnerWindow.document.createElement('dd'); |
| 363 _appElements.testsErrorsElem = testsErrorsElem; |
| 364 testsErrorsElem.textContent = testsErrors.toString(); |
| 365 statList.appendChild(testsErrorsElem); |
| 366 testBody.appendChild(statList); |
| 367 |
| 368 // Create progressbar and add red, green, orange bars |
| 369 HTMLDivElement progressDiv = _runnerWindow.document.createElement('div'); |
| 370 progressDiv.className = 'dt-progressbar'; |
| 371 progressDiv.innerHTML = "<span style='width:100%'></span>"; |
| 372 |
| 373 HTMLSpanElement orange = _runnerWindow.document.createElement('span'); |
| 374 _appElements.orange = orange; |
| 375 orange.className = 'orange'; |
| 376 progressDiv.appendChild(orange); |
| 377 |
| 378 HTMLSpanElement red = _runnerWindow.document.createElement('span'); |
| 379 _appElements.red = red; |
| 380 red.className = 'red'; |
| 381 progressDiv.appendChild(red); |
| 382 |
| 383 HTMLSpanElement green = _runnerWindow.document.createElement('span'); |
| 384 _appElements.green = green; |
| 385 green.className = 'green'; |
| 386 |
| 387 progressDiv.appendChild(green); |
| 388 testBody.appendChild(progressDiv); |
| 389 |
| 390 HTMLDivElement hiddenElem = _runnerWindow.document.createElement('div'); |
| 391 hiddenElem.className = 'dt-hide'; |
| 392 hiddenElem.innerHTML = |
| 393 "<a id='dt-export' download='test_results.csv' href='#' />"; |
| 394 testBody.appendChild(hiddenElem); |
| 395 |
| 396 if(_appElements.mainElem != null) { |
| 397 _appElements.mainElem.appendChild(testBody); |
| 398 } |
| 399 } |
| 400 |
| 401 // Show hide divs |
| 402 _show(_appElements.testBody); |
| 403 _hide(_appElements.coverageBody); |
| 404 } |
| 405 |
| 406 void _showCoverageControls() { |
| 407 HTMLDivElement coverageBody = _appElements.coverageBody; |
| 408 if(coverageBody == null) { |
| 409 coverageBody = _runnerWindow.document.createElement('div'); |
| 410 _appElements.coverageBody = coverageBody; |
| 411 |
| 412 HTMLPreElement covPreElem = _runnerWindow.document.createElement('pre'); |
| 413 _appElements.covPreElem = covPreElem; |
| 414 coverageBody.appendChild(covPreElem); |
| 415 |
| 416 HTMLTableElement covTable = _runnerWindow.document.createElement('table'); |
| 417 covTable.className = 'dt-results'; |
| 418 HTMLTableSectionElement head = |
| 419 _runnerWindow.document.createElement('thead'); |
| 420 head.innerHTML = '<tr><th>Unit <th>Function <th>Statement <th>Branch'; |
| 421 covTable.appendChild(head); |
| 422 HTMLTableSectionElement covTableBody = |
| 423 _runnerWindow.document.createElement('tbody'); |
| 424 _appElements.covTableBody = covTableBody; |
| 425 covTableBody.id = 'dt-results-body'; |
| 426 covTable.appendChild(covTableBody); |
| 427 coverageBody.appendChild(covTable); |
| 428 |
| 429 if(_appElements.mainElem != null) { |
| 430 _appElements.mainElem.appendChild(coverageBody); |
| 431 } |
| 432 } |
| 433 _show(_appElements.coverageBody); |
| 434 _hide(_appElements.testBody); |
| 435 |
| 436 _appElements.covPreElem.textContent = getCoverageSummary(); |
| 437 |
| 438 // Coverage table has unit names and integers and won't have special chars |
| 439 _appElements.covTableBody.innerHTML = getCoverageDetails(); |
| 440 } |
| 441 |
| 442 void _show(HTMLElement show) { |
| 443 if(show != null) { |
| 444 if(show.classList.contains('dt-hide')) { |
| 445 show.classList.remove('dt-hide'); |
| 446 } |
| 447 show.classList.add('dt-show'); |
| 448 } |
| 449 } |
| 450 |
| 451 void _hide(HTMLElement hide) { |
| 452 if(hide != null) { |
| 453 if(hide.classList.contains('dt-show')) { |
| 454 hide.classList.remove('dt-show'); |
| 455 } |
| 456 hide.classList.add('dt-hide'); |
| 457 } |
| 458 } |
| 459 |
| 460 void _dartestMaximize() { |
| 461 _hide(_appElements.containerDiv); |
| 462 _runnerWindow = window.open('', 'dartest-window', |
| 463 DARTestCss._fullAppWindowFeatures); |
| 464 _runnerWindow.document.title = 'Dartest'; |
| 465 _fullAppElements = new AppElements(); |
| 466 _appElements = _fullAppElements; |
| 467 DARTestCss.inject(_runnerWindow.document, false); |
| 468 run(); |
| 469 if(testsRun > 0) { |
| 470 tests.forEach((final t) => _updateDARTestUI(t)); |
| 471 } |
| 472 } |
| 473 |
| 474 void _dartestMinimize() { |
| 475 _runnerWindow.close(); |
| 476 _runnerWindow = window; |
| 477 _appElements = _inAppElements; |
| 478 _show(_appElements.containerDiv); |
| 479 } |
| 480 |
| 481 void _exportTestResults() { |
| 482 String csvData = getTestResultsCsv(); |
| 483 _log(csvData); |
| 484 HTMLAnchorElement exportLink = |
| 485 _runnerWindow.document.getElementById('dt-export'); |
| 486 |
| 487 /** Bug: Can't instantiate WebKitBlobBuilder |
| 488 * If this bug is fixed, we can remove the urlencode and lpad function. |
| 489 * |
| 490 * WebKitBlobBuilder bb = new WebKitBlobBuilder(); |
| 491 * bb.append(csvData); |
| 492 * Blob blob = bb.getBlob('text/plain;charset=${document.characterSet}'); |
| 493 * exportLink.href = window.webkitURL.createObjectURL(blob); |
| 494 **/ |
| 495 |
| 496 exportLink.href = 'data:text/csv,' + _urlencode(csvData); |
| 497 |
| 498 MouseEvent ev = document.createEvent("MouseEvents"); |
| 499 ev.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0 |
| 500 , false, false, false, false, 0, null); |
| 501 exportLink.dispatchEvent(ev); |
| 502 |
| 503 } |
| 504 |
| 505 static String _urlencode(String s) { |
| 506 StringBuffer out = new StringBuffer(); |
| 507 for(int i = 0; i < s.length; i++) { |
| 508 int cc = s.charCodeAt(i); |
| 509 if((cc >= 48 && cc <= 57) || (cc >= 65 && cc <= 90) || |
| 510 (cc >= 97 && cc <= 122)) { |
| 511 out.add(s[i]); |
| 512 } else { |
| 513 out.add('%${_lpad(cc.toRadixString(16),2).toUpperCase()}'); |
| 514 } |
| 515 } |
| 516 return out.toString(); |
| 517 } |
| 518 |
| 519 static String _lpad(String s, int n) { |
| 520 if(s.length < n) { |
| 521 for(int i = 0; i < n - s.length; i++) { |
| 522 s = '0'+s; |
| 523 } |
| 524 } |
| 525 return s; |
| 526 } |
| 527 } |
OLD | NEW |