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