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 /** | |
6 * A simple unit test library for running tests in a browser. | |
7 * | |
8 * Provides enhanced HTML output with collapsible group headers | |
9 * and other at-a-glance information about the test results. | |
10 */ | |
11 #library('unittest_html_enhanced_config'); | |
12 | |
13 #import('dart:html'); | |
14 #import('unittest.dart'); | |
15 | |
16 | |
17 class HtmlEnhancedConfiguration extends Configuration { | |
18 /** Whether this is run within dartium layout tests. */ | |
19 final bool _isLayoutTest; | |
20 HtmlEnhancedConfiguration(this._isLayoutTest); | |
21 | |
22 // TODO(rnystrom): Get rid of this if we get canonical closures for methods. | |
23 EventListener _onErrorClosure; | |
24 EventListener _onMessageClosure; | |
25 | |
26 void _installHandlers() { | |
27 if (_onErrorClosure == null) { | |
28 _onErrorClosure = | |
29 (e) => handleExternalError(e, '(DOM callback has errors)'); | |
30 // Listen for uncaught errors. | |
31 window.on.error.add(_onErrorClosure); | |
32 } | |
33 if (_onMessageClosure == null) { | |
34 _onMessageClosure = (e) => processMessage(e); | |
35 // Listen for errors from JS. | |
36 window.on.message.add(_onMessageClosure); | |
37 } | |
38 } | |
39 | |
40 void _uninstallHandlers() { | |
41 if (_onErrorClosure != null) { | |
42 window.on.error.remove(_onErrorClosure); | |
43 _onErrorClosure = null; | |
44 } | |
45 if (_onMessageClosure != null) { | |
46 window.on.message.remove(_onMessageClosure); | |
47 _onMessageClosure = null; | |
48 } | |
49 } | |
50 | |
51 void processMessage(e) { | |
52 if ('unittest-suite-external-error' == e.data) { | |
53 handleExternalError('<unknown>', '(external error detected)'); | |
54 } | |
55 } | |
56 | |
57 void onInit() { | |
58 _installHandlers(); | |
59 //initialize and load CSS | |
60 final String _CSSID = '_unittestcss_'; | |
61 | |
62 var cssElement = document.head.query('#${_CSSID}'); | |
63 if (cssElement == null){ | |
64 document.head.elements.add(new Element.html( | |
65 '<style id="${_CSSID}"></style>')); | |
66 cssElement = document.head.query('#${_CSSID}'); | |
67 } | |
68 | |
69 cssElement.innerHTML = _htmlTestCSS; | |
70 } | |
71 | |
72 void onStart() { | |
73 window.postMessage('unittest-suite-wait-for-done', '*'); | |
74 // Listen for uncaught errors. | |
75 window.on.error.add(_onErrorClosure); | |
76 } | |
77 | |
78 void onTestResult(TestCase testCase) {} | |
79 | |
80 void onDone(int passed, int failed, int errors, List<TestCase> results, | |
81 String uncaughtError) { | |
82 _uninstallHandlers(); | |
83 | |
84 _showInteractiveResultsInPage(passed, failed, errors, results, | |
85 _isLayoutTest, uncaughtError); | |
86 | |
87 window.postMessage('unittest-suite-done', '*'); | |
88 } | |
89 | |
90 void _showInteractiveResultsInPage(int passed, int failed, int errors, | |
91 List<TestCase> results, bool isLayoutTest, String uncaughtError) { | |
92 if (isLayoutTest && passed == results.length) { | |
93 document.body.innerHTML = "PASS"; | |
94 } else { | |
95 // changed the StringBuffer to an Element fragment | |
96 Element te = new Element.html('<div class="unittest-table"></div>'); | |
97 | |
98 te.elements.add(new Element.html(passed == results.length | |
99 ? "<div class='unittest-overall unittest-pass'>PASS</div>" | |
100 : "<div class='unittest-overall unittest-fail'>FAIL</div>")); | |
101 | |
102 // moved summary to the top since web browsers | |
103 // don't auto-scroll to the bottom like consoles typically do. | |
104 if (passed == results.length && uncaughtError == null) { | |
105 te.elements.add(new Element.html(""" | |
106 <div class='unittest-pass'>All ${passed} tests passed</div>""")); | |
107 } else { | |
108 | |
109 if (uncaughtError != null) { | |
110 te.elements.add(new Element.html(""" | |
111 <div class='unittest-summary'> | |
112 <span class='unittest-error'>Uncaught error: $uncaughtError</span> | |
113 </div>""")); | |
114 } | |
115 | |
116 te.elements.add(new Element.html(""" | |
117 <div class='unittest-summary'> | |
118 <span class='unittest-pass'>Total ${passed} passed</span>, | |
119 <span class='unittest-fail'>${failed} failed</span>, | |
120 <span class='unittest-error'> | |
121 ${errors + (uncaughtError == null ? 0 : 1)} errors</span> | |
122 </div>""")); | |
123 } | |
124 | |
125 te.elements.add(new Element.html(""" | |
126 <div><button id='btnCollapseAll'>Collapse All</button></div> | |
127 """)); | |
128 | |
129 // handle the click event for the collapse all button | |
130 te.query('#btnCollapseAll').on.click.add((_){ | |
131 document | |
132 .queryAll('.unittest-row') | |
133 .forEach((el) => el.attributes['class'] = el.attributes['class'] | |
134 .replaceAll('unittest-row ', 'unittest-row-hidden ')); | |
135 }); | |
136 | |
137 var previousGroup = ''; | |
138 var groupPassFail = true; | |
139 final indentAmount = 50; | |
140 | |
141 // order by group and sort numerically within each group | |
142 var groupedBy = new LinkedHashMap<String, List<TestCase>>(); | |
143 | |
144 for (final t in results){ | |
145 if (!groupedBy.containsKey(t.currentGroup)){ | |
146 groupedBy[t.currentGroup] = new List<TestCase>(); | |
147 } | |
148 | |
149 groupedBy[t.currentGroup].add(t); | |
150 } | |
151 | |
152 // flatten the list again with tests ordered | |
153 List<TestCase> flattened = new List<TestCase>(); | |
154 | |
155 groupedBy | |
156 .values | |
157 .forEach((tList){ | |
158 tList.sort((tcA, tcB) => tcA.id - tcB.id); | |
159 flattened.addAll(tList); | |
160 } | |
161 ); | |
162 | |
163 var nonAlphanumeric = new RegExp('[^a-z0-9A-Z]'); | |
164 | |
165 // output group headers and test rows | |
166 for (final test_ in flattened) { | |
167 | |
168 // replace everything but numbers and letters from the group name with | |
169 // '_' so we can use in id and class properties. | |
170 var safeGroup = test_.currentGroup.replaceAll(nonAlphanumeric,'_'); | |
171 | |
172 if (test_.currentGroup != previousGroup){ | |
173 | |
174 previousGroup = test_.currentGroup; | |
175 | |
176 var testsInGroup = results.filter( | |
177 (TestCase t) => t.currentGroup == previousGroup); | |
178 var groupTotalTestCount = testsInGroup.length; | |
179 var groupTestPassedCount = testsInGroup.filter( | |
180 (TestCase t) => t.result == 'pass').length; | |
181 groupPassFail = groupTotalTestCount == groupTestPassedCount; | |
182 var passFailClass = "unittest-group-status unittest-group-" | |
183 "status-${groupPassFail ? 'pass' : 'fail'}"; | |
184 | |
185 te.elements.add(new Element.html(""" | |
186 <div> | |
187 <div id='${safeGroup}' | |
188 class='unittest-group ${safeGroup} test${safeGroup}'> | |
189 <div ${_isIE ? "style='display:inline-block' ": ""} | |
190 class='unittest-row-status'> | |
191 <div class='$passFailClass'></div> | |
192 </div> | |
193 <div ${_isIE ? "style='display:inline-block' ": ""}> | |
194 ${test_.currentGroup}</div> | |
195 | |
196 <div ${_isIE ? "style='display:inline-block' ": ""}> | |
197 (${groupTestPassedCount}/${groupTotalTestCount})</div> | |
198 </div> | |
199 </div>""")); | |
200 | |
201 // 'safeGroup' could be empty | |
202 var grp = (safeGroup == '') ? null : te.query('#${safeGroup}'); | |
203 if (grp != null){ | |
204 grp.on.click.add((_){ | |
205 var row = document.query('.unittest-row-${safeGroup}'); | |
206 if (row.attributes['class'].contains('unittest-row ')){ | |
207 document.queryAll('.unittest-row-${safeGroup}').forEach( | |
208 (e) => e.attributes['class'] = e.attributes['class'] | |
209 .replaceAll('unittest-row ', 'unittest-row-hidden ')); | |
210 }else{ | |
211 document.queryAll('.unittest-row-${safeGroup}').forEach( | |
212 (e) => e.attributes['class'] = e.attributes['class'] | |
213 .replaceAll('unittest-row-hidden', 'unittest-row')); | |
214 } | |
215 }); | |
216 } | |
217 } | |
218 | |
219 _buildRow(test_, te, safeGroup, !groupPassFail); | |
220 } | |
221 | |
222 document.body.elements.clear(); | |
223 document.body.elements.add(te); | |
224 } | |
225 } | |
226 | |
227 void _buildRow(TestCase test_, Element te, String groupID, bool isVisible) { | |
228 var background = 'unittest-row-${test_.id % 2 == 0 ? "even" : "odd"}'; | |
229 var display = '${isVisible ? "unittest-row" : "unittest-row-hidden"}'; | |
230 | |
231 // TODO (prujohn@gmail.com) I had to borrow this from html_print.dart | |
232 // Probably should put it in some more common location. | |
233 String _htmlEscape(String string) { | |
234 return string.replaceAll('&', '&') | |
235 .replaceAll('<','<') | |
236 .replaceAll('>','>'); | |
237 } | |
238 | |
239 addRowElement(id, status, description){ | |
240 te.elements.add( | |
241 new Element.html( | |
242 ''' <div> | |
243 <div class='$display unittest-row-${groupID} $background'> | |
244 <div ${_isIE ? "style='display:inline-block' ": ""} | |
245 class='unittest-row-id'>$id</div> | |
246 <div ${_isIE ? "style='display:inline-block' ": ""} | |
247 class="unittest-row-status unittest-${test_.result}"> | |
248 $status</div> | |
249 <div ${_isIE ? "style='display:inline-block' ": ""} | |
250 class='unittest-row-description'>$description</div> | |
251 </div> | |
252 </div>''' | |
253 ) | |
254 ); | |
255 } | |
256 | |
257 if (!test_.isComplete) { | |
258 addRowElement('${test_.id}', 'NO STATUS', 'Test did not complete.'); | |
259 return; | |
260 } | |
261 | |
262 addRowElement('${test_.id}', '${test_.result.toUpperCase()}', | |
263 '${test_.description}. ${_htmlEscape(test_.message)}'); | |
264 | |
265 if (test_.stackTrace != null) { | |
266 addRowElement('', '', '<pre>${_htmlEscape(test_.stackTrace)}</pre>'); | |
267 } | |
268 } | |
269 | |
270 | |
271 static bool get _isIE => document.window.navigator.userAgent.contains('MSIE'); | |
272 | |
273 String get _htmlTestCSS => | |
274 ''' | |
275 body{ | |
276 font-size: 14px; | |
277 font-family: 'Open Sans', 'Lucida Sans Unicode', 'Lucida Grande',''' | |
278 ''' sans-serif; | |
279 background: WhiteSmoke; | |
280 } | |
281 | |
282 .unittest-group | |
283 { | |
284 background: rgb(75,75,75); | |
285 width:98%; | |
286 color: WhiteSmoke; | |
287 font-weight: bold; | |
288 padding: 6px; | |
289 cursor: pointer; | |
290 | |
291 /* Provide some visual separation between groups for IE */ | |
292 ${_isIE ? "border-bottom:solid black 1px;": ""} | |
293 ${_isIE ? "border-top:solid #777777 1px;": ""} | |
294 | |
295 background-image: -webkit-linear-gradient(bottom, rgb(50,50,50) 0%, ''' | |
296 '''rgb(100,100,100) 100%); | |
297 background-image: -moz-linear-gradient(bottom, rgb(50,50,50) 0%, ''' | |
298 '''rgb(100,100,100) 100%); | |
299 background-image: -ms-linear-gradient(bottom, rgb(50,50,50) 0%, ''' | |
300 '''rgb(100,100,100) 100%); | |
301 background-image: linear-gradient(bottom, rgb(50,50,50) 0%, ''' | |
302 '''rgb(100,100,100) 100%); | |
303 | |
304 display: -webkit-box; | |
305 display: -moz-box; | |
306 display: -ms-box; | |
307 display: box; | |
308 | |
309 -webkit-box-orient: horizontal; | |
310 -moz-box-orient: horizontal; | |
311 -ms-box-orient: horizontal; | |
312 box-orient: horizontal; | |
313 | |
314 -webkit-box-align: center; | |
315 -moz-box-align: center; | |
316 -ms-box-align: center; | |
317 box-align: center; | |
318 } | |
319 | |
320 .unittest-group-status | |
321 { | |
322 width: 20px; | |
323 height: 20px; | |
324 border-radius: 20px; | |
325 margin-left: 10px; | |
326 } | |
327 | |
328 .unittest-group-status-pass{ | |
329 background: Green; | |
330 background: ''' | |
331 '''-webkit-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
332 background: ''' | |
333 '''-moz-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
334 background: ''' | |
335 '''-ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
336 background: ''' | |
337 '''radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
338 } | |
339 | |
340 .unittest-group-status-fail{ | |
341 background: Red; | |
342 background: ''' | |
343 '''-webkit-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); | |
344 background: ''' | |
345 '''-moz-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); | |
346 background: ''' | |
347 '''-ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
348 background: radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); | |
349 } | |
350 | |
351 .unittest-overall{ | |
352 font-size: 20px; | |
353 } | |
354 | |
355 .unittest-summary{ | |
356 font-size: 18px; | |
357 } | |
358 | |
359 .unittest-pass{ | |
360 color: Green; | |
361 } | |
362 | |
363 .unittest-fail, .unittest-error | |
364 { | |
365 color: Red; | |
366 } | |
367 | |
368 .unittest-row | |
369 { | |
370 display: -webkit-box; | |
371 display: -moz-box; | |
372 display: -ms-box; | |
373 display: box; | |
374 -webkit-box-orient: horizontal; | |
375 -moz-box-orient: horizontal; | |
376 -ms-box-orient: horizontal; | |
377 box-orient: horizontal; | |
378 width: 100%; | |
379 } | |
380 | |
381 .unittest-row-hidden | |
382 { | |
383 display: none; | |
384 } | |
385 | |
386 .unittest-row-odd | |
387 { | |
388 background: WhiteSmoke; | |
389 } | |
390 | |
391 .unittest-row-even | |
392 { | |
393 background: #E5E5E5; | |
394 } | |
395 | |
396 .unittest-row-id | |
397 { | |
398 width: 3em; | |
399 } | |
400 | |
401 .unittest-row-status | |
402 { | |
403 width: 4em; | |
404 } | |
405 | |
406 .unittest-row-description | |
407 { | |
408 } | |
409 | |
410 '''; | |
411 } | |
412 | |
413 void useHtmlEnhancedConfiguration([bool isLayoutTest = false]) { | |
414 configure(new HtmlEnhancedConfiguration(isLayoutTest)); | |
415 } | |
OLD | NEW |