UNPKG

node-webodf

Version:

WebODF - JavaScript Document Engine http://webodf.org/

489 lines (453 loc) 18.5 kB
/** * Copyright (C) 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 Node, NodeFilter, runtime, core, gui, ops, odf, xmldom*/ /** * @constructor * @param {core.UnitTestRunner} runner * @implements {core.UnitTest} */ ops.OperationTests = function OperationTests(runner) { "use strict"; var self = this, r = runner, t, tests, opsTestHelper = new ops.OperationTestHelper(); function serialize(element) { var serializer = new xmldom.LSSerializer(); return serializer.writeToString(element, odf.Namespaces.namespaceMap); } function sortChildrenByNSAttribute(element, attrns, attrLocalName) { var child = element.firstChild, childArray = [], i; while (child) { if (child.nodeType === Node.ELEMENT_NODE) { childArray.push(child); } child = child.nextSibling; } childArray.sort(function (a, b) { var attra = a.getAttributeNS(attrns, attrLocalName), attrb = b.getAttributeNS(attrns, attrLocalName); return attra === attrb ? 0 : (attra > attrb ? 1 : -1); }); for (i = 0; i < childArray.length; i += 1) { element.appendChild(childArray[i]); } } /** * Sort the children of the element by their * tag names for easy comparison and uniform * order. * @param {!Element} element * @return {undefined} */ function sortChildrenByTagName(element) { var child = element.firstElementChild, childArray = [], i; while(child) { childArray.push(child); child = child.nextElementSibling; } childArray.sort(function(a, b) { var namea = a.prefix + ":" + a.localName, nameb = b.prefix + ":" + b.localName; return namea === nameb ? 0 : (namea > nameb ? 1 : -1); }); for(i = 0; i < childArray.length; i += 1) { element.appendChild(childArray[i]); } } function parseOperation(node) { var op = {}, child = node.firstChild, atts = node.attributes, att, n = atts.length, i, value; // read plain data by attributes for (i = 0; i < n; i += 1) { att = atts.item(i); value = att.value; if (/^(length|number|position|fo:font-size|fo:margin-right)$/.test(att.localName)) { value = parseInt(value, 10); } op[att.nodeName] = value; } // read complex data by childs while (child) { if (child.nodeType === Node.ELEMENT_NODE) { op[child.nodeName] = parseOperation(child); } child = child.nextSibling; } return op; } function checkWhitespaceTexts(element, expectedChar) { var text = element.firstChild; return (element.childNodes.length === 1 && element.hasAttributeNS(odf.Namespaces.textns, "c") === false && text.nodeType === Node.TEXT_NODE && text.textContent === expectedChar); } function checkWhitespace(rootElement, localName, expectedChar) { var i, spaceElements = rootElement.getElementsByTagNameNS(odf.Namespaces.textns, localName); for (i = 0; i < spaceElements.length; i += 1) { if (!checkWhitespaceTexts(spaceElements[i], expectedChar)) { return false; } } return true; } /** * Returns true if the specified node is an empty text node * @param {!Node} node * @return {!number} */ function emptyNodes(node) { if (node.nodeType === Node.TEXT_NODE && node.length === 0) { return NodeFilter.FILTER_ACCEPT; } return NodeFilter.FILTER_REJECT; } /** * Check that there are no empty text nodes in the supplied rootElement * @param {!Node} rootElement * @return {undefined} */ function checkForEmptyTextNodes(rootElement) { var walker = rootElement.ownerDocument.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, emptyNodes, false), node; node = walker.nextNode(); if (node) { r.testFailed("Empty text nodes were found"); } } /** * Verify the StepCache to ensure it is consistent * @return {undefined} */ function verifyStepsCache() { var rootNode = t.odtDocument.getRootNode(); // Asking for the maximum available step will cause the cache to reverify itself completely t.odtDocument.convertDomPointToCursorStep(rootNode, rootNode.childNodes.length, core.StepDirection.PREVIOUS); } function parseTest(name, node) { var hasSetup = node.getAttribute("hasSetup") === "true", isFailing = node.getAttribute("isFailing") === "true", before = node.firstElementChild, opsElement = before.nextElementSibling, after = opsElement.nextElementSibling, ops = [], op, setup; runtime.assert(before.localName === "before", "Expected <before/> in " + name + "."); runtime.assert(checkWhitespace(before, "s", " "), "Unexpanded text:s element or text:c attribute found in " + name + "."); runtime.assert(checkWhitespace(before, "tab", "\t"), "Unexpanded text:tab element found in " + name + "."); runtime.assert(opsElement.localName === "ops", "Expected <ops/> in " + name + "."); runtime.assert(after.localName === "after", "Expected <after/> in " + name + "."); runtime.assert(checkWhitespace(after, "s", " "), "Unexpanded text:s element or text:c attribute found in " + name + "."); runtime.assert(checkWhitespace(after, "tab", "\t"), "Unexpanded text:tab element found in " + name + "."); opsTestHelper.removeInsignificantTextNodes(node); op = opsElement.firstElementChild; while (op) { runtime.assert(op.localName === "op", "Expected <op/> in " + name + "."); ops.push(parseOperation(op)); op = op.nextElementSibling; } setup = self.setUps.hasOwnProperty(name) ? self.setUps[name]() : null; if (hasSetup) { runtime.assert(Boolean(setup), "Required setup for " + name + " was not found."); } return { isFailing: isFailing, setup : setup, before: before, ops: ops, after: after }; } /** * Creates a deep copy of all child nodes of the source element * and adds them as child nodes to the target element. * If the target element had child nodes before, they are removed. * @param {!Element} targetElement * @param {!Element} sourceElement * @return {undefined} */ function copyChildNodes(targetElement, sourceElement) { while (targetElement.firstChild) { targetElement.removeChild(targetElement.firstChild); } var n = sourceElement.firstChild; while (n) { if (sourceElement.ownerDocument === targetElement.ownerDocument) { targetElement.appendChild(n.cloneNode(true)); } else { targetElement.appendChild(targetElement.ownerDocument.importNode(n, true)); } n = n.nextSibling; } } function getOfficeNSElement(node, localName) { var e = node.getElementsByTagNameNS(odf.Namespaces.officens, localName); if (e.length === 1) { return e[0]; } return null; } function getOfficeTextElement(node) { return getOfficeNSElement(node, "text"); } function getOfficeStylesElement(node) { return getOfficeNSElement(node, "styles"); } function getOfficeAutoStylesElement(node) { return getOfficeNSElement(node, "automatic-styles"); } function getOfficeMetaElement(node) { return getOfficeNSElement(node, "meta"); } function runTest(test) { var text = t.odtDocument.getRootNode(), factory = new ops.OperationFactory(), i, op, textbefore = getOfficeTextElement(test.before), textafter = getOfficeTextElement(test.after), styles = t.odfContainer.rootElement.styles, meta = t.odfContainer.rootElement.meta, autostyles = t.odfContainer.rootElement.automaticStyles, stylesbefore = getOfficeStylesElement(test.before), stylesafter = getOfficeStylesElement(test.after), autostylesbefore = getOfficeAutoStylesElement(test.before), autostylesafter = getOfficeAutoStylesElement(test.after), metabefore = getOfficeMetaElement(test.before), metaafter = getOfficeMetaElement(test.after); // inject test data if (stylesbefore) { copyChildNodes(styles, stylesbefore); } if (autostylesbefore) { copyChildNodes(autostyles, autostylesbefore); } if (metabefore) { copyChildNodes(meta, metabefore); } copyChildNodes(text, textbefore); if (test.setup) { test.setup.setUp(); } // execute test ops for (i = 0; i < test.ops.length; i += 1) { op = factory.create(test.ops[i]); op.execute(t.odtDocument); if (metabefore) { t.odtDocument.emit(ops.OdtDocument.signalOperationEnd, op); } checkForEmptyTextNodes(t.odtDocument.getCanvas().getElement()); } verifyStepsCache(); // check result if (stylesbefore) { stylesafter.normalize(); // for now just normalize the order of the styles to create // comparability // any possible orderless listing in the style subchilds will be // only cared for once it is needed sortChildrenByNSAttribute(stylesafter, odf.Namespaces.stylens, "name"); styles.normalize(); sortChildrenByNSAttribute(styles, odf.Namespaces.stylens, "name"); if (!r.areNodesEqual(styles, stylesafter)) { t.styles = serialize(styles); t.stylesafter = serialize(stylesafter); } else { t.styles = t.stylesafter = "OK"; } r.shouldBe(t, "t.styles", "t.stylesafter"); } if (autostylesbefore) { autostylesbefore.normalize(); sortChildrenByNSAttribute(autostylesafter, odf.Namespaces.stylens, "name"); autostyles.normalize(); sortChildrenByNSAttribute(autostyles, odf.Namespaces.stylens, "name"); if (!r.areNodesEqual(autostyles, autostylesafter)) { t.autostyles = serialize(autostyles); t.autostylesafter = serialize(autostylesafter); } else { t.autostyles = t.autostylesafter = "OK"; } r.shouldBe(t, "t.autostyles", "t.autostylesafter"); } if (metabefore) { metaafter.normalize(); // Sort the metadata fields by tag name // for easy comparing sortChildrenByTagName(metaafter); meta.normalize(); sortChildrenByTagName(meta); if (!r.areNodesEqual(meta, metaafter)) { t.meta = serialize(meta); t.metaafter = serialize(metaafter); } else { t.meta = t.metaafter = "OK"; } r.shouldBe(t, "t.meta", "t.metaafter"); } textafter.normalize(); text.normalize(); if (!r.areNodesEqual(text, textafter)) { t.text = serialize(text); t.after = serialize(textafter); } else { t.text = t.after = "OK"; } r.shouldBe(t, "t.text", "t.after"); if (test.setup) { test.setup.tearDown(); } } function makeTestIntoFunction(name, test) { var f = function () { runTest(test); }; return {f: f, name: name, expectFail: test.isFailing}; } function makeTestsIntoFunction(tests) { var functions = [], i; for (i in tests) { if (tests.hasOwnProperty(i)) { functions.push(makeTestIntoFunction(i, tests[i])); } } return functions; } function loadTests(url, tests) { var s = /**@type{!string}*/(runtime.readFileSync(url, "utf-8")), xml = runtime.parseXML(s), n, testName; runtime.assert(s.length > 0, "XML file is empty."); runtime.assert(xml.documentElement.localName === "tests", "Element is not <tests/>."); n = xml.documentElement.firstElementChild; while (n) { testName = n.getAttribute("name"); runtime.assert(n.localName === "test", "Element is not <test/>."); runtime.assert(!tests.hasOwnProperty(testName), "Test name " + testName + " is not unique."); tests[testName] = parseTest(testName, n); n = n.nextElementSibling; } } function loadTestFiles(urls) { var optests = {}, i; for (i = 0; i < urls.length; i += 1) { loadTests(urls[i], optests); } return optests; } this.setUp = function () { var testarea, properties; t = {}; testarea = core.UnitTest.provideTestAreaDiv(); t.odfcanvas = new odf.OdfCanvas(testarea); t.odfContainer = new odf.OdfContainer(odf.OdfContainer.DocumentType.TEXT, null); t.odfcanvas.setOdfContainer(t.odfContainer); t.odtDocument = new ops.OdtDocument(t.odfcanvas); properties = new ops.MemberProperties(); properties.color = "black"; properties.fullName = "Alice"; properties.imageUrl = ""; t.odtDocument.addMember(new ops.Member('Alice', properties)); }; this.tearDown = function () { t.odfcanvas.destroy(function () { return; }); t = {}; core.UnitTest.cleanupTestAreaDiv(); }; this.tests = function () { var pre = r.resourcePrefix(); if (!tests) { tests = makeTestsIntoFunction(loadTestFiles([ pre + "ops/operationtests.xml", pre + "ops/allowedpositions.xml" ])); } return tests; }; this.asyncTests = function () { return [ ]; }; /*jslint emptyblock: true*/ function linkAnnotationEndToStart() { return { setUp: function () { var rootElement = t.odfContainer.rootElement, annotation = rootElement.getElementsByTagNameNS(odf.Namespaces.officens, "annotation")[0], annotationEnd = rootElement.getElementsByTagNameNS(odf.Namespaces.officens, "annotation-end")[0]; annotation.annotationEndElement = annotationEnd; }, tearDown: function () {} }; } this.setUps = { "ApplyDirectStyling_FixesCursorPositions" : function () { // Test specifically requires the cursor node to have a child element of some sort to // reproduce an issue where the cursor ends up in an invalid position after the operation function appendToCursor(cursor) { cursor.getNode().appendChild(t.odtDocument.getDOMDocument().createElement("span")); } return { setUp: function () {t.odtDocument.subscribe(ops.Document.signalCursorAdded, appendToCursor); }, tearDown: function () {t.odtDocument.unsubscribe(ops.Document.signalCursorAdded, appendToCursor); } }; }, "RemoveAnnotation_ranged" : linkAnnotationEndToStart, "RemoveAnnotation_rangedZero" : linkAnnotationEndToStart, "RemoveText_CopesWithEmptyTextNodes" : function () { return { setUp: function () { var rootElement = t.odfContainer.rootElement, doc = rootElement.ownerDocument, // Using doc.getElementById("paddedByEmptyTextNodes"); is blocked by firefox // being strict about attributes named "id" from a DTD it does not know paddedElement = doc.querySelector("*[id='paddedByEmptyTextNodes']"); paddedElement.insertBefore(doc.createTextNode(""), paddedElement.firstChild); paddedElement.appendChild(doc.createTextNode("")); }, tearDown: function () {} }; } }; /*jslint emptyblock: false*/ }; ops.OperationTests.prototype.description = function () { "use strict"; return "Test the ODT operations described in an XML file."; };