OLD | NEW |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * A simple unit test library for running tests in a browser. | 6 * A simple unit test library for running tests in a browser. |
7 * | 7 * |
8 * Provides enhanced HTML output with collapsible group headers | 8 * Provides enhanced HTML output with collapsible group headers |
9 * and other at-a-glance information about the test results. | 9 * and other at-a-glance information about the test results. |
10 */ | 10 */ |
11 library unittest_html_enhanced_config; | 11 library unittest_html_enhanced_config; |
12 | 12 |
| 13 import 'dart:async'; |
13 import 'dart:collection' show LinkedHashMap; | 14 import 'dart:collection' show LinkedHashMap; |
14 import 'dart:html'; | 15 import 'dart:html'; |
15 import 'unittest.dart'; | 16 import 'unittest.dart'; |
16 | 17 |
17 class HtmlEnhancedConfiguration extends Configuration { | 18 class HtmlEnhancedConfiguration extends Configuration { |
18 /** Whether this is run within dartium layout tests. */ | 19 /** Whether this is run within dartium layout tests. */ |
19 final bool _isLayoutTest; | 20 final bool _isLayoutTest; |
20 HtmlEnhancedConfiguration(this._isLayoutTest); | 21 HtmlEnhancedConfiguration(this._isLayoutTest); |
21 | 22 |
22 // TODO(rnystrom): Get rid of this if we get canonical closures for methods. | 23 var _onErrorSubscription = null; |
23 EventListener _onErrorClosure; | 24 var _onMessageSubscription = null; |
24 EventListener _onMessageClosure; | |
25 | 25 |
26 void _installHandlers() { | 26 void _installOnErrorHandler() { |
27 if (_onErrorClosure == null) { | 27 if (_onErrorSubscription == null) { |
28 _onErrorClosure = | |
29 (e) => handleExternalError(e, '(DOM callback has errors)'); | |
30 // Listen for uncaught errors. | 28 // Listen for uncaught errors. |
31 window.on.error.add(_onErrorClosure); | 29 _onErrorSubscription = window.onError.listen( |
32 } | 30 (e) => handleExternalError(e, '(DOM callback has errors)')); |
33 if (_onMessageClosure == null) { | |
34 _onMessageClosure = (e) => processMessage(e); | |
35 // Listen for errors from JS. | |
36 window.on.message.add(_onMessageClosure); | |
37 } | 31 } |
38 } | 32 } |
39 | 33 |
40 void _uninstallHandlers() { | 34 void _installOnMessageHandler() { |
41 if (_onErrorClosure != null) { | 35 if (_onMessageSubscription == null) { |
42 window.on.error.remove(_onErrorClosure); | 36 // Listen for errors from JS. |
43 _onErrorClosure = null; | 37 _onMessageSubscription = window.onMessage.listen( |
44 } | 38 (e) => processMessage(e)); |
45 if (_onMessageClosure != null) { | |
46 window.on.message.remove(_onMessageClosure); | |
47 _onMessageClosure = null; | |
48 } | 39 } |
49 } | 40 } |
50 | 41 |
| 42 void _installHandlers() { |
| 43 _installOnErrorHandler(); |
| 44 _installOnMessageHandler(); |
| 45 } |
| 46 |
| 47 void _uninstallHandlers() { |
| 48 if (_onErrorSubscription != null) { |
| 49 _onErrorSubscription.cancel(); |
| 50 _onErrorSubscription = null; |
| 51 } |
| 52 if (_onMessageSubscription != null) { |
| 53 _onMessageSubscription.cancel(); |
| 54 _onMessageSubscription = null; |
| 55 } |
| 56 } |
| 57 |
51 void processMessage(e) { | 58 void processMessage(e) { |
52 if ('unittest-suite-external-error' == e.data) { | 59 if ('unittest-suite-external-error' == e.data) { |
53 handleExternalError('<unknown>', '(external error detected)'); | 60 handleExternalError('<unknown>', '(external error detected)'); |
54 } | 61 } |
55 } | 62 } |
56 | 63 |
57 void onInit() { | 64 void onInit() { |
58 _installHandlers(); | 65 _installHandlers(); |
59 //initialize and load CSS | 66 //initialize and load CSS |
60 final String _CSSID = '_unittestcss_'; | 67 final String _CSSID = '_unittestcss_'; |
61 | 68 |
62 var cssElement = document.head.query('#${_CSSID}'); | 69 var cssElement = document.head.query('#${_CSSID}'); |
63 if (cssElement == null){ | 70 if (cssElement == null){ |
64 document.head.elements.add(new Element.html( | 71 document.head.children.add(new Element.html( |
65 '<style id="${_CSSID}"></style>')); | 72 '<style id="${_CSSID}"></style>')); |
66 cssElement = document.head.query('#${_CSSID}'); | 73 cssElement = document.head.query('#${_CSSID}'); |
67 } | 74 } |
68 | 75 |
69 cssElement.innerHtml = _htmlTestCSS; | 76 cssElement.innerHtml = _htmlTestCSS; |
70 window.postMessage('unittest-suite-wait-for-done', '*'); | 77 window.postMessage('unittest-suite-wait-for-done', '*'); |
71 } | 78 } |
72 | 79 |
73 void onStart() { | 80 void onStart() { |
74 // Listen for uncaught errors. | 81 // Listen for uncaught errors. |
75 window.on.error.add(_onErrorClosure); | 82 _installOnErrorHandler(); |
76 } | 83 } |
77 | 84 |
78 void onTestResult(TestCase testCase) {} | 85 void onTestResult(TestCase testCase) {} |
79 | 86 |
80 void onSummary(int passed, int failed, int errors, List<TestCase> results, | 87 void onSummary(int passed, int failed, int errors, List<TestCase> results, |
81 String uncaughtError) { | 88 String uncaughtError) { |
82 _showInteractiveResultsInPage(passed, failed, errors, results, | 89 _showInteractiveResultsInPage(passed, failed, errors, results, |
83 _isLayoutTest, uncaughtError); | 90 _isLayoutTest, uncaughtError); |
84 } | 91 } |
85 | 92 |
86 void onDone(bool success) { | 93 void onDone(bool success) { |
87 _uninstallHandlers(); | 94 _uninstallHandlers(); |
88 window.postMessage('unittest-suite-done', '*'); | 95 window.postMessage('unittest-suite-done', '*'); |
89 } | 96 } |
90 | 97 |
91 void _showInteractiveResultsInPage(int passed, int failed, int errors, | 98 void _showInteractiveResultsInPage(int passed, int failed, int errors, |
92 List<TestCase> results, bool isLayoutTest, String uncaughtError) { | 99 List<TestCase> results, bool isLayoutTest, String uncaughtError) { |
93 if (isLayoutTest && passed == results.length) { | 100 if (isLayoutTest && passed == results.length) { |
94 document.body.innerHtml = "PASS"; | 101 document.body.innerHtml = "PASS"; |
95 } else { | 102 } else { |
96 // changed the StringBuffer to an Element fragment | 103 // changed the StringBuffer to an Element fragment |
97 Element te = new Element.html('<div class="unittest-table"></div>'); | 104 Element te = new Element.html('<div class="unittest-table"></div>'); |
98 | 105 |
99 te.elements.add(new Element.html(passed == results.length | 106 te.children.add(new Element.html(passed == results.length |
100 ? "<div class='unittest-overall unittest-pass'>PASS</div>" | 107 ? "<div class='unittest-overall unittest-pass'>PASS</div>" |
101 : "<div class='unittest-overall unittest-fail'>FAIL</div>")); | 108 : "<div class='unittest-overall unittest-fail'>FAIL</div>")); |
102 | 109 |
103 // moved summary to the top since web browsers | 110 // moved summary to the top since web browsers |
104 // don't auto-scroll to the bottom like consoles typically do. | 111 // don't auto-scroll to the bottom like consoles typically do. |
105 if (passed == results.length && uncaughtError == null) { | 112 if (passed == results.length && uncaughtError == null) { |
106 te.elements.add(new Element.html(""" | 113 te.children.add(new Element.html(""" |
107 <div class='unittest-pass'>All ${passed} tests passed</div>""")); | 114 <div class='unittest-pass'>All ${passed} tests passed</div>""")); |
108 } else { | 115 } else { |
109 | 116 |
110 if (uncaughtError != null) { | 117 if (uncaughtError != null) { |
111 te.elements.add(new Element.html(""" | 118 te.children.add(new Element.html(""" |
112 <div class='unittest-summary'> | 119 <div class='unittest-summary'> |
113 <span class='unittest-error'>Uncaught error: $uncaughtError</span> | 120 <span class='unittest-error'>Uncaught error: $uncaughtError</span> |
114 </div>""")); | 121 </div>""")); |
115 } | 122 } |
116 | 123 |
117 te.elements.add(new Element.html(""" | 124 te.children.add(new Element.html(""" |
118 <div class='unittest-summary'> | 125 <div class='unittest-summary'> |
119 <span class='unittest-pass'>Total ${passed} passed</span>, | 126 <span class='unittest-pass'>Total ${passed} passed</span>, |
120 <span class='unittest-fail'>${failed} failed</span>, | 127 <span class='unittest-fail'>${failed} failed</span>, |
121 <span class='unittest-error'> | 128 <span class='unittest-error'> |
122 ${errors + (uncaughtError == null ? 0 : 1)} errors</span> | 129 ${errors + (uncaughtError == null ? 0 : 1)} errors</span> |
123 </div>""")); | 130 </div>""")); |
124 } | 131 } |
125 | 132 |
126 te.elements.add(new Element.html(""" | 133 te.children.add(new Element.html(""" |
127 <div><button id='btnCollapseAll'>Collapse All</button></div> | 134 <div><button id='btnCollapseAll'>Collapse All</button></div> |
128 """)); | 135 """)); |
129 | 136 |
130 // handle the click event for the collapse all button | 137 // handle the click event for the collapse all button |
131 te.query('#btnCollapseAll').on.click.add((_){ | 138 te.query('#btnCollapseAll').onClick.listen((_){ |
132 document | 139 document |
133 .queryAll('.unittest-row') | 140 .queryAll('.unittest-row') |
134 .forEach((el) => el.attributes['class'] = el.attributes['class'] | 141 .forEach((el) => el.attributes['class'] = el.attributes['class'] |
135 .replaceAll('unittest-row ', 'unittest-row-hidden ')); | 142 .replaceAll('unittest-row ', 'unittest-row-hidden ')); |
136 }); | 143 }); |
137 | 144 |
138 var previousGroup = ''; | 145 var previousGroup = ''; |
139 var groupPassFail = true; | 146 var groupPassFail = true; |
140 final indentAmount = 50; | 147 final indentAmount = 50; |
141 | 148 |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
177 var testsInGroup = results | 184 var testsInGroup = results |
178 .where((TestCase t) => t.currentGroup == previousGroup) | 185 .where((TestCase t) => t.currentGroup == previousGroup) |
179 .toList(); | 186 .toList(); |
180 var groupTotalTestCount = testsInGroup.length; | 187 var groupTotalTestCount = testsInGroup.length; |
181 var groupTestPassedCount = testsInGroup.where( | 188 var groupTestPassedCount = testsInGroup.where( |
182 (TestCase t) => t.result == 'pass').length; | 189 (TestCase t) => t.result == 'pass').length; |
183 groupPassFail = groupTotalTestCount == groupTestPassedCount; | 190 groupPassFail = groupTotalTestCount == groupTestPassedCount; |
184 var passFailClass = "unittest-group-status unittest-group-" | 191 var passFailClass = "unittest-group-status unittest-group-" |
185 "status-${groupPassFail ? 'pass' : 'fail'}"; | 192 "status-${groupPassFail ? 'pass' : 'fail'}"; |
186 | 193 |
187 te.elements.add(new Element.html(""" | 194 te.children.add(new Element.html(""" |
188 <div> | 195 <div> |
189 <div id='${safeGroup}' | 196 <div id='${safeGroup}' |
190 class='unittest-group ${safeGroup} test${safeGroup}'> | 197 class='unittest-group ${safeGroup} test${safeGroup}'> |
191 <div ${_isIE ? "style='display:inline-block' ": ""} | 198 <div ${_isIE ? "style='display:inline-block' ": ""} |
192 class='unittest-row-status'> | 199 class='unittest-row-status'> |
193 <div class='$passFailClass'></div> | 200 <div class='$passFailClass'></div> |
194 </div> | 201 </div> |
195 <div ${_isIE ? "style='display:inline-block' ": ""}> | 202 <div ${_isIE ? "style='display:inline-block' ": ""}> |
196 ${test_.currentGroup}</div> | 203 ${test_.currentGroup}</div> |
197 | 204 |
198 <div ${_isIE ? "style='display:inline-block' ": ""}> | 205 <div ${_isIE ? "style='display:inline-block' ": ""}> |
199 (${groupTestPassedCount}/${groupTotalTestCount})</div> | 206 (${groupTestPassedCount}/${groupTotalTestCount})</div> |
200 </div> | 207 </div> |
201 </div>""")); | 208 </div>""")); |
202 | 209 |
203 // 'safeGroup' could be empty | 210 // 'safeGroup' could be empty |
204 var grp = (safeGroup == '') ? null : te.query('#${safeGroup}'); | 211 var grp = (safeGroup == '') ? null : te.query('#${safeGroup}'); |
205 if (grp != null){ | 212 if (grp != null){ |
206 grp.on.click.add((_){ | 213 grp.onClick.listen((_){ |
207 var row = document.query('.unittest-row-${safeGroup}'); | 214 var row = document.query('.unittest-row-${safeGroup}'); |
208 if (row.attributes['class'].contains('unittest-row ')){ | 215 if (row.attributes['class'].contains('unittest-row ')){ |
209 document.queryAll('.unittest-row-${safeGroup}').forEach( | 216 document.queryAll('.unittest-row-${safeGroup}').forEach( |
210 (e) => e.attributes['class'] = e.attributes['class'] | 217 (e) => e.attributes['class'] = e.attributes['class'] |
211 .replaceAll('unittest-row ', 'unittest-row-hidden ')); | 218 .replaceAll('unittest-row ', 'unittest-row-hidden ')); |
212 }else{ | 219 }else{ |
213 document.queryAll('.unittest-row-${safeGroup}').forEach( | 220 document.queryAll('.unittest-row-${safeGroup}').forEach( |
214 (e) => e.attributes['class'] = e.attributes['class'] | 221 (e) => e.attributes['class'] = e.attributes['class'] |
215 .replaceAll('unittest-row-hidden', 'unittest-row')); | 222 .replaceAll('unittest-row-hidden', 'unittest-row')); |
216 } | 223 } |
217 }); | 224 }); |
218 } | 225 } |
219 } | 226 } |
220 | 227 |
221 _buildRow(test_, te, safeGroup, !groupPassFail); | 228 _buildRow(test_, te, safeGroup, !groupPassFail); |
222 } | 229 } |
223 | 230 |
224 document.body.elements.clear(); | 231 document.body.children.clear(); |
225 document.body.elements.add(te); | 232 document.body.children.add(te); |
226 } | 233 } |
227 } | 234 } |
228 | 235 |
229 void _buildRow(TestCase test_, Element te, String groupID, bool isVisible) { | 236 void _buildRow(TestCase test_, Element te, String groupID, bool isVisible) { |
230 var background = 'unittest-row-${test_.id % 2 == 0 ? "even" : "odd"}'; | 237 var background = 'unittest-row-${test_.id % 2 == 0 ? "even" : "odd"}'; |
231 var display = '${isVisible ? "unittest-row" : "unittest-row-hidden"}'; | 238 var display = '${isVisible ? "unittest-row" : "unittest-row-hidden"}'; |
232 | 239 |
233 // TODO (prujohn@gmail.com) I had to borrow this from html_print.dart | 240 // TODO (prujohn@gmail.com) I had to borrow this from html_print.dart |
234 // Probably should put it in some more common location. | 241 // Probably should put it in some more common location. |
235 String _htmlEscape(String string) { | 242 String _htmlEscape(String string) { |
236 return string.replaceAll('&', '&') | 243 return string.replaceAll('&', '&') |
237 .replaceAll('<','<') | 244 .replaceAll('<','<') |
238 .replaceAll('>','>'); | 245 .replaceAll('>','>'); |
239 } | 246 } |
240 | 247 |
241 addRowElement(id, status, description){ | 248 addRowElement(id, status, description){ |
242 te.elements.add( | 249 te.children.add( |
243 new Element.html( | 250 new Element.html( |
244 ''' <div> | 251 ''' <div> |
245 <div class='$display unittest-row-${groupID} $background'> | 252 <div class='$display unittest-row-${groupID} $background'> |
246 <div ${_isIE ? "style='display:inline-block' ": ""} | 253 <div ${_isIE ? "style='display:inline-block' ": ""} |
247 class='unittest-row-id'>$id</div> | 254 class='unittest-row-id'>$id</div> |
248 <div ${_isIE ? "style='display:inline-block' ": ""} | 255 <div ${_isIE ? "style='display:inline-block' ": ""} |
249 class="unittest-row-status unittest-${test_.result}"> | 256 class="unittest-row-status unittest-${test_.result}"> |
250 $status</div> | 257 $status</div> |
251 <div ${_isIE ? "style='display:inline-block' ": ""} | 258 <div ${_isIE ? "style='display:inline-block' ": ""} |
252 class='unittest-row-description'>$description</div> | 259 class='unittest-row-description'>$description</div> |
(...skipping 10 matching lines...) Expand all Loading... |
263 | 270 |
264 addRowElement('${test_.id}', '${test_.result.toUpperCase()}', | 271 addRowElement('${test_.id}', '${test_.result.toUpperCase()}', |
265 '${test_.description}. ${_htmlEscape(test_.message)}'); | 272 '${test_.description}. ${_htmlEscape(test_.message)}'); |
266 | 273 |
267 if (test_.stackTrace != null) { | 274 if (test_.stackTrace != null) { |
268 addRowElement('', '', '<pre>${_htmlEscape(test_.stackTrace)}</pre>'); | 275 addRowElement('', '', '<pre>${_htmlEscape(test_.stackTrace)}</pre>'); |
269 } | 276 } |
270 } | 277 } |
271 | 278 |
272 | 279 |
273 static bool get _isIE => document.window.navigator.userAgent.contains('MSIE'); | 280 static bool get _isIE => window.navigator.userAgent.contains('MSIE'); |
274 | 281 |
275 String get _htmlTestCSS => | 282 String get _htmlTestCSS => |
276 ''' | 283 ''' |
277 body{ | 284 body{ |
278 font-size: 14px; | 285 font-size: 14px; |
279 font-family: 'Open Sans', 'Lucida Sans Unicode', 'Lucida Grande',''' | 286 font-family: 'Open Sans', 'Lucida Sans Unicode', 'Lucida Grande',''' |
280 ''' sans-serif; | 287 ''' sans-serif; |
281 background: WhiteSmoke; | 288 background: WhiteSmoke; |
282 } | 289 } |
283 | 290 |
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
409 { | 416 { |
410 } | 417 } |
411 | 418 |
412 '''; | 419 '''; |
413 } | 420 } |
414 | 421 |
415 void useHtmlEnhancedConfiguration([bool isLayoutTest = false]) { | 422 void useHtmlEnhancedConfiguration([bool isLayoutTest = false]) { |
416 if (config != null) return; | 423 if (config != null) return; |
417 configure(new HtmlEnhancedConfiguration(isLayoutTest)); | 424 configure(new HtmlEnhancedConfiguration(isLayoutTest)); |
418 } | 425 } |
OLD | NEW |