|
1 /** |
|
2 * This script is used for testing XUL templates. Call test_template within |
|
3 * a load event handler. |
|
4 * |
|
5 * A test should have a root node with the datasources attribute with the |
|
6 * id 'root', and a few global variables defined in the test's XUL file: |
|
7 * |
|
8 * testid: the testid, used when outputting test results |
|
9 * expectedOutput: e4x data containing the expected output. It can optionally |
|
10 * be enclosed in an <output> element as most tests generate |
|
11 * more than one node of output. |
|
12 * isTreeBuilder: true for dont-build-content trees, false otherwise |
|
13 * queryType: 'rdf', 'xml', etc. |
|
14 * needsOpen: true for menu tests where the root menu must be opened before |
|
15 * comparing results |
|
16 * notWorkingYet: true if this test isn't working yet, outputs todo results |
|
17 * notWorkingYetDynamic: true if the dynamic changes portion of the test |
|
18 * isn't working yet, outputs todo results |
|
19 * changes: an array of functions to perform in sequence to test dynamic changes |
|
20 * to the datasource. |
|
21 * |
|
22 * If the <output> element has an unordered attribute set to true, the |
|
23 * children within it must all appear to match, but may appear in any order. |
|
24 * If the unordered attribute is not set, the children must appear in the same |
|
25 * order. |
|
26 * |
|
27 * If the 'changes' array is used, it should be an array of functions. Each |
|
28 * function will be called in order and a comparison of the output will be |
|
29 * performed. This allows changes to be made to the datasource to ensure that |
|
30 * the generated template output has been updated. Within the expected output |
|
31 * XML, the step attribute may be set to a number on an element to indicate |
|
32 * that an element only applies before or after a particular change. If step |
|
33 * is set to a positive number, that element will only exist after that step in |
|
34 * the list of changes made. If step is set to a negative number, that element |
|
35 * will only exist until that step. Steps are numbered starting at 1. For |
|
36 * example: |
|
37 * <label value="Cat"/> |
|
38 * <label step="2" value="Dog"/> |
|
39 * <label step="-5" value="Mouse"/> |
|
40 * The first element will always exist. The second element will only appear |
|
41 * after the second change is made. The third element will only appear until |
|
42 * the fifth change and it will no longer be present at later steps. |
|
43 * |
|
44 * If the anyid attribute is set to true on an element in the expected output, |
|
45 * then the value of the id attribute on that element is not compared for a |
|
46 * match. This is used, for example, for xml datasources, where the ids set on |
|
47 * the generated output are pseudo-random. |
|
48 */ |
|
49 |
|
50 const ZOO_NS = "http://www.some-fictitious-zoo.com/"; |
|
51 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
52 const debug = false; |
|
53 |
|
54 var expectedConsoleMessages = []; |
|
55 var expectLoggedMessages = null; |
|
56 |
|
57 try { |
|
58 const RDF = Components.classes["@mozilla.org/rdf/rdf-service;1"]. |
|
59 getService(Components.interfaces.nsIRDFService); |
|
60 const ContainerUtils = Components.classes["@mozilla.org/rdf/container-utils;1"]. |
|
61 getService(Components.interfaces.nsIRDFContainerUtils); |
|
62 } catch(ex) { } |
|
63 |
|
64 var xmlDoc; |
|
65 |
|
66 function test_template() |
|
67 { |
|
68 var root = document.getElementById("root"); |
|
69 |
|
70 var ds; |
|
71 if (queryType == "rdf" && RDF) { |
|
72 var ioService = Components.classes["@mozilla.org/network/io-service;1"]. |
|
73 getService(Components.interfaces.nsIIOService); |
|
74 |
|
75 var src = window.location.href.replace(/test_tmpl.*xul/, "animals.rdf"); |
|
76 ds = RDF.GetDataSourceBlocking(src); |
|
77 |
|
78 if (expectLoggedMessages) { |
|
79 Components.classes["@mozilla.org/consoleservice;1"]. |
|
80 getService(Components.interfaces.nsIConsoleService).reset(); |
|
81 } |
|
82 |
|
83 if (root.getAttribute("datasources") == "rdf:null") |
|
84 root.setAttribute("datasources", "animals.rdf"); |
|
85 } |
|
86 else if (queryType == "xml") { |
|
87 var src = window.location.href.replace(/test_tmpl.*xul/, "animals.xml"); |
|
88 xmlDoc = new XMLHttpRequest(); |
|
89 xmlDoc.open("get", src, false); |
|
90 xmlDoc.send(null); |
|
91 } |
|
92 |
|
93 // open menus if necessary |
|
94 if (needsOpen) |
|
95 root.open = true; |
|
96 |
|
97 if (expectLoggedMessages) |
|
98 expectLoggedMessages(); |
|
99 |
|
100 checkResults(root, 0); |
|
101 |
|
102 if (changes.length) { |
|
103 var usedds = ds; |
|
104 // within these chrome tests, RDF datasources won't be modifiable unless |
|
105 // an in-memory datasource is used instead. Call copyRDFDataSource to |
|
106 // copy the datasource. |
|
107 if (queryType == "rdf") |
|
108 usedds = copyRDFDataSource(root, ds); |
|
109 if (needsOpen) |
|
110 root.open = true; |
|
111 setTimeout(iterateChanged, 0, root, usedds); |
|
112 } |
|
113 else { |
|
114 if (needsOpen) |
|
115 root.open = false; |
|
116 if (expectedConsoleMessages.length) |
|
117 compareConsoleMessages(); |
|
118 SimpleTest.finish(); |
|
119 } |
|
120 } |
|
121 |
|
122 function iterateChanged(root, ds) |
|
123 { |
|
124 Components.classes["@mozilla.org/consoleservice;1"]. |
|
125 getService(Components.interfaces.nsIConsoleService).reset(); |
|
126 |
|
127 for (var c = 0; c < changes.length; c++) { |
|
128 changes[c](ds, root); |
|
129 checkResults(root, c + 1); |
|
130 } |
|
131 |
|
132 if (needsOpen) |
|
133 root.open = false; |
|
134 if (expectedConsoleMessages.length) |
|
135 compareConsoleMessages(); |
|
136 SimpleTest.finish(); |
|
137 } |
|
138 |
|
139 function checkResults(root, step) |
|
140 { |
|
141 var output = expectedOutput.cloneNode(true); |
|
142 setForCurrentStep(output, step); |
|
143 |
|
144 var error; |
|
145 var actualoutput = root; |
|
146 if (isTreeBuilder) { |
|
147 // convert the tree's view data into the equivalent DOM structure |
|
148 // for easier comparison |
|
149 actualoutput = treeViewToDOM(root); |
|
150 var treechildrenElements = [e for (e of output.children) if (e.localName === "treechildren")]; |
|
151 error = compareOutput(actualoutput, treechildrenElements[0], false); |
|
152 } |
|
153 else { |
|
154 error = compareOutput(actualoutput, output, true); |
|
155 } |
|
156 |
|
157 var adjtestid = testid; |
|
158 if (step > 0) |
|
159 adjtestid += " dynamic step " + step; |
|
160 |
|
161 var stilltodo = ((step == 0 && notWorkingYet) || (step > 0 && notWorkingYetDynamic)); |
|
162 if (stilltodo) |
|
163 todo(false, adjtestid); |
|
164 else |
|
165 ok(!error, adjtestid); |
|
166 |
|
167 if ((!stilltodo && error) || debug) { |
|
168 // for debugging, serialize the XML output |
|
169 var serializedXML = ""; |
|
170 var rootNodes = actualoutput.childNodes; |
|
171 for (var n = 0; n < rootNodes.length; n++) { |
|
172 var node = rootNodes[n]; |
|
173 if (node.localName != "template") |
|
174 serializedXML += ((new XMLSerializer()).serializeToString(node)); |
|
175 } |
|
176 |
|
177 // remove the XUL namespace declarations to make the output more readable |
|
178 const nsrepl = new RegExp("xmlns=\"" + XUL_NS + "\" ", "g"); |
|
179 serializedXML = serializedXML.replace(nsrepl, ""); |
|
180 if (debug) |
|
181 dump("-------- " + adjtestid + " " + error + ":\n" + serializedXML + "\n"); |
|
182 if (!stilltodo && error) |
|
183 is(serializedXML, "Same", "Error is: " + error); |
|
184 } |
|
185 } |
|
186 |
|
187 /** |
|
188 * Adjust the expected output to acccount for any step attributes. |
|
189 */ |
|
190 function setForCurrentStep(content, currentStep) |
|
191 { |
|
192 var todelete = []; |
|
193 for (var child of content.childNodes) { |
|
194 if (child.nodeType === Node.ELEMENT_NODE) { |
|
195 var stepstr = child.getAttribute("step") || ""; |
|
196 var stepsarr = stepstr.split(","); |
|
197 for (var s = 0; s < stepsarr.length; s++) { |
|
198 var step = parseInt(stepsarr[s]); |
|
199 if ((step > 0 && step > currentStep) || |
|
200 (step < 0 && -step <= currentStep)) { |
|
201 todelete.push(child); |
|
202 } |
|
203 } |
|
204 } else if (child.nodeType === Node.TEXT_NODE) { |
|
205 // Drop empty text nodes. |
|
206 if (child.nodeValue.trim() === "") |
|
207 todelete.push(child); |
|
208 } |
|
209 } |
|
210 |
|
211 for (var e of todelete) |
|
212 content.removeChild(e); |
|
213 |
|
214 for (var child of content.children) { |
|
215 child.removeAttribute("step"); |
|
216 setForCurrentStep(child, currentStep); |
|
217 } |
|
218 } |
|
219 |
|
220 /** |
|
221 * Compares the 'actual' DOM output with the 'expected' output. This function |
|
222 * is called recursively, with isroot true if actual refers to the root of the |
|
223 * template. Returns a null string if they are equal and an error string if |
|
224 * they are not equal. This function is called recursively as it iterates |
|
225 * through each node in the DOM tree. |
|
226 */ |
|
227 function compareOutput(actual, expected, isroot) |
|
228 { |
|
229 if (isroot && expected.localName != "data") |
|
230 return "expected must be a <data> element"; |
|
231 |
|
232 var t; |
|
233 |
|
234 // compare text nodes |
|
235 if (expected.nodeType == Node.TEXT_NODE) { |
|
236 if (actual.nodeValue !== expected.nodeValue.trim()) |
|
237 return "Text " + actual.nodeValue + " doesn't match " + expected.nodeValue; |
|
238 return ""; |
|
239 } |
|
240 |
|
241 if (!isroot) { |
|
242 var anyid = false; |
|
243 // make sure that the tags match |
|
244 if (actual.localName != expected.localName) |
|
245 return "Tag name " + expected.localName + " not found"; |
|
246 |
|
247 // loop through the attributes in the expected node and compare their |
|
248 // values with the corresponding attribute on the actual node |
|
249 |
|
250 var expectedAttrs = expected.attributes; |
|
251 for (var a = 0; a < expectedAttrs.length; a++) { |
|
252 var attr = expectedAttrs[a]; |
|
253 var expectval = attr.value; |
|
254 // skip checking the id when anyid="true", however make sure to |
|
255 // ensure that the id is actually present. |
|
256 if (attr.name == "anyid" && expectval == "true") { |
|
257 anyid = true; |
|
258 if (!actual.hasAttribute("id")) |
|
259 return "expected id attribute"; |
|
260 } |
|
261 else if (actual.getAttribute(attr.name) != expectval) { |
|
262 return "attribute " + attr.name + " is '" + |
|
263 actual.getAttribute(attr.name) + "' instead of '" + expectval + "'"; |
|
264 } |
|
265 } |
|
266 |
|
267 // now loop through the actual attributes and make sure that there aren't |
|
268 // any extra attributes that weren't expected |
|
269 var length = actual.attributes.length; |
|
270 for (t = 0; t < length; t++) { |
|
271 var aattr = actual.attributes[t]; |
|
272 var expectval = expected.getAttribute(aattr.name); |
|
273 // ignore some attributes that don't matter |
|
274 if (expectval != actual.getAttribute(aattr.name) && |
|
275 aattr.name != "staticHint" && aattr.name != "xmlns" && |
|
276 (aattr.name != "id" || !anyid)) |
|
277 return "extra attribute " + aattr.name; |
|
278 } |
|
279 } |
|
280 |
|
281 // ensure that the node has the right number of children. Subtract one for |
|
282 // the root node to account for the <template> node. |
|
283 length = actual.childNodes.length - (isroot ? 1 : 0); |
|
284 if (length != expected.childNodes.length) |
|
285 return "incorrect child node count of " + actual.localName + " " + length + |
|
286 " expected " + expected.childNodes.length; |
|
287 |
|
288 // if <data unordered="true"> is used, then the child nodes may be in any order |
|
289 var unordered = (expected.localName == "data" && expected.getAttribute("unordered") == "true"); |
|
290 |
|
291 // next, loop over the children and call compareOutput recursively on each one |
|
292 var adj = 0; |
|
293 for (t = 0; t < actual.childNodes.length; t++) { |
|
294 var actualnode = actual.childNodes[t]; |
|
295 // skip the <template> element, and add one to the indices when looking |
|
296 // at the later nodes to account for it |
|
297 if (isroot && actualnode.localName == "template") { |
|
298 adj++; |
|
299 } |
|
300 else { |
|
301 var output = "unexpected"; |
|
302 if (unordered) { |
|
303 var expectedChildren = expected.childNodes; |
|
304 for (var e = 0; e < expectedChildren.length; e++) { |
|
305 output = compareOutput(actualnode, expectedChildren[e], false); |
|
306 if (!output) |
|
307 break; |
|
308 } |
|
309 } |
|
310 else { |
|
311 output = compareOutput(actualnode, expected.childNodes[t - adj], false); |
|
312 } |
|
313 |
|
314 // an error was returned, so return early |
|
315 if (output) |
|
316 return output; |
|
317 } |
|
318 } |
|
319 |
|
320 return ""; |
|
321 } |
|
322 |
|
323 /* |
|
324 * copy the datasource into an in-memory datasource so that it can be modified |
|
325 */ |
|
326 function copyRDFDataSource(root, sourceds) |
|
327 { |
|
328 var dsourcesArr = []; |
|
329 var composite = root.database; |
|
330 var dsources = composite.GetDataSources(); |
|
331 while (dsources.hasMoreElements()) { |
|
332 sourceds = dsources.getNext().QueryInterface(Components.interfaces.nsIRDFDataSource); |
|
333 dsourcesArr.push(sourceds); |
|
334 } |
|
335 |
|
336 for (var d = 0; d < dsourcesArr.length; d++) |
|
337 composite.RemoveDataSource(dsourcesArr[d]); |
|
338 |
|
339 var newds = Components.classes["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. |
|
340 createInstance(Components.interfaces.nsIRDFDataSource); |
|
341 |
|
342 var sourcelist = sourceds.GetAllResources(); |
|
343 while (sourcelist.hasMoreElements()) { |
|
344 var source = sourcelist.getNext(); |
|
345 var props = sourceds.ArcLabelsOut(source); |
|
346 while (props.hasMoreElements()) { |
|
347 var prop = props.getNext(); |
|
348 if (prop instanceof Components.interfaces.nsIRDFResource) { |
|
349 var targets = sourceds.GetTargets(source, prop, true); |
|
350 while (targets.hasMoreElements()) |
|
351 newds.Assert(source, prop, targets.getNext(), true); |
|
352 } |
|
353 } |
|
354 } |
|
355 |
|
356 composite.AddDataSource(newds); |
|
357 root.builder.rebuild(); |
|
358 |
|
359 return newds; |
|
360 } |
|
361 |
|
362 /** |
|
363 * Converts a tree view (nsITreeView) into the equivalent DOM tree. |
|
364 * Returns the treechildren |
|
365 */ |
|
366 function treeViewToDOM(tree) |
|
367 { |
|
368 var treechildren = document.createElement("treechildren"); |
|
369 |
|
370 if (tree.view) |
|
371 treeViewToDOMInner(tree.columns, treechildren, tree.view, tree.builder, 0, 0); |
|
372 |
|
373 return treechildren; |
|
374 } |
|
375 |
|
376 function treeViewToDOMInner(columns, treechildren, view, builder, start, level) |
|
377 { |
|
378 var end = view.rowCount; |
|
379 |
|
380 for (var i = start; i < end; i++) { |
|
381 if (view.getLevel(i) < level) |
|
382 return i - 1; |
|
383 |
|
384 var id = builder ? builder.getResourceAtIndex(i).Value : "id" + i; |
|
385 var item = document.createElement("treeitem"); |
|
386 item.setAttribute("id", id); |
|
387 treechildren.appendChild(item); |
|
388 |
|
389 var row = document.createElement("treerow"); |
|
390 item.appendChild(row); |
|
391 |
|
392 for (var c = 0; c < columns.length; c++) { |
|
393 var cell = document.createElement("treecell"); |
|
394 var label = view.getCellText(i, columns[c]); |
|
395 if (label) |
|
396 cell.setAttribute("label", label); |
|
397 row.appendChild(cell); |
|
398 } |
|
399 |
|
400 if (view.isContainer(i)) { |
|
401 item.setAttribute("container", "true"); |
|
402 item.setAttribute("empty", view.isContainerEmpty(i) ? "true" : "false"); |
|
403 |
|
404 if (!view.isContainerEmpty(i) && view.isContainerOpen(i)) { |
|
405 item.setAttribute("open", "true"); |
|
406 |
|
407 var innertreechildren = document.createElement("treechildren"); |
|
408 item.appendChild(innertreechildren); |
|
409 |
|
410 i = treeViewToDOMInner(columns, innertreechildren, view, builder, i + 1, level + 1); |
|
411 } |
|
412 } |
|
413 } |
|
414 |
|
415 return i; |
|
416 } |
|
417 |
|
418 function expectConsoleMessage(ref, id, isNew, isActive, extra) |
|
419 { |
|
420 var message = "In template with id root" + |
|
421 (ref ? " using ref " + ref : "") + "\n " + |
|
422 (isNew ? "New " : "Removed ") + (isActive ? "active" : "inactive") + |
|
423 " result for query " + extra + ": " + id; |
|
424 expectedConsoleMessages.push(message); |
|
425 } |
|
426 |
|
427 function compareConsoleMessages() |
|
428 { |
|
429 var consoleService = Components.classes["@mozilla.org/consoleservice;1"]. |
|
430 getService(Components.interfaces.nsIConsoleService); |
|
431 var messages = consoleService.getMessageArray() || []; |
|
432 messages = messages.map(function (m) m.message); |
|
433 // Copy to avoid modifying expectedConsoleMessages |
|
434 var expect = expectedConsoleMessages.concat(); |
|
435 for (var m = 0; m < messages.length; m++) { |
|
436 if (messages[m] == expect[0]) { |
|
437 ok(true, "found message " + expect.shift()); |
|
438 } |
|
439 } |
|
440 if (expect.length != 0) { |
|
441 ok(false, "failed to find expected console messages: " + expect); |
|
442 } |
|
443 } |
|
444 |
|
445 function copyToProfile(filename) |
|
446 { |
|
447 if (Cc === undefined) { |
|
448 var Cc = Components.classes; |
|
449 var Ci = Components.interfaces; |
|
450 } |
|
451 |
|
452 var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] |
|
453 .getService(Ci.mozIJSSubScriptLoader); |
|
454 loader.loadSubScript("chrome://mochikit/content/chrome-harness.js"); |
|
455 |
|
456 var file = Cc["@mozilla.org/file/directory_service;1"] |
|
457 .getService(Ci.nsIProperties) |
|
458 .get("ProfD", Ci.nsIFile); |
|
459 file.append(filename); |
|
460 |
|
461 var parentURI = getResolvedURI(getRootDirectory(window.location.href)); |
|
462 if (parentURI.JARFile) { |
|
463 parentURI = extractJarToTmp(parentURI); |
|
464 } else { |
|
465 var fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"]. |
|
466 getService(Ci.nsIFileProtocolHandler); |
|
467 parentURI = fileHandler.getFileFromURLSpec(parentURI.spec); |
|
468 } |
|
469 |
|
470 parentURI = parentURI.QueryInterface(Ci.nsILocalFile); |
|
471 parentURI.append(filename); |
|
472 try { |
|
473 var retVal = parentURI.copyToFollowingLinks(file.parent, filename); |
|
474 } catch (ex) { |
|
475 //ignore this error as the file could exist already |
|
476 } |
|
477 } |