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