|
1 /* |
|
2 * This script is used for menu and popup tests. Call startPopupTests to start |
|
3 * the tests, passing an array of tests as an argument. Each test is an object |
|
4 * with the following properties: |
|
5 * testname - name of the test |
|
6 * test - function to call to perform the test |
|
7 * events - a list of events that are expected to be fired in sequence |
|
8 * as a result of calling the 'test' function. This list should be |
|
9 * an array of strings of the form "eventtype targetid" where |
|
10 * 'eventtype' is the event type and 'targetid' is the id of |
|
11 * target of the event. This function will be passed two |
|
12 * arguments, the testname and the step argument. |
|
13 * Alternatively, events may be a function which returns the array |
|
14 * of events. This can be used when the events vary per platform. |
|
15 * result - function to call after all the events have fired to check |
|
16 * for additional results. May be null. This function will be |
|
17 * passed two arguments, the testname and the step argument. |
|
18 * steps - optional array of values. The test will be repeated for |
|
19 * each step, passing each successive value within the array to |
|
20 * the test and result functions |
|
21 * autohide - if set, should be set to the id of a popup to hide after |
|
22 * the test is complete. This is a convenience for some tests. |
|
23 * condition - an optional function which, if it returns false, causes the |
|
24 * test to be skipped. |
|
25 * end - used for debugging. Set to true to stop the tests after running |
|
26 * this one. |
|
27 */ |
|
28 |
|
29 const menuactiveAttribute = "_moz-menuactive"; |
|
30 |
|
31 var gPopupTests = null; |
|
32 var gTestIndex = -1; |
|
33 var gTestStepIndex = 0; |
|
34 var gTestEventIndex = 0; |
|
35 var gAutoHide = false; |
|
36 var gExpectedEventDetails = null; |
|
37 var gExpectedTriggerNode = null; |
|
38 var gWindowUtils; |
|
39 var gPopupWidth = -1, gPopupHeight = -1; |
|
40 |
|
41 function startPopupTests(tests) |
|
42 { |
|
43 document.addEventListener("popupshowing", eventOccurred, false); |
|
44 document.addEventListener("popupshown", eventOccurred, false); |
|
45 document.addEventListener("popuphiding", eventOccurred, false); |
|
46 document.addEventListener("popuphidden", eventOccurred, false); |
|
47 document.addEventListener("command", eventOccurred, false); |
|
48 document.addEventListener("DOMMenuItemActive", eventOccurred, false); |
|
49 document.addEventListener("DOMMenuItemInactive", eventOccurred, false); |
|
50 document.addEventListener("DOMMenuInactive", eventOccurred, false); |
|
51 document.addEventListener("DOMMenuBarActive", eventOccurred, false); |
|
52 document.addEventListener("DOMMenuBarInactive", eventOccurred, false); |
|
53 |
|
54 gPopupTests = tests; |
|
55 gWindowUtils = SpecialPowers.getDOMWindowUtils(window); |
|
56 |
|
57 goNext(); |
|
58 } |
|
59 |
|
60 function finish() |
|
61 { |
|
62 if (window.opener) { |
|
63 window.close(); |
|
64 window.opener.SimpleTest.finish(); |
|
65 return; |
|
66 } |
|
67 SimpleTest.finish(); |
|
68 return; |
|
69 } |
|
70 |
|
71 function ok(condition, message) { |
|
72 if (window.opener) |
|
73 window.opener.SimpleTest.ok(condition, message); |
|
74 else |
|
75 SimpleTest.ok(condition, message); |
|
76 } |
|
77 |
|
78 function is(left, right, message) { |
|
79 if (window.opener) |
|
80 window.opener.SimpleTest.is(left, right, message); |
|
81 else |
|
82 SimpleTest.is(left, right, message); |
|
83 } |
|
84 |
|
85 function disableNonTestMouse(aDisable) { |
|
86 gWindowUtils.disableNonTestMouseEvents(aDisable); |
|
87 } |
|
88 |
|
89 function eventOccurred(event) |
|
90 { |
|
91 if (gPopupTests.length <= gTestIndex) { |
|
92 ok(false, "Extra " + event.type + " event fired"); |
|
93 return; |
|
94 } |
|
95 |
|
96 var test = gPopupTests[gTestIndex]; |
|
97 if ("autohide" in test && gAutoHide) { |
|
98 if (event.type == "DOMMenuInactive") { |
|
99 gAutoHide = false; |
|
100 setTimeout(goNextStep, 0); |
|
101 } |
|
102 return; |
|
103 } |
|
104 |
|
105 var events = test.events; |
|
106 if (typeof events == "function") |
|
107 events = events(); |
|
108 if (events) { |
|
109 if (events.length <= gTestEventIndex) { |
|
110 ok(false, "Extra " + event.type + " event fired for " + event.target.id + |
|
111 " " +gPopupTests[gTestIndex].testname); |
|
112 return; |
|
113 } |
|
114 |
|
115 var eventitem = events[gTestEventIndex].split(" "); |
|
116 var matches; |
|
117 if (eventitem[1] == "#tooltip") { |
|
118 is(event.originalTarget.localName, "tooltip", |
|
119 test.testname + " event.originalTarget.localName is 'tooltip'"); |
|
120 is(event.originalTarget.getAttribute("default"), "true", |
|
121 test.testname + " event.originalTarget default attribute is 'true'"); |
|
122 matches = event.originalTarget.localName == "tooltip" && |
|
123 event.originalTarget.getAttribute("default") == "true"; |
|
124 } else { |
|
125 is(event.type, eventitem[0], |
|
126 test.testname + " event type " + event.type + " fired"); |
|
127 is(event.target.id, eventitem[1], |
|
128 test.testname + " event target ID " + event.target.id); |
|
129 matches = eventitem[0] == event.type && eventitem[1] == event.target.id; |
|
130 } |
|
131 |
|
132 var modifiersMask = eventitem[2]; |
|
133 if (modifiersMask) { |
|
134 var m = ""; |
|
135 m += event.altKey ? '1' : '0'; |
|
136 m += event.ctrlKey ? '1' : '0'; |
|
137 m += event.shiftKey ? '1' : '0'; |
|
138 m += event.metaKey ? '1' : '0'; |
|
139 is(m, modifiersMask, test.testname + " modifiers mask matches"); |
|
140 } |
|
141 |
|
142 var expectedState; |
|
143 switch (event.type) { |
|
144 case "popupshowing": expectedState = "showing"; break; |
|
145 case "popupshown": expectedState = "open"; break; |
|
146 case "popuphiding": expectedState = "hiding"; break; |
|
147 case "popuphidden": expectedState = "closed"; break; |
|
148 } |
|
149 |
|
150 if (gExpectedTriggerNode && event.type == "popupshowing") { |
|
151 if (gExpectedTriggerNode == "notset") // check against null instead |
|
152 gExpectedTriggerNode = null; |
|
153 |
|
154 is(event.originalTarget.triggerNode, gExpectedTriggerNode, test.testname + " popupshowing triggerNode"); |
|
155 var isTooltip = (event.target.localName == "tooltip"); |
|
156 is(document.popupNode, isTooltip ? null : gExpectedTriggerNode, |
|
157 test.testname + " popupshowing document.popupNode"); |
|
158 is(document.tooltipNode, isTooltip ? gExpectedTriggerNode : null, |
|
159 test.testname + " popupshowing document.tooltipNode"); |
|
160 } |
|
161 |
|
162 if (expectedState) |
|
163 is(event.originalTarget.state, expectedState, |
|
164 test.testname + " " + event.type + " state"); |
|
165 |
|
166 if (matches) { |
|
167 gTestEventIndex++ |
|
168 if (events.length <= gTestEventIndex) |
|
169 setTimeout(checkResult, 0); |
|
170 } |
|
171 } |
|
172 } |
|
173 |
|
174 function checkResult() |
|
175 { |
|
176 var step = null; |
|
177 var test = gPopupTests[gTestIndex]; |
|
178 if ("steps" in test) |
|
179 step = test.steps[gTestStepIndex]; |
|
180 |
|
181 if ("result" in test) |
|
182 test.result(test.testname, step); |
|
183 |
|
184 if ("autohide" in test) { |
|
185 gAutoHide = true; |
|
186 document.getElementById(test.autohide).hidePopup(); |
|
187 return; |
|
188 } |
|
189 |
|
190 goNextStep(); |
|
191 } |
|
192 |
|
193 function goNextStep() |
|
194 { |
|
195 gTestEventIndex = 0; |
|
196 |
|
197 var step = null; |
|
198 var test = gPopupTests[gTestIndex]; |
|
199 if ("steps" in test) { |
|
200 gTestStepIndex++; |
|
201 step = test.steps[gTestStepIndex]; |
|
202 if (gTestStepIndex < test.steps.length) { |
|
203 test.test(test.testname, step); |
|
204 return; |
|
205 } |
|
206 } |
|
207 |
|
208 goNext(); |
|
209 } |
|
210 |
|
211 function goNext() |
|
212 { |
|
213 // We want to continue after the next animation frame so that |
|
214 // we're in a stable state and don't get spurious mouse events at unexpected targets. |
|
215 window.requestAnimationFrame( |
|
216 function() { |
|
217 setTimeout(goNextStepSync, 0); |
|
218 } |
|
219 ); |
|
220 } |
|
221 |
|
222 function goNextStepSync() |
|
223 { |
|
224 if (gTestIndex >= 0 && "end" in gPopupTests[gTestIndex] && gPopupTests[gTestIndex].end) { |
|
225 finish(); |
|
226 return; |
|
227 } |
|
228 |
|
229 gTestIndex++; |
|
230 gTestStepIndex = 0; |
|
231 if (gTestIndex < gPopupTests.length) { |
|
232 var test = gPopupTests[gTestIndex]; |
|
233 // Set the location hash so it's easy to see which test is running |
|
234 document.location.hash = test.testname; |
|
235 |
|
236 // skip the test if the condition returns false |
|
237 if ("condition" in test && !test.condition()) { |
|
238 goNext(); |
|
239 return; |
|
240 } |
|
241 |
|
242 // start with the first step if there are any |
|
243 var step = null; |
|
244 if ("steps" in test) |
|
245 step = test.steps[gTestStepIndex]; |
|
246 |
|
247 test.test(test.testname, step); |
|
248 |
|
249 // no events to check for so just check the result |
|
250 if (!("events" in test)) |
|
251 checkResult(); |
|
252 } |
|
253 else { |
|
254 finish(); |
|
255 } |
|
256 } |
|
257 |
|
258 function openMenu(menu) |
|
259 { |
|
260 if ("open" in menu) { |
|
261 menu.open = true; |
|
262 } |
|
263 else { |
|
264 var bo = menu.boxObject; |
|
265 if (bo instanceof SpecialPowers.Ci.nsIMenuBoxObject) |
|
266 bo.openMenu(true); |
|
267 else |
|
268 synthesizeMouse(menu, 4, 4, { }); |
|
269 } |
|
270 } |
|
271 |
|
272 function closeMenu(menu, popup) |
|
273 { |
|
274 if ("open" in menu) { |
|
275 menu.open = false; |
|
276 } |
|
277 else { |
|
278 var bo = menu.boxObject; |
|
279 if (bo instanceof SpecialPowers.Ci.nsIMenuBoxObject) |
|
280 bo.openMenu(false); |
|
281 else |
|
282 popup.hidePopup(); |
|
283 } |
|
284 } |
|
285 |
|
286 function checkActive(popup, id, testname) |
|
287 { |
|
288 var activeok = true; |
|
289 var children = popup.childNodes; |
|
290 for (var c = 0; c < children.length; c++) { |
|
291 var child = children[c]; |
|
292 if ((id == child.id && child.getAttribute(menuactiveAttribute) != "true") || |
|
293 (id != child.id && child.hasAttribute(menuactiveAttribute) != "")) { |
|
294 activeok = false; |
|
295 break; |
|
296 } |
|
297 } |
|
298 ok(activeok, testname + " item " + (id ? id : "none") + " active"); |
|
299 } |
|
300 |
|
301 function checkOpen(menuid, testname) |
|
302 { |
|
303 var menu = document.getElementById(menuid); |
|
304 if ("open" in menu) |
|
305 ok(menu.open, testname + " " + menuid + " menu is open"); |
|
306 else if (menu.boxObject instanceof SpecialPowers.Ci.nsIMenuBoxObject) |
|
307 ok(menu.getAttribute("open") == "true", testname + " " + menuid + " menu is open"); |
|
308 } |
|
309 |
|
310 function checkClosed(menuid, testname) |
|
311 { |
|
312 var menu = document.getElementById(menuid); |
|
313 if ("open" in menu) |
|
314 ok(!menu.open, testname + " " + menuid + " menu is open"); |
|
315 else if (menu.boxObject instanceof SpecialPowers.Ci.nsIMenuBoxObject) |
|
316 ok(!menu.hasAttribute("open"), testname + " " + menuid + " menu is closed"); |
|
317 } |
|
318 |
|
319 function convertPosition(anchor, align) |
|
320 { |
|
321 if (anchor == "topleft" && align == "topleft") return "overlap"; |
|
322 if (anchor == "topleft" && align == "topright") return "start_before"; |
|
323 if (anchor == "topleft" && align == "bottomleft") return "before_start"; |
|
324 if (anchor == "topright" && align == "topleft") return "end_before"; |
|
325 if (anchor == "topright" && align == "bottomright") return "before_end"; |
|
326 if (anchor == "bottomleft" && align == "bottomright") return "start_after"; |
|
327 if (anchor == "bottomleft" && align == "topleft") return "after_start"; |
|
328 if (anchor == "bottomright" && align == "bottomleft") return "end_after"; |
|
329 if (anchor == "bottomright" && align == "topright") return "after_end"; |
|
330 return ""; |
|
331 } |
|
332 |
|
333 /* |
|
334 * When checking position of the bottom or right edge of the popup's rect, |
|
335 * use this instead of strict equality check of rounded values, |
|
336 * because we snap the top/left edges to pixel boundaries, |
|
337 * which can shift the bottom/right up to 0.5px from its "ideal" location, |
|
338 * and could cause it to round differently. (See bug 622507.) |
|
339 */ |
|
340 function isWithinHalfPixel(a, b) |
|
341 { |
|
342 return Math.abs(a - b) <= 0.5; |
|
343 } |
|
344 |
|
345 function compareEdge(anchor, popup, edge, offsetX, offsetY, testname) |
|
346 { |
|
347 testname += " " + edge; |
|
348 |
|
349 checkOpen(anchor.id, testname); |
|
350 |
|
351 var anchorrect = anchor.getBoundingClientRect(); |
|
352 var popuprect = popup.getBoundingClientRect(); |
|
353 var check1 = false, check2 = false; |
|
354 |
|
355 if (gPopupWidth == -1) { |
|
356 ok((Math.round(popuprect.right) - Math.round(popuprect.left)) && |
|
357 (Math.round(popuprect.bottom) - Math.round(popuprect.top)), |
|
358 testname + " size"); |
|
359 } |
|
360 else { |
|
361 is(Math.round(popuprect.width), gPopupWidth, testname + " width"); |
|
362 is(Math.round(popuprect.height), gPopupHeight, testname + " height"); |
|
363 } |
|
364 |
|
365 var spaceIdx = edge.indexOf(" "); |
|
366 if (spaceIdx > 0) { |
|
367 let cornerX, cornerY; |
|
368 let [anchor, align] = edge.split(" "); |
|
369 switch (anchor) { |
|
370 case "topleft": cornerX = anchorrect.left; cornerY = anchorrect.top; break; |
|
371 case "topcenter": cornerX = anchorrect.left + anchorrect.width / 2; cornerY = anchorrect.top; break; |
|
372 case "topright": cornerX = anchorrect.right; cornerY = anchorrect.top; break; |
|
373 case "leftcenter": cornerX = anchorrect.left; cornerY = anchorrect.top + anchorrect.height / 2; break; |
|
374 case "rightcenter": cornerX = anchorrect.right; cornerY = anchorrect.top + anchorrect.height / 2; break; |
|
375 case "bottomleft": cornerX = anchorrect.left; cornerY = anchorrect.bottom; break; |
|
376 case "bottomcenter": cornerX = anchorrect.left + anchorrect.width / 2; cornerY = anchorrect.bottom; break; |
|
377 case "bottomright": cornerX = anchorrect.right; cornerY = anchorrect.bottom; break; |
|
378 } |
|
379 |
|
380 switch (align) { |
|
381 case "topleft": cornerX += offsetX; cornerY += offsetY; break; |
|
382 case "topright": cornerX += -popuprect.width + offsetX; cornerY += offsetY; break; |
|
383 case "bottomleft": cornerX += offsetX; cornerY += -popuprect.height + offsetY; break; |
|
384 case "bottomright": cornerX += -popuprect.width + offsetX; cornerY += -popuprect.height + offsetY; break; |
|
385 } |
|
386 |
|
387 is(Math.round(popuprect.left), Math.round(cornerX), testname + " x position"); |
|
388 is(Math.round(popuprect.top), Math.round(cornerY), testname + " y position"); |
|
389 return; |
|
390 } |
|
391 |
|
392 if (edge == "after_pointer") { |
|
393 is(Math.round(popuprect.left), Math.round(anchorrect.left) + offsetX, testname + " x position"); |
|
394 is(Math.round(popuprect.top), Math.round(anchorrect.top) + offsetY + 21, testname + " y position"); |
|
395 return; |
|
396 } |
|
397 |
|
398 if (edge == "overlap") { |
|
399 ok(Math.round(anchorrect.left) + offsetY == Math.round(popuprect.left) && |
|
400 Math.round(anchorrect.top) + offsetY == Math.round(popuprect.top), |
|
401 testname + " position"); |
|
402 return; |
|
403 } |
|
404 |
|
405 if (edge.indexOf("before") == 0) |
|
406 check1 = isWithinHalfPixel(anchorrect.top + offsetY, popuprect.bottom); |
|
407 else if (edge.indexOf("after") == 0) |
|
408 check1 = (Math.round(anchorrect.bottom) + offsetY == Math.round(popuprect.top)); |
|
409 else if (edge.indexOf("start") == 0) |
|
410 check1 = isWithinHalfPixel(anchorrect.left + offsetX, popuprect.right); |
|
411 else if (edge.indexOf("end") == 0) |
|
412 check1 = (Math.round(anchorrect.right) + offsetX == Math.round(popuprect.left)); |
|
413 |
|
414 if (0 < edge.indexOf("before")) |
|
415 check2 = (Math.round(anchorrect.top) + offsetY == Math.round(popuprect.top)); |
|
416 else if (0 < edge.indexOf("after")) |
|
417 check2 = isWithinHalfPixel(anchorrect.bottom + offsetY, popuprect.bottom); |
|
418 else if (0 < edge.indexOf("start")) |
|
419 check2 = (Math.round(anchorrect.left) + offsetX == Math.round(popuprect.left)); |
|
420 else if (0 < edge.indexOf("end")) |
|
421 check2 = isWithinHalfPixel(anchorrect.right + offsetX, popuprect.right); |
|
422 |
|
423 ok(check1 && check2, testname + " position"); |
|
424 } |