diff options
Diffstat (limited to 'dom/xul/templates/tests/chrome/templates_shared.js')
-rw-r--r-- | dom/xul/templates/tests/chrome/templates_shared.js | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/dom/xul/templates/tests/chrome/templates_shared.js b/dom/xul/templates/tests/chrome/templates_shared.js new file mode 100644 index 000000000..85c49e52e --- /dev/null +++ b/dom/xul/templates/tests/chrome/templates_shared.js @@ -0,0 +1,488 @@ +/** + * This script is used for testing XUL templates. Call test_template within + * a load event handler. + * + * A test should have a root node with the datasources attribute with the + * id 'root', and a few global variables defined in the test's XUL file: + * + * testid: the testid, used when outputting test results + * expectedOutput: e4x data containing the expected output. It can optionally + * be enclosed in an <output> element as most tests generate + * more than one node of output. + * isTreeBuilder: true for dont-build-content trees, false otherwise + * queryType: 'rdf', 'xml', etc. + * needsOpen: true for menu tests where the root menu must be opened before + * comparing results + * notWorkingYet: true if this test isn't working yet, outputs todo results + * notWorkingYetDynamic: true if the dynamic changes portion of the test + * isn't working yet, outputs todo results + * changes: an array of functions to perform in sequence to test dynamic changes + * to the datasource. + * + * If the <output> element has an unordered attribute set to true, the + * children within it must all appear to match, but may appear in any order. + * If the unordered attribute is not set, the children must appear in the same + * order. + * + * If the 'changes' array is used, it should be an array of functions. Each + * function will be called in order and a comparison of the output will be + * performed. This allows changes to be made to the datasource to ensure that + * the generated template output has been updated. Within the expected output + * XML, the step attribute may be set to a number on an element to indicate + * that an element only applies before or after a particular change. If step + * is set to a positive number, that element will only exist after that step in + * the list of changes made. If step is set to a negative number, that element + * will only exist until that step. Steps are numbered starting at 1. For + * example: + * <label value="Cat"/> + * <label step="2" value="Dog"/> + * <label step="-5" value="Mouse"/> + * The first element will always exist. The second element will only appear + * after the second change is made. The third element will only appear until + * the fifth change and it will no longer be present at later steps. + * + * If the anyid attribute is set to true on an element in the expected output, + * then the value of the id attribute on that element is not compared for a + * match. This is used, for example, for xml datasources, where the ids set on + * the generated output are pseudo-random. + */ + +const ZOO_NS = "http://www.some-fictitious-zoo.com/"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const debug = false; + +var expectedConsoleMessages = []; +var expectLoggedMessages = null; + +function get_RDF() { + try { + return Components.classes["@mozilla.org/rdf/rdf-service;1"]. + getService(Components.interfaces.nsIRDFService); + } catch (ex) { } +} + +function get_ContainerUtils() +{ + try { + return Components.classes["@mozilla.org/rdf/container-utils;1"]. + getService(Components.interfaces.nsIRDFContainerUtils); + } catch(ex) { } +} + +const RDF = get_RDF(); +const ContainerUtils = get_ContainerUtils(); + +var xmlDoc; + +function test_template() +{ + var root = document.getElementById("root"); + + var ds; + if (queryType == "rdf" && RDF) { + var ioService = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + + var src = window.location.href.replace(/test_tmpl.*xul/, "animals.rdf"); + ds = RDF.GetDataSourceBlocking(src); + + if (expectLoggedMessages) { + Components.classes["@mozilla.org/consoleservice;1"]. + getService(Components.interfaces.nsIConsoleService).reset(); + } + + if (root.getAttribute("datasources") == "rdf:null") + root.setAttribute("datasources", "animals.rdf"); + } + else if (queryType == "xml") { + var src = window.location.href.replace(/test_tmpl.*xul/, "animals.xml"); + xmlDoc = new XMLHttpRequest(); + xmlDoc.open("get", src, false); + xmlDoc.send(null); + } + + // open menus if necessary + if (needsOpen) + root.open = true; + + if (expectLoggedMessages) + expectLoggedMessages(); + + checkResults(root, 0); + + if (changes.length) { + var usedds = ds; + // within these chrome tests, RDF datasources won't be modifiable unless + // an in-memory datasource is used instead. Call copyRDFDataSource to + // copy the datasource. + if (queryType == "rdf") + usedds = copyRDFDataSource(root, ds); + if (needsOpen) + root.open = true; + setTimeout(iterateChanged, 0, root, usedds); + } + else { + if (needsOpen) + root.open = false; + if (expectedConsoleMessages.length) + compareConsoleMessages(); + SimpleTest.finish(); + } +} + +function iterateChanged(root, ds) +{ + Components.classes["@mozilla.org/consoleservice;1"]. + getService(Components.interfaces.nsIConsoleService).reset(); + + for (var c = 0; c < changes.length; c++) { + changes[c](ds, root); + checkResults(root, c + 1); + } + + if (needsOpen) + root.open = false; + if (expectedConsoleMessages.length) + compareConsoleMessages(); + SimpleTest.finish(); +} + +function checkResults(root, step) +{ + var output = expectedOutput.cloneNode(true); + setForCurrentStep(output, step); + + var error; + var actualoutput = root; + if (isTreeBuilder) { + // convert the tree's view data into the equivalent DOM structure + // for easier comparison + actualoutput = treeViewToDOM(root); + var treechildrenElements = [...output.children].filter((e) => e.localName === "treechildren"); + error = compareOutput(actualoutput, treechildrenElements[0], false); + } + else { + error = compareOutput(actualoutput, output, true); + } + + var adjtestid = testid; + if (step > 0) + adjtestid += " dynamic step " + step; + + var stilltodo = ((step == 0 && notWorkingYet) || (step > 0 && notWorkingYetDynamic)); + if (stilltodo) + todo(false, adjtestid); + else + ok(!error, adjtestid); + + if ((!stilltodo && error) || debug) { + // for debugging, serialize the XML output + var serializedXML = ""; + var rootNodes = actualoutput.childNodes; + for (var n = 0; n < rootNodes.length; n++) { + var node = rootNodes[n]; + if (node.localName != "template") + serializedXML += ((new XMLSerializer()).serializeToString(node)); + } + + // remove the XUL namespace declarations to make the output more readable + const nsrepl = new RegExp("xmlns=\"" + XUL_NS + "\" ", "g"); + serializedXML = serializedXML.replace(nsrepl, ""); + if (debug) + dump("-------- " + adjtestid + " " + error + ":\n" + serializedXML + "\n"); + if (!stilltodo && error) + is(serializedXML, "Same", "Error is: " + error); + } +} + +/** + * Adjust the expected output to acccount for any step attributes. + */ +function setForCurrentStep(content, currentStep) +{ + var todelete = []; + for (var child of content.childNodes) { + if (child.nodeType === Node.ELEMENT_NODE) { + var stepstr = child.getAttribute("step") || ""; + var stepsarr = stepstr.split(","); + for (var s = 0; s < stepsarr.length; s++) { + var step = parseInt(stepsarr[s]); + if ((step > 0 && step > currentStep) || + (step < 0 && -step <= currentStep)) { + todelete.push(child); + } + } + } else if (child.nodeType === Node.TEXT_NODE) { + // Drop empty text nodes. + if (child.nodeValue.trim() === "") + todelete.push(child); + } + } + + for (var e of todelete) + content.removeChild(e); + + for (var child of content.children) { + child.removeAttribute("step"); + setForCurrentStep(child, currentStep); + } +} + +/** + * Compares the 'actual' DOM output with the 'expected' output. This function + * is called recursively, with isroot true if actual refers to the root of the + * template. Returns a null string if they are equal and an error string if + * they are not equal. This function is called recursively as it iterates + * through each node in the DOM tree. + */ +function compareOutput(actual, expected, isroot) +{ + if (isroot && expected.localName != "data") + return "expected must be a <data> element"; + + var t; + + // compare text nodes + if (expected.nodeType == Node.TEXT_NODE) { + if (actual.nodeValue !== expected.nodeValue.trim()) + return "Text " + actual.nodeValue + " doesn't match " + expected.nodeValue; + return ""; + } + + if (!isroot) { + var anyid = false; + // make sure that the tags match + if (actual.localName != expected.localName) + return "Tag name " + expected.localName + " not found"; + + // loop through the attributes in the expected node and compare their + // values with the corresponding attribute on the actual node + + var expectedAttrs = expected.attributes; + for (var a = 0; a < expectedAttrs.length; a++) { + var attr = expectedAttrs[a]; + var expectval = attr.value; + // skip checking the id when anyid="true", however make sure to + // ensure that the id is actually present. + if (attr.name == "anyid" && expectval == "true") { + anyid = true; + if (!actual.hasAttribute("id")) + return "expected id attribute"; + } + else if (actual.getAttribute(attr.name) != expectval) { + return "attribute " + attr.name + " is '" + + actual.getAttribute(attr.name) + "' instead of '" + expectval + "'"; + } + } + + // now loop through the actual attributes and make sure that there aren't + // any extra attributes that weren't expected + var length = actual.attributes.length; + for (t = 0; t < length; t++) { + var aattr = actual.attributes[t]; + var expectval = expected.getAttribute(aattr.name); + // ignore some attributes that don't matter + if (expectval != actual.getAttribute(aattr.name) && + aattr.name != "staticHint" && aattr.name != "xmlns" && + (aattr.name != "id" || !anyid)) + return "extra attribute " + aattr.name; + } + } + + // ensure that the node has the right number of children. Subtract one for + // the root node to account for the <template> node. + length = actual.childNodes.length - (isroot ? 1 : 0); + if (length != expected.childNodes.length) + return "incorrect child node count of " + actual.localName + " " + length + + " expected " + expected.childNodes.length; + + // if <data unordered="true"> is used, then the child nodes may be in any order + var unordered = (expected.localName == "data" && expected.getAttribute("unordered") == "true"); + + // next, loop over the children and call compareOutput recursively on each one + var adj = 0; + for (t = 0; t < actual.childNodes.length; t++) { + var actualnode = actual.childNodes[t]; + // skip the <template> element, and add one to the indices when looking + // at the later nodes to account for it + if (isroot && actualnode.localName == "template") { + adj++; + } + else { + var output = "unexpected"; + if (unordered) { + var expectedChildren = expected.childNodes; + for (var e = 0; e < expectedChildren.length; e++) { + output = compareOutput(actualnode, expectedChildren[e], false); + if (!output) + break; + } + } + else { + output = compareOutput(actualnode, expected.childNodes[t - adj], false); + } + + // an error was returned, so return early + if (output) + return output; + } + } + + return ""; +} + +/* + * copy the datasource into an in-memory datasource so that it can be modified + */ +function copyRDFDataSource(root, sourceds) +{ + var dsourcesArr = []; + var composite = root.database; + var dsources = composite.GetDataSources(); + while (dsources.hasMoreElements()) { + sourceds = dsources.getNext().QueryInterface(Components.interfaces.nsIRDFDataSource); + dsourcesArr.push(sourceds); + } + + for (var d = 0; d < dsourcesArr.length; d++) + composite.RemoveDataSource(dsourcesArr[d]); + + var newds = Components.classes["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. + createInstance(Components.interfaces.nsIRDFDataSource); + + var sourcelist = sourceds.GetAllResources(); + while (sourcelist.hasMoreElements()) { + var source = sourcelist.getNext(); + var props = sourceds.ArcLabelsOut(source); + while (props.hasMoreElements()) { + var prop = props.getNext(); + if (prop instanceof Components.interfaces.nsIRDFResource) { + var targets = sourceds.GetTargets(source, prop, true); + while (targets.hasMoreElements()) + newds.Assert(source, prop, targets.getNext(), true); + } + } + } + + composite.AddDataSource(newds); + root.builder.rebuild(); + + return newds; +} + +/** + * Converts a tree view (nsITreeView) into the equivalent DOM tree. + * Returns the treechildren + */ +function treeViewToDOM(tree) +{ + var treechildren = document.createElement("treechildren"); + + if (tree.view) + treeViewToDOMInner(tree.columns, treechildren, tree.view, tree.builder, 0, 0); + + return treechildren; +} + +function treeViewToDOMInner(columns, treechildren, view, builder, start, level) +{ + var end = view.rowCount; + + for (var i = start; i < end; i++) { + if (view.getLevel(i) < level) + return i - 1; + + var id = builder ? builder.getResourceAtIndex(i).Value : "id" + i; + var item = document.createElement("treeitem"); + item.setAttribute("id", id); + treechildren.appendChild(item); + + var row = document.createElement("treerow"); + item.appendChild(row); + + for (var c = 0; c < columns.length; c++) { + var cell = document.createElement("treecell"); + var label = view.getCellText(i, columns[c]); + if (label) + cell.setAttribute("label", label); + row.appendChild(cell); + } + + if (view.isContainer(i)) { + item.setAttribute("container", "true"); + item.setAttribute("empty", view.isContainerEmpty(i) ? "true" : "false"); + + if (!view.isContainerEmpty(i) && view.isContainerOpen(i)) { + item.setAttribute("open", "true"); + + var innertreechildren = document.createElement("treechildren"); + item.appendChild(innertreechildren); + + i = treeViewToDOMInner(columns, innertreechildren, view, builder, i + 1, level + 1); + } + } + } + + return i; +} + +function expectConsoleMessage(ref, id, isNew, isActive, extra) +{ + var message = "In template with id root" + + (ref ? " using ref " + ref : "") + "\n " + + (isNew ? "New " : "Removed ") + (isActive ? "active" : "inactive") + + " result for query " + extra + ": " + id; + expectedConsoleMessages.push(message); +} + +function compareConsoleMessages() +{ + var consoleService = Components.classes["@mozilla.org/consoleservice;1"]. + getService(Components.interfaces.nsIConsoleService); + var messages = consoleService.getMessageArray() || []; + messages = messages.map(m => m.message); + // Copy to avoid modifying expectedConsoleMessages + var expect = expectedConsoleMessages.concat(); + for (var m = 0; m < messages.length; m++) { + if (messages[m] == expect[0]) { + ok(true, "found message " + expect.shift()); + } + } + if (expect.length != 0) { + ok(false, "failed to find expected console messages: " + expect); + } +} + +function copyToProfile(filename) +{ + if (Cc === undefined) { + var Cc = Components.classes; + var Ci = Components.interfaces; + } + + var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader); + loader.loadSubScript("chrome://mochikit/content/chrome-harness.js"); + + var file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append(filename); + + var parentURI = getResolvedURI(getRootDirectory(window.location.href)); + if (parentURI.JARFile) { + parentURI = extractJarToTmp(parentURI); + } else { + var fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"]. + getService(Ci.nsIFileProtocolHandler); + parentURI = fileHandler.getFileFromURLSpec(parentURI.spec); + } + + parentURI = parentURI.QueryInterface(Ci.nsILocalFile); + parentURI.append(filename); + try { + var retVal = parentURI.copyToFollowingLinks(file.parent, filename); + } catch (ex) { + //ignore this error as the file could exist already + } +} |