summaryrefslogtreecommitdiffstats
path: root/dom/xul/templates/tests/chrome/templates_shared.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/xul/templates/tests/chrome/templates_shared.js')
-rw-r--r--dom/xul/templates/tests/chrome/templates_shared.js488
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
+ }
+}