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         if (typeof v === "object") {
422             try {
423                 return JSON.stringify(v);
424             } catch (ignore) {
425                 // JSON serialization will fail if there is a cyclic dependency of some sort.
426                 // Just fall through to returning a normal string in this instance.
427             }
428         }
429         return String(v);
430     }
431     /**
432      * @param {!Object} t
433      * @param {!string} a
434      * @param {!string} b
435      * @param {!number=} absoluteTolerance  absolute tolerance for number comparison
436      * @return {undefined}
437      */
438     function shouldBe(t, a, b, absoluteTolerance) {
439         if (typeof a !== "string" || typeof b !== "string") {
440             debug("WARN: shouldBe() expects string arguments");
441         }
442         var exception, av, bv;
443         try {
444             av = eval(a);
445         } catch (/**@type{*}*/e) {
446             exception = e;
447         }
448         bv = eval(b);
449 
450         if (exception) {
451             testFailed(a + " should be " + bv + ". Threw exception " +
452                     exception);
453         } else if (isResultCorrect(av, bv, absoluteTolerance)) {
454             testPassed(a + " is " + b);
455         } else if (String(typeof av) === String(typeof bv)) {
456             testFailed(a + " should be " + stringify(bv) + ". Was " + stringify(av) + ".");
457         } else {
458             testFailed(a + " should be " + bv + " (of type " + typeof bv +
459                     "). Was " + av + " (of type " + typeof av + ").");
460         }
461     }
462 
463     /**
464      * @param {!Object} t context in which values to be tested are placed
465      * @param {!string} a the value to be checked
466      * @return {undefined}
467      */
468     function shouldBeNonNull(t, a) {
469         var exception, av;
470         try {
471             av = eval(a);
472         } catch (/**@type{*}*/e) {
473             exception = e;
474         }
475 
476         if (exception) {
477             testFailed(a + " should be non-null. Threw exception " + exception);
478         } else if (av !== null) {
479             testPassed(a + " is non-null.");
480         } else {
481             testFailed(a + " should be non-null. Was " + av);
482         }
483     }
484     /**
485      * @param {!Object} t context in which values to be tested are placed
486      * @param {!string} a the value to be checked
487      * @return {undefined}
488      */
489     function shouldBeNull(t, a) {
490         shouldBe(t, a, "null");
491     }
492 
493     /**
494      * @param {!Object} a
495      * @param {!Object} b
496      * @return {!boolean}
497      */
498     areObjectsEqual = function (a, b) {
499         var akeys = Object.keys(a),
500             bkeys = Object.keys(b);
501         akeys.sort();
502         bkeys.sort();
503         return areArraysEqual(akeys, bkeys)
504             && Object.keys(a).every(function (key) {
505                 var /**@type{*}*/
506                     aval = a[key],
507                     /**@type{*}*/
508                     bval = b[key];
509                 if (!isResultCorrect(aval, bval)) {
510                     testFailed(aval + " should be " + bval + " for key " + key);
511                     return false;
512                 }
513                 return true;
514             });
515     };
516 
517     this.areNodesEqual = areNodesEqual;
518     this.shouldBeNull = shouldBeNull;
519     this.shouldBeNonNull = shouldBeNonNull;
520     this.shouldBe = shouldBe;
521     this.testFailed = testFailed;
522 
523     /**
524      * @return {!number}
525      */
526     this.countFailedTests = function () {
527         return failedTests;
528     };
529     /**
530      * @param {!Array.<T>} functions
531      * @return {!Array.<!{f:T,name:string}>}
532      * @template T
533      */
534     this.name = function (functions) {
535         var i, fname,
536             nf = [],
537             l = functions.length;
538         nf.length = l;
539         for (i = 0; i < l; i += 1) {
540             fname = Runtime.getFunctionName(functions[i]) || "";
541             if (fname === "") {
542                 throw "Found a function without a name.";
543             }
544             nf[i] = {f: functions[i], name: fname};
545         }
546         return nf;
547     };
548 };
549 
550 /**
551  * @constructor
552  */
553 core.UnitTester = function UnitTester() {
554     "use strict";
555     var self = this,
556         /**@type{!number}*/
557         failedTests = 0,
558         logger = new core.UnitTestLogger(),
559         results = {},
560         inBrowser = runtime.type() === "BrowserRuntime";
561     /**
562      * @type {string}
563      */
564     this.resourcePrefix = "";
565     /**
566      * @param {!string} text
567      * @param {!string} code
568      * @return {!string}
569      **/
570     function link(text, code) {
571         // NASTY HACK, DO NOT RE-USE. String concatenation with uncontrolled user input is a bad idea for building DOM
572         // fragments everyone. If you feel tempted to extract the HTML escape thing from here, please force yourself to
573         // visit http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/ first, and learn a better
574         // approach to take.
575 
576         return "<span style='color:blue;cursor:pointer' onclick='" + code + "'>"
577             + text.replace(/</g, "<") + "</span>";
578     }
579     /**
580      * @type {function(!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number})}
581      */
582     this.reporter = function (r) {
583         var i, m;
584         if (inBrowser) {
585             runtime.log("<span>Running "
586                 + link(r.description, "runTest(\"" + r.suite[0] + "\",\""
587                                   + r.description + "\")") + "</span>");
588         } else {
589             runtime.log("Running " + r.description);
590         }
591         if (!r.success) {
592             for (i = 0; i < r.log.length; i += 1) {
593                 m = r.log[i];
594                 runtime.log(m.category, m.message);
595             }
596         }
597     };
598     /**
599      * @param {!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number}} r
600      */
601     function report(r) {
602         if (self.reporter) {
603             self.reporter(r);
604         }
605     }
606     /**
607      * Run the tests from TestClass.
608      * If parameter testNames is supplied only the tests with the names
609      * supplied in that array will be executed.
610      *
611      * @param {!function(new:core.UnitTest,core.UnitTestRunner)} TestClass
612      *              The constructor for the test class.
613      * @param {!function():undefined} callback
614      * @param {!Array.<!string>} testNames
615      * @return {undefined}
616      */
617     this.runTests = function (TestClass, callback, testNames) {
618         var testName = Runtime.getFunctionName(TestClass) || "",
619             /**@type{!string}*/
620             tname,
621             runner = new core.UnitTestRunner(self.resourcePrefix, logger),
622             test = new TestClass(runner),
623             testResults = {},
624             i,
625             /**@type{function()|function(function())}*/
626             t,
627             tests,
628             texpectFail,
629             lastFailCount;
630 
631         // check that this test has not been run or started yet
632         if (results.hasOwnProperty(testName)) {
633             runtime.log("Test " + testName + " has already run.");
634             return;
635         }
636 
637         if (inBrowser) {
638             runtime.log("<span>Running "
639                 + link(testName, "runSuite(\"" + testName + "\");")
640                 + ": " + test.description() + "</span>");
641         } else {
642             runtime.log("Running " + testName + ": " + test.description());
643         }
644         tests = test.tests();
645         for (i = 0; i < tests.length; i += 1) {
646             t = tests[i].f;
647             tname = tests[i].name;
648             texpectFail = (tests[i].expectFail === true);
649             if (testNames.length && testNames.indexOf(tname) === -1) {
650                 continue;
651             }
652             lastFailCount = runner.countFailedTests();
653             test.setUp();
654             logger.startTest(testName, tname);
655             if (texpectFail) {
656                 runner.beginExpectFail();
657             }
658             try {
659                 t();
660             } catch(/**@type{!Error}*/e) {
661                 runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack);
662             }
663             if (texpectFail) {
664                 runner.endExpectFail();
665             }
666             report(logger.endTest());
667             test.tearDown();
668             testResults[tname] = lastFailCount === runner.countFailedTests();
669         }
670         /**
671          * @param {!Array.<!core.AsyncTestData>} todo
672          * @return {undefined}
673          */
674         function runAsyncTests(todo) {
675             var fname,
676                 expectFail;
677             if (todo.length === 0) {
678                 results[testName] = testResults;
679                 failedTests += runner.countFailedTests();
680                 callback();
681                 return;
682             }
683             function tearDownAndRunNext() {
684                 if (expectFail) {
685                     runner.endExpectFail();
686                 }
687                 report(logger.endTest());
688                 test.tearDown();
689                 testResults[fname] = lastFailCount === runner.countFailedTests();
690                 runAsyncTests(todo.slice(1));
691             }
692             t = todo[0].f;
693             fname = todo[0].name;
694             expectFail = (todo[0].expectFail === true);
695             lastFailCount = runner.countFailedTests();
696             if (testNames.length && testNames.indexOf(fname) === -1) {
697                 runAsyncTests(todo.slice(1));
698             } else {
699                 test.setUp();
700                 logger.startTest(testName, fname);
701                 if (expectFail) {
702                     runner.beginExpectFail();
703                 }
704                 try {
705                     t(tearDownAndRunNext);
706                 } catch(/**@type{!Error}*/e) {
707                     runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack);
708                     tearDownAndRunNext();
709                 }
710             }
711         }
712         runAsyncTests(test.asyncTests());
713     };
714     /**
715      * @return {!number}
716      **/
717     this.failedTestsCount = function () {
718         return failedTests;
719     };
720     /**
721      * @return {!Object}
722      **/
723     this.results = function () {
724         return results;
725     };
726 };
727