webcit_before_automake is now the trunk
[citadel.git] / webcit / static / unittest.js
1 // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 //           (c) 2005 Jon Tirsen (http://www.tirsen.com)
3 //           (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/)
4 //
5 // Permission is hereby granted, free of charge, to any person obtaining
6 // a copy of this software and associated documentation files (the
7 // "Software"), to deal in the Software without restriction, including
8 // without limitation the rights to use, copy, modify, merge, publish,
9 // distribute, sublicense, and/or sell copies of the Software, and to
10 // permit persons to whom the Software is furnished to do so, subject to
11 // the following conditions:
12 // 
13 // The above copyright notice and this permission notice shall be
14 // included in all copies or substantial portions of the Software.
15 // 
16 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24
25 // experimental, Firefox-only
26 Event.simulateMouse = function(element, eventName) {
27   var options = Object.extend({
28     pointerX: 0,
29     pointerY: 0,
30     buttons: 0
31   }, arguments[2] || {});
32   var oEvent = document.createEvent("MouseEvents");
33   oEvent.initMouseEvent(eventName, true, true, document.defaultView, 
34     options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 
35     false, false, false, false, 0, $(element));
36   
37   if(this.mark) Element.remove(this.mark);
38   this.mark = document.createElement('div');
39   this.mark.appendChild(document.createTextNode(" "));
40   document.body.appendChild(this.mark);
41   this.mark.style.position = 'absolute';
42   this.mark.style.top = options.pointerY + "px";
43   this.mark.style.left = options.pointerX + "px";
44   this.mark.style.width = "5px";
45   this.mark.style.height = "5px;";
46   this.mark.style.borderTop = "1px solid red;"
47   this.mark.style.borderLeft = "1px solid red;"
48   
49   if(this.step)
50     alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
51   
52   $(element).dispatchEvent(oEvent);
53 };
54
55 // Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
56 // You need to downgrade to 1.0.4 for now to get this working
57 // See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
58 Event.simulateKey = function(element, eventName) {
59   var options = Object.extend({
60     ctrlKey: false,
61     altKey: false,
62     shiftKey: false,
63     metaKey: false,
64     keyCode: 0,
65     charCode: 0
66   }, arguments[2] || {});
67
68   var oEvent = document.createEvent("KeyEvents");
69   oEvent.initKeyEvent(eventName, true, true, window, 
70     options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
71     options.keyCode, options.charCode );
72   $(element).dispatchEvent(oEvent);
73 };
74
75 Event.simulateKeys = function(element, command) {
76   for(var i=0; i<command.length; i++) {
77     Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
78   }
79 };
80
81 var Test = {}
82 Test.Unit = {};
83
84 // security exception workaround
85 Test.Unit.inspect = function(obj) {
86   var info = [];
87
88   if(typeof obj=="string" || 
89      typeof obj=="number") {
90     return obj;
91   } else {
92     for(property in obj)
93       if(typeof obj[property]!="function")
94         info.push(property + ' => ' + 
95           (typeof obj[property] == "string" ?
96             '"' + obj[property] + '"' :
97             obj[property]));
98   }
99
100   return ("'" + obj + "' #" + typeof obj + 
101     ": {" + info.join(", ") + "}");
102 }
103
104 Test.Unit.Logger = Class.create();
105 Test.Unit.Logger.prototype = {
106   initialize: function(log) {
107     this.log = $(log);
108     if (this.log) {
109       this._createLogTable();
110     }
111   },
112   start: function(testName) {
113     if (!this.log) return;
114     this.testName = testName;
115     this.lastLogLine = document.createElement('tr');
116     this.statusCell = document.createElement('td');
117     this.nameCell = document.createElement('td');
118     this.nameCell.appendChild(document.createTextNode(testName));
119     this.messageCell = document.createElement('td');
120     this.lastLogLine.appendChild(this.statusCell);
121     this.lastLogLine.appendChild(this.nameCell);
122     this.lastLogLine.appendChild(this.messageCell);
123     this.loglines.appendChild(this.lastLogLine);
124   },
125   finish: function(status, summary) {
126     if (!this.log) return;
127     this.lastLogLine.className = status;
128     this.statusCell.innerHTML = status;
129     this.messageCell.innerHTML = this._toHTML(summary);
130   },
131   message: function(message) {
132     if (!this.log) return;
133     this.messageCell.innerHTML = this._toHTML(message);
134   },
135   summary: function(summary) {
136     if (!this.log) return;
137     this.logsummary.innerHTML = this._toHTML(summary);
138   },
139   _createLogTable: function() {
140     this.log.innerHTML =
141     '<div id="logsummary"></div>' +
142     '<table id="logtable">' +
143     '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
144     '<tbody id="loglines"></tbody>' +
145     '</table>';
146     this.logsummary = $('logsummary')
147     this.loglines = $('loglines');
148   },
149   _toHTML: function(txt) {
150     return txt.escapeHTML().replace(/\n/g,"<br/>");
151   }
152 }
153
154 Test.Unit.Runner = Class.create();
155 Test.Unit.Runner.prototype = {
156   initialize: function(testcases) {
157     this.options = Object.extend({
158       testLog: 'testlog'
159     }, arguments[1] || {});
160     this.options.resultsURL = this.parseResultsURLQueryParameter();
161     if (this.options.testLog) {
162       this.options.testLog = $(this.options.testLog) || null;
163     }
164     if(this.options.tests) {
165       this.tests = [];
166       for(var i = 0; i < this.options.tests.length; i++) {
167         if(/^test/.test(this.options.tests[i])) {
168           this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
169         }
170       }
171     } else {
172       if (this.options.test) {
173         this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
174       } else {
175         this.tests = [];
176         for(var testcase in testcases) {
177           if(/^test/.test(testcase)) {
178             this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"]));
179           }
180         }
181       }
182     }
183     this.currentTest = 0;
184     this.logger = new Test.Unit.Logger(this.options.testLog);
185     setTimeout(this.runTests.bind(this), 1000);
186   },
187   parseResultsURLQueryParameter: function() {
188     return window.location.search.parseQuery()["resultsURL"];
189   },
190   // Returns:
191   //  "ERROR" if there was an error,
192   //  "FAILURE" if there was a failure, or
193   //  "SUCCESS" if there was neither
194   getResult: function() {
195     var hasFailure = false;
196     for(var i=0;i<this.tests.length;i++) {
197       if (this.tests[i].errors > 0) {
198         return "ERROR";
199       }
200       if (this.tests[i].failures > 0) {
201         hasFailure = true;
202       }
203     }
204     if (hasFailure) {
205       return "FAILURE";
206     } else {
207       return "SUCCESS";
208     }
209   },
210   postResults: function() {
211     if (this.options.resultsURL) {
212       new Ajax.Request(this.options.resultsURL, 
213         { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
214     }
215   },
216   runTests: function() {
217     var test = this.tests[this.currentTest];
218     if (!test) {
219       // finished!
220       this.postResults();
221       this.logger.summary(this.summary());
222       return;
223     }
224     if(!test.isWaiting) {
225       this.logger.start(test.name);
226     }
227     test.run();
228     if(test.isWaiting) {
229       this.logger.message("Waiting for " + test.timeToWait + "ms");
230       setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
231     } else {
232       this.logger.finish(test.status(), test.summary());
233       this.currentTest++;
234       // tail recursive, hopefully the browser will skip the stackframe
235       this.runTests();
236     }
237   },
238   summary: function() {
239     var assertions = 0;
240     var failures = 0;
241     var errors = 0;
242     var messages = [];
243     for(var i=0;i<this.tests.length;i++) {
244       assertions +=   this.tests[i].assertions;
245       failures   +=   this.tests[i].failures;
246       errors     +=   this.tests[i].errors;
247     }
248     return (
249       this.tests.length + " tests, " + 
250       assertions + " assertions, " + 
251       failures   + " failures, " +
252       errors     + " errors");
253   }
254 }
255
256 Test.Unit.Assertions = Class.create();
257 Test.Unit.Assertions.prototype = {
258   initialize: function() {
259     this.assertions = 0;
260     this.failures   = 0;
261     this.errors     = 0;
262     this.messages   = [];
263   },
264   summary: function() {
265     return (
266       this.assertions + " assertions, " + 
267       this.failures   + " failures, " +
268       this.errors     + " errors" + "\n" +
269       this.messages.join("\n"));
270   },
271   pass: function() {
272     this.assertions++;
273   },
274   fail: function(message) {
275     this.failures++;
276     this.messages.push("Failure: " + message);
277   },
278   error: function(error) {
279     this.errors++;
280     this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
281   },
282   status: function() {
283     if (this.failures > 0) return 'failed';
284     if (this.errors > 0) return 'error';
285     return 'passed';
286   },
287   assert: function(expression) {
288     var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
289     try { expression ? this.pass() : 
290       this.fail(message); }
291     catch(e) { this.error(e); }
292   },
293   assertEqual: function(expected, actual) {
294     var message = arguments[2] || "assertEqual";
295     try { (expected == actual) ? this.pass() :
296       this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
297         '", actual "' + Test.Unit.inspect(actual) + '"'); }
298     catch(e) { this.error(e); }
299   },
300   assertNotEqual: function(expected, actual) {
301     var message = arguments[2] || "assertNotEqual";
302     try { (expected != actual) ? this.pass() : 
303       this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
304     catch(e) { this.error(e); }
305   },
306   assertNull: function(obj) {
307     var message = arguments[1] || 'assertNull'
308     try { (obj==null) ? this.pass() : 
309       this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
310     catch(e) { this.error(e); }
311   },
312   assertHidden: function(element) {
313     var message = arguments[1] || 'assertHidden';
314     this.assertEqual("none", element.style.display, message);
315   },
316   assertNotNull: function(object) {
317     var message = arguments[1] || 'assertNotNull';
318     this.assert(object != null, message);
319   },
320   assertInstanceOf: function(expected, actual) {
321     var message = arguments[2] || 'assertInstanceOf';
322     try { 
323       (actual instanceof expected) ? this.pass() : 
324       this.fail(message + ": object was not an instance of the expected type"); }
325     catch(e) { this.error(e); } 
326   },
327   assertNotInstanceOf: function(expected, actual) {
328     var message = arguments[2] || 'assertNotInstanceOf';
329     try { 
330       !(actual instanceof expected) ? this.pass() : 
331       this.fail(message + ": object was an instance of the not expected type"); }
332     catch(e) { this.error(e); } 
333   },
334   _isVisible: function(element) {
335     element = $(element);
336     if(!element.parentNode) return true;
337     this.assertNotNull(element);
338     if(element.style && Element.getStyle(element, 'display') == 'none')
339       return false;
340     
341     return this._isVisible(element.parentNode);
342   },
343   assertNotVisible: function(element) {
344     this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
345   },
346   assertVisible: function(element) {
347     this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
348   }
349 }
350
351 Test.Unit.Testcase = Class.create();
352 Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
353   initialize: function(name, test, setup, teardown) {
354     Test.Unit.Assertions.prototype.initialize.bind(this)();
355     this.name           = name;
356     this.test           = test || function() {};
357     this.setup          = setup || function() {};
358     this.teardown       = teardown || function() {};
359     this.isWaiting      = false;
360     this.timeToWait     = 1000;
361   },
362   wait: function(time, nextPart) {
363     this.isWaiting = true;
364     this.test = nextPart;
365     this.timeToWait = time;
366   },
367   run: function() {
368     try {
369       try {
370         if (!this.isWaiting) this.setup.bind(this)();
371         this.isWaiting = false;
372         this.test.bind(this)();
373       } finally {
374         if(!this.isWaiting) {
375           this.teardown.bind(this)();
376         }
377       }
378     }
379     catch(e) { this.error(e); }
380   }
381 });