|
1 //////////////////////////////////////////////////////////////////////////////// |
|
2 // Constants |
|
3 |
|
4 const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; |
|
5 const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE; |
|
6 const EVENT_DOCUMENT_LOAD_COMPLETE = nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE; |
|
7 const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD; |
|
8 const EVENT_DOCUMENT_LOAD_STOPPED = nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED; |
|
9 const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE; |
|
10 const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS; |
|
11 const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; |
|
12 const EVENT_MENU_START = nsIAccessibleEvent.EVENT_MENU_START; |
|
13 const EVENT_MENU_END = nsIAccessibleEvent.EVENT_MENU_END; |
|
14 const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START; |
|
15 const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END; |
|
16 const EVENT_OBJECT_ATTRIBUTE_CHANGED = nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED; |
|
17 const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER; |
|
18 const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START; |
|
19 const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION; |
|
20 const EVENT_SELECTION_ADD = nsIAccessibleEvent.EVENT_SELECTION_ADD; |
|
21 const EVENT_SELECTION_REMOVE = nsIAccessibleEvent.EVENT_SELECTION_REMOVE; |
|
22 const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN; |
|
23 const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW; |
|
24 const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE; |
|
25 const EVENT_TEXT_ATTRIBUTE_CHANGED = nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED; |
|
26 const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED; |
|
27 const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED; |
|
28 const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED; |
|
29 const EVENT_TEXT_SELECTION_CHANGED = nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; |
|
30 const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE; |
|
31 const EVENT_VIRTUALCURSOR_CHANGED = nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED; |
|
32 |
|
33 const kNotFromUserInput = 0; |
|
34 const kFromUserInput = 1; |
|
35 |
|
36 //////////////////////////////////////////////////////////////////////////////// |
|
37 // General |
|
38 |
|
39 Components.utils.import("resource://gre/modules/Services.jsm"); |
|
40 |
|
41 /** |
|
42 * Set up this variable to dump events into DOM. |
|
43 */ |
|
44 var gA11yEventDumpID = ""; |
|
45 |
|
46 /** |
|
47 * Set up this variable to dump event processing into console. |
|
48 */ |
|
49 var gA11yEventDumpToConsole = false; |
|
50 |
|
51 /** |
|
52 * Set up this variable to dump event processing into error console. |
|
53 */ |
|
54 var gA11yEventDumpToAppConsole = false; |
|
55 |
|
56 /** |
|
57 * Semicolon separated set of logging features. |
|
58 */ |
|
59 var gA11yEventDumpFeature = ""; |
|
60 |
|
61 /** |
|
62 * Executes the function when requested event is handled. |
|
63 * |
|
64 * @param aEventType [in] event type |
|
65 * @param aTarget [in] event target |
|
66 * @param aFunc [in] function to call when event is handled |
|
67 * @param aContext [in, optional] object in which context the function is |
|
68 * called |
|
69 * @param aArg1 [in, optional] argument passed into the function |
|
70 * @param aArg2 [in, optional] argument passed into the function |
|
71 */ |
|
72 function waitForEvent(aEventType, aTargetOrFunc, aFunc, aContext, aArg1, aArg2) |
|
73 { |
|
74 var handler = { |
|
75 handleEvent: function handleEvent(aEvent) { |
|
76 |
|
77 var target = aTargetOrFunc; |
|
78 if (typeof aTargetOrFunc == "function") |
|
79 target = aTargetOrFunc.call(); |
|
80 |
|
81 if (target) { |
|
82 if (target instanceof nsIAccessible && |
|
83 target != aEvent.accessible) |
|
84 return; |
|
85 |
|
86 if (target instanceof nsIDOMNode && |
|
87 target != aEvent.DOMNode) |
|
88 return; |
|
89 } |
|
90 |
|
91 unregisterA11yEventListener(aEventType, this); |
|
92 |
|
93 window.setTimeout( |
|
94 function () |
|
95 { |
|
96 aFunc.call(aContext, aArg1, aArg2); |
|
97 }, |
|
98 0 |
|
99 ); |
|
100 } |
|
101 }; |
|
102 |
|
103 registerA11yEventListener(aEventType, handler); |
|
104 } |
|
105 |
|
106 /** |
|
107 * Generate mouse move over image map what creates image map accessible (async). |
|
108 * See waitForImageMap() function. |
|
109 */ |
|
110 function waveOverImageMap(aImageMapID) |
|
111 { |
|
112 var imageMapNode = getNode(aImageMapID); |
|
113 synthesizeMouse(imageMapNode, 10, 10, { type: "mousemove" }, |
|
114 imageMapNode.ownerDocument.defaultView); |
|
115 } |
|
116 |
|
117 /** |
|
118 * Call the given function when the tree of the given image map is built. |
|
119 */ |
|
120 function waitForImageMap(aImageMapID, aTestFunc) |
|
121 { |
|
122 waveOverImageMap(aImageMapID); |
|
123 |
|
124 var imageMapAcc = getAccessible(aImageMapID); |
|
125 if (imageMapAcc.firstChild) |
|
126 return aTestFunc(); |
|
127 |
|
128 waitForEvent(EVENT_REORDER, imageMapAcc, aTestFunc); |
|
129 } |
|
130 |
|
131 /** |
|
132 * Register accessibility event listener. |
|
133 * |
|
134 * @param aEventType the accessible event type (see nsIAccessibleEvent for |
|
135 * available constants). |
|
136 * @param aEventHandler event listener object, when accessible event of the |
|
137 * given type is handled then 'handleEvent' method of |
|
138 * this object is invoked with nsIAccessibleEvent object |
|
139 * as the first argument. |
|
140 */ |
|
141 function registerA11yEventListener(aEventType, aEventHandler) |
|
142 { |
|
143 listenA11yEvents(true); |
|
144 addA11yEventListener(aEventType, aEventHandler); |
|
145 } |
|
146 |
|
147 /** |
|
148 * Unregister accessibility event listener. Must be called for every registered |
|
149 * event listener (see registerA11yEventListener() function) when the listener |
|
150 * is not needed. |
|
151 */ |
|
152 function unregisterA11yEventListener(aEventType, aEventHandler) |
|
153 { |
|
154 removeA11yEventListener(aEventType, aEventHandler); |
|
155 listenA11yEvents(false); |
|
156 } |
|
157 |
|
158 |
|
159 //////////////////////////////////////////////////////////////////////////////// |
|
160 // Event queue |
|
161 |
|
162 /** |
|
163 * Return value of invoke method of invoker object. Indicates invoker was unable |
|
164 * to prepare action. |
|
165 */ |
|
166 const INVOKER_ACTION_FAILED = 1; |
|
167 |
|
168 /** |
|
169 * Return value of eventQueue.onFinish. Indicates eventQueue should not finish |
|
170 * tests. |
|
171 */ |
|
172 const DO_NOT_FINISH_TEST = 1; |
|
173 |
|
174 /** |
|
175 * Creates event queue for the given event type. The queue consists of invoker |
|
176 * objects, each of them generates the event of the event type. When queue is |
|
177 * started then every invoker object is asked to generate event after timeout. |
|
178 * When event is caught then current invoker object is asked to check whether |
|
179 * event was handled correctly. |
|
180 * |
|
181 * Invoker interface is: |
|
182 * |
|
183 * var invoker = { |
|
184 * // Generates accessible event or event sequence. If returns |
|
185 * // INVOKER_ACTION_FAILED constant then stop tests. |
|
186 * invoke: function(){}, |
|
187 * |
|
188 * // [optional] Invoker's check of handled event for correctness. |
|
189 * check: function(aEvent){}, |
|
190 * |
|
191 * // [optional] Invoker's check before the next invoker is proceeded. |
|
192 * finalCheck: function(aEvent){}, |
|
193 * |
|
194 * // [optional] Is called when event of any registered type is handled. |
|
195 * debugCheck: function(aEvent){}, |
|
196 * |
|
197 * // [ignored if 'eventSeq' is defined] DOM node event is generated for |
|
198 * // (used in the case when invoker expects single event). |
|
199 * DOMNode getter: function() {}, |
|
200 * |
|
201 * // [optional] if true then event sequences are ignored (no failure if |
|
202 * // sequences are empty). Use you need to invoke an action, do some check |
|
203 * // after timeout and proceed a next invoker. |
|
204 * noEventsOnAction getter: function() {}, |
|
205 * |
|
206 * // Array of checker objects defining expected events on invoker's action. |
|
207 * // |
|
208 * // Checker object interface: |
|
209 * // |
|
210 * // var checker = { |
|
211 * // * DOM or a11y event type. * |
|
212 * // type getter: function() {}, |
|
213 * // |
|
214 * // * DOM node or accessible. * |
|
215 * // target getter: function() {}, |
|
216 * // |
|
217 * // * DOM event phase (false - bubbling). * |
|
218 * // phase getter: function() {}, |
|
219 * // |
|
220 * // * Callback, called to match handled event. * |
|
221 * // match : function(aEvent) {}, |
|
222 * // |
|
223 * // * Callback, called when event is handled |
|
224 * // check: function(aEvent) {}, |
|
225 * // |
|
226 * // * Checker ID * |
|
227 * // getID: function() {}, |
|
228 * // |
|
229 * // * Event that don't have predefined order relative other events. * |
|
230 * // async getter: function() {}, |
|
231 * // |
|
232 * // * Event that is not expected. * |
|
233 * // unexpected getter: function() {}, |
|
234 * // |
|
235 * // * No other event of the same type is not allowed. * |
|
236 * // unique getter: function() {} |
|
237 * // }; |
|
238 * eventSeq getter() {}, |
|
239 * |
|
240 * // Array of checker objects defining unexpected events on invoker's |
|
241 * // action. |
|
242 * unexpectedEventSeq getter() {}, |
|
243 * |
|
244 * // The ID of invoker. |
|
245 * getID: function(){} // returns invoker ID |
|
246 * }; |
|
247 * |
|
248 * // Used to add a possible scenario of expected/unexpected events on |
|
249 * // invoker's action. |
|
250 * defineScenario(aInvokerObj, aEventSeq, aUnexpectedEventSeq) |
|
251 * |
|
252 * |
|
253 * @param aEventType [in, optional] the default event type (isn't used if |
|
254 * invoker defines eventSeq property). |
|
255 */ |
|
256 function eventQueue(aEventType) |
|
257 { |
|
258 // public |
|
259 |
|
260 /** |
|
261 * Add invoker object into queue. |
|
262 */ |
|
263 this.push = function eventQueue_push(aEventInvoker) |
|
264 { |
|
265 this.mInvokers.push(aEventInvoker); |
|
266 } |
|
267 |
|
268 /** |
|
269 * Start the queue processing. |
|
270 */ |
|
271 this.invoke = function eventQueue_invoke() |
|
272 { |
|
273 listenA11yEvents(true); |
|
274 |
|
275 // XXX: Intermittent test_events_caretmove.html fails withouth timeout, |
|
276 // see bug 474952. |
|
277 this.processNextInvokerInTimeout(true); |
|
278 } |
|
279 |
|
280 /** |
|
281 * This function is called when all events in the queue were handled. |
|
282 * Override it if you need to be notified of this. |
|
283 */ |
|
284 this.onFinish = function eventQueue_finish() |
|
285 { |
|
286 } |
|
287 |
|
288 // private |
|
289 |
|
290 /** |
|
291 * Process next invoker. |
|
292 */ |
|
293 this.processNextInvoker = function eventQueue_processNextInvoker() |
|
294 { |
|
295 // Some scenario was matched, we wait on next invoker processing. |
|
296 if (this.mNextInvokerStatus == kInvokerCanceled) { |
|
297 this.setInvokerStatus(kInvokerNotScheduled, |
|
298 "scenario was matched, wait for next invoker activation"); |
|
299 return; |
|
300 } |
|
301 |
|
302 this.setInvokerStatus(kInvokerNotScheduled, "the next invoker is processed now"); |
|
303 |
|
304 // Finish processing of the current invoker if any. |
|
305 var testFailed = false; |
|
306 |
|
307 var invoker = this.getInvoker(); |
|
308 if (invoker) { |
|
309 if ("finalCheck" in invoker) |
|
310 invoker.finalCheck(); |
|
311 |
|
312 if (this.mScenarios && this.mScenarios.length) { |
|
313 var matchIdx = -1; |
|
314 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
315 var eventSeq = this.mScenarios[scnIdx]; |
|
316 if (!this.areExpectedEventsLeft(eventSeq)) { |
|
317 for (var idx = 0; idx < eventSeq.length; idx++) { |
|
318 var checker = eventSeq[idx]; |
|
319 if (checker.unexpected && checker.wasCaught || |
|
320 !checker.unexpected && checker.wasCaught != 1) { |
|
321 break; |
|
322 } |
|
323 } |
|
324 |
|
325 // Ok, we have matched scenario. Report it was completed ok. In |
|
326 // case of empty scenario guess it was matched but if later we |
|
327 // find out that non empty scenario was matched then it will be |
|
328 // a final match. |
|
329 if (idx == eventSeq.length) { |
|
330 if (matchIdx != -1 && eventSeq.length > 0 && |
|
331 this.mScenarios[matchIdx].length > 0) { |
|
332 ok(false, |
|
333 "We have a matched scenario at index " + matchIdx + " already."); |
|
334 } |
|
335 |
|
336 if (matchIdx == -1 || eventSeq.length > 0) |
|
337 matchIdx = scnIdx; |
|
338 |
|
339 // Report everythign is ok. |
|
340 for (var idx = 0; idx < eventSeq.length; idx++) { |
|
341 var checker = eventSeq[idx]; |
|
342 |
|
343 var typeStr = eventQueue.getEventTypeAsString(checker); |
|
344 var msg = "Test with ID = '" + this.getEventID(checker) + |
|
345 "' succeed. "; |
|
346 |
|
347 if (checker.unexpected) |
|
348 ok(true, msg + "There's no unexpected " + typeStr + " event."); |
|
349 else |
|
350 ok(true, msg + "Event " + typeStr + " was handled."); |
|
351 } |
|
352 } |
|
353 } |
|
354 } |
|
355 |
|
356 // We don't have completely matched scenario. Report each failure/success |
|
357 // for every scenario. |
|
358 if (matchIdx == -1) { |
|
359 testFailed = true; |
|
360 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
361 var eventSeq = this.mScenarios[scnIdx]; |
|
362 for (var idx = 0; idx < eventSeq.length; idx++) { |
|
363 var checker = eventSeq[idx]; |
|
364 |
|
365 var typeStr = eventQueue.getEventTypeAsString(checker); |
|
366 var msg = "Scenario #" + scnIdx + " of test with ID = '" + |
|
367 this.getEventID(checker) + "' failed. "; |
|
368 |
|
369 if (checker.wasCaught > 1) |
|
370 ok(false, msg + "Dupe " + typeStr + " event."); |
|
371 |
|
372 if (checker.unexpected) { |
|
373 if (checker.wasCaught) |
|
374 ok(false, msg + "There's unexpected " + typeStr + " event."); |
|
375 } else if (!checker.wasCaught) { |
|
376 ok(false, msg + typeStr + " event was missed."); |
|
377 } |
|
378 } |
|
379 } |
|
380 } |
|
381 } |
|
382 } |
|
383 |
|
384 this.clearEventHandler(); |
|
385 |
|
386 // Check if need to stop the test. |
|
387 if (testFailed || this.mIndex == this.mInvokers.length - 1) { |
|
388 listenA11yEvents(false); |
|
389 |
|
390 var res = this.onFinish(); |
|
391 if (res != DO_NOT_FINISH_TEST) |
|
392 SimpleTest.finish(); |
|
393 |
|
394 return; |
|
395 } |
|
396 |
|
397 // Start processing of next invoker. |
|
398 invoker = this.getNextInvoker(); |
|
399 |
|
400 // Set up event listeners. Process a next invoker if no events were added. |
|
401 if (!this.setEventHandler(invoker)) { |
|
402 this.processNextInvoker(); |
|
403 return; |
|
404 } |
|
405 |
|
406 if (gLogger.isEnabled()) { |
|
407 gLogger.logToConsole("Event queue: \n invoke: " + invoker.getID()); |
|
408 gLogger.logToDOM("EQ: invoke: " + invoker.getID(), true); |
|
409 } |
|
410 |
|
411 var infoText = "Invoke the '" + invoker.getID() + "' test { "; |
|
412 var scnCount = this.mScenarios ? this.mScenarios.length : 0; |
|
413 for (var scnIdx = 0; scnIdx < scnCount; scnIdx++) { |
|
414 infoText += "scenario #" + scnIdx + ": "; |
|
415 var eventSeq = this.mScenarios[scnIdx]; |
|
416 for (var idx = 0; idx < eventSeq.length; idx++) { |
|
417 infoText += eventSeq[idx].unexpected ? "un" : "" + |
|
418 "expected '" + eventQueue.getEventTypeAsString(eventSeq[idx]) + |
|
419 "' event; "; |
|
420 } |
|
421 } |
|
422 infoText += " }"; |
|
423 info(infoText); |
|
424 |
|
425 if (invoker.invoke() == INVOKER_ACTION_FAILED) { |
|
426 // Invoker failed to prepare action, fail and finish tests. |
|
427 this.processNextInvoker(); |
|
428 return; |
|
429 } |
|
430 |
|
431 if (this.hasUnexpectedEventsScenario()) |
|
432 this.processNextInvokerInTimeout(true); |
|
433 } |
|
434 |
|
435 this.processNextInvokerInTimeout = |
|
436 function eventQueue_processNextInvokerInTimeout(aUncondProcess) |
|
437 { |
|
438 this.setInvokerStatus(kInvokerPending, "Process next invoker in timeout"); |
|
439 |
|
440 // No need to wait extra timeout when a) we know we don't need to do that |
|
441 // and b) there's no any single unexpected event. |
|
442 if (!aUncondProcess && this.areAllEventsExpected()) { |
|
443 // We need delay to avoid events coalesce from different invokers. |
|
444 var queue = this; |
|
445 SimpleTest.executeSoon(function() { queue.processNextInvoker(); }); |
|
446 return; |
|
447 } |
|
448 |
|
449 // Check in timeout invoker didn't fire registered events. |
|
450 window.setTimeout(function(aQueue) { aQueue.processNextInvoker(); }, 300, |
|
451 this); |
|
452 } |
|
453 |
|
454 /** |
|
455 * Handle events for the current invoker. |
|
456 */ |
|
457 this.handleEvent = function eventQueue_handleEvent(aEvent) |
|
458 { |
|
459 var invoker = this.getInvoker(); |
|
460 if (!invoker) // skip events before test was started |
|
461 return; |
|
462 |
|
463 if (!this.mScenarios) { |
|
464 // Bad invoker object, error will be reported before processing of next |
|
465 // invoker in the queue. |
|
466 this.processNextInvoker(); |
|
467 return; |
|
468 } |
|
469 |
|
470 if ("debugCheck" in invoker) |
|
471 invoker.debugCheck(aEvent); |
|
472 |
|
473 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
474 var eventSeq = this.mScenarios[scnIdx]; |
|
475 for (var idx = 0; idx < eventSeq.length; idx++) { |
|
476 var checker = eventSeq[idx]; |
|
477 |
|
478 // Search through handled expected events to report error if one of them |
|
479 // is handled for a second time. |
|
480 if (!checker.unexpected && (checker.wasCaught > 0) && |
|
481 eventQueue.isSameEvent(checker, aEvent)) { |
|
482 checker.wasCaught++; |
|
483 continue; |
|
484 } |
|
485 |
|
486 // Search through unexpected events, any match results in error report |
|
487 // after this invoker processing (in case of matched scenario only). |
|
488 if (checker.unexpected && eventQueue.compareEvents(checker, aEvent)) { |
|
489 checker.wasCaught++; |
|
490 continue; |
|
491 } |
|
492 |
|
493 // Report an error if we hanlded not expected event of unique type |
|
494 // (i.e. event types are matched, targets differs). |
|
495 if (!checker.unexpected && checker.unique && |
|
496 eventQueue.compareEventTypes(checker, aEvent)) { |
|
497 var isExppected = false; |
|
498 for (var jdx = 0; jdx < eventSeq.length; jdx++) { |
|
499 isExpected = eventQueue.compareEvents(eventSeq[jdx], aEvent); |
|
500 if (isExpected) |
|
501 break; |
|
502 } |
|
503 |
|
504 if (!isExpected) { |
|
505 ok(false, |
|
506 "Unique type " + |
|
507 eventQueue.getEventTypeAsString(checker) + " event was handled."); |
|
508 } |
|
509 } |
|
510 } |
|
511 } |
|
512 |
|
513 var hasMatchedCheckers = false; |
|
514 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
515 var eventSeq = this.mScenarios[scnIdx]; |
|
516 |
|
517 // Check if handled event matches expected sync event. |
|
518 var nextChecker = this.getNextExpectedEvent(eventSeq); |
|
519 if (nextChecker) { |
|
520 if (eventQueue.compareEvents(nextChecker, aEvent)) { |
|
521 this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx); |
|
522 hasMatchedCheckers = true; |
|
523 continue; |
|
524 } |
|
525 } |
|
526 |
|
527 // Check if handled event matches any expected async events. |
|
528 for (idx = 0; idx < eventSeq.length; idx++) { |
|
529 if (!eventSeq[idx].unexpected && eventSeq[idx].async) { |
|
530 if (eventQueue.compareEvents(eventSeq[idx], aEvent)) { |
|
531 this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx); |
|
532 hasMatchedCheckers = true; |
|
533 break; |
|
534 } |
|
535 } |
|
536 } |
|
537 } |
|
538 |
|
539 if (hasMatchedCheckers) { |
|
540 var invoker = this.getInvoker(); |
|
541 if ("check" in invoker) |
|
542 invoker.check(aEvent); |
|
543 } |
|
544 |
|
545 // If we don't have more events to wait then schedule next invoker. |
|
546 if (this.hasMatchedScenario()) { |
|
547 if (this.mNextInvokerStatus == kInvokerNotScheduled) { |
|
548 this.processNextInvokerInTimeout(); |
|
549 |
|
550 } else if (this.mNextInvokerStatus == kInvokerCanceled) { |
|
551 this.setInvokerStatus(kInvokerPending, |
|
552 "Full match. Void the cancelation of next invoker processing"); |
|
553 } |
|
554 return; |
|
555 } |
|
556 |
|
557 // If we have scheduled a next invoker then cancel in case of match. |
|
558 if ((this.mNextInvokerStatus == kInvokerPending) && hasMatchedCheckers) { |
|
559 this.setInvokerStatus(kInvokerCanceled, |
|
560 "Cancel the scheduled invoker in case of match"); |
|
561 } |
|
562 } |
|
563 |
|
564 // Helpers |
|
565 this.processMatchedChecker = |
|
566 function eventQueue_function(aEvent, aMatchedChecker, aScenarioIdx, aEventIdx) |
|
567 { |
|
568 aMatchedChecker.wasCaught++; |
|
569 |
|
570 if ("check" in aMatchedChecker) |
|
571 aMatchedChecker.check(aEvent); |
|
572 |
|
573 eventQueue.logEvent(aEvent, aMatchedChecker, aScenarioIdx, aEventIdx, |
|
574 this.areExpectedEventsLeft(), |
|
575 this.mNextInvokerStatus); |
|
576 } |
|
577 |
|
578 this.getNextExpectedEvent = |
|
579 function eventQueue_getNextExpectedEvent(aEventSeq) |
|
580 { |
|
581 if (!("idx" in aEventSeq)) |
|
582 aEventSeq.idx = 0; |
|
583 |
|
584 while (aEventSeq.idx < aEventSeq.length && |
|
585 (aEventSeq[aEventSeq.idx].unexpected || |
|
586 aEventSeq[aEventSeq.idx].async || |
|
587 aEventSeq[aEventSeq.idx].wasCaught > 0)) { |
|
588 aEventSeq.idx++; |
|
589 } |
|
590 |
|
591 return aEventSeq.idx != aEventSeq.length ? aEventSeq[aEventSeq.idx] : null; |
|
592 } |
|
593 |
|
594 this.areExpectedEventsLeft = |
|
595 function eventQueue_areExpectedEventsLeft(aScenario) |
|
596 { |
|
597 function scenarioHasUnhandledExpectedEvent(aEventSeq) |
|
598 { |
|
599 // Check if we have unhandled async (can be anywhere in the sequance) or |
|
600 // sync expcected events yet. |
|
601 for (var idx = 0; idx < aEventSeq.length; idx++) { |
|
602 if (!aEventSeq[idx].unexpected && !aEventSeq[idx].wasCaught) |
|
603 return true; |
|
604 } |
|
605 |
|
606 return false; |
|
607 } |
|
608 |
|
609 if (aScenario) |
|
610 return scenarioHasUnhandledExpectedEvent(aScenario); |
|
611 |
|
612 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
613 var eventSeq = this.mScenarios[scnIdx]; |
|
614 if (scenarioHasUnhandledExpectedEvent(eventSeq)) |
|
615 return true; |
|
616 } |
|
617 return false; |
|
618 } |
|
619 |
|
620 this.areAllEventsExpected = |
|
621 function eventQueue_areAllEventsExpected() |
|
622 { |
|
623 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
624 var eventSeq = this.mScenarios[scnIdx]; |
|
625 for (var idx = 0; idx < eventSeq.length; idx++) { |
|
626 if (eventSeq[idx].unexpected) |
|
627 return false; |
|
628 } |
|
629 } |
|
630 |
|
631 return true; |
|
632 } |
|
633 |
|
634 this.isUnexpectedEventScenario = |
|
635 function eventQueue_isUnexpectedEventsScenario(aScenario) |
|
636 { |
|
637 for (var idx = 0; idx < aScenario.length; idx++) { |
|
638 if (!aScenario[idx].unexpected) |
|
639 break; |
|
640 } |
|
641 |
|
642 return idx == aScenario.length; |
|
643 } |
|
644 |
|
645 this.hasUnexpectedEventsScenario = |
|
646 function eventQueue_hasUnexpectedEventsScenario() |
|
647 { |
|
648 if (this.getInvoker().noEventsOnAction) |
|
649 return true; |
|
650 |
|
651 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
652 if (this.isUnexpectedEventScenario(this.mScenarios[scnIdx])) |
|
653 return true; |
|
654 } |
|
655 |
|
656 return false; |
|
657 } |
|
658 |
|
659 this.hasMatchedScenario = |
|
660 function eventQueue_hasMatchedScenario() |
|
661 { |
|
662 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
663 var scn = this.mScenarios[scnIdx]; |
|
664 if (!this.isUnexpectedEventScenario(scn) && !this.areExpectedEventsLeft(scn)) |
|
665 return true; |
|
666 } |
|
667 return false; |
|
668 } |
|
669 |
|
670 this.getInvoker = function eventQueue_getInvoker() |
|
671 { |
|
672 return this.mInvokers[this.mIndex]; |
|
673 } |
|
674 |
|
675 this.getNextInvoker = function eventQueue_getNextInvoker() |
|
676 { |
|
677 return this.mInvokers[++this.mIndex]; |
|
678 } |
|
679 |
|
680 this.setEventHandler = function eventQueue_setEventHandler(aInvoker) |
|
681 { |
|
682 if (!("scenarios" in aInvoker) || aInvoker.scenarios.length == 0) { |
|
683 var eventSeq = aInvoker.eventSeq; |
|
684 var unexpectedEventSeq = aInvoker.unexpectedEventSeq; |
|
685 if (!eventSeq && !unexpectedEventSeq && this.mDefEventType) |
|
686 eventSeq = [ new invokerChecker(this.mDefEventType, aInvoker.DOMNode) ]; |
|
687 |
|
688 if (eventSeq || unexpectedEventSeq) |
|
689 defineScenario(aInvoker, eventSeq, unexpectedEventSeq); |
|
690 } |
|
691 |
|
692 if (aInvoker.noEventsOnAction) |
|
693 return true; |
|
694 |
|
695 this.mScenarios = aInvoker.scenarios; |
|
696 if (!this.mScenarios || !this.mScenarios.length) { |
|
697 ok(false, "Broken invoker '" + aInvoker.getID() + "'"); |
|
698 return false; |
|
699 } |
|
700 |
|
701 // Register event listeners. |
|
702 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
703 var eventSeq = this.mScenarios[scnIdx]; |
|
704 |
|
705 if (gLogger.isEnabled()) { |
|
706 var msg = "scenario #" + scnIdx + |
|
707 ", registered events number: " + eventSeq.length; |
|
708 gLogger.logToConsole(msg); |
|
709 gLogger.logToDOM(msg, true); |
|
710 } |
|
711 |
|
712 // Do not warn about empty event sequances when more than one scenario |
|
713 // was registered. |
|
714 if (this.mScenarios.length == 1 && eventSeq.length == 0) { |
|
715 ok(false, |
|
716 "Broken scenario #" + scnIdx + " of invoker '" + aInvoker.getID() + |
|
717 "'. No registered events"); |
|
718 return false; |
|
719 } |
|
720 |
|
721 for (var idx = 0; idx < eventSeq.length; idx++) |
|
722 eventSeq[idx].wasCaught = 0; |
|
723 |
|
724 for (var idx = 0; idx < eventSeq.length; idx++) { |
|
725 if (gLogger.isEnabled()) { |
|
726 var msg = "registered"; |
|
727 if (eventSeq[idx].unexpected) |
|
728 msg += " unexpected"; |
|
729 if (eventSeq[idx].async) |
|
730 msg += " async"; |
|
731 |
|
732 msg += ": event type: " + |
|
733 eventQueue.getEventTypeAsString(eventSeq[idx]) + |
|
734 ", target: " + eventQueue.getEventTargetDescr(eventSeq[idx], true); |
|
735 |
|
736 gLogger.logToConsole(msg); |
|
737 gLogger.logToDOM(msg, true); |
|
738 } |
|
739 |
|
740 var eventType = eventSeq[idx].type; |
|
741 if (typeof eventType == "string") { |
|
742 // DOM event |
|
743 var target = eventSeq[idx].target; |
|
744 if (!target) { |
|
745 ok(false, "no target for DOM event!"); |
|
746 return false; |
|
747 } |
|
748 var phase = eventQueue.getEventPhase(eventSeq[idx]); |
|
749 target.ownerDocument.addEventListener(eventType, this, phase); |
|
750 |
|
751 } else { |
|
752 // A11y event |
|
753 addA11yEventListener(eventType, this); |
|
754 } |
|
755 } |
|
756 } |
|
757 |
|
758 return true; |
|
759 } |
|
760 |
|
761 this.clearEventHandler = function eventQueue_clearEventHandler() |
|
762 { |
|
763 if (!this.mScenarios) |
|
764 return; |
|
765 |
|
766 for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { |
|
767 var eventSeq = this.mScenarios[scnIdx]; |
|
768 for (var idx = 0; idx < eventSeq.length; idx++) { |
|
769 var eventType = eventSeq[idx].type; |
|
770 if (typeof eventType == "string") { |
|
771 // DOM event |
|
772 var target = eventSeq[idx].target; |
|
773 var phase = eventQueue.getEventPhase(eventSeq[idx]); |
|
774 target.ownerDocument.removeEventListener(eventType, this, phase); |
|
775 |
|
776 } else { |
|
777 // A11y event |
|
778 removeA11yEventListener(eventType, this); |
|
779 } |
|
780 } |
|
781 } |
|
782 this.mScenarios = null; |
|
783 } |
|
784 |
|
785 this.getEventID = function eventQueue_getEventID(aChecker) |
|
786 { |
|
787 if ("getID" in aChecker) |
|
788 return aChecker.getID(); |
|
789 |
|
790 var invoker = this.getInvoker(); |
|
791 return invoker.getID(); |
|
792 } |
|
793 |
|
794 this.setInvokerStatus = function eventQueue_setInvokerStatus(aStatus, aLogMsg) |
|
795 { |
|
796 this.mNextInvokerStatus = aStatus; |
|
797 |
|
798 // Uncomment it to debug invoker processing logic. |
|
799 //gLogger.log(eventQueue.invokerStatusToMsg(aStatus, aLogMsg)); |
|
800 } |
|
801 |
|
802 this.mDefEventType = aEventType; |
|
803 |
|
804 this.mInvokers = new Array(); |
|
805 this.mIndex = -1; |
|
806 this.mScenarios = null; |
|
807 |
|
808 this.mNextInvokerStatus = kInvokerNotScheduled; |
|
809 } |
|
810 |
|
811 //////////////////////////////////////////////////////////////////////////////// |
|
812 // eventQueue static members and constants |
|
813 |
|
814 const kInvokerNotScheduled = 0; |
|
815 const kInvokerPending = 1; |
|
816 const kInvokerCanceled = 2; |
|
817 |
|
818 eventQueue.getEventTypeAsString = |
|
819 function eventQueue_getEventTypeAsString(aEventOrChecker) |
|
820 { |
|
821 if (aEventOrChecker instanceof nsIDOMEvent) |
|
822 return aEventOrChecker.type; |
|
823 |
|
824 if (aEventOrChecker instanceof nsIAccessibleEvent) |
|
825 return eventTypeToString(aEventOrChecker.eventType); |
|
826 |
|
827 return (typeof aEventOrChecker.type == "string") ? |
|
828 aEventOrChecker.type : eventTypeToString(aEventOrChecker.type); |
|
829 } |
|
830 |
|
831 eventQueue.getEventTargetDescr = |
|
832 function eventQueue_getEventTargetDescr(aEventOrChecker, aDontForceTarget) |
|
833 { |
|
834 if (aEventOrChecker instanceof nsIDOMEvent) |
|
835 return prettyName(aEventOrChecker.originalTarget); |
|
836 |
|
837 if (aEventOrChecker instanceof nsIDOMEvent) |
|
838 return prettyName(aEventOrChecker.accessible); |
|
839 |
|
840 var descr = aEventOrChecker.targetDescr; |
|
841 if (descr) |
|
842 return descr; |
|
843 |
|
844 if (aDontForceTarget) |
|
845 return "no target description"; |
|
846 |
|
847 var target = ("target" in aEventOrChecker) ? aEventOrChecker.target : null; |
|
848 return prettyName(target); |
|
849 } |
|
850 |
|
851 eventQueue.getEventPhase = function eventQueue_getEventPhase(aChecker) |
|
852 { |
|
853 return ("phase" in aChecker) ? aChecker.phase : true; |
|
854 } |
|
855 |
|
856 eventQueue.compareEventTypes = |
|
857 function eventQueue_compareEventTypes(aChecker, aEvent) |
|
858 { |
|
859 var eventType = (aEvent instanceof nsIDOMEvent) ? |
|
860 aEvent.type : aEvent.eventType; |
|
861 return aChecker.type == eventType; |
|
862 } |
|
863 |
|
864 eventQueue.compareEvents = function eventQueue_compareEvents(aChecker, aEvent) |
|
865 { |
|
866 if (!eventQueue.compareEventTypes(aChecker, aEvent)) |
|
867 return false; |
|
868 |
|
869 // If checker provides "match" function then allow the checker to decide |
|
870 // whether event is matched. |
|
871 if ("match" in aChecker) |
|
872 return aChecker.match(aEvent); |
|
873 |
|
874 var target1 = aChecker.target; |
|
875 if (target1 instanceof nsIAccessible) { |
|
876 var target2 = (aEvent instanceof nsIDOMEvent) ? |
|
877 getAccessible(aEvent.target) : aEvent.accessible; |
|
878 |
|
879 return target1 == target2; |
|
880 } |
|
881 |
|
882 // If original target isn't suitable then extend interface to support target |
|
883 // (original target is used in test_elm_media.html). |
|
884 var target2 = (aEvent instanceof nsIDOMEvent) ? |
|
885 aEvent.originalTarget : aEvent.DOMNode; |
|
886 return target1 == target2; |
|
887 } |
|
888 |
|
889 eventQueue.isSameEvent = function eventQueue_isSameEvent(aChecker, aEvent) |
|
890 { |
|
891 // We don't have stored info about handled event other than its type and |
|
892 // target, thus we should filter text change and state change events since |
|
893 // they may occur on the same element because of complex changes. |
|
894 return this.compareEvents(aChecker, aEvent) && |
|
895 !(aEvent instanceof nsIAccessibleTextChangeEvent) && |
|
896 !(aEvent instanceof nsIAccessibleStateChangeEvent); |
|
897 } |
|
898 |
|
899 eventQueue.invokerStatusToMsg = |
|
900 function eventQueue_invokerStatusToMsg(aInvokerStatus, aMsg) |
|
901 { |
|
902 var msg = "invoker status: "; |
|
903 switch (aInvokerStatus) { |
|
904 case kInvokerNotScheduled: |
|
905 msg += "not scheduled"; |
|
906 break; |
|
907 case kInvokerPending: |
|
908 msg += "pending"; |
|
909 break; |
|
910 case kInvokerCanceled: |
|
911 msg += "canceled"; |
|
912 break; |
|
913 } |
|
914 |
|
915 if (aMsg) |
|
916 msg += " (" + aMsg + ")"; |
|
917 |
|
918 return msg; |
|
919 } |
|
920 |
|
921 eventQueue.logEvent = function eventQueue_logEvent(aOrigEvent, aMatchedChecker, |
|
922 aScenarioIdx, aEventIdx, |
|
923 aAreExpectedEventsLeft, |
|
924 aInvokerStatus) |
|
925 { |
|
926 // Dump DOM event information. Skip a11y event since it is dumped by |
|
927 // gA11yEventObserver. |
|
928 if (aOrigEvent instanceof nsIDOMEvent) { |
|
929 var info = "Event type: " + eventQueue.getEventTypeAsString(aOrigEvent); |
|
930 info += ". Target: " + eventQueue.getEventTargetDescr(aOrigEvent); |
|
931 gLogger.logToDOM(info); |
|
932 } |
|
933 |
|
934 var infoMsg = "unhandled expected events: " + aAreExpectedEventsLeft + |
|
935 ", " + eventQueue.invokerStatusToMsg(aInvokerStatus); |
|
936 |
|
937 var currType = eventQueue.getEventTypeAsString(aMatchedChecker); |
|
938 var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker); |
|
939 var consoleMsg = "*****\nScenario " + aScenarioIdx + |
|
940 ", event " + aEventIdx + " matched: " + currType + "\n" + infoMsg + "\n*****"; |
|
941 gLogger.logToConsole(consoleMsg); |
|
942 |
|
943 var emphText = "matched "; |
|
944 var msg = "EQ event, type: " + currType + ", target: " + currTargetDescr + |
|
945 ", " + infoMsg; |
|
946 gLogger.logToDOM(msg, true, emphText); |
|
947 } |
|
948 |
|
949 |
|
950 //////////////////////////////////////////////////////////////////////////////// |
|
951 // Action sequence |
|
952 |
|
953 /** |
|
954 * Deal with action sequence. Used when you need to execute couple of actions |
|
955 * each after other one. |
|
956 */ |
|
957 function sequence() |
|
958 { |
|
959 /** |
|
960 * Append new sequence item. |
|
961 * |
|
962 * @param aProcessor [in] object implementing interface |
|
963 * { |
|
964 * // execute item action |
|
965 * process: function() {}, |
|
966 * // callback, is called when item was processed |
|
967 * onProcessed: function() {} |
|
968 * }; |
|
969 * @param aEventType [in] event type of expected event on item action |
|
970 * @param aTarget [in] event target of expected event on item action |
|
971 * @param aItemID [in] identifier of item |
|
972 */ |
|
973 this.append = function sequence_append(aProcessor, aEventType, aTarget, |
|
974 aItemID) |
|
975 { |
|
976 var item = new sequenceItem(aProcessor, aEventType, aTarget, aItemID); |
|
977 this.items.push(item); |
|
978 } |
|
979 |
|
980 /** |
|
981 * Process next sequence item. |
|
982 */ |
|
983 this.processNext = function sequence_processNext() |
|
984 { |
|
985 this.idx++; |
|
986 if (this.idx >= this.items.length) { |
|
987 ok(false, "End of sequence: nothing to process!"); |
|
988 SimpleTest.finish(); |
|
989 return; |
|
990 } |
|
991 |
|
992 this.items[this.idx].startProcess(); |
|
993 } |
|
994 |
|
995 this.items = new Array(); |
|
996 this.idx = -1; |
|
997 } |
|
998 |
|
999 |
|
1000 //////////////////////////////////////////////////////////////////////////////// |
|
1001 // Event queue invokers |
|
1002 |
|
1003 /** |
|
1004 * Defines a scenario of expected/unexpected events. Each invoker can have |
|
1005 * one or more scenarios of events. Only one scenario must be completed. |
|
1006 */ |
|
1007 function defineScenario(aInvoker, aEventSeq, aUnexpectedEventSeq) |
|
1008 { |
|
1009 if (!("scenarios" in aInvoker)) |
|
1010 aInvoker.scenarios = new Array(); |
|
1011 |
|
1012 // Create unified event sequence concatenating expected and unexpected |
|
1013 // events. |
|
1014 if (!aEventSeq) |
|
1015 aEventSeq = []; |
|
1016 |
|
1017 for (var idx = 0; idx < aEventSeq.length; idx++) { |
|
1018 aEventSeq[idx].unexpected |= false; |
|
1019 aEventSeq[idx].async |= false; |
|
1020 } |
|
1021 |
|
1022 if (aUnexpectedEventSeq) { |
|
1023 for (var idx = 0; idx < aUnexpectedEventSeq.length; idx++) { |
|
1024 aUnexpectedEventSeq[idx].unexpected = true; |
|
1025 aUnexpectedEventSeq[idx].async = false; |
|
1026 } |
|
1027 |
|
1028 aEventSeq = aEventSeq.concat(aUnexpectedEventSeq); |
|
1029 } |
|
1030 |
|
1031 aInvoker.scenarios.push(aEventSeq); |
|
1032 } |
|
1033 |
|
1034 |
|
1035 /** |
|
1036 * Invokers defined below take a checker object (or array of checker objects). |
|
1037 * An invoker listens for default event type registered in event queue object |
|
1038 * until its checker is provided. |
|
1039 * |
|
1040 * Note, checker object or array of checker objects is optional. |
|
1041 */ |
|
1042 |
|
1043 /** |
|
1044 * Click invoker. |
|
1045 */ |
|
1046 function synthClick(aNodeOrID, aCheckerOrEventSeq, aArgs) |
|
1047 { |
|
1048 this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); |
|
1049 |
|
1050 this.invoke = function synthClick_invoke() |
|
1051 { |
|
1052 var targetNode = this.DOMNode; |
|
1053 if (targetNode instanceof nsIDOMDocument) { |
|
1054 targetNode = |
|
1055 this.DOMNode.body ? this.DOMNode.body : this.DOMNode.documentElement; |
|
1056 } |
|
1057 |
|
1058 // Scroll the node into view, otherwise synth click may fail. |
|
1059 if (targetNode instanceof nsIDOMHTMLElement) { |
|
1060 targetNode.scrollIntoView(true); |
|
1061 } else if (targetNode instanceof nsIDOMXULElement) { |
|
1062 var targetAcc = getAccessible(targetNode); |
|
1063 targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE); |
|
1064 } |
|
1065 |
|
1066 var x = 1, y = 1; |
|
1067 if (aArgs && ("where" in aArgs) && aArgs.where == "right") { |
|
1068 if (targetNode instanceof nsIDOMHTMLElement) |
|
1069 x = targetNode.offsetWidth - 1; |
|
1070 else if (targetNode instanceof nsIDOMXULElement) |
|
1071 x = targetNode.boxObject.width - 1; |
|
1072 } |
|
1073 synthesizeMouse(targetNode, x, y, aArgs ? aArgs : {}); |
|
1074 } |
|
1075 |
|
1076 this.finalCheck = function synthClick_finalCheck() |
|
1077 { |
|
1078 // Scroll top window back. |
|
1079 window.top.scrollTo(0, 0); |
|
1080 } |
|
1081 |
|
1082 this.getID = function synthClick_getID() |
|
1083 { |
|
1084 return prettyName(aNodeOrID) + " click"; |
|
1085 } |
|
1086 } |
|
1087 |
|
1088 /** |
|
1089 * Mouse move invoker. |
|
1090 */ |
|
1091 function synthMouseMove(aID, aCheckerOrEventSeq) |
|
1092 { |
|
1093 this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); |
|
1094 |
|
1095 this.invoke = function synthMouseMove_invoke() |
|
1096 { |
|
1097 synthesizeMouse(this.DOMNode, 1, 1, { type: "mousemove" }); |
|
1098 synthesizeMouse(this.DOMNode, 2, 2, { type: "mousemove" }); |
|
1099 } |
|
1100 |
|
1101 this.getID = function synthMouseMove_getID() |
|
1102 { |
|
1103 return prettyName(aID) + " mouse move"; |
|
1104 } |
|
1105 } |
|
1106 |
|
1107 /** |
|
1108 * General key press invoker. |
|
1109 */ |
|
1110 function synthKey(aNodeOrID, aKey, aArgs, aCheckerOrEventSeq) |
|
1111 { |
|
1112 this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); |
|
1113 |
|
1114 this.invoke = function synthKey_invoke() |
|
1115 { |
|
1116 synthesizeKey(this.mKey, this.mArgs, this.mWindow); |
|
1117 } |
|
1118 |
|
1119 this.getID = function synthKey_getID() |
|
1120 { |
|
1121 var key = this.mKey; |
|
1122 switch (this.mKey) { |
|
1123 case "VK_TAB": |
|
1124 key = "tab"; |
|
1125 break; |
|
1126 case "VK_DOWN": |
|
1127 key = "down"; |
|
1128 break; |
|
1129 case "VK_UP": |
|
1130 key = "up"; |
|
1131 break; |
|
1132 case "VK_LEFT": |
|
1133 key = "left"; |
|
1134 break; |
|
1135 case "VK_RIGHT": |
|
1136 key = "right"; |
|
1137 break; |
|
1138 case "VK_HOME": |
|
1139 key = "home"; |
|
1140 break; |
|
1141 case "VK_END": |
|
1142 key = "end"; |
|
1143 break; |
|
1144 case "VK_ESCAPE": |
|
1145 key = "escape"; |
|
1146 break; |
|
1147 case "VK_RETURN": |
|
1148 key = "enter"; |
|
1149 break; |
|
1150 } |
|
1151 if (aArgs) { |
|
1152 if (aArgs.shiftKey) |
|
1153 key += " shift"; |
|
1154 if (aArgs.ctrlKey) |
|
1155 key += " ctrl"; |
|
1156 if (aArgs.altKey) |
|
1157 key += " alt"; |
|
1158 } |
|
1159 return prettyName(aNodeOrID) + " '" + key + " ' key"; |
|
1160 } |
|
1161 |
|
1162 this.mKey = aKey; |
|
1163 this.mArgs = aArgs ? aArgs : {}; |
|
1164 this.mWindow = aArgs ? aArgs.window : null; |
|
1165 } |
|
1166 |
|
1167 /** |
|
1168 * Tab key invoker. |
|
1169 */ |
|
1170 function synthTab(aNodeOrID, aCheckerOrEventSeq, aWindow) |
|
1171 { |
|
1172 this.__proto__ = new synthKey(aNodeOrID, "VK_TAB", |
|
1173 { shiftKey: false, window: aWindow }, |
|
1174 aCheckerOrEventSeq); |
|
1175 } |
|
1176 |
|
1177 /** |
|
1178 * Shift tab key invoker. |
|
1179 */ |
|
1180 function synthShiftTab(aNodeOrID, aCheckerOrEventSeq) |
|
1181 { |
|
1182 this.__proto__ = new synthKey(aNodeOrID, "VK_TAB", { shiftKey: true }, |
|
1183 aCheckerOrEventSeq); |
|
1184 } |
|
1185 |
|
1186 /** |
|
1187 * Escape key invoker. |
|
1188 */ |
|
1189 function synthEscapeKey(aNodeOrID, aCheckerOrEventSeq) |
|
1190 { |
|
1191 this.__proto__ = new synthKey(aNodeOrID, "VK_ESCAPE", null, |
|
1192 aCheckerOrEventSeq); |
|
1193 } |
|
1194 |
|
1195 /** |
|
1196 * Down arrow key invoker. |
|
1197 */ |
|
1198 function synthDownKey(aNodeOrID, aCheckerOrEventSeq, aArgs) |
|
1199 { |
|
1200 this.__proto__ = new synthKey(aNodeOrID, "VK_DOWN", aArgs, |
|
1201 aCheckerOrEventSeq); |
|
1202 } |
|
1203 |
|
1204 /** |
|
1205 * Up arrow key invoker. |
|
1206 */ |
|
1207 function synthUpKey(aNodeOrID, aCheckerOrEventSeq, aArgs) |
|
1208 { |
|
1209 this.__proto__ = new synthKey(aNodeOrID, "VK_UP", aArgs, |
|
1210 aCheckerOrEventSeq); |
|
1211 } |
|
1212 |
|
1213 /** |
|
1214 * Left arrow key invoker. |
|
1215 */ |
|
1216 function synthLeftKey(aNodeOrID, aCheckerOrEventSeq, aArgs) |
|
1217 { |
|
1218 this.__proto__ = new synthKey(aNodeOrID, "VK_LEFT", aArgs, aCheckerOrEventSeq); |
|
1219 } |
|
1220 |
|
1221 /** |
|
1222 * Right arrow key invoker. |
|
1223 */ |
|
1224 function synthRightKey(aNodeOrID, aCheckerOrEventSeq, aArgs) |
|
1225 { |
|
1226 this.__proto__ = new synthKey(aNodeOrID, "VK_RIGHT", aArgs, aCheckerOrEventSeq); |
|
1227 } |
|
1228 |
|
1229 /** |
|
1230 * Home key invoker. |
|
1231 */ |
|
1232 function synthHomeKey(aNodeOrID, aCheckerOrEventSeq) |
|
1233 { |
|
1234 this.__proto__ = new synthKey(aNodeOrID, "VK_HOME", null, aCheckerOrEventSeq); |
|
1235 } |
|
1236 |
|
1237 /** |
|
1238 * End key invoker. |
|
1239 */ |
|
1240 function synthEndKey(aNodeOrID, aCheckerOrEventSeq) |
|
1241 { |
|
1242 this.__proto__ = new synthKey(aNodeOrID, "VK_END", null, aCheckerOrEventSeq); |
|
1243 } |
|
1244 |
|
1245 /** |
|
1246 * Enter key invoker |
|
1247 */ |
|
1248 function synthEnterKey(aID, aCheckerOrEventSeq) |
|
1249 { |
|
1250 this.__proto__ = new synthKey(aID, "VK_RETURN", null, aCheckerOrEventSeq); |
|
1251 } |
|
1252 |
|
1253 /** |
|
1254 * Synth alt + down arrow to open combobox. |
|
1255 */ |
|
1256 function synthOpenComboboxKey(aID, aCheckerOrEventSeq) |
|
1257 { |
|
1258 this.__proto__ = new synthDownKey(aID, aCheckerOrEventSeq, { altKey: true }); |
|
1259 |
|
1260 this.getID = function synthOpenComboboxKey_getID() |
|
1261 { |
|
1262 return "open combobox (atl + down arrow) " + prettyName(aID); |
|
1263 } |
|
1264 } |
|
1265 |
|
1266 /** |
|
1267 * Focus invoker. |
|
1268 */ |
|
1269 function synthFocus(aNodeOrID, aCheckerOrEventSeq) |
|
1270 { |
|
1271 var checkerOfEventSeq = |
|
1272 aCheckerOrEventSeq ? aCheckerOrEventSeq : new focusChecker(aNodeOrID); |
|
1273 this.__proto__ = new synthAction(aNodeOrID, checkerOfEventSeq); |
|
1274 |
|
1275 this.invoke = function synthFocus_invoke() |
|
1276 { |
|
1277 if (this.DOMNode instanceof Components.interfaces.nsIDOMNSEditableElement && |
|
1278 this.DOMNode.editor || |
|
1279 this.DOMNode instanceof Components.interfaces.nsIDOMXULTextBoxElement) { |
|
1280 this.DOMNode.selectionStart = this.DOMNode.selectionEnd = this.DOMNode.value.length; |
|
1281 } |
|
1282 this.DOMNode.focus(); |
|
1283 } |
|
1284 |
|
1285 this.getID = function synthFocus_getID() |
|
1286 { |
|
1287 return prettyName(aNodeOrID) + " focus"; |
|
1288 } |
|
1289 } |
|
1290 |
|
1291 /** |
|
1292 * Focus invoker. Focus the HTML body of content document of iframe. |
|
1293 */ |
|
1294 function synthFocusOnFrame(aNodeOrID, aCheckerOrEventSeq) |
|
1295 { |
|
1296 var frameDoc = getNode(aNodeOrID).contentDocument; |
|
1297 var checkerOrEventSeq = |
|
1298 aCheckerOrEventSeq ? aCheckerOrEventSeq : new focusChecker(frameDoc); |
|
1299 this.__proto__ = new synthAction(frameDoc, checkerOrEventSeq); |
|
1300 |
|
1301 this.invoke = function synthFocus_invoke() |
|
1302 { |
|
1303 this.DOMNode.body.focus(); |
|
1304 } |
|
1305 |
|
1306 this.getID = function synthFocus_getID() |
|
1307 { |
|
1308 return prettyName(aNodeOrID) + " frame document focus"; |
|
1309 } |
|
1310 } |
|
1311 |
|
1312 /** |
|
1313 * Change the current item when the widget doesn't have a focus. |
|
1314 */ |
|
1315 function changeCurrentItem(aID, aItemID) |
|
1316 { |
|
1317 this.eventSeq = [ new nofocusChecker() ]; |
|
1318 |
|
1319 this.invoke = function changeCurrentItem_invoke() |
|
1320 { |
|
1321 var controlNode = getNode(aID); |
|
1322 var itemNode = getNode(aItemID); |
|
1323 |
|
1324 // HTML |
|
1325 if (controlNode.localName == "input") { |
|
1326 if (controlNode.checked) |
|
1327 this.reportError(); |
|
1328 |
|
1329 controlNode.checked = true; |
|
1330 return; |
|
1331 } |
|
1332 |
|
1333 if (controlNode.localName == "select") { |
|
1334 if (controlNode.selectedIndex == itemNode.index) |
|
1335 this.reportError(); |
|
1336 |
|
1337 controlNode.selectedIndex = itemNode.index; |
|
1338 return; |
|
1339 } |
|
1340 |
|
1341 // XUL |
|
1342 if (controlNode.localName == "tree") { |
|
1343 if (controlNode.currentIndex == aItemID) |
|
1344 this.reportError(); |
|
1345 |
|
1346 controlNode.currentIndex = aItemID; |
|
1347 return; |
|
1348 } |
|
1349 |
|
1350 if (controlNode.localName == "menulist") { |
|
1351 if (controlNode.selectedItem == itemNode) |
|
1352 this.reportError(); |
|
1353 |
|
1354 controlNode.selectedItem = itemNode; |
|
1355 return; |
|
1356 } |
|
1357 |
|
1358 if (controlNode.currentItem == itemNode) |
|
1359 ok(false, "Error in test: proposed current item is already current" + prettyName(aID)); |
|
1360 |
|
1361 controlNode.currentItem = itemNode; |
|
1362 } |
|
1363 |
|
1364 this.getID = function changeCurrentItem_getID() |
|
1365 { |
|
1366 return "current item change for " + prettyName(aID); |
|
1367 } |
|
1368 |
|
1369 this.reportError = function changeCurrentItem_reportError() |
|
1370 { |
|
1371 ok(false, |
|
1372 "Error in test: proposed current item '" + aItemID + "' is already current"); |
|
1373 } |
|
1374 } |
|
1375 |
|
1376 /** |
|
1377 * Toggle top menu invoker. |
|
1378 */ |
|
1379 function toggleTopMenu(aID, aCheckerOrEventSeq) |
|
1380 { |
|
1381 this.__proto__ = new synthKey(aID, "VK_ALT", null, |
|
1382 aCheckerOrEventSeq); |
|
1383 |
|
1384 this.getID = function toggleTopMenu_getID() |
|
1385 { |
|
1386 return "toggle top menu on " + prettyName(aID); |
|
1387 } |
|
1388 } |
|
1389 |
|
1390 /** |
|
1391 * Context menu invoker. |
|
1392 */ |
|
1393 function synthContextMenu(aID, aCheckerOrEventSeq) |
|
1394 { |
|
1395 this.__proto__ = new synthClick(aID, aCheckerOrEventSeq, |
|
1396 { button: 0, type: "contextmenu" }); |
|
1397 |
|
1398 this.getID = function synthContextMenu_getID() |
|
1399 { |
|
1400 return "context menu on " + prettyName(aID); |
|
1401 } |
|
1402 } |
|
1403 |
|
1404 /** |
|
1405 * Open combobox, autocomplete and etc popup, check expandable states. |
|
1406 */ |
|
1407 function openCombobox(aComboboxID) |
|
1408 { |
|
1409 this.eventSeq = [ |
|
1410 new stateChangeChecker(STATE_EXPANDED, false, true, aComboboxID) |
|
1411 ]; |
|
1412 |
|
1413 this.invoke = function openCombobox_invoke() |
|
1414 { |
|
1415 getNode(aComboboxID).focus(); |
|
1416 synthesizeKey("VK_DOWN", { altKey: true }); |
|
1417 } |
|
1418 |
|
1419 this.getID = function openCombobox_getID() |
|
1420 { |
|
1421 return "open combobox " + prettyName(aComboboxID); |
|
1422 } |
|
1423 } |
|
1424 |
|
1425 /** |
|
1426 * Close combobox, autocomplete and etc popup, check expandable states. |
|
1427 */ |
|
1428 function closeCombobox(aComboboxID) |
|
1429 { |
|
1430 this.eventSeq = [ |
|
1431 new stateChangeChecker(STATE_EXPANDED, false, false, aComboboxID) |
|
1432 ]; |
|
1433 |
|
1434 this.invoke = function closeCombobox_invoke() |
|
1435 { |
|
1436 synthesizeKey("VK_ESCAPE", { }); |
|
1437 } |
|
1438 |
|
1439 this.getID = function closeCombobox_getID() |
|
1440 { |
|
1441 return "close combobox " + prettyName(aComboboxID); |
|
1442 } |
|
1443 } |
|
1444 |
|
1445 |
|
1446 /** |
|
1447 * Select all invoker. |
|
1448 */ |
|
1449 function synthSelectAll(aNodeOrID, aCheckerOrEventSeq) |
|
1450 { |
|
1451 this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); |
|
1452 |
|
1453 this.invoke = function synthSelectAll_invoke() |
|
1454 { |
|
1455 if (this.DOMNode instanceof Components.interfaces.nsIDOMHTMLInputElement || |
|
1456 this.DOMNode instanceof Components.interfaces.nsIDOMXULTextBoxElement) { |
|
1457 this.DOMNode.select(); |
|
1458 |
|
1459 } else { |
|
1460 window.getSelection().selectAllChildren(this.DOMNode); |
|
1461 } |
|
1462 } |
|
1463 |
|
1464 this.getID = function synthSelectAll_getID() |
|
1465 { |
|
1466 return aNodeOrID + " selectall"; |
|
1467 } |
|
1468 } |
|
1469 |
|
1470 /** |
|
1471 * Move the caret to the end of line. |
|
1472 */ |
|
1473 function moveToLineEnd(aID, aCaretOffset) |
|
1474 { |
|
1475 if (MAC) { |
|
1476 this.__proto__ = new synthKey(aID, "VK_RIGHT", { metaKey: true }, |
|
1477 new caretMoveChecker(aCaretOffset, aID)); |
|
1478 } else { |
|
1479 this.__proto__ = new synthEndKey(aID, |
|
1480 new caretMoveChecker(aCaretOffset, aID)); |
|
1481 } |
|
1482 |
|
1483 this.getID = function moveToLineEnd_getID() |
|
1484 { |
|
1485 return "move to line end in " + prettyName(aID); |
|
1486 } |
|
1487 } |
|
1488 |
|
1489 /** |
|
1490 * Move the caret to the end of previous line if any. |
|
1491 */ |
|
1492 function moveToPrevLineEnd(aID, aCaretOffset) |
|
1493 { |
|
1494 this.__proto__ = new synthAction(aID, new caretMoveChecker(aCaretOffset, aID)); |
|
1495 |
|
1496 this.invoke = function moveToPrevLineEnd_invoke() |
|
1497 { |
|
1498 synthesizeKey("VK_UP", { }); |
|
1499 |
|
1500 if (MAC) |
|
1501 synthesizeKey("VK_RIGHT", { metaKey: true }); |
|
1502 else |
|
1503 synthesizeKey("VK_END", { }); |
|
1504 } |
|
1505 |
|
1506 this.getID = function moveToPrevLineEnd_getID() |
|
1507 { |
|
1508 return "move to previous line end in " + prettyName(aID); |
|
1509 } |
|
1510 } |
|
1511 |
|
1512 /** |
|
1513 * Move the caret to begining of the line. |
|
1514 */ |
|
1515 function moveToLineStart(aID, aCaretOffset) |
|
1516 { |
|
1517 if (MAC) { |
|
1518 this.__proto__ = new synthKey(aID, "VK_LEFT", { metaKey: true }, |
|
1519 new caretMoveChecker(aCaretOffset, aID)); |
|
1520 } else { |
|
1521 this.__proto__ = new synthHomeKey(aID, |
|
1522 new caretMoveChecker(aCaretOffset, aID)); |
|
1523 } |
|
1524 |
|
1525 this.getID = function moveToLineEnd_getID() |
|
1526 { |
|
1527 return "move to line start in " + prettyName(aID); |
|
1528 } |
|
1529 } |
|
1530 |
|
1531 /** |
|
1532 * Move the caret to begining of the text. |
|
1533 */ |
|
1534 function moveToTextStart(aID) |
|
1535 { |
|
1536 if (MAC) { |
|
1537 this.__proto__ = new synthKey(aID, "VK_UP", { metaKey: true }, |
|
1538 new caretMoveChecker(0, aID)); |
|
1539 } else { |
|
1540 this.__proto__ = new synthKey(aID, "VK_HOME", { ctrlKey: true }, |
|
1541 new caretMoveChecker(0, aID)); |
|
1542 } |
|
1543 |
|
1544 this.getID = function moveToTextStart_getID() |
|
1545 { |
|
1546 return "move to text start in " + prettyName(aID); |
|
1547 } |
|
1548 } |
|
1549 |
|
1550 /** |
|
1551 * Move the caret in text accessible. |
|
1552 */ |
|
1553 function moveCaretToDOMPoint(aID, aDOMPointNodeID, aDOMPointOffset, |
|
1554 aExpectedOffset, aFocusTargetID, |
|
1555 aCheckFunc) |
|
1556 { |
|
1557 this.target = getAccessible(aID, [nsIAccessibleText]); |
|
1558 this.DOMPointNode = getNode(aDOMPointNodeID); |
|
1559 this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; |
|
1560 this.focusNode = this.focus ? this.focus.DOMNode : null; |
|
1561 |
|
1562 this.invoke = function moveCaretToDOMPoint_invoke() |
|
1563 { |
|
1564 if (this.focusNode) |
|
1565 this.focusNode.focus(); |
|
1566 |
|
1567 var selection = this.DOMPointNode.ownerDocument.defaultView.getSelection(); |
|
1568 var selRange = selection.getRangeAt(0); |
|
1569 selRange.setStart(this.DOMPointNode, aDOMPointOffset); |
|
1570 selRange.collapse(true); |
|
1571 |
|
1572 selection.removeRange(selRange); |
|
1573 selection.addRange(selRange); |
|
1574 } |
|
1575 |
|
1576 this.getID = function moveCaretToDOMPoint_getID() |
|
1577 { |
|
1578 return "Set caret on " + prettyName(aID) + " at point: " + |
|
1579 prettyName(aDOMPointNodeID) + " node with offset " + aDOMPointOffset; |
|
1580 } |
|
1581 |
|
1582 this.finalCheck = function moveCaretToDOMPoint_finalCheck() |
|
1583 { |
|
1584 if (aCheckFunc) |
|
1585 aCheckFunc.call(); |
|
1586 } |
|
1587 |
|
1588 this.eventSeq = [ |
|
1589 new caretMoveChecker(aExpectedOffset, this.target) |
|
1590 ]; |
|
1591 |
|
1592 if (this.focus) |
|
1593 this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); |
|
1594 } |
|
1595 |
|
1596 /** |
|
1597 * Set caret offset in text accessible. |
|
1598 */ |
|
1599 function setCaretOffset(aID, aOffset, aFocusTargetID) |
|
1600 { |
|
1601 this.target = getAccessible(aID, [nsIAccessibleText]); |
|
1602 this.offset = aOffset == -1 ? this.target.characterCount: aOffset; |
|
1603 this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; |
|
1604 |
|
1605 this.invoke = function setCaretOffset_invoke() |
|
1606 { |
|
1607 this.target.caretOffset = this.offset; |
|
1608 } |
|
1609 |
|
1610 this.getID = function setCaretOffset_getID() |
|
1611 { |
|
1612 return "Set caretOffset on " + prettyName(aID) + " at " + this.offset; |
|
1613 } |
|
1614 |
|
1615 this.eventSeq = [ |
|
1616 new caretMoveChecker(this.offset, this.target) |
|
1617 ]; |
|
1618 |
|
1619 if (this.focus) |
|
1620 this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); |
|
1621 } |
|
1622 |
|
1623 |
|
1624 //////////////////////////////////////////////////////////////////////////////// |
|
1625 // Event queue checkers |
|
1626 |
|
1627 /** |
|
1628 * Common invoker checker (see eventSeq of eventQueue). |
|
1629 */ |
|
1630 function invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg, aIsAsync) |
|
1631 { |
|
1632 this.type = aEventType; |
|
1633 this.async = aIsAsync; |
|
1634 |
|
1635 this.__defineGetter__("target", invokerChecker_targetGetter); |
|
1636 this.__defineSetter__("target", invokerChecker_targetSetter); |
|
1637 |
|
1638 // implementation details |
|
1639 function invokerChecker_targetGetter() |
|
1640 { |
|
1641 if (typeof this.mTarget == "function") |
|
1642 return this.mTarget.call(null, this.mTargetFuncArg); |
|
1643 if (typeof this.mTarget == "string") |
|
1644 return getNode(this.mTarget); |
|
1645 |
|
1646 return this.mTarget; |
|
1647 } |
|
1648 |
|
1649 function invokerChecker_targetSetter(aValue) |
|
1650 { |
|
1651 this.mTarget = aValue; |
|
1652 return this.mTarget; |
|
1653 } |
|
1654 |
|
1655 this.__defineGetter__("targetDescr", invokerChecker_targetDescrGetter); |
|
1656 |
|
1657 function invokerChecker_targetDescrGetter() |
|
1658 { |
|
1659 if (typeof this.mTarget == "function") |
|
1660 return this.mTarget.name + ", arg: " + this.mTargetFuncArg; |
|
1661 |
|
1662 return prettyName(this.mTarget); |
|
1663 } |
|
1664 |
|
1665 this.mTarget = aTargetOrFunc; |
|
1666 this.mTargetFuncArg = aTargetFuncArg; |
|
1667 } |
|
1668 |
|
1669 /** |
|
1670 * Generic invoker checker for unexpected events. |
|
1671 */ |
|
1672 function unexpectedInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) |
|
1673 { |
|
1674 this.__proto__ = new invokerChecker(aEventType, aTargetOrFunc, |
|
1675 aTargetFuncArg, true); |
|
1676 |
|
1677 this.unexpected = true; |
|
1678 } |
|
1679 |
|
1680 /** |
|
1681 * Common invoker checker for async events. |
|
1682 */ |
|
1683 function asyncInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) |
|
1684 { |
|
1685 this.__proto__ = new invokerChecker(aEventType, aTargetOrFunc, |
|
1686 aTargetFuncArg, true); |
|
1687 } |
|
1688 |
|
1689 function focusChecker(aTargetOrFunc, aTargetFuncArg) |
|
1690 { |
|
1691 this.__proto__ = new invokerChecker(EVENT_FOCUS, aTargetOrFunc, |
|
1692 aTargetFuncArg, false); |
|
1693 |
|
1694 this.unique = true; // focus event must be unique for invoker action |
|
1695 |
|
1696 this.check = function focusChecker_check(aEvent) |
|
1697 { |
|
1698 testStates(aEvent.accessible, STATE_FOCUSED); |
|
1699 } |
|
1700 } |
|
1701 |
|
1702 function nofocusChecker(aID) |
|
1703 { |
|
1704 this.__proto__ = new focusChecker(aID); |
|
1705 this.unexpected = true; |
|
1706 } |
|
1707 |
|
1708 /** |
|
1709 * Text inserted/removed events checker. |
|
1710 * @param aFromUser [in, optional] kNotFromUserInput or kFromUserInput |
|
1711 */ |
|
1712 function textChangeChecker(aID, aStart, aEnd, aTextOrFunc, aIsInserted, aFromUser) |
|
1713 { |
|
1714 this.target = getNode(aID); |
|
1715 this.type = aIsInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED; |
|
1716 this.startOffset = aStart; |
|
1717 this.endOffset = aEnd; |
|
1718 this.textOrFunc = aTextOrFunc; |
|
1719 |
|
1720 this.check = function textChangeChecker_check(aEvent) |
|
1721 { |
|
1722 aEvent.QueryInterface(nsIAccessibleTextChangeEvent); |
|
1723 |
|
1724 var modifiedText = (typeof this.textOrFunc == "function") ? |
|
1725 this.textOrFunc() : this.textOrFunc; |
|
1726 var modifiedTextLen = |
|
1727 (this.endOffset == -1) ? modifiedText.length : aEnd - aStart; |
|
1728 |
|
1729 is(aEvent.start, this.startOffset, |
|
1730 "Wrong start offset for " + prettyName(aID)); |
|
1731 is(aEvent.length, modifiedTextLen, "Wrong length for " + prettyName(aID)); |
|
1732 var changeInfo = (aIsInserted ? "inserted" : "removed"); |
|
1733 is(aEvent.isInserted, aIsInserted, |
|
1734 "Text was " + changeInfo + " for " + prettyName(aID)); |
|
1735 is(aEvent.modifiedText, modifiedText, |
|
1736 "Wrong " + changeInfo + " text for " + prettyName(aID)); |
|
1737 if (typeof aFromUser != "undefined") |
|
1738 is(aEvent.isFromUserInput, aFromUser, |
|
1739 "wrong value of isFromUserInput() for " + prettyName(aID)); |
|
1740 } |
|
1741 } |
|
1742 |
|
1743 /** |
|
1744 * Caret move events checker. |
|
1745 */ |
|
1746 function caretMoveChecker(aCaretOffset, aTargetOrFunc, aTargetFuncArg) |
|
1747 { |
|
1748 this.__proto__ = new invokerChecker(EVENT_TEXT_CARET_MOVED, |
|
1749 aTargetOrFunc, aTargetFuncArg); |
|
1750 |
|
1751 this.check = function caretMoveChecker_check(aEvent) |
|
1752 { |
|
1753 is(aEvent.QueryInterface(nsIAccessibleCaretMoveEvent).caretOffset, |
|
1754 aCaretOffset, |
|
1755 "Wrong caret offset for " + prettyName(aEvent.accessible)); |
|
1756 } |
|
1757 } |
|
1758 |
|
1759 /** |
|
1760 * Text selection change checker. |
|
1761 */ |
|
1762 function textSelectionChecker(aID, aStartOffset, aEndOffset) |
|
1763 { |
|
1764 this.__proto__ = new invokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID); |
|
1765 |
|
1766 this.check = function textSelectionChecker_check(aEvent) |
|
1767 { |
|
1768 if (aStartOffset == aEndOffset) { |
|
1769 is(getAccessible(aID, [nsIAccessibleText]).caretOffset, aStartOffset, |
|
1770 "Wrong collapsed selection!"); |
|
1771 } else { |
|
1772 testTextGetSelection(aID, aStartOffset, aEndOffset, 0); |
|
1773 } |
|
1774 } |
|
1775 } |
|
1776 |
|
1777 /** |
|
1778 * State change checker. |
|
1779 */ |
|
1780 function stateChangeChecker(aState, aIsExtraState, aIsEnabled, |
|
1781 aTargetOrFunc, aTargetFuncArg, aIsAsync, |
|
1782 aSkipCurrentStateCheck) |
|
1783 { |
|
1784 this.__proto__ = new invokerChecker(EVENT_STATE_CHANGE, aTargetOrFunc, |
|
1785 aTargetFuncArg, aIsAsync); |
|
1786 |
|
1787 this.check = function stateChangeChecker_check(aEvent) |
|
1788 { |
|
1789 var event = null; |
|
1790 try { |
|
1791 var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); |
|
1792 } catch (e) { |
|
1793 ok(false, "State change event was expected"); |
|
1794 } |
|
1795 |
|
1796 if (!event) |
|
1797 return; |
|
1798 |
|
1799 is(event.isExtraState, aIsExtraState, |
|
1800 "Wrong extra state bit of the statechange event."); |
|
1801 isState(event.state, aState, aIsExtraState, |
|
1802 "Wrong state of the statechange event."); |
|
1803 is(event.isEnabled, aIsEnabled, |
|
1804 "Wrong state of statechange event state"); |
|
1805 |
|
1806 if (aSkipCurrentStateCheck) { |
|
1807 todo(false, "State checking was skipped!"); |
|
1808 return; |
|
1809 } |
|
1810 |
|
1811 var state = aIsEnabled ? (aIsExtraState ? 0 : aState) : 0; |
|
1812 var extraState = aIsEnabled ? (aIsExtraState ? aState : 0) : 0; |
|
1813 var unxpdState = aIsEnabled ? 0 : (aIsExtraState ? 0 : aState); |
|
1814 var unxpdExtraState = aIsEnabled ? 0 : (aIsExtraState ? aState : 0); |
|
1815 testStates(event.accessible, state, extraState, unxpdState, unxpdExtraState); |
|
1816 } |
|
1817 |
|
1818 this.match = function stateChangeChecker_match(aEvent) |
|
1819 { |
|
1820 if (aEvent instanceof nsIAccessibleStateChangeEvent) { |
|
1821 var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); |
|
1822 return (aEvent.accessible == getAccessible(this.target)) && |
|
1823 (scEvent.state == aState); |
|
1824 } |
|
1825 return false; |
|
1826 } |
|
1827 } |
|
1828 |
|
1829 function asyncStateChangeChecker(aState, aIsExtraState, aIsEnabled, |
|
1830 aTargetOrFunc, aTargetFuncArg) |
|
1831 { |
|
1832 this.__proto__ = new stateChangeChecker(aState, aIsExtraState, aIsEnabled, |
|
1833 aTargetOrFunc, aTargetFuncArg, true); |
|
1834 } |
|
1835 |
|
1836 /** |
|
1837 * Expanded state change checker. |
|
1838 */ |
|
1839 function expandedStateChecker(aIsEnabled, aTargetOrFunc, aTargetFuncArg) |
|
1840 { |
|
1841 this.__proto__ = new invokerChecker(EVENT_STATE_CHANGE, aTargetOrFunc, |
|
1842 aTargetFuncArg); |
|
1843 |
|
1844 this.check = function expandedStateChecker_check(aEvent) |
|
1845 { |
|
1846 var event = null; |
|
1847 try { |
|
1848 var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); |
|
1849 } catch (e) { |
|
1850 ok(false, "State change event was expected"); |
|
1851 } |
|
1852 |
|
1853 if (!event) |
|
1854 return; |
|
1855 |
|
1856 is(event.state, STATE_EXPANDED, "Wrong state of the statechange event."); |
|
1857 is(event.isExtraState, false, |
|
1858 "Wrong extra state bit of the statechange event."); |
|
1859 is(event.isEnabled, aIsEnabled, |
|
1860 "Wrong state of statechange event state"); |
|
1861 |
|
1862 testStates(event.accessible, |
|
1863 (aIsEnabled ? STATE_EXPANDED : STATE_COLLAPSED)); |
|
1864 } |
|
1865 } |
|
1866 |
|
1867 //////////////////////////////////////////////////////////////////////////////// |
|
1868 // Event sequances (array of predefined checkers) |
|
1869 |
|
1870 /** |
|
1871 * Event seq for single selection change. |
|
1872 */ |
|
1873 function selChangeSeq(aUnselectedID, aSelectedID) |
|
1874 { |
|
1875 if (!aUnselectedID) { |
|
1876 return [ |
|
1877 new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), |
|
1878 new invokerChecker(EVENT_SELECTION, aSelectedID) |
|
1879 ]; |
|
1880 } |
|
1881 |
|
1882 // Return two possible scenarios: depending on widget type when selection is |
|
1883 // moved the the order of items that get selected and unselected may vary. |
|
1884 return [ |
|
1885 [ |
|
1886 new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), |
|
1887 new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), |
|
1888 new invokerChecker(EVENT_SELECTION, aSelectedID) |
|
1889 ], |
|
1890 [ |
|
1891 new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), |
|
1892 new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), |
|
1893 new invokerChecker(EVENT_SELECTION, aSelectedID) |
|
1894 ] |
|
1895 ]; |
|
1896 } |
|
1897 |
|
1898 /** |
|
1899 * Event seq for item removed form the selection. |
|
1900 */ |
|
1901 function selRemoveSeq(aUnselectedID) |
|
1902 { |
|
1903 return [ |
|
1904 new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), |
|
1905 new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID) |
|
1906 ]; |
|
1907 } |
|
1908 |
|
1909 /** |
|
1910 * Event seq for item added to the selection. |
|
1911 */ |
|
1912 function selAddSeq(aSelectedID) |
|
1913 { |
|
1914 return [ |
|
1915 new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), |
|
1916 new invokerChecker(EVENT_SELECTION_ADD, aSelectedID) |
|
1917 ]; |
|
1918 } |
|
1919 |
|
1920 //////////////////////////////////////////////////////////////////////////////// |
|
1921 // Private implementation details. |
|
1922 //////////////////////////////////////////////////////////////////////////////// |
|
1923 |
|
1924 |
|
1925 //////////////////////////////////////////////////////////////////////////////// |
|
1926 // General |
|
1927 |
|
1928 var gA11yEventListeners = {}; |
|
1929 var gA11yEventApplicantsCount = 0; |
|
1930 |
|
1931 var gA11yEventObserver = |
|
1932 { |
|
1933 observe: function observe(aSubject, aTopic, aData) |
|
1934 { |
|
1935 if (aTopic != "accessible-event") |
|
1936 return; |
|
1937 |
|
1938 var event; |
|
1939 try { |
|
1940 event = aSubject.QueryInterface(nsIAccessibleEvent); |
|
1941 } catch (ex) { |
|
1942 // After a test is aborted (i.e. timed out by the harness), this exception is soon triggered. |
|
1943 // Remove the leftover observer, otherwise it "leaks" to all the following tests. |
|
1944 Services.obs.removeObserver(this, "accessible-event"); |
|
1945 // Forward the exception, with added explanation. |
|
1946 throw "[accessible/events.js, gA11yEventObserver.observe] This is expected if a previous test has been aborted... Initial exception was: [ " + ex + " ]"; |
|
1947 } |
|
1948 var listenersArray = gA11yEventListeners[event.eventType]; |
|
1949 |
|
1950 var eventFromDumpArea = false; |
|
1951 if (gLogger.isEnabled()) { // debug stuff |
|
1952 eventFromDumpArea = true; |
|
1953 |
|
1954 var target = event.DOMNode; |
|
1955 var dumpElm = gA11yEventDumpID ? |
|
1956 document.getElementById(gA11yEventDumpID) : null; |
|
1957 |
|
1958 if (dumpElm) { |
|
1959 var parent = target; |
|
1960 while (parent && parent != dumpElm) |
|
1961 parent = parent.parentNode; |
|
1962 } |
|
1963 |
|
1964 if (!dumpElm || parent != dumpElm) { |
|
1965 var type = eventTypeToString(event.eventType); |
|
1966 var info = "Event type: " + type; |
|
1967 |
|
1968 if (event instanceof nsIAccessibleStateChangeEvent) { |
|
1969 var stateStr = statesToString(event.isExtraState ? 0 : event.state, |
|
1970 event.isExtraState ? event.state : 0); |
|
1971 info += ", state: " + stateStr + ", is enabled: " + event.isEnabled; |
|
1972 |
|
1973 } else if (event instanceof nsIAccessibleTextChangeEvent) { |
|
1974 info += ", start: " + event.start + ", length: " + event.length + |
|
1975 ", " + (event.isInserted ? "inserted" : "removed") + |
|
1976 " text: " + event.modifiedText; |
|
1977 } |
|
1978 |
|
1979 info += ". Target: " + prettyName(event.accessible); |
|
1980 |
|
1981 if (listenersArray) |
|
1982 info += ". Listeners count: " + listenersArray.length; |
|
1983 |
|
1984 if (gLogger.hasFeature("parentchain:" + type)) { |
|
1985 info += "\nParent chain:\n"; |
|
1986 var acc = event.accessible; |
|
1987 while (acc) { |
|
1988 info += " " + prettyName(acc) + "\n"; |
|
1989 acc = acc.parent; |
|
1990 } |
|
1991 } |
|
1992 |
|
1993 eventFromDumpArea = false; |
|
1994 gLogger.log(info); |
|
1995 } |
|
1996 } |
|
1997 |
|
1998 // Do not notify listeners if event is result of event log changes. |
|
1999 if (!listenersArray || eventFromDumpArea) |
|
2000 return; |
|
2001 |
|
2002 for (var index = 0; index < listenersArray.length; index++) |
|
2003 listenersArray[index].handleEvent(event); |
|
2004 } |
|
2005 }; |
|
2006 |
|
2007 function listenA11yEvents(aStartToListen) |
|
2008 { |
|
2009 if (aStartToListen) { |
|
2010 // Add observer when adding the first applicant only. |
|
2011 if (!(gA11yEventApplicantsCount++)) |
|
2012 Services.obs.addObserver(gA11yEventObserver, "accessible-event", false); |
|
2013 } else { |
|
2014 // Remove observer when there are no more applicants only. |
|
2015 // '< 0' case should not happen, but just in case: removeObserver() will throw. |
|
2016 if (--gA11yEventApplicantsCount <= 0) |
|
2017 Services.obs.removeObserver(gA11yEventObserver, "accessible-event"); |
|
2018 } |
|
2019 } |
|
2020 |
|
2021 function addA11yEventListener(aEventType, aEventHandler) |
|
2022 { |
|
2023 if (!(aEventType in gA11yEventListeners)) |
|
2024 gA11yEventListeners[aEventType] = new Array(); |
|
2025 |
|
2026 var listenersArray = gA11yEventListeners[aEventType]; |
|
2027 var index = listenersArray.indexOf(aEventHandler); |
|
2028 if (index == -1) |
|
2029 listenersArray.push(aEventHandler); |
|
2030 } |
|
2031 |
|
2032 function removeA11yEventListener(aEventType, aEventHandler) |
|
2033 { |
|
2034 var listenersArray = gA11yEventListeners[aEventType]; |
|
2035 if (!listenersArray) |
|
2036 return false; |
|
2037 |
|
2038 var index = listenersArray.indexOf(aEventHandler); |
|
2039 if (index == -1) |
|
2040 return false; |
|
2041 |
|
2042 listenersArray.splice(index, 1); |
|
2043 |
|
2044 if (!listenersArray.length) { |
|
2045 gA11yEventListeners[aEventType] = null; |
|
2046 delete gA11yEventListeners[aEventType]; |
|
2047 } |
|
2048 |
|
2049 return true; |
|
2050 } |
|
2051 |
|
2052 /** |
|
2053 * Used to dump debug information. |
|
2054 */ |
|
2055 var gLogger = |
|
2056 { |
|
2057 /** |
|
2058 * Return true if dump is enabled. |
|
2059 */ |
|
2060 isEnabled: function debugOutput_isEnabled() |
|
2061 { |
|
2062 return gA11yEventDumpID || gA11yEventDumpToConsole || |
|
2063 gA11yEventDumpToAppConsole; |
|
2064 }, |
|
2065 |
|
2066 /** |
|
2067 * Dump information into DOM and console if applicable. |
|
2068 */ |
|
2069 log: function logger_log(aMsg) |
|
2070 { |
|
2071 this.logToConsole(aMsg); |
|
2072 this.logToAppConsole(aMsg); |
|
2073 this.logToDOM(aMsg); |
|
2074 }, |
|
2075 |
|
2076 /** |
|
2077 * Log message to DOM. |
|
2078 * |
|
2079 * @param aMsg [in] the primary message |
|
2080 * @param aHasIndent [in, optional] if specified the message has an indent |
|
2081 * @param aPreEmphText [in, optional] the text is colored and appended prior |
|
2082 * primary message |
|
2083 */ |
|
2084 logToDOM: function logger_logToDOM(aMsg, aHasIndent, aPreEmphText) |
|
2085 { |
|
2086 if (gA11yEventDumpID == "") |
|
2087 return; |
|
2088 |
|
2089 var dumpElm = document.getElementById(gA11yEventDumpID); |
|
2090 if (!dumpElm) { |
|
2091 ok(false, |
|
2092 "No dump element '" + gA11yEventDumpID + "' within the document!"); |
|
2093 return; |
|
2094 } |
|
2095 |
|
2096 var containerTagName = document instanceof nsIDOMHTMLDocument ? |
|
2097 "div" : "description"; |
|
2098 |
|
2099 var container = document.createElement(containerTagName); |
|
2100 if (aHasIndent) |
|
2101 container.setAttribute("style", "padding-left: 10px;"); |
|
2102 |
|
2103 if (aPreEmphText) { |
|
2104 var inlineTagName = document instanceof nsIDOMHTMLDocument ? |
|
2105 "span" : "description"; |
|
2106 var emphElm = document.createElement(inlineTagName); |
|
2107 emphElm.setAttribute("style", "color: blue;"); |
|
2108 emphElm.textContent = aPreEmphText; |
|
2109 |
|
2110 container.appendChild(emphElm); |
|
2111 } |
|
2112 |
|
2113 var textNode = document.createTextNode(aMsg); |
|
2114 container.appendChild(textNode); |
|
2115 |
|
2116 dumpElm.appendChild(container); |
|
2117 }, |
|
2118 |
|
2119 /** |
|
2120 * Log message to console. |
|
2121 */ |
|
2122 logToConsole: function logger_logToConsole(aMsg) |
|
2123 { |
|
2124 if (gA11yEventDumpToConsole) |
|
2125 dump("\n" + aMsg + "\n"); |
|
2126 }, |
|
2127 |
|
2128 /** |
|
2129 * Log message to error console. |
|
2130 */ |
|
2131 logToAppConsole: function logger_logToAppConsole(aMsg) |
|
2132 { |
|
2133 if (gA11yEventDumpToAppConsole) |
|
2134 Services.console.logStringMessage("events: " + aMsg); |
|
2135 }, |
|
2136 |
|
2137 /** |
|
2138 * Return true if logging feature is enabled. |
|
2139 */ |
|
2140 hasFeature: function logger_hasFeature(aFeature) |
|
2141 { |
|
2142 var startIdx = gA11yEventDumpFeature.indexOf(aFeature); |
|
2143 if (startIdx == - 1) |
|
2144 return false; |
|
2145 |
|
2146 var endIdx = startIdx + aFeature.length; |
|
2147 return endIdx == gA11yEventDumpFeature.length || |
|
2148 gA11yEventDumpFeature[endIdx] == ";"; |
|
2149 } |
|
2150 }; |
|
2151 |
|
2152 |
|
2153 //////////////////////////////////////////////////////////////////////////////// |
|
2154 // Sequence |
|
2155 |
|
2156 /** |
|
2157 * Base class of sequence item. |
|
2158 */ |
|
2159 function sequenceItem(aProcessor, aEventType, aTarget, aItemID) |
|
2160 { |
|
2161 // private |
|
2162 |
|
2163 this.startProcess = function sequenceItem_startProcess() |
|
2164 { |
|
2165 this.queue.invoke(); |
|
2166 } |
|
2167 |
|
2168 var item = this; |
|
2169 |
|
2170 this.queue = new eventQueue(); |
|
2171 this.queue.onFinish = function() |
|
2172 { |
|
2173 aProcessor.onProcessed(); |
|
2174 return DO_NOT_FINISH_TEST; |
|
2175 } |
|
2176 |
|
2177 var invoker = { |
|
2178 invoke: function invoker_invoke() { |
|
2179 return aProcessor.process(); |
|
2180 }, |
|
2181 getID: function invoker_getID() |
|
2182 { |
|
2183 return aItemID; |
|
2184 }, |
|
2185 eventSeq: [ new invokerChecker(aEventType, aTarget) ] |
|
2186 }; |
|
2187 |
|
2188 this.queue.push(invoker); |
|
2189 } |
|
2190 |
|
2191 //////////////////////////////////////////////////////////////////////////////// |
|
2192 // Event queue invokers |
|
2193 |
|
2194 /** |
|
2195 * Invoker base class for prepare an action. |
|
2196 */ |
|
2197 function synthAction(aNodeOrID, aEventsObj) |
|
2198 { |
|
2199 this.DOMNode = getNode(aNodeOrID); |
|
2200 |
|
2201 if (aEventsObj) { |
|
2202 var scenarios = null; |
|
2203 if (aEventsObj instanceof Array) { |
|
2204 if (aEventsObj[0] instanceof Array) |
|
2205 scenarios = aEventsObj; // scenarios |
|
2206 else |
|
2207 scenarios = [ aEventsObj ]; // event sequance |
|
2208 } else { |
|
2209 scenarios = [ [ aEventsObj ] ]; // a single checker object |
|
2210 } |
|
2211 |
|
2212 for (var i = 0; i < scenarios.length; i++) |
|
2213 defineScenario(this, scenarios[i]); |
|
2214 } |
|
2215 |
|
2216 this.getID = function synthAction_getID() |
|
2217 { return prettyName(aNodeOrID) + " action"; } |
|
2218 } |