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/. */
7 // This is loaded by head.js, so has the same globals, hence we import the
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 = {
67 * Describe an event in string format.
68 * @param {nsIAccessibleEvent} event event to strigify
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
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}`;
85 info += `. Target: ${prettyName(event.accessible)}`;
89 function matchEvent(event, matchCriteria) {
94 let acc = event.accessible;
95 switch (typeof matchCriteria) {
97 let id = getAccessibleDOMNodeID(acc);
98 if (id === matchCriteria) {
99 EventsLogger.log(`Event matches DOMNode id: ${id}`);
104 if (matchCriteria(event)) {
106 `Lambda function matches event: ${eventToString(event)}`
112 if (matchCriteria instanceof nsIAccessible) {
113 if (acc === matchCriteria) {
114 EventsLogger.log(`Event matches accessible: ${prettyName(acc)}`);
117 } else if (event.DOMNode == matchCriteria) {
119 `Event matches DOM node: ${prettyName(event.DOMNode)}`
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
134 * @param {String|nsIAccessible|Function} matchCriteria expected content
137 * @param {String} message Message to prepend to logging.
138 * @return {Promise} promise that resolves to an
141 function waitForEvent(eventType, matchCriteria, message) {
142 return new Promise(resolve => {
143 let eventObserver = {
144 observe(subject, topic) {
145 if (topic !== "accessible-event") {
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));
156 // If event type does not match expected type, skip the event.
157 if (event.eventType !== eventType) {
161 if (matchEvent(event, matchCriteria)) {
163 `Correct event type: ${eventTypeToString(eventType)}`
165 Services.obs.removeObserver(this, "accessible-event");
168 `${message ? message + ": " : ""}Received ${eventTypeToString(
176 Services.obs.addObserver(eventObserver, "accessible-event");
180 class UnexpectedEvents {
181 constructor(unexpected) {
182 if (unexpected.length) {
183 this.unexpected = unexpected;
184 Services.obs.addObserver(this, "accessible-event");
188 observe(subject, topic) {
189 if (topic !== "accessible-event") {
193 let event = subject.QueryInterface(nsIAccessibleEvent);
195 let unexpectedEvent = this.unexpected.find(
196 ([etype, criteria]) =>
197 etype === event.eventType && matchEvent(event, criteria)
200 if (unexpectedEvent) {
201 ok(false, `Got unexpected event: ${eventToString(event)}`);
206 if (this.unexpected) {
207 Services.obs.removeObserver(this, "accessible-event");
213 * A helper function that waits for a sequence of accessible events in
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
223 async function waitForEvents(
227 invokerOrWindow = null
229 let expected = events.expected || events;
230 // Next expected event index.
233 let unexpectedListener = events.unexpected
234 ? new UnexpectedEvents(events.unexpected)
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++];
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();
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);
270 await flushQueue(invokerOrWindow ? invokerOrWindow : window);
273 unexpectedListener.stop();
278 results.every(([, isOrdered]) => isOrdered),
279 `${message ? message + ": " : ""}Correct event order`
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) {
294 e.QueryInterface(nsIAccessibleStateChangeEvent);
297 e.isExtraState == isExtra &&
298 isEnabled == e.isEnabled &&
299 (typeof id == "string"
300 ? id == getAccessibleDOMNodeID(e.accessible)
301 : getAccessible(id) == e.accessible)
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
321 function selectAllTextAndFocus(id) {
322 const elem = getNode(id);
324 elem.selectionStart = elem.selectionEnd = elem.value.length;