|
1 <?xml version="1.0"?> |
|
2 <?xml-stylesheet href="chrome://global/skin" type="text/css"?> |
|
3 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" |
|
4 type="text/css"?> |
|
5 |
|
6 <!-- Firefox searchbar --> |
|
7 <?xml-stylesheet href="chrome://browser/content/browser.css" |
|
8 type="text/css"?> |
|
9 <!-- SeaMonkey searchbar --> |
|
10 <?xml-stylesheet href="chrome://navigator/content/navigator.css" |
|
11 type="text/css"?> |
|
12 |
|
13 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" |
|
14 title="Accessible focus event testing"> |
|
15 |
|
16 <script type="application/javascript" |
|
17 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> |
|
18 <script type="application/javascript" |
|
19 src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> |
|
20 |
|
21 <script type="application/javascript" |
|
22 src="../common.js" /> |
|
23 <script type="application/javascript" |
|
24 src="../role.js" /> |
|
25 <script type="application/javascript" |
|
26 src="../states.js" /> |
|
27 <script type="application/javascript" |
|
28 src="../events.js" /> |
|
29 |
|
30 <script type="application/javascript" |
|
31 src="../autocomplete.js" /> |
|
32 |
|
33 <script type="application/javascript"> |
|
34 <![CDATA[ |
|
35 //////////////////////////////////////////////////////////////////////////// |
|
36 // Hacky stuffs |
|
37 |
|
38 // This is the hack needed for searchbar work outside of browser. |
|
39 function getBrowser() |
|
40 { |
|
41 return { |
|
42 mCurrentBrowser: { engines: new Array() } |
|
43 }; |
|
44 } |
|
45 |
|
46 //////////////////////////////////////////////////////////////////////////// |
|
47 // Invokers |
|
48 |
|
49 function loadFormAutoComplete(aIFrameID) |
|
50 { |
|
51 this.iframeNode = getNode(aIFrameID); |
|
52 this.iframe = getAccessible(aIFrameID); |
|
53 |
|
54 this.eventSeq = [ |
|
55 new invokerChecker(EVENT_REORDER, this.iframe) |
|
56 ]; |
|
57 |
|
58 this.invoke = function loadFormAutoComplete_invoke() |
|
59 { |
|
60 var url = "data:text/html,<html><body><form id='form'>" + |
|
61 "<input id='input' name='a11ytest-formautocomplete'>" + |
|
62 "</form></body></html>"; |
|
63 this.iframeNode.setAttribute("src", url); |
|
64 } |
|
65 |
|
66 this.getID = function loadFormAutoComplete_getID() |
|
67 { |
|
68 return "load form autocomplete page"; |
|
69 } |
|
70 } |
|
71 |
|
72 function initFormAutoCompleteBy(aIFrameID, aAutoCompleteValue) |
|
73 { |
|
74 this.iframe = getAccessible(aIFrameID); |
|
75 |
|
76 this.eventSeq = [ |
|
77 new invokerChecker(EVENT_REORDER, this.iframe) |
|
78 ]; |
|
79 |
|
80 this.invoke = function initFormAutoCompleteBy_invoke() |
|
81 { |
|
82 var iframeDOMDoc = getIFrameDOMDoc(aIFrameID); |
|
83 |
|
84 var inputNode = iframeDOMDoc.getElementById("input"); |
|
85 inputNode.value = aAutoCompleteValue; |
|
86 var formNode = iframeDOMDoc.getElementById("form"); |
|
87 formNode.submit(); |
|
88 } |
|
89 |
|
90 this.getID = function initFormAutoCompleteBy_getID() |
|
91 { |
|
92 return "init form autocomplete by '" + aAutoCompleteValue + "'"; |
|
93 } |
|
94 } |
|
95 |
|
96 function loadHTML5ListAutoComplete(aIFrameID) |
|
97 { |
|
98 this.iframeNode = getNode(aIFrameID); |
|
99 this.iframe = getAccessible(aIFrameID); |
|
100 |
|
101 this.eventSeq = [ |
|
102 new invokerChecker(EVENT_REORDER, this.iframe) |
|
103 ]; |
|
104 |
|
105 this.invoke = function loadHTML5ListAutoComplete_invoke() |
|
106 { |
|
107 var url = "data:text/html,<html><body>" + |
|
108 "<datalist id='cities'><option>hello</option><option>hi</option></datalist>" + |
|
109 "<input id='input' list='cities'>" + |
|
110 "</body></html>"; |
|
111 this.iframeNode.setAttribute("src", url); |
|
112 } |
|
113 |
|
114 this.getID = function loadHTML5ListAutoComplete_getID() |
|
115 { |
|
116 return "load HTML5 list autocomplete page"; |
|
117 } |
|
118 } |
|
119 |
|
120 function removeChar(aID, aCheckerOrEventSeq) |
|
121 { |
|
122 this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); |
|
123 |
|
124 this.invoke = function removeChar_invoke() |
|
125 { |
|
126 synthesizeKey("VK_LEFT", { shiftKey: true }); |
|
127 synthesizeKey("VK_DELETE", {}); |
|
128 } |
|
129 |
|
130 this.getID = function removeChar_getID() |
|
131 { |
|
132 return "remove char on " + prettyName(aID); |
|
133 } |
|
134 } |
|
135 |
|
136 function replaceOnChar(aID, aChar, aCheckerOrEventSeq) |
|
137 { |
|
138 this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); |
|
139 |
|
140 this.invoke = function replaceOnChar_invoke() |
|
141 { |
|
142 this.DOMNode.select(); |
|
143 synthesizeKey(aChar, {}); |
|
144 } |
|
145 |
|
146 this.getID = function replaceOnChar_getID() |
|
147 { |
|
148 return "replace on char '" + aChar + "' for" + prettyName(aID); |
|
149 } |
|
150 } |
|
151 |
|
152 function focusOnMouseOver(aIDFunc, aIDFuncArg) |
|
153 { |
|
154 this.eventSeq = [ new focusChecker(aIDFunc, aIDFuncArg) ]; |
|
155 |
|
156 this.invoke = function focusOnMouseOver_invoke() |
|
157 { |
|
158 this.id = aIDFunc.call(null, aIDFuncArg); |
|
159 this.node = getNode(this.id); |
|
160 this.window = this.node.ownerDocument.defaultView; |
|
161 |
|
162 if (this.node.localName == "tree") { |
|
163 var tree = getAccessible(this.node); |
|
164 var accessible = getAccessible(this.id); |
|
165 if (tree != accessible) { |
|
166 var itemX = {}, itemY = {}, treeX = {}, treeY = {}; |
|
167 accessible.getBounds(itemX, itemY, {}, {}); |
|
168 tree.getBounds(treeX, treeY, {}, {}); |
|
169 this.x = itemX.value - treeX.value; |
|
170 this.y = itemY.value - treeY.value; |
|
171 } |
|
172 } |
|
173 |
|
174 // Generate mouse move events in timeouts until autocomplete popup list |
|
175 // doesn't have it, the reason is do that because autocomplete popup |
|
176 // ignores mousemove events firing in too short range. |
|
177 synthesizeMouse(this.node, this.x, this.y, { type: "mousemove" }); |
|
178 this.doMouseMoveFlood(this); |
|
179 } |
|
180 |
|
181 this.finalCheck = function focusOnMouseOver_getID() |
|
182 { |
|
183 this.isFlooding = false; |
|
184 } |
|
185 |
|
186 this.getID = function focusOnMouseOver_getID() |
|
187 { |
|
188 return "mouse over on " + prettyName(aIDFunc.call(null, aIDFuncArg)); |
|
189 } |
|
190 |
|
191 this.doMouseMoveFlood = function focusOnMouseOver_doMouseMoveFlood(aThis) |
|
192 { |
|
193 synthesizeMouse(aThis.node, aThis.x + 1, aThis.y + 1, |
|
194 { type: "mousemove" }, aThis.window); |
|
195 |
|
196 if (aThis.isFlooding) |
|
197 aThis.window.setTimeout(aThis.doMouseMoveFlood, 0, aThis); |
|
198 } |
|
199 |
|
200 this.id = null; |
|
201 this.node = null; |
|
202 this.window = null; |
|
203 |
|
204 this.isFlooding = true; |
|
205 this.x = 1; |
|
206 this.y = 1; |
|
207 } |
|
208 |
|
209 function selectByClick(aIDFunc, aIDFuncArg, |
|
210 aFocusTargetFunc, aFocusTargetFuncArg) |
|
211 { |
|
212 this.eventSeq = [ new focusChecker(aFocusTargetFunc, aFocusTargetFuncArg) ]; |
|
213 |
|
214 this.invoke = function selectByClick_invoke() |
|
215 { |
|
216 var id = aIDFunc.call(null, aIDFuncArg); |
|
217 var node = getNode(id); |
|
218 var targetWindow = node.ownerDocument.defaultView; |
|
219 |
|
220 var x = 0, y = 0; |
|
221 if (node.localName == "tree") { |
|
222 var tree = getAccessible(node); |
|
223 var accessible = getAccessible(id); |
|
224 if (tree != accessible) { |
|
225 var itemX = {}, itemY = {}, treeX = {}, treeY = {}; |
|
226 accessible.getBounds(itemX, itemY, {}, {}); |
|
227 tree.getBounds(treeX, treeY, {}, {}); |
|
228 x = itemX.value - treeX.value; |
|
229 y = itemY.value - treeY.value; |
|
230 } |
|
231 } |
|
232 |
|
233 synthesizeMouseAtCenter(node, {}, targetWindow); |
|
234 } |
|
235 |
|
236 this.getID = function selectByClick_getID() |
|
237 { |
|
238 return "select by click " + prettyName(aIDFunc.call(null, aIDFuncArg)); |
|
239 } |
|
240 } |
|
241 |
|
242 //////////////////////////////////////////////////////////////////////////// |
|
243 // Target getters |
|
244 |
|
245 function getItem(aItemObj) |
|
246 { |
|
247 var autocomplete = aItemObj.autocomplete; |
|
248 var autocompleteNode = aItemObj.autocompleteNode; |
|
249 |
|
250 // XUL searchbar |
|
251 if (autocompleteNode.localName == "searchbar") { |
|
252 var popupNode = autocompleteNode._popup; |
|
253 if (popupNode) { |
|
254 var list = getAccessible(popupNode); |
|
255 return list.getChildAt(aItemObj.index); |
|
256 } |
|
257 } |
|
258 |
|
259 // XUL autocomplete |
|
260 var popupNode = autocompleteNode.popup; |
|
261 if (!popupNode) { |
|
262 // HTML form autocomplete |
|
263 var controller = Components.classes["@mozilla.org/autocomplete/controller;1"]. |
|
264 getService(Components.interfaces.nsIAutoCompleteController); |
|
265 popupNode = controller.input.popup.QueryInterface(nsIDOMNode); |
|
266 } |
|
267 |
|
268 if (popupNode) { |
|
269 if ("richlistbox" in popupNode) { |
|
270 var list = getAccessible(popupNode.richlistbox); |
|
271 return list.getChildAt(aItemObj.index); |
|
272 } |
|
273 |
|
274 var list = getAccessible(popupNode.tree); |
|
275 return list.getChildAt(aItemObj.index + 1); |
|
276 } |
|
277 } |
|
278 |
|
279 function getTextEntry(aID) |
|
280 { |
|
281 // For form autocompletes the autocomplete widget and text entry widget |
|
282 // is the same widget, for XUL autocompletes the text entry is a first |
|
283 // child. |
|
284 var localName = getNode(aID).localName; |
|
285 |
|
286 // XUL autocomplete |
|
287 if (localName == "textbox") |
|
288 return getAccessible(aID).firstChild; |
|
289 |
|
290 // HTML form autocomplete |
|
291 if (localName == "input") |
|
292 return getAccessible(aID); |
|
293 |
|
294 // XUL searchbar |
|
295 if (localName == "searchbar") |
|
296 return getAccessible(getNode(aID).textbox.inputField); |
|
297 |
|
298 return null; |
|
299 } |
|
300 |
|
301 function itemObj(aID, aIdx) |
|
302 { |
|
303 this.autocompleteNode = getNode(aID); |
|
304 |
|
305 this.autocomplete = this.autocompleteNode.localName == "searchbar" ? |
|
306 getAccessible(this.autocompleteNode.textbox) : |
|
307 getAccessible(this.autocompleteNode); |
|
308 |
|
309 this.index = aIdx; |
|
310 } |
|
311 |
|
312 function getIFrameDOMDoc(aIFrameID) |
|
313 { |
|
314 return getNode(aIFrameID).contentDocument; |
|
315 } |
|
316 |
|
317 //////////////////////////////////////////////////////////////////////////// |
|
318 // Test helpers |
|
319 |
|
320 function queueAutoCompleteTests(aID) |
|
321 { |
|
322 // focus autocomplete text entry |
|
323 gQueue.push(new synthFocus(aID, new focusChecker(getTextEntry, aID))); |
|
324 |
|
325 // open autocomplete popup |
|
326 gQueue.push(new synthDownKey(aID, new nofocusChecker())); |
|
327 |
|
328 // select second option ('hi' option), focus on it |
|
329 gQueue.push(new synthUpKey(aID, |
|
330 new focusChecker(getItem, new itemObj(aID, 1)))); |
|
331 |
|
332 // choose selected option, focus on text entry |
|
333 gQueue.push(new synthEnterKey(aID, new focusChecker(getTextEntry, aID))); |
|
334 |
|
335 // remove char, autocomplete popup appears |
|
336 gQueue.push(new removeChar(aID, new nofocusChecker())); |
|
337 |
|
338 // select first option ('hello' option), focus on it |
|
339 gQueue.push(new synthDownKey(aID, |
|
340 new focusChecker(getItem, new itemObj(aID, 0)))); |
|
341 |
|
342 // mouse move on second option ('hi' option), focus on it |
|
343 gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 1))); |
|
344 |
|
345 // autocomplete popup updated (no selected item), focus on textentry |
|
346 gQueue.push(new synthKey(aID, "e", null, new focusChecker(getTextEntry, aID))); |
|
347 |
|
348 // select first option ('hello' option), focus on it |
|
349 gQueue.push(new synthDownKey(aID, |
|
350 new focusChecker(getItem, new itemObj(aID, 0)))); |
|
351 |
|
352 // popup gets hidden, focus on textentry |
|
353 gQueue.push(new synthRightKey(aID, new focusChecker(getTextEntry, aID))); |
|
354 |
|
355 // popup gets open, no focus |
|
356 gQueue.push(new synthOpenComboboxKey(aID, new nofocusChecker())); |
|
357 |
|
358 // select first option again ('hello' option), focus on it |
|
359 gQueue.push(new synthDownKey(aID, |
|
360 new focusChecker(getItem, new itemObj(aID, 0)))); |
|
361 |
|
362 // no option is selected, focus on text entry |
|
363 gQueue.push(new synthUpKey(aID, new focusChecker(getTextEntry, aID))); |
|
364 |
|
365 // close popup, no focus |
|
366 gQueue.push(new synthEscapeKey(aID, new nofocusChecker())); |
|
367 |
|
368 // autocomplete popup appears (no selected item), focus stays on textentry |
|
369 gQueue.push(new replaceOnChar(aID, "h", new nofocusChecker())); |
|
370 |
|
371 // mouse move on first option ('hello' option), focus on it |
|
372 gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 0))); |
|
373 |
|
374 // click first option ('hello' option), popup closes, focus on text entry |
|
375 gQueue.push(new selectByClick(getItem, new itemObj(aID, 0), getTextEntry, aID)); |
|
376 } |
|
377 |
|
378 //////////////////////////////////////////////////////////////////////////// |
|
379 // Tests |
|
380 |
|
381 //gA11yEventDumpID = "eventdump"; // debug stuff |
|
382 //gA11yEventDumpToConsole = true; // debug stuff |
|
383 |
|
384 var gInitQueue = null; |
|
385 function initTests() |
|
386 { |
|
387 if (SEAMONKEY || MAC) { |
|
388 todo(false, "Skipping this test on SeaMonkey ftb. (Bug 718237), and on Mac (bug 746177)"); |
|
389 shutdownAutoComplete(); |
|
390 SimpleTest.finish(); |
|
391 return; |
|
392 } |
|
393 |
|
394 gInitQueue = new eventQueue(); |
|
395 gInitQueue.push(new loadFormAutoComplete("iframe")); |
|
396 gInitQueue.push(new initFormAutoCompleteBy("iframe", "hello")); |
|
397 gInitQueue.push(new initFormAutoCompleteBy("iframe", "hi")); |
|
398 gInitQueue.push(new loadHTML5ListAutoComplete("iframe2")); |
|
399 gInitQueue.onFinish = function initQueue_onFinish() |
|
400 { |
|
401 SimpleTest.executeSoon(doTests); |
|
402 return DO_NOT_FINISH_TEST; |
|
403 } |
|
404 gInitQueue.invoke(); |
|
405 } |
|
406 |
|
407 var gQueue = null; |
|
408 function doTests() |
|
409 { |
|
410 // Test focus events. |
|
411 gQueue = new eventQueue(); |
|
412 |
|
413 //////////////////////////////////////////////////////////////////////////// |
|
414 // tree popup autocomplete tests |
|
415 queueAutoCompleteTests("autocomplete"); |
|
416 |
|
417 //////////////////////////////////////////////////////////////////////////// |
|
418 // richlistbox popup autocomplete tests |
|
419 queueAutoCompleteTests("richautocomplete"); |
|
420 |
|
421 //////////////////////////////////////////////////////////////////////////// |
|
422 // HTML form autocomplete tests |
|
423 queueAutoCompleteTests(getIFrameDOMDoc("iframe").getElementById("input")); |
|
424 |
|
425 //////////////////////////////////////////////////////////////////////////// |
|
426 // HTML5 list autocomplete tests |
|
427 queueAutoCompleteTests(getIFrameDOMDoc("iframe2").getElementById("input")); |
|
428 |
|
429 //////////////////////////////////////////////////////////////////////////// |
|
430 // searchbar tests |
|
431 |
|
432 // focus searchbar, focus on text entry |
|
433 gQueue.push(new synthFocus("searchbar", |
|
434 new focusChecker(getTextEntry, "searchbar"))); |
|
435 // open search engine popup, no focus |
|
436 gQueue.push(new synthOpenComboboxKey("searchbar", new nofocusChecker())); |
|
437 // select first item, focus on it |
|
438 gQueue.push(new synthDownKey("searchbar", |
|
439 new focusChecker(getItem, new itemObj("searchbar", 0)))); |
|
440 // mouse over on second item, focus on it |
|
441 gQueue.push(new focusOnMouseOver(getItem, new itemObj("searchbar", 1))); |
|
442 // press enter key, focus on text entry |
|
443 gQueue.push(new synthEnterKey("searchbar", |
|
444 new focusChecker(getTextEntry, "searchbar"))); |
|
445 // click on search button, open popup, focus goes to document |
|
446 var searchBtn = getAccessible(getNode("searchbar").searchButton); |
|
447 gQueue.push(new synthClick(searchBtn, new focusChecker(document))); |
|
448 // select first item, focus on it |
|
449 gQueue.push(new synthDownKey("searchbar", |
|
450 new focusChecker(getItem, new itemObj("searchbar", 0)))); |
|
451 // close popup, focus goes on document |
|
452 gQueue.push(new synthEscapeKey("searchbar", new focusChecker(document))); |
|
453 |
|
454 gQueue.onFinish = function() |
|
455 { |
|
456 // unregister 'test-a11y-search' autocomplete search |
|
457 shutdownAutoComplete(); |
|
458 } |
|
459 gQueue.invoke(); // Will call SimpleTest.finish(); |
|
460 } |
|
461 |
|
462 SimpleTest.waitForExplicitFinish(); |
|
463 |
|
464 // Register 'test-a11y-search' autocomplete search. |
|
465 // XPFE AutoComplete needs to register early. |
|
466 initAutoComplete([ "hello", "hi" ], |
|
467 [ "Beep beep'm beep beep yeah", "Baby you can drive my car" ]); |
|
468 |
|
469 addA11yLoadEvent(initTests); |
|
470 ]]> |
|
471 </script> |
|
472 |
|
473 <hbox flex="1" style="overflow: auto;"> |
|
474 <body xmlns="http://www.w3.org/1999/xhtml"> |
|
475 <a target="_blank" |
|
476 href="https://bugzilla.mozilla.org/show_bug.cgi?id=383759" |
|
477 title="Focus event inconsistent for search box autocomplete"> |
|
478 Mozilla Bug 383759 |
|
479 </a> |
|
480 <a target="_blank" |
|
481 href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" |
|
482 title="Rework accessible focus handling"> |
|
483 Mozilla Bug 673958 |
|
484 </a> |
|
485 <a target="_blank" |
|
486 href="https://bugzilla.mozilla.org/show_bug.cgi?id=559766" |
|
487 title="Add accessibility support for @list on HTML input and for HTML datalist"> |
|
488 Mozilla Bug 559766 |
|
489 </a> |
|
490 <p id="display"></p> |
|
491 <div id="content" style="display: none"></div> |
|
492 <pre id="test"> |
|
493 </pre> |
|
494 </body> |
|
495 |
|
496 <vbox flex="1"> |
|
497 <textbox id="autocomplete" type="autocomplete" |
|
498 autocompletesearch="test-a11y-search"/> |
|
499 |
|
500 <textbox id="richautocomplete" type="autocomplete" |
|
501 autocompletesearch="test-a11y-search" |
|
502 autocompletepopup="richpopup"/> |
|
503 <panel id="richpopup" type="autocomplete-richlistbox" noautofocus="true"/> |
|
504 |
|
505 <iframe id="iframe"/> |
|
506 |
|
507 <iframe id="iframe2"/> |
|
508 |
|
509 <searchbar id="searchbar"/> |
|
510 |
|
511 <vbox id="eventdump"/> |
|
512 </vbox> |
|
513 </hbox> |
|
514 </window> |