Common.sys.mjs (11383B)
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 import { Assert } from "resource://testing-common/Assert.sys.mjs"; 6 7 const MAX_TRIM_LENGTH = 100; 8 9 export const CommonUtils = { 10 /** 11 * Constant passed to getAccessible to indicate that it shouldn't fail if 12 * there is no accessible. 13 */ 14 DONOTFAIL_IF_NO_ACC: 1, 15 16 /** 17 * Constant passed to getAccessible to indicate that it shouldn't fail if it 18 * does not support an interface. 19 */ 20 DONOTFAIL_IF_NO_INTERFACE: 2, 21 22 /** 23 * nsIAccessibilityService service. 24 */ 25 get accService() { 26 if (!this._accService) { 27 this._accService = Cc["@mozilla.org/accessibilityService;1"].getService( 28 Ci.nsIAccessibilityService 29 ); 30 } 31 32 return this._accService; 33 }, 34 35 clearAccService() { 36 this._accService = null; 37 Cu.forceGC(); 38 }, 39 40 /** 41 * Adds an observer for an 'a11y-consumers-changed' event. 42 */ 43 addAccConsumersChangedObserver() { 44 const deferred = {}; 45 this._accConsumersChanged = new Promise(resolve => { 46 deferred.resolve = resolve; 47 }); 48 const observe = (subject, topic, data) => { 49 Services.obs.removeObserver(observe, "a11y-consumers-changed"); 50 deferred.resolve(JSON.parse(data)); 51 }; 52 Services.obs.addObserver(observe, "a11y-consumers-changed"); 53 }, 54 55 /** 56 * Returns a promise that resolves when 'a11y-consumers-changed' event is 57 * fired. 58 * 59 * @return {Promise} 60 * event promise evaluating to event's data 61 */ 62 observeAccConsumersChanged() { 63 return this._accConsumersChanged; 64 }, 65 66 /** 67 * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "1" 68 * which indicates that an accessibility service is initialized in the current 69 * process. 70 */ 71 addAccServiceInitializedObserver() { 72 const deferred = {}; 73 this._accServiceInitialized = new Promise((resolve, reject) => { 74 deferred.resolve = resolve; 75 deferred.reject = reject; 76 }); 77 const observe = (subject, topic, data) => { 78 if (data === "1") { 79 Services.obs.removeObserver(observe, "a11y-init-or-shutdown"); 80 deferred.resolve(); 81 } else { 82 deferred.reject("Accessibility service is shutdown unexpectedly."); 83 } 84 }; 85 Services.obs.addObserver(observe, "a11y-init-or-shutdown"); 86 }, 87 88 /** 89 * Returns a promise that resolves when an accessibility service is 90 * initialized in the current process. Otherwise (if the service is shutdown) 91 * the promise is rejected. 92 */ 93 observeAccServiceInitialized() { 94 return this._accServiceInitialized; 95 }, 96 97 /** 98 * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "0" 99 * which indicates that an accessibility service is shutdown in the current 100 * process. 101 */ 102 addAccServiceShutdownObserver() { 103 const deferred = {}; 104 this._accServiceShutdown = new Promise((resolve, reject) => { 105 deferred.resolve = resolve; 106 deferred.reject = reject; 107 }); 108 const observe = (subject, topic, data) => { 109 if (data === "0") { 110 Services.obs.removeObserver(observe, "a11y-init-or-shutdown"); 111 deferred.resolve(); 112 } else { 113 deferred.reject("Accessibility service is initialized unexpectedly."); 114 } 115 }; 116 Services.obs.addObserver(observe, "a11y-init-or-shutdown"); 117 }, 118 119 /** 120 * Returns a promise that resolves when an accessibility service is shutdown 121 * in the current process. Otherwise (if the service is initialized) the 122 * promise is rejected. 123 */ 124 observeAccServiceShutdown() { 125 return this._accServiceShutdown; 126 }, 127 128 /** 129 * Obtain DOMNode id from an accessible. This simply queries the .id property 130 * on the accessible, but it catches exceptions which might occur if the 131 * accessible has died or was constructed from a pseudoelement 132 * like ::details-content. 133 * 134 * @param {nsIAccessible} accessible accessible 135 * @return {string?} DOMNode id if available 136 */ 137 getAccessibleDOMNodeID(accessible) { 138 try { 139 return accessible.id; 140 } catch (e) { 141 // This will fail if the accessible has died, or if 142 // the accessible was constructed from a pseudoelement 143 // like ::details-content. 144 } 145 return null; 146 }, 147 148 getObjAddress(obj) { 149 const exp = /native\s*@\s*(0x[a-f0-9]+)/g; 150 const match = exp.exec(obj.toString()); 151 if (match) { 152 return match[1]; 153 } 154 155 return obj.toString(); 156 }, 157 158 getNodePrettyName(node) { 159 try { 160 let tag = ""; 161 if (node.nodeType == Node.DOCUMENT_NODE) { 162 tag = "document"; 163 } else { 164 tag = node.localName; 165 if (node.nodeType == Node.ELEMENT_NODE && node.hasAttribute("id")) { 166 tag += `@id="${node.getAttribute("id")}"`; 167 } 168 } 169 170 return `"${tag} node", address: ${this.getObjAddress(node)}`; 171 } catch (e) { 172 return `" no node info "`; 173 } 174 }, 175 176 /** 177 * Convert role to human readable string. 178 */ 179 roleToString(role) { 180 return this.accService.getStringRole(role); 181 }, 182 183 /** 184 * Shorten a long string if it exceeds MAX_TRIM_LENGTH. 185 * 186 * @param aString the string to shorten. 187 * 188 * @returns the shortened string. 189 */ 190 shortenString(str) { 191 if (str.length <= MAX_TRIM_LENGTH) { 192 return str; 193 } 194 195 // Trim the string if its length is > MAX_TRIM_LENGTH characters. 196 const trimOffset = MAX_TRIM_LENGTH / 2; 197 198 return `${str.substring(0, trimOffset - 1)}…${str.substring( 199 str.length - trimOffset, 200 str.length 201 )}`; 202 }, 203 204 normalizeAccTreeObj(obj) { 205 const key = Object.keys(obj)[0]; 206 const roleName = `ROLE_${key}`; 207 if (roleName in Ci.nsIAccessibleRole) { 208 return { 209 role: Ci.nsIAccessibleRole[roleName], 210 children: obj[key], 211 }; 212 } 213 214 return obj; 215 }, 216 217 stringifyTree(obj) { 218 let text = this.roleToString(obj.role) + ": [ "; 219 if ("children" in obj) { 220 for (let i = 0; i < obj.children.length; i++) { 221 const c = this.normalizeAccTreeObj(obj.children[i]); 222 text += this.stringifyTree(c); 223 if (i < obj.children.length - 1) { 224 text += ", "; 225 } 226 } 227 } 228 229 return `${text}] `; 230 }, 231 232 /** 233 * Return pretty name for identifier, it may be ID, DOM node or accessible. 234 */ 235 prettyName(identifier) { 236 if (identifier instanceof Array) { 237 let msg = ""; 238 for (let idx = 0; idx < identifier.length; idx++) { 239 if (msg != "") { 240 msg += ", "; 241 } 242 243 msg += this.prettyName(identifier[idx]); 244 } 245 return msg; 246 } 247 248 if (identifier instanceof Ci.nsIAccessible) { 249 const acc = this.getAccessible(identifier); 250 const domID = this.getAccessibleDOMNodeID(acc); 251 let msg = "["; 252 try { 253 if (Services.appinfo.browserTabsRemoteAutostart) { 254 if (domID) { 255 msg += `DOM node id: ${domID}, `; 256 } 257 } else { 258 msg += `${this.getNodePrettyName(acc.DOMNode)}, `; 259 } 260 msg += `role: ${this.roleToString(acc.role)}`; 261 if (acc.name) { 262 msg += `, name: "${this.shortenString(acc.name)}"`; 263 } 264 } catch (e) { 265 msg += "defunct"; 266 } 267 268 if (acc) { 269 msg += `, address: ${this.getObjAddress(acc)}`; 270 } 271 msg += "]"; 272 273 return msg; 274 } 275 276 if (Node.isInstance(identifier)) { 277 return `[ ${this.getNodePrettyName(identifier)} ]`; 278 } 279 280 if (identifier && typeof identifier === "object") { 281 const treeObj = this.normalizeAccTreeObj(identifier); 282 if ("role" in treeObj) { 283 return `{ ${this.stringifyTree(treeObj)} }`; 284 } 285 286 return JSON.stringify(identifier); 287 } 288 289 return ` "${identifier}" `; 290 }, 291 292 /** 293 * Return accessible for the given identifier (may be ID attribute or DOM 294 * element or accessible object) or null. 295 * 296 * @param accOrElmOrID 297 * identifier to get an accessible implementing the given interfaces 298 * @param aInterfaces 299 * [optional] the interface or an array interfaces to query it/them 300 * from obtained accessible 301 * @param elmObj 302 * [optional] object to store DOM element which accessible is obtained 303 * for 304 * @param doNotFailIf 305 * [optional] no error for special cases (see DONOTFAIL_IF_NO_ACC, 306 * DONOTFAIL_IF_NO_INTERFACE) 307 * @param doc 308 * [optional] document for when accOrElmOrID is an ID. 309 */ 310 getAccessible(accOrElmOrID, interfaces, elmObj, doNotFailIf, doc) { 311 if (!accOrElmOrID) { 312 return null; 313 } 314 315 let elm = null; 316 if (accOrElmOrID instanceof Ci.nsIAccessible) { 317 try { 318 elm = accOrElmOrID.DOMNode; 319 } catch (e) {} 320 } else if (Node.isInstance(accOrElmOrID)) { 321 elm = accOrElmOrID; 322 } else { 323 elm = doc.getElementById(accOrElmOrID); 324 if (!elm) { 325 Assert.ok(false, `Can't get DOM element for ${accOrElmOrID}`); 326 return null; 327 } 328 } 329 330 if (elmObj && typeof elmObj == "object") { 331 elmObj.value = elm; 332 } 333 334 let acc = accOrElmOrID instanceof Ci.nsIAccessible ? accOrElmOrID : null; 335 if (!acc) { 336 try { 337 acc = this.accService.getAccessibleFor(elm); 338 } catch (e) {} 339 340 if (!acc) { 341 if (!(doNotFailIf & this.DONOTFAIL_IF_NO_ACC)) { 342 Assert.ok( 343 false, 344 `Can't get accessible for ${this.prettyName(accOrElmOrID)}` 345 ); 346 } 347 348 return null; 349 } 350 } 351 352 if (!interfaces) { 353 return acc; 354 } 355 356 if (!(interfaces instanceof Array)) { 357 interfaces = [interfaces]; 358 } 359 360 for (let index = 0; index < interfaces.length; index++) { 361 if (acc instanceof interfaces[index]) { 362 continue; 363 } 364 365 try { 366 acc.QueryInterface(interfaces[index]); 367 } catch (e) { 368 if (!(doNotFailIf & this.DONOTFAIL_IF_NO_INTERFACE)) { 369 Assert.ok( 370 false, 371 `Can't query ${interfaces[index]} for ${accOrElmOrID}` 372 ); 373 } 374 375 return null; 376 } 377 } 378 379 return acc; 380 }, 381 382 /** 383 * Return the DOM node by identifier (may be accessible, DOM node or ID). 384 */ 385 getNode(accOrNodeOrID, doc) { 386 if (!accOrNodeOrID) { 387 return null; 388 } 389 390 if (Node.isInstance(accOrNodeOrID)) { 391 return accOrNodeOrID; 392 } 393 394 if (accOrNodeOrID instanceof Ci.nsIAccessible) { 395 return accOrNodeOrID.DOMNode; 396 } 397 398 const node = doc.getElementById(accOrNodeOrID); 399 if (!node) { 400 Assert.ok(false, `Can't get DOM element for ${accOrNodeOrID}`); 401 return null; 402 } 403 404 return node; 405 }, 406 407 /** 408 * Return root accessible. 409 * 410 * @param {DOMNode} doc 411 * Chrome document. 412 * 413 * @return {nsIAccessible} 414 * Accessible object for chrome window. 415 */ 416 getRootAccessible(doc) { 417 const acc = this.getAccessible(doc); 418 return acc ? acc.rootDocument.QueryInterface(Ci.nsIAccessible) : null; 419 }, 420 421 /** 422 * Analogy of SimpleTest.is function used to compare objects. 423 */ 424 isObject(obj, expectedObj, msg) { 425 if (obj == expectedObj) { 426 Assert.ok(true, msg); 427 return; 428 } 429 430 Assert.ok( 431 false, 432 `${msg} - got "${this.prettyName(obj)}", expected "${this.prettyName( 433 expectedObj 434 )}"` 435 ); 436 }, 437 };