1 | |
---|
2 | /** |
---|
3 | * Module dependencies. |
---|
4 | */ |
---|
5 | |
---|
6 | var Base = require('./base') |
---|
7 | , utils = require('../utils') |
---|
8 | , Progress = require('../browser/progress') |
---|
9 | , escape = utils.escape; |
---|
10 | |
---|
11 | /** |
---|
12 | * Save timer references to avoid Sinon interfering (see GH-237). |
---|
13 | */ |
---|
14 | |
---|
15 | var Date = global.Date |
---|
16 | , setTimeout = global.setTimeout |
---|
17 | , setInterval = global.setInterval |
---|
18 | , clearTimeout = global.clearTimeout |
---|
19 | , clearInterval = global.clearInterval; |
---|
20 | |
---|
21 | /** |
---|
22 | * Expose `HTML`. |
---|
23 | */ |
---|
24 | |
---|
25 | exports = module.exports = HTML; |
---|
26 | |
---|
27 | /** |
---|
28 | * Stats template. |
---|
29 | */ |
---|
30 | |
---|
31 | var statsTemplate = '<ul id="mocha-stats">' |
---|
32 | + '<li class="progress"><canvas width="40" height="40"></canvas></li>' |
---|
33 | + '<li class="passes"><a href="#">passes:</a> <em>0</em></li>' |
---|
34 | + '<li class="failures"><a href="#">failures:</a> <em>0</em></li>' |
---|
35 | + '<li class="duration">duration: <em>0</em>s</li>' |
---|
36 | + '</ul>'; |
---|
37 | |
---|
38 | /** |
---|
39 | * Initialize a new `HTML` reporter. |
---|
40 | * |
---|
41 | * @param {Runner} runner |
---|
42 | * @api public |
---|
43 | */ |
---|
44 | |
---|
45 | function HTML(runner, root) { |
---|
46 | Base.call(this, runner); |
---|
47 | |
---|
48 | var self = this |
---|
49 | , stats = this.stats |
---|
50 | , total = runner.total |
---|
51 | , stat = fragment(statsTemplate) |
---|
52 | , items = stat.getElementsByTagName('li') |
---|
53 | , passes = items[1].getElementsByTagName('em')[0] |
---|
54 | , passesLink = items[1].getElementsByTagName('a')[0] |
---|
55 | , failures = items[2].getElementsByTagName('em')[0] |
---|
56 | , failuresLink = items[2].getElementsByTagName('a')[0] |
---|
57 | , duration = items[3].getElementsByTagName('em')[0] |
---|
58 | , canvas = stat.getElementsByTagName('canvas')[0] |
---|
59 | , report = fragment('<ul id="mocha-report"></ul>') |
---|
60 | , stack = [report] |
---|
61 | , progress |
---|
62 | , ctx |
---|
63 | |
---|
64 | root = root || document.getElementById('mocha'); |
---|
65 | |
---|
66 | if (canvas.getContext) { |
---|
67 | var ratio = window.devicePixelRatio || 1; |
---|
68 | canvas.style.width = canvas.width; |
---|
69 | canvas.style.height = canvas.height; |
---|
70 | canvas.width *= ratio; |
---|
71 | canvas.height *= ratio; |
---|
72 | ctx = canvas.getContext('2d'); |
---|
73 | ctx.scale(ratio, ratio); |
---|
74 | progress = new Progress; |
---|
75 | } |
---|
76 | |
---|
77 | if (!root) return error('#mocha div missing, add it to your document'); |
---|
78 | |
---|
79 | // pass toggle |
---|
80 | on(passesLink, 'click', function(){ |
---|
81 | unhide(); |
---|
82 | var name = /pass/.test(report.className) ? '' : ' pass'; |
---|
83 | report.className = report.className.replace(/fail|pass/g, '') + name; |
---|
84 | if (report.className.trim()) hideSuitesWithout('test pass'); |
---|
85 | }); |
---|
86 | |
---|
87 | // failure toggle |
---|
88 | on(failuresLink, 'click', function(){ |
---|
89 | unhide(); |
---|
90 | var name = /fail/.test(report.className) ? '' : ' fail'; |
---|
91 | report.className = report.className.replace(/fail|pass/g, '') + name; |
---|
92 | if (report.className.trim()) hideSuitesWithout('test fail'); |
---|
93 | }); |
---|
94 | |
---|
95 | root.appendChild(stat); |
---|
96 | root.appendChild(report); |
---|
97 | |
---|
98 | if (progress) progress.size(40); |
---|
99 | |
---|
100 | runner.on('suite', function(suite){ |
---|
101 | if (suite.root) return; |
---|
102 | |
---|
103 | // suite |
---|
104 | var url = self.suiteURL(suite); |
---|
105 | var el = fragment('<li class="suite"><h1><a href="%s">%s</a></h1></li>', url, escape(suite.title)); |
---|
106 | |
---|
107 | // container |
---|
108 | stack[0].appendChild(el); |
---|
109 | stack.unshift(document.createElement('ul')); |
---|
110 | el.appendChild(stack[0]); |
---|
111 | }); |
---|
112 | |
---|
113 | runner.on('suite end', function(suite){ |
---|
114 | if (suite.root) return; |
---|
115 | stack.shift(); |
---|
116 | }); |
---|
117 | |
---|
118 | runner.on('fail', function(test, err){ |
---|
119 | if ('hook' == test.type) runner.emit('test end', test); |
---|
120 | }); |
---|
121 | |
---|
122 | runner.on('test end', function(test){ |
---|
123 | // TODO: add to stats |
---|
124 | var percent = stats.tests / this.total * 100 | 0; |
---|
125 | if (progress) progress.update(percent).draw(ctx); |
---|
126 | |
---|
127 | // update stats |
---|
128 | var ms = new Date - stats.start; |
---|
129 | text(passes, stats.passes); |
---|
130 | text(failures, stats.failures); |
---|
131 | text(duration, (ms / 1000).toFixed(2)); |
---|
132 | |
---|
133 | // test |
---|
134 | if ('passed' == test.state) { |
---|
135 | var url = self.testURL(test); |
---|
136 | var el = fragment('<li class="test pass %e"><h2>%e<span class="duration">%ems</span> <a href="%s" class="replay">â£</a></h2></li>', test.speed, test.title, test.duration, url); |
---|
137 | } else if (test.pending) { |
---|
138 | var el = fragment('<li class="test pass pending"><h2>%e</h2></li>', test.title); |
---|
139 | } else { |
---|
140 | var el = fragment('<li class="test fail"><h2>%e <a href="?grep=%e" class="replay">â£</a></h2></li>', test.title, encodeURIComponent(test.fullTitle())); |
---|
141 | var str = test.err.stack || test.err.toString(); |
---|
142 | |
---|
143 | // FF / Opera do not add the message |
---|
144 | if (!~str.indexOf(test.err.message)) { |
---|
145 | str = test.err.message + '\n' + str; |
---|
146 | } |
---|
147 | |
---|
148 | // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we |
---|
149 | // check for the result of the stringifying. |
---|
150 | if ('[object Error]' == str) str = test.err.message; |
---|
151 | |
---|
152 | // Safari doesn't give you a stack. Let's at least provide a source line. |
---|
153 | if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) { |
---|
154 | str += "\n(" + test.err.sourceURL + ":" + test.err.line + ")"; |
---|
155 | } |
---|
156 | |
---|
157 | el.appendChild(fragment('<pre class="error">%e</pre>', str)); |
---|
158 | } |
---|
159 | |
---|
160 | // toggle code |
---|
161 | // TODO: defer |
---|
162 | if (!test.pending) { |
---|
163 | var h2 = el.getElementsByTagName('h2')[0]; |
---|
164 | |
---|
165 | on(h2, 'click', function(){ |
---|
166 | pre.style.display = 'none' == pre.style.display |
---|
167 | ? 'block' |
---|
168 | : 'none'; |
---|
169 | }); |
---|
170 | |
---|
171 | var pre = fragment('<pre><code>%e</code></pre>', utils.clean(test.fn.toString())); |
---|
172 | el.appendChild(pre); |
---|
173 | pre.style.display = 'none'; |
---|
174 | } |
---|
175 | |
---|
176 | // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. |
---|
177 | if (stack[0]) stack[0].appendChild(el); |
---|
178 | }); |
---|
179 | } |
---|
180 | |
---|
181 | /** |
---|
182 | * Provide suite URL |
---|
183 | * |
---|
184 | * @param {Object} [suite] |
---|
185 | */ |
---|
186 | |
---|
187 | HTML.prototype.suiteURL = function(suite){ |
---|
188 | return '?grep=' + encodeURIComponent(suite.fullTitle()); |
---|
189 | }; |
---|
190 | |
---|
191 | /** |
---|
192 | * Provide test URL |
---|
193 | * |
---|
194 | * @param {Object} [test] |
---|
195 | */ |
---|
196 | |
---|
197 | HTML.prototype.testURL = function(test){ |
---|
198 | return '?grep=' + encodeURIComponent(test.fullTitle()); |
---|
199 | }; |
---|
200 | |
---|
201 | /** |
---|
202 | * Display error `msg`. |
---|
203 | */ |
---|
204 | |
---|
205 | function error(msg) { |
---|
206 | document.body.appendChild(fragment('<div id="mocha-error">%s</div>', msg)); |
---|
207 | } |
---|
208 | |
---|
209 | /** |
---|
210 | * Return a DOM fragment from `html`. |
---|
211 | */ |
---|
212 | |
---|
213 | function fragment(html) { |
---|
214 | var args = arguments |
---|
215 | , div = document.createElement('div') |
---|
216 | , i = 1; |
---|
217 | |
---|
218 | div.innerHTML = html.replace(/%([se])/g, function(_, type){ |
---|
219 | switch (type) { |
---|
220 | case 's': return String(args[i++]); |
---|
221 | case 'e': return escape(args[i++]); |
---|
222 | } |
---|
223 | }); |
---|
224 | |
---|
225 | return div.firstChild; |
---|
226 | } |
---|
227 | |
---|
228 | /** |
---|
229 | * Check for suites that do not have elements |
---|
230 | * with `classname`, and hide them. |
---|
231 | */ |
---|
232 | |
---|
233 | function hideSuitesWithout(classname) { |
---|
234 | var suites = document.getElementsByClassName('suite'); |
---|
235 | for (var i = 0; i < suites.length; i++) { |
---|
236 | var els = suites[i].getElementsByClassName(classname); |
---|
237 | if (0 == els.length) suites[i].className += ' hidden'; |
---|
238 | } |
---|
239 | } |
---|
240 | |
---|
241 | /** |
---|
242 | * Unhide .hidden suites. |
---|
243 | */ |
---|
244 | |
---|
245 | function unhide() { |
---|
246 | var els = document.getElementsByClassName('suite hidden'); |
---|
247 | for (var i = 0; i < els.length; ++i) { |
---|
248 | els[i].className = els[i].className.replace('suite hidden', 'suite'); |
---|
249 | } |
---|
250 | } |
---|
251 | |
---|
252 | /** |
---|
253 | * Set `el` text to `str`. |
---|
254 | */ |
---|
255 | |
---|
256 | function text(el, str) { |
---|
257 | if (el.textContent) { |
---|
258 | el.textContent = str; |
---|
259 | } else { |
---|
260 | el.innerText = str; |
---|
261 | } |
---|
262 | } |
---|
263 | |
---|
264 | /** |
---|
265 | * Listen on `event` with callback `fn`. |
---|
266 | */ |
---|
267 | |
---|
268 | function on(el, event, fn) { |
---|
269 | if (el.addEventListener) { |
---|
270 | el.addEventListener(event, fn, false); |
---|
271 | } else { |
---|
272 | el.attachEvent('on' + event, fn); |
---|
273 | } |
---|
274 | } |
---|