/** * 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 } }