UNPKG

node-webodf

Version:

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

631 lines (583 loc) 22.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, runtime, core, gui, ops, odf, xmldom*/ /** * @constructor * @param {core.UnitTestRunner} runner * @implements {core.UnitTest} */ ops.TransformationTests = function TransformationTests(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); } /** * Creates a deep copy of the spec * @param {!Object} spec * @return {!Object} */ function cloneSpec(spec) { var result = {}; Object.keys(spec).forEach(function (key) { if (typeof spec[key] === 'object') { result[key] = cloneSpec(spec[key]); } else { result[key] = spec[key]; } }); return result; } /** * Creates a deep copy of the specs * @param {!Array.<!Object>} specs * @return {!Array.<!Object>} */ function cloneSpecs(specs) { var i, result = []; for (i = 0; i < specs.length; i += 1) { result.push(cloneSpec(specs[i])); } return result; } /** * @param {!Array.<!{optype:string}>} opspecs * @return {!Array.<!ops.Operation>} */ function operations(opspecs) { var ops = []; opspecs.forEach(function (opspec) { ops.push(t.operationFactory.create(opspec)); }); return ops; } /** * Traverse the tree and sort cursors that are at the same position, * so identic sets of cursors are in an identic order. * @param {!Element} element * @param {!string} attrns * @param {!string} attrLocalName * @return {undefined} */ 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]); } } /** * Traverse the tree and sort cursors that are at the same position, * so identic sets of cursors are in an identic order. * @param {!Node} node * @return {undefined} */ function normalizeCursorsAndAnchors(node) { var i, child = node.firstChild, cursorNs = "urn:webodf:names:cursor", cursorLocalName = "cursor", anchorLocalName = "anchor", memberIdLocalName = "memberId", collections; /** * @param {!Element} elementA * @param {!Element} elementB * @return {!number} */ function compareMemberId(elementA, elementB) { var memberIdA = elementA.getAttributeNS(cursorNs, memberIdLocalName), memberIdB = elementB.getAttributeNS(cursorNs, memberIdLocalName); if (memberIdA < memberIdB) { return -1; } if (memberIdA > memberIdB) { return 1; } return 0; } while (child) { if (child.nodeType === Node.ELEMENT_NODE && child.namespaceURI === cursorNs && (child.localName === cursorLocalName || child.localName === anchorLocalName)) { collections = { "cursor": [], "anchor": [] }; collections[child.localName].push(child); // collect any next cursors child = child.nextSibling; while (child && child.nodeType === Node.ELEMENT_NODE && child.namespaceURI === cursorNs && (child.localName === cursorLocalName || child.localName === anchorLocalName)) { collections[child.localName].push(child); child = child.nextSibling; } // at this point "child" is behind the last anchor/cursor or undefined // if more than 1, reinsert them sorted if (collections[cursorLocalName].length + collections[anchorLocalName].length > 1) { // sort collections[cursorLocalName].sort(compareMemberId); collections[anchorLocalName].sort(compareMemberId); // remove and reinsert sorted for (i = 0; i < collections[cursorLocalName].length; i += 1) { node.insertBefore(collections[cursorLocalName][i], child); } for (i = 0; i < collections[anchorLocalName].length; i += 1) { node.insertBefore(collections[anchorLocalName][i], child); } } } if (child) { if (child.nodeType === Node.ELEMENT_NODE) { normalizeCursorsAndAnchors(child); } child = child.nextSibling; } } } /** * @param {!Element} element * @return {!Object} */ function parseOperation(element) { var op = {}, child = element.firstChild, atts = element.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; switch(att.localName) { case "length": case "number": case "position": case "fontSize": case "topMargin": case "bottomMargin": case "leftMargin": case "rightMargin": case "sourceParagraphPosition": case "destinationStartPosition": case "sourceStartPosition": value = parseInt(value, 10); break; } op[att.nodeName] = value; } // read complex data by childs while (child) { if (child.nodeType === Node.ELEMENT_NODE) { op[child.nodeName] = parseOperation(/**@type{!Element}*/(child)); } child = child.nextSibling; } return op; } /** * @param {!string} name * @param {!Element} opsElement * @return {!Array.<!Object>} */ function parseOpspecs(name, opsElement) { var op = opsElement.firstElementChild, opspecs = []; while (op) { runtime.assert(op.localName === "op", "Expected <op/> in " + name + "."); opspecs.push(parseOperation(op)); op = op.nextElementSibling; } return opspecs; } /** * @param {!Element} element * @return {!boolean} */ function checkWhitespaceTexts(element) { var text = element.firstChild; return (element.childNodes.length === 1 && element.hasAttributeNS(odf.Namespaces.textns, "c") === false && text.nodeType === Node.TEXT_NODE && text.textContent === " "); } /** * @param {!Element} rootElement * @return {!boolean} */ function checkWhitespace(rootElement) { var i, spaceElements = rootElement.getElementsByTagNameNS(odf.Namespaces.textns, "s"); for (i = 0; i < spaceElements.length; i += 1) { if (!checkWhitespaceTexts(spaceElements[i])) { return false; } } return true; } /** * @param {!string} name * @param {!Element} element * @return {!{isFailing, setup, before, opspecsA, opspecsB, after}} */ function parseTest(name, element) { var before = element.getElementsByTagName("before")[0], opsAElement = element.getElementsByTagName("opsA")[0], opsBElement = element.getElementsByTagName("opsB")[0], after = element.getElementsByTagName("after")[0], setupName = element.getAttribute("setup"), isFailing = element.getAttribute("isFailing") === "true", setup; runtime.assert(Boolean(before), "Expected <before/> in " + name + "."); runtime.assert(checkWhitespace(before), "Unexpanded test:s element or text:c attribute found in " + name + "."); runtime.assert(Boolean(opsAElement), "Expected <ops/> in " + name + "."); runtime.assert(Boolean(opsBElement), "Expected <ops/> in " + name + "."); runtime.assert(Boolean(after), "Expected <after/> in " + name + "."); runtime.assert(checkWhitespace(after), "Unexpanded test:s element or text:c attribute found in " + name + "."); opsTestHelper.removeInsignificantTextNodes(element); setup = self.setUps.hasOwnProperty(setupName) ? self.setUps[setupName]() : null; if (setupName) { runtime.assert(Boolean(setup), "Required setup for " + name + " was not found."); } return { isFailing: isFailing, setup : setup, before: before, opspecsA: parseOpspecs(name, opsAElement), opspecsB: parseOpspecs(name, opsBElement), 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; } } /** * @param {!Element} element * @param {!string} localName * @return {?Element} */ function getOfficeNSElement(element, localName) { var e = element.getElementsByTagNameNS(odf.Namespaces.officens, localName); if (e.length === 1) { return e[0]; } return null; } /** * @param {!Element} element * @return {!Element} */ function getOfficeTextElement(element) { var textElement = getOfficeNSElement(element, "text"); runtime.assert(textElement !== null, "Missing <office:text>!"); return /**@type {!Element}*/(textElement); } /** * @param {!Element} element * @return {?Element} */ function getOfficeStylesElement(element) { return getOfficeNSElement(element, "styles"); } /** * @param {!Element} element * @return {?Element} */ function getOfficeMetaElement(element) { return getOfficeNSElement(element, "meta"); } /** * @param {!Array.<!Object>} opspecs * @param {!Array.<!Object>} transformedOpspecs * @param {!Element} before * @param {!Element} after * @param {?{setUp:!function(), tearDown:!function()}} setup * @return {undefined} */ function compareOpsExecution(opspecs, transformedOpspecs, before, after, setup) { var odtDocument, text, textbefore = getOfficeTextElement(before), textafter = getOfficeTextElement(after), styles, stylesbefore = getOfficeStylesElement(before), stylesafter = getOfficeStylesElement(after), meta, metabefore = getOfficeMetaElement(before), metaafter = getOfficeMetaElement(after), transformedOps = operations(transformedOpspecs), i, op; t.odfContainer = new odf.OdfContainer(odf.OdfContainer.DocumentType.TEXT, null); t.odfcanvas.setOdfContainer(t.odfContainer); odtDocument = new ops.OdtDocument(t.odfcanvas); ["Alice", "Bob"].forEach(function(name) { var properties = new ops.MemberProperties(); properties.color = "black"; properties.fullName = name; properties.imageUrl = ""; odtDocument.addMember(new ops.Member(name, properties)); }); text = odtDocument.getRootNode(); styles = t.odfContainer.rootElement.styles; meta = /**@type{!Element}*/(t.odfContainer.rootElement.meta); runtime.assert(Boolean(meta), "Missing <office:meta>!"); // inject test data if (stylesbefore) { copyChildNodes(styles, stylesbefore); } if (metabefore) { copyChildNodes(meta, metabefore); } copyChildNodes(text, textbefore); if (setup) { setup.setUp(); } // execute opspecs for (i = 0; i < opspecs.length; i += 1) { // runtime.log("Going to apply:"+runtime.toJson(opspecs[i])); op = t.operationFactory.create(opspecs[i]); t.opResult = op.execute(odtDocument); r.shouldBe(t, "t.opResult", "true"); } // execute transformedOps for (i = 0; i < transformedOps.length; i += 1) { // runtime.log("Going to apply:"+runtime.toJson(transformedOps[i].spec())); t.opResult = transformedOps[i].execute(odtDocument); r.shouldBe(t, "t.opResult", "true"); } // 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 (metabefore) { metaafter.normalize(); // Sort the metadata fields by tag name // for easy comparing sortChildrenByTagName(/**@type{!Element}*/(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(); normalizeCursorsAndAnchors(textafter); text.normalize(); normalizeCursorsAndAnchors(text); 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 (setup) { setup.tearDown(); } } function runTest(test) { var transformer = new ops.OperationTransformer(); t.transformResult = transformer.transform(cloneSpecs(test.opspecsA), cloneSpecs(test.opspecsB)); r.shouldBeNonNull(t, "t.transformResult"); if (t.transformResult) { compareOpsExecution(test.opspecsA, t.transformResult.opSpecsB, test.before, test.after, test.setup); compareOpsExecution(test.opspecsB, t.transformResult.opSpecsA, test.before, test.after, test.setup); } } 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 trafoTests = {}, i; for (i = 0; i < urls.length; i += 1) { loadTests(urls[i], trafoTests); } return trafoTests; } this.setUp = function () { var testarea; t = {}; testarea = core.UnitTest.provideTestAreaDiv(); t.odfcanvas = new odf.OdfCanvas(testarea); t.operationFactory = new ops.OperationFactory(); }; this.tearDown = function () { t = {}; core.UnitTest.cleanupTestAreaDiv(); }; this.tests = function () { var pre = r.resourcePrefix(); if (!tests) { tests = makeTestsIntoFunction(loadTestFiles( [pre + "ops/transformationtests.xml"] )); } return tests; }; this.asyncTests = function () { return [ ]; }; /*jslint emptyblock: true*/ function linkAnnotationEndToStart() { return { setUp: function () { var rootElement = t.odfContainer.rootElement, annotationElements = rootElement.getElementsByTagNameNS(odf.Namespaces.officens, "annotation"), annotationEndElements = rootElement.getElementsByTagNameNS(odf.Namespaces.officens, "annotation-end"), annotations = {}, i, name; for (i = 0; i < annotationElements.length; i += 1) { name = annotationElements[i].getAttributeNS(odf.Namespaces.officens, "name"); annotations[name] = annotationElements[i]; } for (i = 0; i < annotationEndElements.length; i += 1) { name = annotationEndElements[i].getAttributeNS(odf.Namespaces.officens, "name"); annotations[name].annotationEndElement = annotationEndElements[i]; } }, tearDown: function () {} }; } /*jslint emptyblock: false*/ this.setUps = { "linkAnnotationEndToStart" : linkAnnotationEndToStart }; }; ops.TransformationTests.prototype.description = function () { "use strict"; return "Test the transformations of ODT operations described in an XML file on consistent ODT dom results."; };