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