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; |
+ } |
+} |