no bug - Correct some typos in the comments. a=typo-fix
[gecko.git] / accessible / tests / mochitest / promisified-events.js
blob223be16147f6fd186053910156cc85bf506758d3
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 // This is loaded by head.js, so has the same globals, hence we import the
8 // globals from there.
9 /* import-globals-from common.js */
11 /* exported EVENT_ANNOUNCEMENT, EVENT_REORDER, EVENT_SCROLLING,
12             EVENT_SCROLLING_END, EVENT_SHOW, EVENT_TEXT_INSERTED,
13             EVENT_TEXT_REMOVED, EVENT_DOCUMENT_LOAD_COMPLETE, EVENT_HIDE,
14             EVENT_TEXT_ATTRIBUTE_CHANGED, EVENT_TEXT_CARET_MOVED, EVENT_SELECTION,
15             EVENT_DESCRIPTION_CHANGE, EVENT_NAME_CHANGE, EVENT_STATE_CHANGE,
16             EVENT_VALUE_CHANGE, EVENT_TEXT_VALUE_CHANGE, EVENT_FOCUS,
17             EVENT_DOCUMENT_RELOAD, EVENT_VIRTUALCURSOR_CHANGED, EVENT_ALERT,
18             EVENT_OBJECT_ATTRIBUTE_CHANGED, UnexpectedEvents, waitForEvent,
19             waitForEvents, waitForOrderedEvents, waitForStateChange,
20             stateChangeEventArgs */
22 const EVENT_ANNOUNCEMENT = nsIAccessibleEvent.EVENT_ANNOUNCEMENT;
23 const EVENT_DOCUMENT_LOAD_COMPLETE =
24   nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE;
25 const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE;
26 const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER;
27 const EVENT_SCROLLING = nsIAccessibleEvent.EVENT_SCROLLING;
28 const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START;
29 const EVENT_SCROLLING_END = nsIAccessibleEvent.EVENT_SCROLLING_END;
30 const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION;
31 const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN;
32 const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW;
33 const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE;
34 const EVENT_TEXT_ATTRIBUTE_CHANGED =
35   nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED;
36 const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED;
37 const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED;
38 const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED;
39 const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE;
40 const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE;
41 const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE;
42 const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE;
43 const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS;
44 const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD;
45 const EVENT_VIRTUALCURSOR_CHANGED =
46   nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED;
47 const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT;
48 const EVENT_TEXT_SELECTION_CHANGED =
49   nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED;
50 const EVENT_LIVE_REGION_ADDED = nsIAccessibleEvent.EVENT_LIVE_REGION_ADDED;
51 const EVENT_LIVE_REGION_REMOVED = nsIAccessibleEvent.EVENT_LIVE_REGION_REMOVED;
52 const EVENT_OBJECT_ATTRIBUTE_CHANGED =
53   nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED;
54 const EVENT_INNER_REORDER = nsIAccessibleEvent.EVENT_INNER_REORDER;
56 const EventsLogger = {
57   enabled: false,
59   log(msg) {
60     if (this.enabled) {
61       info(msg);
62     }
63   },
66 /**
67  * Describe an event in string format.
68  * @param  {nsIAccessibleEvent}  event  event to strigify
69  */
70 function eventToString(event) {
71   let type = eventTypeToString(event.eventType);
72   let info = `Event type: ${type}`;
74   if (event instanceof nsIAccessibleStateChangeEvent) {
75     let stateStr = statesToString(
76       event.isExtraState ? 0 : event.state,
77       event.isExtraState ? event.state : 0
78     );
79     info += `, state: ${stateStr}, is enabled: ${event.isEnabled}`;
80   } else if (event instanceof nsIAccessibleTextChangeEvent) {
81     let tcType = event.isInserted ? "inserted" : "removed";
82     info += `, start: ${event.start}, length: ${event.length}, ${tcType} text: ${event.modifiedText}`;
83   }
85   info += `. Target: ${prettyName(event.accessible)}`;
86   return info;
89 function matchEvent(event, matchCriteria) {
90   if (!matchCriteria) {
91     return true;
92   }
94   let acc = event.accessible;
95   switch (typeof matchCriteria) {
96     case "string":
97       let id = getAccessibleDOMNodeID(acc);
98       if (id === matchCriteria) {
99         EventsLogger.log(`Event matches DOMNode id: ${id}`);
100         return true;
101       }
102       break;
103     case "function":
104       if (matchCriteria(event)) {
105         EventsLogger.log(
106           `Lambda function matches event: ${eventToString(event)}`
107         );
108         return true;
109       }
110       break;
111     default:
112       if (matchCriteria instanceof nsIAccessible) {
113         if (acc === matchCriteria) {
114           EventsLogger.log(`Event matches accessible: ${prettyName(acc)}`);
115           return true;
116         }
117       } else if (event.DOMNode == matchCriteria) {
118         EventsLogger.log(
119           `Event matches DOM node: ${prettyName(event.DOMNode)}`
120         );
121         return true;
122       }
123   }
125   return false;
129  * A helper function that returns a promise that resolves when an accessible
130  * event of the given type with the given target (defined by its id or
131  * accessible) is observed.
132  * @param  {Number}                eventType        expected accessible event
133  *                                                  type
134  * @param  {String|nsIAccessible|Function}  matchCriteria  expected content
135  *                                                         element id
136  *                                                         for the event
137  * @param  {String}                message          Message to prepend to logging.
138  * @return {Promise}                                promise that resolves to an
139  *                                                  event
140  */
141 function waitForEvent(eventType, matchCriteria, message) {
142   return new Promise(resolve => {
143     let eventObserver = {
144       observe(subject, topic) {
145         if (topic !== "accessible-event") {
146           return;
147         }
149         let event = subject.QueryInterface(nsIAccessibleEvent);
150         if (EventsLogger.enabled) {
151           // Avoid calling eventToString if the EventsLogger isn't enabled in order
152           // to avoid an intermittent crash (bug 1307645).
153           EventsLogger.log(eventToString(event));
154         }
156         // If event type does not match expected type, skip the event.
157         if (event.eventType !== eventType) {
158           return;
159         }
161         if (matchEvent(event, matchCriteria)) {
162           EventsLogger.log(
163             `Correct event type: ${eventTypeToString(eventType)}`
164           );
165           Services.obs.removeObserver(this, "accessible-event");
166           ok(
167             true,
168             `${message ? message + ": " : ""}Received ${eventTypeToString(
169               eventType
170             )} event`
171           );
172           resolve(event);
173         }
174       },
175     };
176     Services.obs.addObserver(eventObserver, "accessible-event");
177   });
180 class UnexpectedEvents {
181   constructor(unexpected) {
182     if (unexpected.length) {
183       this.unexpected = unexpected;
184       Services.obs.addObserver(this, "accessible-event");
185     }
186   }
188   observe(subject, topic) {
189     if (topic !== "accessible-event") {
190       return;
191     }
193     let event = subject.QueryInterface(nsIAccessibleEvent);
195     let unexpectedEvent = this.unexpected.find(
196       ([etype, criteria]) =>
197         etype === event.eventType && matchEvent(event, criteria)
198     );
200     if (unexpectedEvent) {
201       ok(false, `Got unexpected event: ${eventToString(event)}`);
202     }
203   }
205   stop() {
206     if (this.unexpected) {
207       Services.obs.removeObserver(this, "accessible-event");
208     }
209   }
213  * A helper function that waits for a sequence of accessible events in
214  * specified order.
215  * @param {Array}   events          a list of events to wait (same format as
216  *                                   waitForEvent arguments)
217  * @param {String}  message         Message to prepend to logging.
218  * @param {Boolean} ordered         Events need to be received in given order.
219  * @param {Object}  invokerOrWindow a local window or a special content invoker
220  *                                   it takes a list of arguments and a task
221  *                                   function.
222  */
223 async function waitForEvents(
224   events,
225   message,
226   ordered = false,
227   invokerOrWindow = null
228 ) {
229   let expected = events.expected || events;
230   // Next expected event index.
231   let currentIdx = 0;
233   let unexpectedListener = events.unexpected
234     ? new UnexpectedEvents(events.unexpected)
235     : null;
237   let results = await Promise.all(
238     expected.map((evt, idx) => {
239       const [eventType, matchCriteria] = evt;
240       return waitForEvent(eventType, matchCriteria, message).then(result => {
241         return [result, idx == currentIdx++];
242       });
243     })
244   );
246   if (unexpectedListener) {
247     let flushQueue = async win => {
248       // Flush all notifications or queued a11y events.
249       win.windowUtils.advanceTimeAndRefresh(100);
251       // Flush all DOM async events.
252       await new Promise(r => win.setTimeout(r, 0));
254       // Flush all notifications or queued a11y events resulting from async DOM events.
255       win.windowUtils.advanceTimeAndRefresh(100);
257       // Flush all notifications or a11y events that may have been queued in the last tick.
258       win.windowUtils.advanceTimeAndRefresh(100);
260       // Return refresh to normal.
261       win.windowUtils.restoreNormalRefresh();
262     };
264     if (invokerOrWindow instanceof Function) {
265       await invokerOrWindow([flushQueue.toString()], async _flushQueue => {
266         // eslint-disable-next-line no-eval, no-undef
267         await eval(_flushQueue)(content);
268       });
269     } else {
270       await flushQueue(invokerOrWindow ? invokerOrWindow : window);
271     }
273     unexpectedListener.stop();
274   }
276   if (ordered) {
277     ok(
278       results.every(([, isOrdered]) => isOrdered),
279       `${message ? message + ": " : ""}Correct event order`
280     );
281   }
283   return results.map(([event]) => event);
286 function waitForOrderedEvents(events, message) {
287   return waitForEvents(events, message, true);
290 function stateChangeEventArgs(id, state, isEnabled, isExtra = false) {
291   return [
292     EVENT_STATE_CHANGE,
293     e => {
294       e.QueryInterface(nsIAccessibleStateChangeEvent);
295       return (
296         e.state == state &&
297         e.isExtraState == isExtra &&
298         isEnabled == e.isEnabled &&
299         (typeof id == "string"
300           ? id == getAccessibleDOMNodeID(e.accessible)
301           : getAccessible(id) == e.accessible)
302       );
303     },
304   ];
307 function waitForStateChange(id, state, isEnabled, isExtra = false) {
308   return waitForEvent(...stateChangeEventArgs(id, state, isEnabled, isExtra));
311 ////////////////////////////////////////////////////////////////////////////////
312 // Utility functions ported from events.js.
315  * This function selects all text in the passed-in element if it has an editor,
316  * before setting focus to it. This simulates behavio with the keyboard when
317  * tabbing to the element. This does explicitly what synthFocus did implicitly.
318  * This should be called only if you really want this behavior.
319  * @param  {string}  id  The element ID to focus
320  */
321 function selectAllTextAndFocus(id) {
322   const elem = getNode(id);
323   if (elem.editor) {
324     elem.selectionStart = elem.selectionEnd = elem.value.length;
325   }
327   elem.focus();