OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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 /// A simple unit test library for running tests in a browser. | 5 @deprecated |
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; | 6 library unittest.html_enhanced_config; |
10 | 7 |
11 import 'dart:collection' show LinkedHashMap; | 8 import 'src/simple_configuration.dart'; |
12 import 'dart:convert'; | |
13 import 'dart:html'; | |
14 import 'unittest.dart'; | |
15 | 9 |
| 10 /// This is a stub class used to preserve compatibility with unittest 0.11.*. |
| 11 /// |
| 12 /// It will be removed before the next version is released. |
| 13 @deprecated |
16 class HtmlEnhancedConfiguration extends SimpleConfiguration { | 14 class HtmlEnhancedConfiguration extends SimpleConfiguration { |
17 /// Whether this is run within dartium layout tests. | 15 HtmlEnhancedConfiguration(bool isLayoutTest); |
18 final bool _isLayoutTest; | |
19 HtmlEnhancedConfiguration(this._isLayoutTest); | |
20 | 16 |
21 var _onErrorSubscription = null; | 17 void processMessage(e) {} |
22 var _onMessageSubscription = null; | |
23 | |
24 void _installOnErrorHandler() { | |
25 if (_onErrorSubscription == null) { | |
26 // Listen for uncaught errors. | |
27 _onErrorSubscription = window.onError | |
28 .listen((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 = | |
36 window.onMessage.listen((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( | |
86 passed, failed, errors, results, _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 if (uncaughtError != null) { | |
113 te.children.add(new Element.html(""" | |
114 <div class='unittest-summary'> | |
115 <span class='unittest-error'>Uncaught error: $uncaughtError</span> | |
116 </div>""")); | |
117 } | |
118 | |
119 te.children.add(new Element.html(""" | |
120 <div class='unittest-summary'> | |
121 <span class='unittest-pass'>Total ${passed} passed</span>, | |
122 <span class='unittest-fail'>${failed} failed</span>, | |
123 <span class='unittest-error'> | |
124 ${errors + (uncaughtError == null ? 0 : 1)} errors</span> | |
125 </div>""")); | |
126 } | |
127 | |
128 te.children.add(new Element.html(""" | |
129 <div><button id='btnCollapseAll'>Collapse All</button></div> | |
130 """)); | |
131 | |
132 // handle the click event for the collapse all button | |
133 te.querySelector('#btnCollapseAll').onClick.listen((_) { | |
134 document | |
135 .querySelectorAll('.unittest-row') | |
136 .forEach((el) => el.attributes['class'] = el.attributes['class'] | |
137 .replaceAll('unittest-row ', 'unittest-row-hidden ')); | |
138 }); | |
139 | |
140 var previousGroup = ''; | |
141 var groupPassFail = true; | |
142 | |
143 // order by group and sort numerically within each group | |
144 var groupedBy = new LinkedHashMap<String, List<TestCase>>(); | |
145 | |
146 for (final t in results) { | |
147 if (!groupedBy.containsKey(t.currentGroup)) { | |
148 groupedBy[t.currentGroup] = new List<TestCase>(); | |
149 } | |
150 | |
151 groupedBy[t.currentGroup].add(t); | |
152 } | |
153 | |
154 // flatten the list again with tests ordered | |
155 List<TestCase> flattened = new List<TestCase>(); | |
156 | |
157 groupedBy.values.forEach((tList) { | |
158 tList.sort((tcA, tcB) => tcA.id - tcB.id); | |
159 flattened.addAll(tList); | |
160 }); | |
161 | |
162 var nonAlphanumeric = new RegExp('[^a-z0-9A-Z]'); | |
163 | |
164 // output group headers and test rows | |
165 for (final test_ in flattened) { | |
166 | |
167 // replace everything but numbers and letters from the group name with | |
168 // '_' so we can use in id and class properties. | |
169 var safeGroup = test_.currentGroup.replaceAll(nonAlphanumeric, '_'); | |
170 | |
171 if (test_.currentGroup != previousGroup) { | |
172 previousGroup = test_.currentGroup; | |
173 | |
174 var testsInGroup = results | |
175 .where((TestCase t) => t.currentGroup == previousGroup) | |
176 .toList(); | |
177 var groupTotalTestCount = testsInGroup.length; | |
178 var groupTestPassedCount = | |
179 testsInGroup.where((TestCase t) => t.result == 'pass').length; | |
180 groupPassFail = groupTotalTestCount == groupTestPassedCount; | |
181 var passFailClass = "unittest-group-status unittest-group-" | |
182 "status-${groupPassFail ? 'pass' : 'fail'}"; | |
183 | |
184 te.children.add(new Element.html(""" | |
185 <div> | |
186 <div id='${safeGroup}' | |
187 class='unittest-group ${safeGroup} test${safeGroup}'> | |
188 <div ${_isIE ? "style='display:inline-block' ": ""} | |
189 class='unittest-row-status'> | |
190 <div class='$passFailClass'></div> | |
191 </div> | |
192 <div ${_isIE ? "style='display:inline-block' ": ""}> | |
193 ${test_.currentGroup}</div> | |
194 | |
195 <div ${_isIE ? "style='display:inline-block' ": ""}> | |
196 (${groupTestPassedCount}/${groupTotalTestCount})</div> | |
197 </div> | |
198 </div>""")); | |
199 | |
200 // 'safeGroup' could be empty | |
201 var grp = | |
202 (safeGroup == '') ? null : te.querySelector('#${safeGroup}'); | |
203 if (grp != null) { | |
204 grp.onClick.listen((_) { | |
205 var row = document.querySelector('.unittest-row-${safeGroup}'); | |
206 if (row.attributes['class'].contains('unittest-row ')) { | |
207 document.querySelectorAll('.unittest-row-${safeGroup}').forEach( | |
208 (e) => e.attributes['class'] = e.attributes['class'] | |
209 .replaceAll('unittest-row ', 'unittest-row-hidden ')); | |
210 } else { | |
211 document.querySelectorAll('.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.children.clear(); | |
223 document.body.children.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 addRowElement(id, status, description) { | |
232 te.children.add(new Element.html(''' <div> | |
233 <div class='$display unittest-row-${groupID} $background'> | |
234 <div ${_isIE ? "style='display:inline-block' ": ""} | |
235 class='unittest-row-id'>$id</div> | |
236 <div ${_isIE ? "style='display:inline-block' ": ""} | |
237 class="unittest-row-status unittest-${test_.result}"> | |
238 $status</div> | |
239 <div ${_isIE ? "style='display:inline-block' ": ""} | |
240 class='unittest-row-description'>$description</div> | |
241 </div> | |
242 </div>''')); | |
243 } | |
244 | |
245 if (!test_.isComplete) { | |
246 addRowElement('${test_.id}', 'NO STATUS', 'Test did not complete.'); | |
247 return; | |
248 } | |
249 | |
250 addRowElement('${test_.id}', '${test_.result.toUpperCase()}', | |
251 '${test_.description}. ${HTML_ESCAPE.convert(test_.message)}'); | |
252 | |
253 if (test_.stackTrace != null) { | |
254 addRowElement('', '', | |
255 '<pre>${HTML_ESCAPE.convert(test_.stackTrace.toString())}</pre>'); | |
256 } | |
257 } | |
258 | |
259 static bool get _isIE => window.navigator.userAgent.contains('MSIE'); | |
260 | |
261 String get _htmlTestCSS => ''' | |
262 body{ | |
263 font-size: 14px; | |
264 font-family: 'Open Sans', 'Lucida Sans Unicode', 'Lucida Grande',''' | |
265 ''' sans-serif; | |
266 background: WhiteSmoke; | |
267 } | |
268 | |
269 .unittest-group | |
270 { | |
271 background: rgb(75,75,75); | |
272 width:98%; | |
273 color: WhiteSmoke; | |
274 font-weight: bold; | |
275 padding: 6px; | |
276 cursor: pointer; | |
277 | |
278 /* Provide some visual separation between groups for IE */ | |
279 ${_isIE ? "border-bottom:solid black 1px;": ""} | |
280 ${_isIE ? "border-top:solid #777777 1px;": ""} | |
281 | |
282 background-image: -webkit-linear-gradient(bottom, rgb(50,50,50) 0%, ''' | |
283 '''rgb(100,100,100) 100%); | |
284 background-image: -moz-linear-gradient(bottom, rgb(50,50,50) 0%, ''' | |
285 '''rgb(100,100,100) 100%); | |
286 background-image: -ms-linear-gradient(bottom, rgb(50,50,50) 0%, ''' | |
287 '''rgb(100,100,100) 100%); | |
288 background-image: linear-gradient(bottom, rgb(50,50,50) 0%, ''' | |
289 '''rgb(100,100,100) 100%); | |
290 | |
291 display: -webkit-box; | |
292 display: -moz-box; | |
293 display: -ms-box; | |
294 display: box; | |
295 | |
296 -webkit-box-orient: horizontal; | |
297 -moz-box-orient: horizontal; | |
298 -ms-box-orient: horizontal; | |
299 box-orient: horizontal; | |
300 | |
301 -webkit-box-align: center; | |
302 -moz-box-align: center; | |
303 -ms-box-align: center; | |
304 box-align: center; | |
305 } | |
306 | |
307 .unittest-group-status | |
308 { | |
309 width: 20px; | |
310 height: 20px; | |
311 border-radius: 20px; | |
312 margin-left: 10px; | |
313 } | |
314 | |
315 .unittest-group-status-pass{ | |
316 background: Green; | |
317 background: ''' | |
318 '''-webkit-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
319 background: ''' | |
320 '''-moz-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
321 background: ''' | |
322 '''-ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
323 background: ''' | |
324 '''radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
325 } | |
326 | |
327 .unittest-group-status-fail{ | |
328 background: Red; | |
329 background: ''' | |
330 '''-webkit-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); | |
331 background: ''' | |
332 '''-moz-radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); | |
333 background: ''' | |
334 '''-ms-radial-gradient(center, ellipse cover, #AAFFAA 0%,Green 100%); | |
335 background: radial-gradient(center, ellipse cover, #FFAAAA 0%,Red 100%); | |
336 } | |
337 | |
338 .unittest-overall{ | |
339 font-size: 20px; | |
340 } | |
341 | |
342 .unittest-summary{ | |
343 font-size: 18px; | |
344 } | |
345 | |
346 .unittest-pass{ | |
347 color: Green; | |
348 } | |
349 | |
350 .unittest-fail, .unittest-error | |
351 { | |
352 color: Red; | |
353 } | |
354 | |
355 .unittest-row | |
356 { | |
357 display: -webkit-box; | |
358 display: -moz-box; | |
359 display: -ms-box; | |
360 display: box; | |
361 -webkit-box-orient: horizontal; | |
362 -moz-box-orient: horizontal; | |
363 -ms-box-orient: horizontal; | |
364 box-orient: horizontal; | |
365 width: 100%; | |
366 } | |
367 | |
368 .unittest-row-hidden | |
369 { | |
370 display: none; | |
371 } | |
372 | |
373 .unittest-row-odd | |
374 { | |
375 background: WhiteSmoke; | |
376 } | |
377 | |
378 .unittest-row-even | |
379 { | |
380 background: #E5E5E5; | |
381 } | |
382 | |
383 .unittest-row-id | |
384 { | |
385 width: 3em; | |
386 } | |
387 | |
388 .unittest-row-status | |
389 { | |
390 width: 4em; | |
391 } | |
392 | |
393 .unittest-row-description | |
394 { | |
395 } | |
396 | |
397 '''; | |
398 } | 18 } |
399 | 19 |
400 void useHtmlEnhancedConfiguration([bool isLayoutTest = false]) { | 20 @deprecated |
401 unittestConfiguration = isLayoutTest ? _singletonLayout : _singletonNotLayout; | 21 void useHtmlEnhancedConfiguration([bool isLayoutTest]) {} |
402 } | |
403 | |
404 final _singletonLayout = new HtmlEnhancedConfiguration(true); | |
405 final _singletonNotLayout = new HtmlEnhancedConfiguration(false); | |
OLD | NEW |