1 // ==========================================================================
  2 // Project:   SproutCore Costello - Property Observing Library
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /*globals CoreTest Q$ */
  9 
 10 sc_require('system/plan');
 11 
 12 /** @static
 13   The runner will automatically run the default CoreTest.plan when the
 14   document is fully loaded.  It will also act as a delegate on the plan,
 15   logging the output to the screen or console.
 16 
 17   @since SproutCore 1.0
 18 */
 19 
 20 
 21 CoreTest.Runner = {
 22 
 23   /**
 24     The CoreTest plan.  If not set, a default plan will be created.
 25   */
 26   plan: null,
 27   errors: null,
 28 
 29   showProgress: false,
 30 
 31   create: function() {
 32     var len = arguments.length,
 33         ret = CoreTest.beget(this),
 34         idx ;
 35 
 36     for(idx=0;idx<len;idx++) CoreTest.mixin(ret, arguments[len]);
 37     if (!ret.plan) ret.plan = CoreTest.Plan.create({ delegate: ret });
 38     window.resizeTo(1400, 800);
 39     Q$(document).ready(function() { ret.begin(); });
 40     return ret ;
 41   },
 42 
 43   begin: function() {
 44     var plan = CoreTest.plan;
 45     plan.delegate = this;
 46     plan.run();
 47   },
 48 
 49   planDidBegin: function(plan) {
 50     // setup the report DOM element.
 51     var str = [
 52       '<div class="core-test">',
 53         '<div class="useragent">UserAgent</div>',
 54         '<div class="testresult">',
 55           '<label class="hide-passed">',
 56             '<input type="checkbox" checked="checked" /> Hide passed tests',
 57           '</label>'
 58     ];
 59 
 60     if (navigator.userAgent.indexOf('MSIE')==-1) {
 61       str.push(
 62           '<label class="show-progress">',
 63             '<input type="checkbox"'+(this.showProgress?' checked="checked"':'')+'" /> Show progress (slower)',
 64           '</label>'
 65       );
 66     }
 67 
 68     str.push(
 69           '<label class="show-progress">',
 70             '<input type="checkbox"'+(this.showProgress?' checked="checked"':'')+'" /> Show progress (slower)',
 71           '</label>',
 72           '<span class="status">Running...</span>',
 73         '</div>',
 74         '<div class="detail">',
 75           '<table>',
 76             '<thead><tr>',
 77               '<th class="desc">Test</th><th>Result</th>',
 78             '</tr></thead>',
 79             '<tbody><tr></tr></tbody>',
 80           '</table>',
 81         '</div>',
 82       '</div>'
 83     );
 84 
 85     this.report = Q$(str.join(''));
 86 
 87     this.report.find('.useragent').html(navigator.userAgent);
 88     this.logq = this.report.find('tbody');
 89     this.testCount = 0 ;
 90 
 91     // listen to change event
 92     var runner = this;
 93     this.hidePassedCheckbox = this.report.find('.hide-passed input');
 94     this.hidePassedCheckbox.change(function() {
 95       runner.hidePassedTestsDidChange();
 96     });
 97 
 98     this.showProgressCheckbox = this.report.find('.show-progress input');
 99     this.showProgressCheckbox.change(function() {
100       runner.showProgressCheckboxDidChange();
101     });
102 
103     Q$('body').append(this.report);
104   },
105 
106   hidePassedTestsDidChange: function() {
107     var checked = this.hidePassedCheckbox.is(':checked');
108 
109     if (checked) {
110       this.logq.addClass('hide-clean');
111     } else {
112       this.logq.removeClass('hide-clean');
113     }
114   },
115 
116   showProgressCheckboxDidChange: function(){
117     this.showProgress = this.showProgressCheckbox.is(':checked');
118     if (this.showProgress) { this.flush(); }
119   },
120 
121   planDidFinish: function(plan, r) {
122     this.flush();
123 
124     var result = this.report.find('.testresult .status');
125     var str = CoreTest.fmt('<span>Completed %@ tests in %@ msec. </span>'
126               +'<span class="total">%@</span> total assertions: ', r.tests,
127               r.runtime, r.total);
128 
129     if (r.passed > 0) {
130       str += CoreTest.fmt(' <span class="passed">%@ passed</span>', r.passed);
131     }
132 
133     if (r.failed > 0) {
134       str += CoreTest.fmt(' <span class="failed">%@ failed</span>', r.failed);
135     }
136 
137     if (r.errors > 0) {
138       str += CoreTest.fmt(' <span class="errors">%@ error%@</span>',
139             r.errors, (r.errors !== 1 ? 's' : ''));
140     }
141 
142     if (r.warnings > 0) {
143       str += CoreTest.fmt(' <span class="warnings">%@ warning%@</span>',
144             r.warnings, (r.warnings !== 1 ? 's' : ''));
145     }
146 
147     // if all tests passed, disable hiding them.  if some tests failed, hide
148     // them by default.
149     if (this.errors) this.errors.push('</tr></tbody></table>');
150     if ((r.failed + r.errors + r.warnings) > 0) {
151       this.hidePassedTestsDidChange(); // should be checked by default
152     } else {
153       this.report.find('.hide-passed').addClass('disabled')
154         .find('input').attr('disabled', true);
155       if (this.errors) this.errors.length = 0;
156     }
157     if(CoreTest.showUI) Q$('.core-test').css("right", "360px");
158     result.html(str);
159 
160     CoreTest.finished = true;
161 
162     if (this.errors) CoreTest.errors=this.errors.join('');
163 
164 
165     // Unload the SproutCore event system so that the user can select the text
166     // of the various events.  (It is handy when looking at failed tests.)
167     if (SC  &&  SC.Event  &&  SC.Event.unload) {
168       try {
169         var responder = SC.RootResponder.responder;
170         SC.Event.remove(document, 'selectstart', responder, responder.selectstart);
171       }
172       catch (e) {}
173     }
174 
175     if (typeof window.callPhantom === 'function') {
176       window.callPhantom(r);
177     }
178   },
179 
180   planDidRecord: function(plan, module, test, assertions, timings) {
181     var name = test,
182         s    = { passed: 0, failed: 0, errors: 0, warnings: 0 },
183         len  = assertions.length,
184         clean = '',
185         idx, cur, q;
186 
187     for(idx=0;idx<len;idx++) s[assertions[idx].result]++;
188     if ((s.failed + s.errors + s.warnings) === 0) clean = "clean" ;
189 
190     if (module) name = module.replace(/\n/g, '<br />') + " module: " + test ;
191     name = CoreTest.fmt('%@ - %@msec', name, timings.total_end - timings.total_begin);
192     // place results into a single string to append all at once.
193     var logstr = this.logstr ;
194     var errors =this.errors;
195     if (!logstr) logstr = this.logstr = [];
196     if (!this.errors) {
197       this.errors = ['<style type="text/css">* {font: 12px arial;}'+
198                     '.passed { background-color: #80D175; color: white;}'+
199                     '.failed { background-color: #ea4d4; color: black; }'+
200                     '.errors { background-color: red; color: black; }'+
201                     '.warnings { background-color: #E49723; color: black;}'+
202                     '.desc { text-align: left;}'+
203                     '</style><table style="border:1px solid"><thead>'+
204                     '<tr><th class="desc">'+navigator.userAgent+
205                     '</th><th>Result</th></tr>'+
206                     '</thead><tbody><tr>'];
207     }
208     logstr.push(CoreTest.fmt('<tr class="test %@"><th class="desc" colspan="2">'+
209           '%@ (<span class="passed">%@</span>, <span class="failed">%@</span>,'+
210           ' <span class="errors">%@</span>, <span class="warnings">%@</span>)'+
211           '</th></tr>', clean, name, s.passed, s.failed, s.errors, s.warnings));
212     if(s.failed>0 || s.errors>0){
213       this.errors.push(CoreTest.fmt('<tr class="test %@">'+
214           '<th style="background:grey; color:white" class="desc" colspan="2">'+
215           '%@ (<span class="passed">%@</span>, <span class="failed">%@</span>'+
216           ', <span class="errors">%@</span>, <span class="warnings">%@</span>'+
217           ')</th></tr>', clean, name, s.passed, s.failed, s.errors, s.warnings));
218     }
219 
220     len = assertions.length;
221     for(idx=0;idx<len;idx++) {
222       cur = assertions[idx];
223       clean = cur.result === CoreTest.OK ? 'clean' : 'dirty';
224       logstr.push(CoreTest.fmt('<tr class="%@"><td class="desc">%@</td>'
225           +'<td class="action %@">%@</td></tr>', clean, cur.message, cur.result,
226           (cur.result || '').toUpperCase()));
227       if(clean=='dirty'){
228         this.errors.push(CoreTest.fmt('<tr class="%@"><td class="desc">%@</td>'
229         +'<td class="action %@">%@</td></tr>', clean, cur.message, cur.result,
230         (cur.result || '').toUpperCase()));
231       }
232     }
233 
234     this.testCount++;
235     this.resultStr = CoreTest.fmt("Running – Completed %@ tests so far.", this.testCount);
236   },
237 
238   // called when the plan takes a break.  Good time to flush HTML output.
239   planDidPause: function(plan) {
240     if(!this._cacheResultSelector){
241       this._cacheResultSelector = this.report.find('.testresult .status');
242     }
243     var result = this._cacheResultSelector;
244 
245     if (this.resultStr && navigator.userAgent.indexOf('MSIE')==-1) result.html(this.resultStr);
246     this.resultStr = null ;
247 
248     if (this.showProgress) { this.flush(); }
249   },
250 
251   // flush any pending HTML changes...
252   flush: function() {
253     var logstr = this.logstr,
254         resultStr = this.resultStr,
255         result = this.report.find('.testresult .status');
256 
257     if (logstr) this.logq.append(this.logstr.join('')) ;
258 
259     if (resultStr) result.html(resultStr);
260     this.resultStr = this.logstr = null ;
261   }
262 
263 };
264