node-webodf-ilkkah
Version:
WebODF - JavaScript Document Engine http://webodf.org/
730 lines (699 loc) • 23.4 kB
JavaScript
/**
* Copyright (C) 2012,2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* This file is part of WebODF.
*
* WebODF is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* WebODF is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: http://www.webodf.org/
* @source: https://github.com/kogmbh/WebODF/
*/
/*global runtime, Runtime, core, Node, Element*/
/*jslint evil: true, continue: true, emptyblock: true, unparam: true*/
/**
* @typedef{{f:function(),name:!string,expectFail:boolean}}
*/
core.TestData;
/**
* @typedef{{f:function(function()),name:!string,expectFail:boolean}}
*/
core.AsyncTestData;
/**
* @interface
*/
core.UnitTest = function UnitTest() {"use strict"; };
/**
* @return {undefined}
*/
core.UnitTest.prototype.setUp = function () {"use strict"; };
/**
* @return {undefined}
*/
core.UnitTest.prototype.tearDown = function () {"use strict"; };
/**
* @return {!string}
*/
core.UnitTest.prototype.description = function () {"use strict"; };
/**
* @return {!Array.<!core.TestData>}
*/
core.UnitTest.prototype.tests = function () {"use strict"; };
/**
* @return {!Array.<!core.AsyncTestData>}
*/
core.UnitTest.prototype.asyncTests = function () {"use strict"; };
/**
* @return {!HTMLDivElement}
*/
core.UnitTest.provideTestAreaDiv = function () {
"use strict";
var maindoc = runtime.getWindow().document,
testarea = maindoc.getElementById('testarea');
runtime.assert(!testarea, "Unclean test environment, found a div with id \"testarea\".");
testarea = maindoc.createElement('div');
testarea.setAttribute('id', 'testarea');
maindoc.body.appendChild(testarea);
return /**@type{!HTMLDivElement}*/(testarea);
};
/**
* @return {undefined}
*/
core.UnitTest.cleanupTestAreaDiv = function () {
"use strict";
var maindoc = runtime.getWindow().document,
testarea = maindoc.getElementById('testarea');
runtime.assert((!!testarea && (testarea.parentNode === maindoc.body)), "Test environment broken, found no div with id \"testarea\" below body.");
maindoc.body.removeChild(testarea);
};
/**
* Creates and returns an XML document
* @param {!string} rootElementName name of the root element, "prefix:localName"
* @param {!string} xmlBodyString XML fragment to insert in the document between the root tags
* @param {!Object.<!string, !string>} namespaceMap Name-value pairs that map the
* prefix onto the appropriate uri namespace
* @return {?Document}
*/
core.UnitTest.createXmlDocument = function (rootElementName, xmlBodyString, namespaceMap) {
"use strict";
var /**@type{!string}*/
xmlDoc = "<?xml version='1.0' encoding='UTF-8'?>";
xmlDoc += "<"+ rootElementName;
Object.keys(namespaceMap).forEach(function (key) {
xmlDoc += " xmlns:" + key + '="' + namespaceMap[key] + '"';
});
xmlDoc += ">";
xmlDoc += xmlBodyString;
xmlDoc += "</"+rootElementName+">";
return runtime.parseXML(xmlDoc);
};
/**
* Creates and returns a simple ODT document
* @param {!string} xml Xml fragment to insert in the document between the
* <office:document>..</office:document> tags
* @param {!Object.<string, string>} namespaceMap Name-value pairs that map the
* prefix onto the appropriate uri namespace
* @return {?Document}
*/
core.UnitTest.createOdtDocument = function (xml, namespaceMap) {
"use strict";
return core.UnitTest.createXmlDocument("office:document", xml, namespaceMap);
};
/**
* @constructor
*/
core.UnitTestLogger = function UnitTestLogger() {
"use strict";
var /**@type{!Array.<{category:string,message:string}>}*/
messages = [],
/**@type{number}*/
errors = 0,
start = 0,
suite = "",
test = "";
/**
* @param {string} suiteName
* @param {string} testName
*/
this.startTest = function (suiteName, testName) {
messages = [];
errors = 0;
suite = suiteName;
test = testName;
start = Date.now();
};
/**
* @return {!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number}}
*/
this.endTest = function () {
var end = Date.now();
return {
description: test,
suite: [suite, test],
success: errors === 0,
log: messages,
time: end - start
};
};
/**
* @param {string} msg
*/
this.debug = function (msg) {
messages.push({category: "debug", message: msg});
};
/**
* @param {string} msg
*/
this.fail = function (msg) {
errors += 1;
messages.push({category: "fail", message: msg});
};
/**
* @param {string} msg
*/
this.pass = function (msg) {
messages.push({category: "pass", message: msg});
};
};
/**
* @constructor
* @param {string} resourcePrefix
* @param {!core.UnitTestLogger} logger
*/
core.UnitTestRunner = function UnitTestRunner(resourcePrefix, logger) {
"use strict";
var /**@type{number}*/
failedTests = 0,
/**@type{number}*/
failedTestsOnBeginExpectFail,
areObjectsEqual,
expectFail = false;
/**
* @return {string}
*/
this.resourcePrefix = function () {
return resourcePrefix;
};
/**
* @return {undefined}
*/
this.beginExpectFail = function () {
failedTestsOnBeginExpectFail = failedTests;
expectFail = true;
};
/**
* @return {undefined}
*/
this.endExpectFail = function () {
var hasNoFailedTests = (failedTestsOnBeginExpectFail === failedTests);
expectFail = false;
failedTests = failedTestsOnBeginExpectFail;
if (hasNoFailedTests) {
failedTests += 1;
logger.fail("Expected at least one failed test, but none registered.");
}
};
/**
* @param {string} msg
* @return {undefined}
*/
function debug(msg) {
logger.debug(msg);
}
/**
* @param {string} msg
* @return {undefined}
*/
function testFailed(msg) {
failedTests += 1;
if (!expectFail) {
logger.fail(msg);
} else {
logger.debug(msg);
}
}
/**
* @param {string} msg
* @return {undefined}
*/
function testPassed(msg) {
logger.pass(msg);
}
/**
* @param {!Array.<*>} a actual
* @param {!Array.<*>} b expected
* @return {!boolean}
*/
function areArraysEqual(a, b) {
var i;
try {
if (a.length !== b.length) {
testFailed("array of length " + a.length + " should be "
+ b.length + " long");
return false;
}
for (i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) {
testFailed(a[i] + " should be " + b[i] + " at array index "
+ i);
return false;
}
}
} catch (ex) {
return false;
}
return true;
}
/**
* @param {!Element} a actual
* @param {!Element} b expected
* @param {!boolean} skipReverseCheck
* @return {!boolean}
*/
function areAttributesEqual(a, b, skipReverseCheck) {
var aatts = a.attributes,
n = aatts.length,
i,
att,
v;
for (i = 0; i < n; i += 1) {
att = /**@type{!Attr}*/(aatts.item(i));
if (att.prefix !== "xmlns" && att.namespaceURI !== "urn:webodf:names:steps") {
v = b.getAttributeNS(att.namespaceURI, att.localName);
if (!b.hasAttributeNS(att.namespaceURI, att.localName)) {
testFailed("Attribute " + att.localName + " with value " + att.value + " was not present");
return false;
}
if (v !== att.value) {
testFailed("Attribute " + att.localName + " was " + v + " should be " + att.value);
return false;
}
}
}
return skipReverseCheck ? true : areAttributesEqual(b, a, true);
}
/**
* @param {!Node} a actual
* @param {!Node} b expected
* @return {!boolean}
*/
function areNodesEqual(a, b) {
var an, bn,
atype = a.nodeType,
btype = b.nodeType;
if (atype !== btype) {
testFailed("Nodetype '" + atype + "' should be '" + btype + "'");
return false;
}
if (atype === Node.TEXT_NODE) {
if (/**@type{!Text}*/(a).data === /**@type{!Text}*/(b).data) {
return true;
}
testFailed("Textnode data '" + /**@type{!Text}*/(a).data
+ "' should be '" + /**@type{!Text}*/(b).data + "'");
return false;
}
if (atype === Node.ELEMENT_NODE) {
if (a.namespaceURI !== b.namespaceURI) {
testFailed("namespace '" + a.namespaceURI + "' should be '"
+ b.namespaceURI + "'");
return false;
}
if (a.localName !== b.localName) {
testFailed("localName '" + a.localName + "' should be '"
+ b.localName + "'");
return false;
}
if (!areAttributesEqual(/**@type{!Element}*/(a),
/**@type{!Element}*/(b), false)) {
return false;
}
} else {
runtime.assert(atype === Node.DOCUMENT_FRAGMENT_NODE,
"Only textnodes, elements and document-fragments are supported.");
}
an = a.firstChild;
bn = b.firstChild;
while (an) {
if (!bn) {
testFailed("Nodetype '" + an.nodeType + "' is unexpected here.");
return false;
}
if (!areNodesEqual(an, bn)) {
return false;
}
an = an.nextSibling;
bn = bn.nextSibling;
}
if (bn) {
testFailed("Nodetype '" + bn.nodeType + "' is missing here.");
return false;
}
return true;
}
/**
* @param {!*} actual
* @param {!*} expected
* @param {!number=} absoluteTolerance absolute tolerance for number comparison
* @return {!boolean}
*/
function isResultCorrect(actual, expected, absoluteTolerance) {
var diff;
if (expected === 0) {
return actual === expected && (1 / actual) === (1 / expected);
}
if (actual === expected) {
return true;
}
if (actual === null || expected === null) {
return false;
}
if (typeof expected === "number" && isNaN(expected)) {
return typeof actual === "number" && isNaN(actual);
}
if (typeof expected === "number" && typeof actual === "number") {
// simple to check?
if (actual === expected) {
return true;
}
// default (randomly chosen, no theory behind)
if (absoluteTolerance === undefined) {
absoluteTolerance = 0.0001;
}
runtime.assert(typeof absoluteTolerance === "number", "Absolute tolerance not given as number.");
runtime.assert(absoluteTolerance >= 0, "Absolute tolerance should be given as positive number, was "+absoluteTolerance);
diff = Math.abs(actual - expected);
return (diff <= absoluteTolerance);
}
if (Object.prototype.toString.call(expected) ===
Object.prototype.toString.call([])) {
return areArraysEqual(/**@type{!Array}*/(actual),
/**@type{!Array}*/(expected));
}
if (typeof expected === "object" && typeof actual === "object") {
if (/**@type{!Object}*/(expected).constructor === Element
|| /**@type{!Object}*/(expected).constructor === Node) {
return areNodesEqual(/**@type{!Node}*/(actual),
/**@type{!Node}*/(expected));
}
return areObjectsEqual(/**@type{!Object}*/(actual),
/**@type{!Object}*/(expected));
}
return false;
}
/**
* @param {*} v
* @return {!string}
*/
function stringify(v) {
if (v === 0 && 1 / v < 0) {
return "-0";
}
if (typeof v === "object") {
try {
return JSON.stringify(v);
} catch (ignore) {
// JSON serialization will fail if there is a cyclic dependency of some sort.
// Just fall through to returning a normal string in this instance.
}
}
return String(v);
}
/**
* @param {!Object} t
* @param {!string} a
* @param {!string} b
* @param {!number=} absoluteTolerance absolute tolerance for number comparison
* @return {undefined}
*/
function shouldBe(t, a, b, absoluteTolerance) {
if (typeof a !== "string" || typeof b !== "string") {
debug("WARN: shouldBe() expects string arguments");
}
var exception, av, bv;
try {
av = eval(a);
} catch (/**@type{*}*/e) {
exception = e;
}
bv = eval(b);
if (exception) {
testFailed(a + " should be " + bv + ". Threw exception " +
exception);
} else if (isResultCorrect(av, bv, absoluteTolerance)) {
testPassed(a + " is " + b);
} else if (String(typeof av) === String(typeof bv)) {
testFailed(a + " should be " + stringify(bv) + ". Was " + stringify(av) + ".");
} else {
testFailed(a + " should be " + bv + " (of type " + typeof bv +
"). Was " + av + " (of type " + typeof av + ").");
}
}
/**
* @param {!Object} t context in which values to be tested are placed
* @param {!string} a the value to be checked
* @return {undefined}
*/
function shouldBeNonNull(t, a) {
var exception, av;
try {
av = eval(a);
} catch (/**@type{*}*/e) {
exception = e;
}
if (exception) {
testFailed(a + " should be non-null. Threw exception " + exception);
} else if (av !== null) {
testPassed(a + " is non-null.");
} else {
testFailed(a + " should be non-null. Was " + av);
}
}
/**
* @param {!Object} t context in which values to be tested are placed
* @param {!string} a the value to be checked
* @return {undefined}
*/
function shouldBeNull(t, a) {
shouldBe(t, a, "null");
}
/**
* @param {!Object} a
* @param {!Object} b
* @return {!boolean}
*/
areObjectsEqual = function (a, b) {
var akeys = Object.keys(a),
bkeys = Object.keys(b);
akeys.sort();
bkeys.sort();
return areArraysEqual(akeys, bkeys)
&& Object.keys(a).every(function (key) {
var /**@type{*}*/
aval = a[key],
/**@type{*}*/
bval = b[key];
if (!isResultCorrect(aval, bval)) {
testFailed(aval + " should be " + bval + " for key " + key);
return false;
}
return true;
});
};
this.areNodesEqual = areNodesEqual;
this.shouldBeNull = shouldBeNull;
this.shouldBeNonNull = shouldBeNonNull;
this.shouldBe = shouldBe;
this.testFailed = testFailed;
/**
* @return {!number}
*/
this.countFailedTests = function () {
return failedTests;
};
/**
* @param {!Array.<T>} functions
* @return {!Array.<!{f:T,name:string}>}
* @template T
*/
this.name = function (functions) {
var i, fname,
nf = [],
l = functions.length;
nf.length = l;
for (i = 0; i < l; i += 1) {
fname = Runtime.getFunctionName(functions[i]) || "";
if (fname === "") {
throw "Found a function without a name.";
}
nf[i] = {f: functions[i], name: fname};
}
return nf;
};
};
/**
* @constructor
*/
core.UnitTester = function UnitTester() {
"use strict";
var self = this,
/**@type{!number}*/
failedTests = 0,
logger = new core.UnitTestLogger(),
results = {},
inBrowser = runtime.type() === "BrowserRuntime";
/**
* @type {string}
*/
this.resourcePrefix = "";
/**
* @param {!string} text
* @param {!string} code
* @return {!string}
**/
function link(text, code) {
// NASTY HACK, DO NOT RE-USE. String concatenation with uncontrolled user input is a bad idea for building DOM
// fragments everyone. If you feel tempted to extract the HTML escape thing from here, please force yourself to
// visit http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/ first, and learn a better
// approach to take.
return "<span style='color:blue;cursor:pointer' onclick='" + code + "'>"
+ text.replace(/</g, "<") + "</span>";
}
/**
* @type {function(!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number})}
*/
this.reporter = function (r) {
var i, m;
if (inBrowser) {
runtime.log("<span>Running "
+ link(r.description, "runTest(\"" + r.suite[0] + "\",\""
+ r.description + "\")") + "</span>");
} else {
runtime.log("Running " + r.description);
}
if (!r.success) {
for (i = 0; i < r.log.length; i += 1) {
m = r.log[i];
runtime.log(m.category, m.message);
}
}
};
/**
* @param {!{description:string,suite:!Array.<string>,success:boolean,log:!Array.<{category:string,message:string}>,time:number}} r
*/
function report(r) {
if (self.reporter) {
self.reporter(r);
}
}
/**
* Run the tests from TestClass.
* If parameter testNames is supplied only the tests with the names
* supplied in that array will be executed.
*
* @param {!function(new:core.UnitTest,core.UnitTestRunner)} TestClass
* The constructor for the test class.
* @param {!function():undefined} callback
* @param {!Array.<!string>} testNames
* @return {undefined}
*/
this.runTests = function (TestClass, callback, testNames) {
var testName = Runtime.getFunctionName(TestClass) || "",
/**@type{!string}*/
tname,
runner = new core.UnitTestRunner(self.resourcePrefix, logger),
test = new TestClass(runner),
testResults = {},
i,
/**@type{function()|function(function())}*/
t,
tests,
texpectFail,
lastFailCount;
// check that this test has not been run or started yet
if (results.hasOwnProperty(testName)) {
runtime.log("Test " + testName + " has already run.");
return;
}
if (inBrowser) {
runtime.log("<span>Running "
+ link(testName, "runSuite(\"" + testName + "\");")
+ ": " + test.description() + "</span>");
} else {
runtime.log("Running " + testName + ": " + test.description());
}
tests = test.tests();
for (i = 0; i < tests.length; i += 1) {
t = tests[i].f;
tname = tests[i].name;
texpectFail = (tests[i].expectFail === true);
if (testNames.length && testNames.indexOf(tname) === -1) {
continue;
}
lastFailCount = runner.countFailedTests();
test.setUp();
logger.startTest(testName, tname);
if (texpectFail) {
runner.beginExpectFail();
}
try {
t();
} catch(/**@type{!Error}*/e) {
runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack);
}
if (texpectFail) {
runner.endExpectFail();
}
report(logger.endTest());
test.tearDown();
testResults[tname] = lastFailCount === runner.countFailedTests();
}
/**
* @param {!Array.<!core.AsyncTestData>} todo
* @return {undefined}
*/
function runAsyncTests(todo) {
var fname,
expectFail;
if (todo.length === 0) {
results[testName] = testResults;
failedTests += runner.countFailedTests();
callback();
return;
}
function tearDownAndRunNext() {
if (expectFail) {
runner.endExpectFail();
}
report(logger.endTest());
test.tearDown();
testResults[fname] = lastFailCount === runner.countFailedTests();
runAsyncTests(todo.slice(1));
}
t = todo[0].f;
fname = todo[0].name;
expectFail = (todo[0].expectFail === true);
lastFailCount = runner.countFailedTests();
if (testNames.length && testNames.indexOf(fname) === -1) {
runAsyncTests(todo.slice(1));
} else {
test.setUp();
logger.startTest(testName, fname);
if (expectFail) {
runner.beginExpectFail();
}
try {
t(tearDownAndRunNext);
} catch(/**@type{!Error}*/e) {
runner.testFailed("Unexpected exception encountered: " + e.toString() + "\n" + e.stack);
tearDownAndRunNext();
}
}
}
runAsyncTests(test.asyncTests());
};
/**
* @return {!number}
**/
this.failedTestsCount = function () {
return failedTests;
};
/**
* @return {!Object}
**/
this.results = function () {
return results;
};
};