1 /**
  2  * Copyright (C) 2012,2013 KO GmbH <copyright@kogmbh.com>
  3  *
  4  * @licstart
  5  * This file is part of WebODF.
  6  *
  7  * WebODF is free software: you can redistribute it and/or modify it
  8  * under the terms of the GNU Affero General Public License (GNU AGPL)
  9  * as published by the Free Software Foundation, either version 3 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * WebODF is distributed in the hope that it will be useful, but
 13  * WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU Affero General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Affero General Public License
 18  * along with WebODF.  If not, see <http://www.gnu.org/licenses/>.
 19  * @licend
 20  *
 21  * @source: http://www.webodf.org/
 22  * @source: https://github.com/kogmbh/WebODF/
 23  */
 24 
 25 /*global runtime, Runtime, core, Node, Element*/
 26 /*jslint evil: true, continue: true, emptyblock: true, unparam: true*/
 27 
 28 /**
 29  * @typedef{{f:function(),name:!string,expectFail:boolean}}
 30  */
 31 core.TestData;
 32 /**
 33  * @typedef{{f:function(function()),name:!string,expectFail:boolean}}
 34  */
 35 core.AsyncTestData;
 36 /**
 37  * @interface
 38  */
 39 core.UnitTest = function UnitTest() {"use strict"; };
 40 /**
 41  * @return {undefined}
 42  */
 43 core.UnitTest.prototype.setUp = function () {"use strict"; };
 44 /**
 45  * @return {undefined}
 46  */
 47 core.UnitTest.prototype.tearDown = function () {"use strict"; };
 48 /**
 49  * @return {!string}
 50  */
 51 core.UnitTest.prototype.description = function () {"use strict"; };
 52 /**
 53  * @return {!Array.<!core.TestData>}
 54  */
 55 core.UnitTest.prototype.tests = function () {"use strict"; };
 56 /**
 57  * @return {!Array.<!core.AsyncTestData>}
 58  */
 59 core.UnitTest.prototype.asyncTests = function () {"use strict"; };
 60 
 61 /**
 62  * @return {!HTMLDivElement}
 63  */
 64 core.UnitTest.provideTestAreaDiv = function () {
 65     "use strict";
 66     var maindoc = runtime.getWindow().document,
 67         testarea = maindoc.getElementById('testarea');
 68 
 69     runtime.assert(!testarea, "Unclean test environment, found a div with id \"testarea\".");
 70 
 71     testarea = maindoc.createElement('div');
 72     testarea.setAttribute('id', 'testarea');
 73     maindoc.body.appendChild(testarea);
 74     return /**@type{!HTMLDivElement}*/(testarea);
 75 };
 76 
 77 /**
 78  * @return {undefined}
 79  */
 80 core.UnitTest.cleanupTestAreaDiv = function () {
 81     "use strict";
 82     var maindoc = runtime.getWindow().document,
 83         testarea = maindoc.getElementById('testarea');
 84 
 85     runtime.assert((!!testarea && (testarea.parentNode === maindoc.body)), "Test environment broken, found no div with id \"testarea\" below body.");
 86     maindoc.body.removeChild(testarea);
 87 };
 88 
 89 /**
 90  * Creates and returns an XML document
 91  * @param {!string} rootElementName  name of the root element, "prefix:localName"
 92  * @param {!string} xmlBodyString  XML fragment to insert in the document between the root tags
 93  * @param {!Object.<!string, !string>} namespaceMap Name-value pairs that map the
 94  *                                     prefix onto the appropriate uri namespace
 95  * @return {?Document}
 96  */
 97 core.UnitTest.createXmlDocument = function (rootElementName, xmlBodyString, namespaceMap) {
 98     "use strict";
 99     var /**@type{!string}*/
100         xmlDoc = "<?xml version='1.0' encoding='UTF-8'?>";
101 
102     xmlDoc += "<"+ rootElementName;
103     Object.keys(namespaceMap).forEach(function (key) {
104         xmlDoc += " xmlns:" + key + '="' + namespaceMap[key] + '"';
105     });
106     xmlDoc += ">";
107     xmlDoc += xmlBodyString;
108     xmlDoc += "</"+rootElementName+">";
109 
110     return runtime.parseXML(xmlDoc);
111 };
112 
113 /**
114  * Creates and returns a simple ODT document
115  * @param {!string} xml Xml fragment to insert in the document between the
116  *                      <office:document>..</office:document> tags
117  * @param {!Object.<string, string>} namespaceMap Name-value pairs that map the
118  *                                   prefix onto the appropriate uri namespace
119  * @return {?Document}
120  */
121 core.UnitTest.createOdtDocument = function (xml, namespaceMap) {
122     "use strict";
123     return core.UnitTest.createXmlDocument("office:document", xml, namespaceMap);
124 };
125 
126 
127 /**
128  * @constructor
129  */
130 core.UnitTestLogger = function UnitTestLogger() {
131     "use strict";
132     var /**@type{!Array.<{category:string,message:string}>}*/
133         messages = [],
134         /**@type{number}*/
135         errors = 0,
136         start = 0,
137         suite = "",
138         test = "";
139     /**
140      * @param {string} suiteName
141      * @param {string} testName
142      */
143     this.startTest = function (suiteName, testName) {
144         messages = [];
145         errors = 0;
146         suite = suiteName;
147         test = testName;
148         start = Date.now();
149     };
150     /**
151      * @return {!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number}}
152      */
153     this.endTest = function () {
154         var end = Date.now();
155         return {
156             description: test,
157             suite: [suite, test],
158             success: errors === 0,
159             log: messages,
160             time: end - start
161         };
162     };
163     /**
164      * @param {string} msg
165      */
166     this.debug = function (msg) {
167         messages.push({category: "debug", message: msg});
168     };
169     /**
170      * @param {string} msg
171      */
172     this.fail = function (msg) {
173         errors += 1;
174         messages.push({category: "fail", message: msg});
175     };
176     /**
177      * @param {string} msg
178      */
179     this.pass = function (msg) {
180         messages.push({category: "pass", message: msg});
181     };
182 };
183 
184 /**
185  * @constructor
186  * @param {string} resourcePrefix
187  * @param {!core.UnitTestLogger} logger
188  */
189 core.UnitTestRunner = function UnitTestRunner(resourcePrefix, logger) {
190     "use strict";
191     var /**@type{number}*/
192         failedTests = 0,
193         /**@type{number}*/
194         failedTestsOnBeginExpectFail,
195         areObjectsEqual,
196         expectFail = false;
197     /**
198      * @return {string}
199      */
200     this.resourcePrefix = function () {
201         return resourcePrefix;
202     };
203     /**
204      * @return {undefined}
205      */
206     this.beginExpectFail = function () {
207         failedTestsOnBeginExpectFail = failedTests;
208         expectFail = true;
209     };
210     /**
211      * @return {undefined}
212      */
213     this.endExpectFail = function () {
214         var hasNoFailedTests = (failedTestsOnBeginExpectFail === failedTests);
215         expectFail = false;
216 
217         failedTests = failedTestsOnBeginExpectFail;
218         if (hasNoFailedTests) {
219             failedTests += 1;
220             logger.fail("Expected at least one failed test, but none registered.");
221         }
222     };
223     /**
224      * @param {string} msg
225      * @return {undefined}
226      */
227     function debug(msg) {
228         logger.debug(msg);
229     }
230     /**
231      * @param {string} msg
232      * @return {undefined}
233      */
234     function testFailed(msg) {
235         failedTests += 1;
236         if (!expectFail) {
237             logger.fail(msg);
238         } else {
239             logger.debug(msg);
240         }
241     }
242     /**
243      * @param {string} msg
244      * @return {undefined}
245      */
246     function testPassed(msg) {
247         logger.pass(msg);
248     }
249     /**
250      * @param {!Array.<*>} a actual
251      * @param {!Array.<*>} b expected
252      * @return {!boolean}
253      */
254     function areArraysEqual(a, b) {
255         var i;
256         try {
257             if (a.length !== b.length) {
258                 testFailed("array of length " + a.length + " should be "
259                            + b.length + " long");
260                 return false;
261             }
262             for (i = 0; i < a.length; i += 1) {
263                 if (a[i] !== b[i]) {
264                     testFailed(a[i] + " should be " + b[i] + " at array index "
265                                + i);
266                     return false;
267                 }
268             }
269         } catch (ex) {
270             return false;
271         }
272         return true;
273     }
274     /**
275      * @param {!Element} a actual
276      * @param {!Element} b expected
277      * @param {!boolean} skipReverseCheck
278      * @return {!boolean}
279      */
280     function areAttributesEqual(a, b, skipReverseCheck) {
281         var aatts = a.attributes,
282             n = aatts.length,
283             i,
284             att,
285             v;
286         for (i = 0; i < n; i += 1) {
287             att = /**@type{!Attr}*/(aatts.item(i));
288             if (att.prefix !== "xmlns" && att.namespaceURI !== "urn:webodf:names:steps") {
289                 v = b.getAttributeNS(att.namespaceURI, att.localName);
290                 if (!b.hasAttributeNS(att.namespaceURI, att.localName)) {
291                     testFailed("Attribute " + att.localName + " with value " + att.value + " was not present");
292                     return false;
293                 }
294                 if (v !== att.value) {
295                     testFailed("Attribute " + att.localName + " was " + v + " should be " + att.value);
296                     return false;
297                 }
298             }
299         }
300         return skipReverseCheck ? true : areAttributesEqual(b, a, true);
301     }
302     /**
303      * @param {!Node} a actual
304      * @param {!Node} b expected
305      * @return {!boolean}
306      */
307     function areNodesEqual(a, b) {
308         var an, bn,
309             atype = a.nodeType,
310             btype = b.nodeType;
311         if (atype !== btype) {
312             testFailed("Nodetype '" + atype + "' should be '" + btype + "'");
313             return false;
314         }
315         if (atype === Node.TEXT_NODE) {
316             if (/**@type{!Text}*/(a).data === /**@type{!Text}*/(b).data) {
317                 return true;
318             }
319             testFailed("Textnode data '" + /**@type{!Text}*/(a).data
320                        +  "' should be '" + /**@type{!Text}*/(b).data + "'");
321             return false;
322         }
323         runtime.assert(atype === Node.ELEMENT_NODE,
324             "Only textnodes and elements supported.");
325         if (a.namespaceURI !== b.namespaceURI) {
326             testFailed("namespace '" + a.namespaceURI + "' should be '"
327                     + b.namespaceURI + "'");
328             return false;
329         }
330         if (a.localName !== b.localName) {
331             testFailed("localName '" + a.localName + "' should be '"
332                     + b.localName + "'");
333             return false;
334         }
335         if (!areAttributesEqual(/**@type{!Element}*/(a),
336                                 /**@type{!Element}*/(b), false)) {
337             return false;
338         }
339         an = a.firstChild;
340         bn = b.firstChild;
341         while (an) {
342             if (!bn) {
343                 testFailed("Nodetype '" + an.nodeType + "' is unexpected here.");
344                 return false;
345             }
346             if (!areNodesEqual(an, bn)) {
347                 return false;
348             }
349             an = an.nextSibling;
350             bn = bn.nextSibling;
351         }
352         if (bn) {
353             testFailed("Nodetype '" + bn.nodeType + "' is missing here.");
354             return false;
355         }
356         return true;
357     }
358     /**
359      * @param {!*} actual
360      * @param {!*} expected
361      * @param {!number=} absoluteTolerance  absolute tolerance for number comparison
362      * @return {!boolean}
363      */
364     function isResultCorrect(actual, expected, absoluteTolerance) {
365         var diff;
366 
367         if (expected === 0) {
368             return actual === expected && (1 / actual) === (1 / expected);
369         }
370         if (actual === expected) {
371             return true;
372         }
373         if (actual === null || expected === null) {
374             return false;
375         }
376         if (typeof expected === "number" && isNaN(expected)) {
377             return typeof actual === "number" && isNaN(actual);
378         }
379         if (typeof expected === "number" && typeof actual === "number") {
380             // simple to check?
381             if (actual === expected) {
382                 return true;
383             }
384 
385             // default (randomly chosen, no theory behind)
386             if (absoluteTolerance === undefined) {
387                 absoluteTolerance = 0.0001;
388             }
389 
390             runtime.assert(typeof absoluteTolerance === "number", "Absolute tolerance not given as number.");
391             runtime.assert(absoluteTolerance >= 0, "Absolute tolerance should be given as positive number, was "+absoluteTolerance);
392 
393             diff = Math.abs(actual - expected);
394 
395             return (diff <= absoluteTolerance);
396         }
397         if (Object.prototype.toString.call(expected) ===
398                 Object.prototype.toString.call([])) {
399             return areArraysEqual(/**@type{!Array}*/(actual),
400                                   /**@type{!Array}*/(expected));
401         }
402         if (typeof expected === "object" && typeof actual === "object") {
403             if (/**@type{!Object}*/(expected).constructor === Element
404                     || /**@type{!Object}*/(expected).constructor === Node) {
405                 return areNodesEqual(/**@type{!Node}*/(actual),
406                                      /**@type{!Node}*/(expected));
407             }
408             return areObjectsEqual(/**@type{!Object}*/(actual),
409                                    /**@type{!Object}*/(expected));
410         }
411         return false;
412     }
413     /**
414      * @param {*} v
415      * @return {!string}
416      */
417     function stringify(v) {
418         if (v === 0 && 1 / v < 0) {
419             return "-0";
420         }
421         return String(v);
422     }
423     /**
424      * @param {!Object} t
425      * @param {!string} a
426      * @param {!string} b
427      * @param {!number=} absoluteTolerance  absolute tolerance for number comparison
428      * @return {undefined}
429      */
430     function shouldBe(t, a, b, absoluteTolerance) {
431         if (typeof a !== "string" || typeof b !== "string") {
432             debug("WARN: shouldBe() expects string arguments");
433         }
434         var exception, av, bv;
435         try {
436             av = eval(a);
437         } catch (/**@type{*}*/e) {
438             exception = e;
439         }
440         bv = eval(b);
441 
442         if (exception) {
443             testFailed(a + " should be " + bv + ". Threw exception " +
444                     exception);
445         } else if (isResultCorrect(av, bv, absoluteTolerance)) {
446             testPassed(a + " is " + b);
447         } else if (String(typeof av) === String(typeof bv)) {
448             testFailed(a + " should be " + bv + ". Was " + stringify(av) + ".");
449         } else {
450             testFailed(a + " should be " + bv + " (of type " + typeof bv +
451                     "). Was " + av + " (of type " + typeof av + ").");
452         }
453     }
454 
455     /**
456      * @param {!Object} t context in which values to be tested are placed
457      * @param {!string} a the value to be checked
458      * @return {undefined}
459      */
460     function shouldBeNonNull(t, a) {
461         var exception, av;
462         try {
463             av = eval(a);
464         } catch (/**@type{*}*/e) {
465             exception = e;
466         }
467 
468         if (exception) {
469             testFailed(a + " should be non-null. Threw exception " + exception);
470         } else if (av !== null) {
471             testPassed(a + " is non-null.");
472         } else {
473             testFailed(a + " should be non-null. Was " + av);
474         }
475     }
476     /**
477      * @param {!Object} t context in which values to be tested are placed
478      * @param {!string} a the value to be checked
479      * @return {undefined}
480      */
481     function shouldBeNull(t, a) {
482         shouldBe(t, a, "null");
483     }
484 
485     /**
486      * @param {!Object} a
487      * @param {!Object} b
488      * @return {!boolean}
489      */
490     areObjectsEqual = function (a, b) {
491         var akeys = Object.keys(a),
492             bkeys = Object.keys(b);
493         akeys.sort();
494         bkeys.sort();
495         return areArraysEqual(akeys, bkeys)
496             && Object.keys(a).every(function (key) {
497                 var /**@type{*}*/
498                     aval = a[key],
499                     /**@type{*}*/
500                     bval = b[key];
501                 if (!isResultCorrect(aval, bval)) {
502                     testFailed(aval + " should be " + bval + " for key " + key);
503                     return false;
504                 }
505                 return true;
506             });
507     };
508 
509     this.areNodesEqual = areNodesEqual;
510     this.shouldBeNull = shouldBeNull;
511     this.shouldBeNonNull = shouldBeNonNull;
512     this.shouldBe = shouldBe;
513     this.testFailed = testFailed;
514 
515     /**
516      * @return {!number}
517      */
518     this.countFailedTests = function () {
519         return failedTests;
520     };
521     /**
522      * @param {!Array.<T>} functions
523      * @return {!Array.<!{f:T,name:string}>}
524      * @template T
525      */
526     this.name = function (functions) {
527         var i, fname,
528             nf = [],
529             l = functions.length;
530         nf.length = l;
531         for (i = 0; i < l; i += 1) {
532             fname = Runtime.getFunctionName(functions[i]) || "";
533             if (fname === "") {
534                 throw "Found a function without a name.";
535             }
536             nf[i] = {f: functions[i], name: fname};
537         }
538         return nf;
539     };
540 };
541 
542 /**
543  * @constructor
544  */
545 core.UnitTester = function UnitTester() {
546     "use strict";
547     var self = this,
548         /**@type{!number}*/
549         failedTests = 0,
550         logger = new core.UnitTestLogger(),
551         results = {},
552         inBrowser = runtime.type() === "BrowserRuntime";
553     /**
554      * @type {string}
555      */
556     this.resourcePrefix = "";
557     /**
558      * @param {!string} text
559      * @param {!string} code
560      * @return {!string}
561      **/
562     function link(text, code) {
563         return "<span style='color:blue;cursor:pointer' onclick='" + code + "'>"
564             + text + "</span>";
565     }
566     /**
567      * @type {function(!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number})}
568      */
569     this.reporter = function (r) {
570         var i, m;
571         if (inBrowser) {
572             runtime.log("<span>Running "
573                 + link(r.description, "runTest(\"" + r.suite[0] + "\",\""
574                                   + r.description + "\")") + "</span>");
575         } else {
576             runtime.log("Running " + r.description);
577         }
578         if (!r.success) {
579             for (i = 0; i < r.log.length; i += 1) {
580                 m = r.log[i];
581                 runtime.log(m.category, m.message);
582             }
583         }
584     };
585     /**
586      * @param {!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number}} r
587      */
588     function report(r) {
589         if (self.reporter) {
590             self.reporter(r);
591         }
592     }
593     /**
594      * Run the tests from TestClass.
595      * If parameter testNames is supplied only the tests with the names
596      * supplied in that array will be executed.
597      *
598      * @param {!function(new:core.UnitTest,core.UnitTestRunner)} TestClass
599      *              The constructor for the test class.
600      * @param {!function():undefined} callback
601      * @param {!Array.<!string>} testNames
602      * @return {undefined}
603      */
604     this.runTests = function (TestClass, callback, testNames) {
605         var testName = Runtime.getFunctionName(TestClass) || "",
606             /**@type{!string}*/
607             tname,
608             runner = new core.UnitTestRunner(self.resourcePrefix, logger),
609             test = new TestClass(runner),
610             testResults = {},
611             i,
612             /**@type{function()|function(function())}*/
613             t,
614             tests,
615             texpectFail,
616             lastFailCount;
617 
618         // check that this test has not been run or started yet
619         if (results.hasOwnProperty(testName)) {
620             runtime.log("Test " + testName + " has already run.");
621             return;
622         }
623 
624         if (inBrowser) {
625             runtime.log("<span>Running "
626                 + link(testName, "runSuite(\"" + testName + "\");")
627                 + ": " + test.description() + "</span>");
628         } else {
629             runtime.log("Running " + testName + ": " + test.description);
630         }
631         tests = test.tests();
632         for (i = 0; i < tests.length; i += 1) {
633             t = tests[i].f;
634             tname = tests[i].name;
635             texpectFail = (tests[i].expectFail === true);
636             if (testNames.length && testNames.indexOf(tname) === -1) {
637                 continue;
638             }
639             lastFailCount = runner.countFailedTests();
640             test.setUp();
641             logger.startTest(testName, tname);
642             if (texpectFail) {
643                 runner.beginExpectFail();
644             }
645             try {
646                 t();
647             } catch(/**@type{!Error}*/e) {
648                 runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack);
649             }
650             if (texpectFail) {
651                 runner.endExpectFail();
652             }
653             report(logger.endTest());
654             test.tearDown();
655             testResults[tname] = lastFailCount === runner.countFailedTests();
656         }
657         /**
658          * @param {!Array.<!core.AsyncTestData>} todo
659          * @return {undefined}
660          */
661         function runAsyncTests(todo) {
662             var fname,
663                 expectFail;
664             if (todo.length === 0) {
665                 results[testName] = testResults;
666                 failedTests += runner.countFailedTests();
667                 callback();
668                 return;
669             }
670             function tearDownAndRunNext() {
671                 if (expectFail) {
672                     runner.endExpectFail();
673                 }
674                 report(logger.endTest());
675                 test.tearDown();
676                 testResults[fname] = lastFailCount === runner.countFailedTests();
677                 runAsyncTests(todo.slice(1));
678             }
679             t = todo[0].f;
680             fname = todo[0].name;
681             expectFail = (todo[0].expectFail === true);
682             lastFailCount = runner.countFailedTests();
683             if (testNames.length && testNames.indexOf(fname) === -1) {
684                 runAsyncTests(todo.slice(1));
685             } else {
686                 test.setUp();
687                 logger.startTest(testName, fname);
688                 if (expectFail) {
689                     runner.beginExpectFail();
690                 }
691                 try {
692                     t(tearDownAndRunNext);
693                 } catch(/**@type{!Error}*/e) {
694                     runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack);
695                     tearDownAndRunNext();
696                 }
697             }
698         }
699         runAsyncTests(test.asyncTests());
700     };
701     /**
702      * @return {!number}
703      **/
704     this.failedTestsCount = function () {
705         return failedTests;
706     };
707     /**
708      * @return {!Object}
709      **/
710     this.results = function () {
711         return results;
712     };
713 };
714