Chromium Code Reviews| Index: client/testing/dartest/dartest.dart |
| =================================================================== |
| --- client/testing/dartest/dartest.dart (revision 0) |
| +++ client/testing/dartest/dartest.dart (revision 0) |
| @@ -0,0 +1,466 @@ |
| +// 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. |
| + |
| +#library('dartest'); |
| + |
| +#import('dart:dom'); |
|
pdr
2011/12/14 21:02:36
I would strongly recommend using dart:html. It's g
shauvik
2011/12/16 07:19:27
When I started, you couldn't do many things using
|
| +#import('../unittest/unittest_dartest.dart'); |
| + |
| +#source('css.dart'); |
| + |
| +/** DARTest provides a library to run tests in the app */ |
| +class DARTest{ |
|
pdr
2011/12/14 21:02:36
Under the names section of the style guide (http:/
shauvik
2011/12/16 07:19:27
Kelly and I decided to have the same name across a
|
| + Map<String, HTMLElement> inAppElements, fullAppElements, appElements; |
|
pdr
2011/12/14 21:02:36
I don't think these are necessary (if they are, ca
shauvik
2011/12/16 07:19:27
I spoke to Kelly and Joel and its difficult and so
|
| + bool inAppMode; |
|
Kelly Norton
2011/12/15 18:35:31
I suspect you want these to be private _inAppMode?
shauvik
2011/12/16 07:19:27
Done.
|
| + DOMWindow win, appWindow; |
|
Kelly Norton
2011/12/15 18:35:31
private?
shauvik
2011/12/16 07:19:27
Done.
|
| + |
| + DARTest(){ |
| + win = window; |
|
Kelly Norton
2011/12/15 18:35:31
It seems unnecessary to store a reference to windo
shauvik
2011/12/16 07:19:27
Done.
|
| + appWindow = window; |
| + dartestLoggingFunc = log; |
| + inAppMode = true; |
| + inAppElements = new HashMap<String, HTMLElement>(); |
| + appElements = inAppElements; |
| + |
| + DARTestCss.inject(win.document, true); |
|
Kelly Norton
2011/12/15 18:35:31
document should also be available as a top-level g
shauvik
2011/12/16 07:19:27
Done.
|
| + } |
| + |
| + void run() { |
| + renderMain(); |
| + createResultsTable(); |
| + } |
| + |
| + void log(String message){ |
|
pdr
2011/12/14 21:02:36
Log calls should generally be behind a flag at wor
shauvik
2011/12/16 07:19:27
Whole of dartest is behind a compiler flag. In dis
|
| + appWindow.console.log(message); |
| + } |
| + |
| + void addChildToElem(String key, HTMLElement child) { |
|
Kelly Norton
2011/12/15 18:35:31
This seems like a sizeable public API. Are all of
shauvik
2011/12/16 07:19:27
Made it private.
On 2011/12/15 18:35:31, Kelly No
|
| + if(appElements[key] != null) { |
| + appElements[key].appendChild(child); |
| + } |
| + } |
| + |
| + /** Create the results table after loading tests */ |
| + void createResultsTable(){ |
| + log('Creating results table'); |
| + HTMLTableElement table = win.document.createElement('table'); |
|
Kelly Norton
2011/12/15 18:35:31
I'm really confused by all the references to diffe
shauvik
2011/12/16 07:19:27
Done.
|
| + table.className = 'dt-results'; |
| + HTMLTableSectionElement head = win.document.createElement('thead'); |
| + head.innerHTML = '<tr><th>ID <th>Description <th>Result'; |
| + table.appendChild(head); |
| + |
| + HTMLTableSectionElement body = win.document.createElement('tbody'); |
| + body.id = 'dt-results-body'; |
| + body.innerHTML = ''; |
| + for (TestCase t in tests){ |
| + body.innerHTML += "<tr id='dt-test-${t.id}'>" + getTestDetails(t); |
| + body.innerHTML += "<tr id='dt-detail-${t.id}' class='dt-hide'></span>"; |
| + } |
| + table.appendChild(body); |
| + addChildToElem('testBody', table); |
| + } |
| + |
| + /** Update the results table for test */ |
| + void updateResultsTable(TestCase t, DOMWindow domWin){ |
| + HTMLTableRowElement row = domWin.document.getElementById('dt-test-${t.id}'); |
| + row.className = 'dt-result-row'; |
| + row.innerHTML = getTestDetails(t); |
| + |
| + HTMLTableRowElement details = domWin.document.getElementById('dt-detail-${t.id}'); |
|
pdr
2011/12/14 21:02:36
Line > 100 chars
shauvik
2011/12/16 07:19:27
Done.
|
| + details.innerHTML = getTestStats(t); |
| + |
| + row.addEventListener('click', (Event e) { |
| + if(details.className == 'dt-hide') { |
| + details.className = ''; |
| + } else { |
| + details.className = 'dt-hide'; |
| + } |
| + }, true); |
| + } |
| + |
| + /** Escape HTML Special Chars */ |
| + String escape(String str){ |
| + str = str.replaceAll('&','&'); |
| + str = str.replaceAll('<','<'); |
| + str = str.replaceAll('>','>'); |
| + str = str.replaceAll('"','"'); |
| + str = str.replaceAll("'",'''); |
| + str = str.replaceAll('/','/'); |
| + return str; |
| + } |
| + |
| + /** Get test results as table cells */ |
|
pdr
2011/12/14 21:02:36
This can be private so you can rename it using the
shauvik
2011/12/16 07:19:27
Done.
|
| + String getTestDetails(TestCase t) { |
|
pdr
2011/12/14 21:02:36
Instead of building up this table by strings, I wo
pdr
2011/12/14 21:02:36
Without escaping and innerHTML-based DOM manipulat
shauvik
2011/12/16 07:19:27
Done.
shauvik
2011/12/16 07:19:27
Actually I need that value in the class too. So, c
|
| + String desc = escape(t.description); |
| + String result = "none"; |
| + if(t.result != null){ |
| + result = t.result; |
| + } |
| + return "<td>${t.id} <td>$desc <td class='dt-$result' " + |
| + "title='${escape(t.message)}'> ${result.toUpperCase()}"; |
| + } |
| + |
| + String getTestStats(TestCase t) { |
|
pdr
2011/12/14 21:02:36
See comments in getTestDetails
shauvik
2011/12/16 07:19:27
Done.
|
| + String message = ''; |
| + if(t.message != '') { |
| + message = t.message + '<br>'; |
|
Kelly Norton
2011/12/15 18:35:31
This looks problematic. where does t.message get h
shauvik
2011/12/16 07:19:27
Done.
|
| + } |
| + if(t.stackTrace != null) { |
| + message += '<pre>${t.stackTrace}</pre>'; |
|
Kelly Norton
2011/12/15 18:35:31
Similar here, I don't see where t.stackTrace is es
shauvik
2011/12/16 07:19:27
Done.
|
| + } |
| + return "<td colspan='3'> $message took ${printDuration(t.timeDuration)}</td>"; |
|
pdr
2011/12/14 21:02:36
Line > 100 cols
shauvik
2011/12/16 07:19:27
Done.
|
| + } |
| + |
| + /** Update the UI after running test */ |
|
pdr
2011/12/14 21:02:36
Nit: make sure to use a period in your comments :)
shauvik
2011/12/16 07:19:27
Done.
|
| + void updateDARTestUI(TestCase test){ |
|
Kelly Norton
2011/12/15 18:35:31
space before {
shauvik
2011/12/16 07:19:27
Done.
|
| + // Update table |
|
pdr
2011/12/14 21:02:36
unnecessary comment
shauvik
2011/12/16 07:19:27
Done.
|
| + updateResultsTable(test, win); |
| + if(!inAppMode) { |
| + updateResultsTable(test, appWindow); |
| + } |
| + |
| + if(test.result != null) { |
| + log(' Result: ${test.result.toUpperCase()} ${test.message}'); |
| + } |
| + if(test.timeDuration != null){ |
| + log(' took ${printDuration(test.timeDuration)}'); |
| + } |
| + updateStatusProgress(appElements); |
| + if(!inAppMode) { |
| + updateStatusProgress(inAppElements); |
| + } |
| + } |
| + |
| + void updateStatusProgress(Map<String, HTMLElement> elements){ |
|
Kelly Norton
2011/12/15 18:35:31
space before {
shauvik
2011/12/16 07:19:27
Done.
|
| + // Update progressbar |
| + var pPass = ((numTestsRun - numTestsFailed - numTestsErrors) / tests.length) * 100; |
|
pdr
2011/12/14 21:02:36
Line > 100 cols (here and elsewhere).
shauvik
2011/12/16 07:19:27
Done.
|
| + elements['green'].setAttribute('style', 'width:$pPass%'); |
|
Kelly Norton
2011/12/15 18:35:31
Why do you use a map to store references to things
shauvik
2011/12/16 07:19:27
Actually everything is stored in maps. We need to
Kelly Norton
2011/12/16 19:48:13
I'm just saying that a map is definitely not the b
|
| + var pFailed = pPass + (numTestsFailed / tests.length) * 100; |
| + elements['red'].setAttribute('style', 'width:$pFailed%'); |
| + var pErrors = pFailed + (numTestsErrors / tests.length) * 100; |
| + elements['orange'].setAttribute('style', 'width:$pErrors%'); |
| + |
| + // Update status |
| + elements['testsRunElem'].innerText = '$numTestsRun'; |
|
pdr
2011/12/14 21:02:36
There's no need to do this for your strings. Just
Kelly Norton
2011/12/15 18:35:31
You want to use textContent instead of innerText a
shauvik
2011/12/16 07:19:27
Yes, I did that to get rid of int-to-string warnin
shauvik
2011/12/16 07:19:27
Cool, Thanks for the tip. I didn't know innerText
|
| + elements['testsFailedElem'].innerText = '$numTestsFailed'; |
| + elements['testsErrorsElem'].innerText = '$numTestsErrors'; |
| + } |
| + |
| + String printDuration(Duration timeDuration) { |
| + StringBuffer out = new StringBuffer(); |
| + if(timeDuration.inDays > 0){ |
| + out.add('${timeDuration.inDays} days '); |
| + } |
| + if(timeDuration.inHours > 0){ |
| + out.add('${timeDuration.inHours} hrs '); |
| + } |
| + if(timeDuration.inMinutes > 0){ |
| + out.add('${timeDuration.inMinutes} mins '); |
| + } |
| + if(timeDuration.inSeconds > 0){ |
| + out.add('${timeDuration.inSeconds} s '); |
| + } |
| + if(timeDuration.inMilliseconds > 0 || out.length == 0){ |
| + out.add('${timeDuration.inMilliseconds} ms'); |
| + } |
| + return out.toString(); |
| + } |
| + |
| + /** Populates the floating div with controls and toolbar */ |
| + HTMLDivElement renderMain(){ |
| + HTMLDivElement containerDiv = win.document.createElement('div'); |
| + containerDiv.className = 'dt-container'; |
| + appElements['containerDiv'] = containerDiv; |
| + |
| + // Add the test controls |
| + HTMLDivElement mainElem = win.document.createElement('div'); |
| + mainElem.className = 'dt-main'; |
| + appElements['mainElem'] = mainElem; |
| + |
| + showTestControls(); |
| + |
| + // Create header to hold window controls |
| + if(inAppMode) { |
| + HTMLDivElement headDiv = win.document.createElement('div'); |
| + headDiv.className = 'dt-header'; |
| + headDiv.innerHTML = '<b>DARTest: In-App View</b>'; |
| + HTMLImageElement close = win.document.createElement('img'); |
| + close.className = 'dt-header-close'; |
| + close.addEventListener('click', (Event){ |
| + containerDiv.className = 'dt-hide'; |
| + }, true); |
| + HTMLImageElement pop = win.document.createElement('img'); |
| + pop.className = 'dt-header-pop'; |
| + pop.addEventListener('click', (Event) => dartestMaximize(), true); |
| + HTMLImageElement minMax = win.document.createElement('img'); |
| + minMax.className = 'dt-header-min'; |
| + minMax.addEventListener('click', (Event) { |
| + if (mainElem.classList.contains('dt-hide')){ |
| + mainElem.classList.remove('dt-hide'); |
| + mainElem.classList.add('dt-show'); |
| + minMax.className = 'dt-header-min'; |
| + } else { |
| + if (mainElem.classList.contains('dt-show')){ |
| + mainElem.classList.remove('dt-show'); |
| + } |
| + mainElem.classList.add('dt-hide'); |
| + minMax.className = 'dt-header-max'; |
| + } |
| + }, true); |
| + headDiv.appendChild(close); |
| + headDiv.appendChild(pop); |
| + headDiv.appendChild(minMax); |
| + |
| + containerDiv.appendChild(headDiv); |
| + } |
| + |
| + HTMLDivElement tabDiv = win.document.createElement('div'); |
| + tabDiv.className = 'dt-tab'; |
| + HTMLUListElement tabList = win.document.createElement('ul'); |
| + HTMLLIElement testingTab = win.document.createElement('li'); |
| + HTMLLIElement coverageTab = win.document.createElement('li'); |
| + testingTab.className = 'dt-tab-selected'; |
| + testingTab.innerText = 'Testing'; |
| + testingTab.addEventListener('click', (Event) { |
| + showTestControls(); |
| + changeTabs(testingTab, coverageTab); |
| + }, true); |
| + tabList.appendChild(testingTab); |
| + coverageTab.innerText = 'Coverage'; |
| + coverageTab.addEventListener('click', (Event) { |
| + showCoverageControls(); |
| + changeTabs(coverageTab, testingTab); |
| + }, true); |
| + tabList.appendChild(coverageTab); |
| + tabDiv.appendChild(tabList); |
| + containerDiv.appendChild(tabDiv); |
| + |
| + if(!inAppMode) { |
| + HTMLDivElement popIn = win.document.createElement('div'); |
| + popIn.className = 'dt-minimize'; |
| + popIn.innerHTML = 'Pop In ⇲'; |
| + popIn.addEventListener('click', (Event) => dartestMinimize(), true); |
| + containerDiv.appendChild(popIn); |
| + } |
| + |
| + containerDiv.appendChild(mainElem); |
| + win.document.body.appendChild(containerDiv); |
| + } |
| + |
| + void changeTabs(HTMLLIElement clickedTab, HTMLLIElement oldTab){ |
| + oldTab.className = ''; |
| + clickedTab.className = 'dt-tab-selected'; |
| + } |
| + |
| + void showTestControls(){ |
| + HTMLDivElement testBody = appElements['testBody']; |
| + if(testBody == null){ |
| + testBody = win.document.createElement('div'); |
| + appElements['testBody'] = testBody; |
| + |
| + // Create a toolbar to hold action buttons |
| + HTMLDivElement toolDiv = win.document.createElement('div'); |
| + toolDiv.className = 'dt-toolbar'; |
| + HTMLButtonElement runBtn = win.document.createElement('button'); |
| + runBtn.innerHTML = '►'; |
| + runBtn.title = 'Run Tests'; |
| + runBtn.className = 'dt-button dt-run'; |
| + runBtn.addEventListener('click', (Event) { |
| + log('Running tests'); |
| + uiUpdateFunc = updateDARTestUI; |
| + runDARTests(); |
| + }, true); |
| + toolDiv.appendChild(runBtn); |
| + HTMLButtonElement exportBtn = win.document.createElement('button'); |
| + exportBtn.innerHTML = '↷'; |
| + exportBtn.title = 'Export Results'; |
| + exportBtn.className = 'dt-button dt-run'; |
| + exportBtn.addEventListener('click', (Event e) { |
| + log('Exporting results'); |
| + exportTestResults(); |
| + }, true); |
| + toolDiv.appendChild(exportBtn); |
| + testBody.appendChild(toolDiv); |
| + |
| + // Create a datalist element for showing test status"<td class='dt-pass'> PASS"; |
| + HTMLDListElement statList = win.document.createElement('dl'); |
| + statList.className = 'dt-status'; |
| + HTMLElement runsDt = win.document.createElement('dt'); |
| + runsDt.innerText = 'Runs:'; |
| + statList.appendChild(runsDt); |
| + HTMLElement testsRunElem = win.document.createElement('dd'); |
| + appElements['testsRunElem'] = testsRunElem; |
| + testsRunElem.innerText = '$numTestsRun'; |
| + statList.appendChild(testsRunElem); |
| + |
| + HTMLElement failDt = win.document.createElement('dt'); |
| + failDt.innerText = 'Failed:'; |
| + statList.appendChild(failDt); |
| + HTMLElement testsFailedElem = win.document.createElement('dd'); |
| + appElements['testsFailedElem'] = testsFailedElem; |
| + testsFailedElem.innerText = '$numTestsFailed'; |
| + statList.appendChild(testsFailedElem); |
| + |
| + HTMLElement errDt = win.document.createElement('dt'); |
| + errDt.innerText = 'Errors:'; |
| + statList.appendChild(errDt); |
| + HTMLElement testsErrorsElem = win.document.createElement('dd'); |
| + appElements['testsErrorsElem'] = testsErrorsElem; |
| + testsErrorsElem.innerText = '$numTestsErrors'; |
| + statList.appendChild(testsErrorsElem); |
| + testBody.appendChild(statList); |
| + |
| + // Create progressbar and add red, green, orange bars |
| + HTMLDivElement progressDiv = win.document.createElement('div'); |
| + progressDiv.className = 'dt-progressbar'; |
| + progressDiv.innerHTML = "<span style='width:100%'></span>"; |
| + |
| + HTMLSpanElement orange = win.document.createElement('span'); |
| + appElements['orange'] = orange; |
| + orange.className = 'orange'; |
| + progressDiv.appendChild(orange); |
| + |
| + HTMLSpanElement red = win.document.createElement('span'); |
| + appElements['red'] = red; |
| + red.className = 'red'; |
| + progressDiv.appendChild(red); |
| + |
| + HTMLSpanElement green = win.document.createElement('span'); |
| + appElements['green'] = green; |
| + green.className = 'green'; |
| + |
| + progressDiv.appendChild(green); |
| + testBody.appendChild(progressDiv); |
| + |
| + HTMLDivElement hiddenElem = win.document.createElement('div'); |
| + hiddenElem.className = 'dt-hide'; |
| + hiddenElem.innerHTML = "<a id='dt-export' download='test_results.csv' href='#' />"; |
| + testBody.appendChild(hiddenElem); |
| + |
| + addChildToElem('mainElem', testBody); |
| + } |
| + |
| + // Show hide divs |
| + showHide('testBody', 'coverageBody'); |
| + } |
| + |
| + void showCoverageControls(){ |
| + HTMLDivElement coverageBody = appElements['coverageBody']; |
| + if(coverageBody == null){ |
| + coverageBody = win.document.createElement('div'); |
| + appElements['coverageBody'] = coverageBody; |
| + |
| + HTMLPreElement covPreElem = win.document.createElement('pre'); |
| + appElements['covPreElem'] = covPreElem; |
| + coverageBody.appendChild(covPreElem); |
| + |
| + HTMLTableElement covTable = win.document.createElement('table'); |
| + covTable.className = 'dt-results'; |
| + HTMLTableSectionElement head = win.document.createElement('thead'); |
| + head.innerHTML = '<tr><th>Unit <th>Function <th>Statement <th>Branch'; |
| + covTable.appendChild(head); |
| + HTMLTableSectionElement covTableBody = win.document.createElement('tbody'); |
| + appElements['covTableBody'] = covTableBody; |
| + covTableBody.id = 'dt-results-body'; |
| + covTable.appendChild(covTableBody); |
| + coverageBody.appendChild(covTable); |
| + |
| + addChildToElem('mainElem', coverageBody); |
| + } |
| + showHide('coverageBody', 'testBody'); |
| + |
| + appElements['covPreElem'].innerText = getCoverageSummary(); |
| + appElements['covTableBody'].innerHTML = getCoverageDetails(); |
| + } |
| + |
| + void showHide(String toShow, String toHide) { |
|
pdr
2011/12/14 21:02:36
I'd split this into show and hide.
shauvik
2011/12/16 07:19:27
Done.
|
| + HTMLElement show = appElements[toShow]; |
| + HTMLElement hide = appElements[toHide]; |
| + if(show != null){ |
| + if(show.classList.contains('dt-hide')) { |
| + show.classList.remove('dt-hide'); |
| + } |
| + show.classList.add('dt-show'); |
| + } |
| + if(hide != null){ |
| + if(hide.classList.contains('dt-show')){ |
| + hide.classList.remove('dt-show'); |
| + } |
| + hide.classList.add('dt-hide'); |
| + } |
| + } |
| + |
| + void dartestMaximize(){ |
| + showHide('', 'containerDiv'); |
| + win = win.open('', 'dartest-window', 'width=600,height=750'); |
| + log('Window:'+win.toString()); |
|
pdr
2011/12/14 21:02:36
I would remove these logging calls, or put them be
shauvik
2011/12/16 07:19:27
Done.
|
| + win.document.title = 'Dartest'; |
| + inAppMode = false; |
| + fullAppElements = new HashMap<String, HTMLElement>(); |
| + appElements = fullAppElements; |
| + DARTestCss.inject(win.document, false); |
| + run(); |
| + if(numTestsRun > 0) { |
| + populateResults(); |
| + } |
| + } |
| + |
| + void populateResults() { |
|
pdr
2011/12/14 21:02:36
I think this can be:
tests.forEach(TestCase t) =>
shauvik
2011/12/16 07:19:27
Thanks. Its slightly different that what you propo
|
| + for(TestCase t in tests) { |
| + updateDARTestUI(t); |
| + } |
| + } |
| + |
| + void dartestMinimize(){ |
| + win.close(); |
| + win = appWindow; |
| + inAppMode = true; |
| + appElements = inAppElements; |
| + showHide('containerDiv', ''); |
| + } |
| + |
| + void exportTestResults(){ |
| + String csvData = getTestResultsCSV(); |
| + log(csvData); |
| + HTMLAnchorElement exportLink = win.document.getElementById('dt-export'); |
| + |
| + /** Bug: Can't instantiate WebKitBlobBuilder |
| + * If this bug is fixed, we can remove the urlencode and lpad function. |
| + WebKitBlobBuilder bb = new WebKitBlobBuilder(); |
| + bb.append(csvData); |
| + Blob blob = bb.getBlob('text/plain;charset=${document.characterSet}'); |
| + exportLink.href = window.webkitURL.createObjectURL(blob); |
| + **/ |
| + |
| + exportLink.href = 'data:text/csv,'+urlencode(csvData); |
| + |
| + MouseEvent ev = document.createEvent("MouseEvents"); |
| + ev.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0 |
| + , false, false, false, false, 0, null); |
| + exportLink.dispatchEvent(ev); |
| + |
| + } |
| + |
| + String urlencode(String s) { |
|
Kelly Norton
2011/12/15 18:35:31
How is the different from the native encodeURI?
shauvik
2011/12/16 07:19:27
Actually I think encodeURI isn't supported in dart
|
| + StringBuffer out = new StringBuffer(); |
| + for(int i=0;i<s.length;i++) { |
| + int cc = s.charCodeAt(i); |
| + if((cc>=48 && cc<=57) || (cc>=65 && cc<=90) || (cc>=97 && cc<=122)) { |
| + out.add(s[i]); |
| + } else { |
| + out.add('%${lpad(cc.toRadixString(16),2).toUpperCase()}'); |
| + } |
| + } |
| + return out.toString(); |
| + } |
| + |
| + String lpad(String s, int n) { |
| + if(s.length<n) { |
|
Kelly Norton
2011/12/15 18:35:31
s.length < n
Lots of other space problems in this
shauvik
2011/12/16 07:19:27
Done.
|
| + for(int i=0;i<n-s.length;i++) { |
| + s = '0'+s; |
| + } |
| + } |
| + return s; |
| + } |
| +} |