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 import { Assert } from "resource://testing-common/Assert.sys.mjs";
7 const MAX_TRIM_LENGTH = 100;
9 export const CommonUtils = {
11 * Constant passed to getAccessible to indicate that it shouldn't fail if
12 * there is no accessible.
14 DONOTFAIL_IF_NO_ACC: 1,
17 * Constant passed to getAccessible to indicate that it shouldn't fail if it
18 * does not support an interface.
20 DONOTFAIL_IF_NO_INTERFACE: 2,
23 * nsIAccessibilityService service.
26 if (!this._accService) {
27 this._accService = Cc["@mozilla.org/accessibilityService;1"].getService(
28 Ci.nsIAccessibilityService
32 return this._accService;
36 this._accService = null;
41 * Adds an observer for an 'a11y-consumers-changed' event.
43 addAccConsumersChangedObserver() {
45 this._accConsumersChanged = new Promise(resolve => {
46 deferred.resolve = resolve;
48 const observe = (subject, topic, data) => {
49 Services.obs.removeObserver(observe, "a11y-consumers-changed");
50 deferred.resolve(JSON.parse(data));
52 Services.obs.addObserver(observe, "a11y-consumers-changed");
56 * Returns a promise that resolves when 'a11y-consumers-changed' event is
60 * event promise evaluating to event's data
62 observeAccConsumersChanged() {
63 return this._accConsumersChanged;
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
71 addAccServiceInitializedObserver() {
73 this._accServiceInitialized = new Promise((resolve, reject) => {
74 deferred.resolve = resolve;
75 deferred.reject = reject;
77 const observe = (subject, topic, data) => {
79 Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
82 deferred.reject("Accessibility service is shutdown unexpectedly.");
85 Services.obs.addObserver(observe, "a11y-init-or-shutdown");
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.
93 observeAccServiceInitialized() {
94 return this._accServiceInitialized;
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
102 addAccServiceShutdownObserver() {
104 this._accServiceShutdown = new Promise((resolve, reject) => {
105 deferred.resolve = resolve;
106 deferred.reject = reject;
108 const observe = (subject, topic, data) => {
110 Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
113 deferred.reject("Accessibility service is initialized unexpectedly.");
116 Services.obs.addObserver(observe, "a11y-init-or-shutdown");
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.
124 observeAccServiceShutdown() {
125 return this._accServiceShutdown;
129 * Extract DOMNode id from an accessible. If the accessible is in the remote
130 * process, DOMNode is not present in parent process. However, if specified by
131 * the author, DOMNode id will be attached to an accessible object.
133 * @param {nsIAccessible} accessible accessible
134 * @return {String?} DOMNode id if available
136 getAccessibleDOMNodeID(accessible) {
137 if (accessible instanceof Ci.nsIAccessibleDocument) {
138 // If accessible is a document, trying to find its document body id.
140 return accessible.DOMNode.body.id;
142 /* This only works if accessible is not a proxy. */
146 return accessible.DOMNode.id;
148 /* This will fail if DOMNode is in different process. */
151 // When e10s is enabled, accessible will have an "id" property if its
152 // corresponding DOMNode has an id. If accessible is a document, its "id"
153 // property corresponds to the "id" of its body element.
154 return accessible.id;
156 /* This will fail if accessible is not a proxy. */
163 const exp = /native\s*@\s*(0x[a-f0-9]+)/g;
164 const match = exp.exec(obj.toString());
169 return obj.toString();
172 getNodePrettyName(node) {
175 if (node.nodeType == Node.DOCUMENT_NODE) {
178 tag = node.localName;
179 if (node.nodeType == Node.ELEMENT_NODE && node.hasAttribute("id")) {
180 tag += `@id="${node.getAttribute("id")}"`;
184 return `"${tag} node", address: ${this.getObjAddress(node)}`;
186 return `" no node info "`;
191 * Convert role to human readable string.
194 return this.accService.getStringRole(role);
198 * Shorten a long string if it exceeds MAX_TRIM_LENGTH.
200 * @param aString the string to shorten.
202 * @returns the shortened string.
205 if (str.length <= MAX_TRIM_LENGTH) {
209 // Trim the string if its length is > MAX_TRIM_LENGTH characters.
210 const trimOffset = MAX_TRIM_LENGTH / 2;
212 return `${str.substring(0, trimOffset - 1)}…${str.substring(
213 str.length - trimOffset,
218 normalizeAccTreeObj(obj) {
219 const key = Object.keys(obj)[0];
220 const roleName = `ROLE_${key}`;
221 if (roleName in Ci.nsIAccessibleRole) {
223 role: Ci.nsIAccessibleRole[roleName],
232 let text = this.roleToString(obj.role) + ": [ ";
233 if ("children" in obj) {
234 for (let i = 0; i < obj.children.length; i++) {
235 const c = this.normalizeAccTreeObj(obj.children[i]);
236 text += this.stringifyTree(c);
237 if (i < obj.children.length - 1) {
247 * Return pretty name for identifier, it may be ID, DOM node or accessible.
249 prettyName(identifier) {
250 if (identifier instanceof Array) {
252 for (let idx = 0; idx < identifier.length; idx++) {
257 msg += this.prettyName(identifier[idx]);
262 if (identifier instanceof Ci.nsIAccessible) {
263 const acc = this.getAccessible(identifier);
264 const domID = this.getAccessibleDOMNodeID(acc);
267 if (Services.appinfo.browserTabsRemoteAutostart) {
269 msg += `DOM node id: ${domID}, `;
272 msg += `${this.getNodePrettyName(acc.DOMNode)}, `;
274 msg += `role: ${this.roleToString(acc.role)}`;
276 msg += `, name: "${this.shortenString(acc.name)}"`;
283 msg += `, address: ${this.getObjAddress(acc)}`;
290 if (Node.isInstance(identifier)) {
291 return `[ ${this.getNodePrettyName(identifier)} ]`;
294 if (identifier && typeof identifier === "object") {
295 const treeObj = this.normalizeAccTreeObj(identifier);
296 if ("role" in treeObj) {
297 return `{ ${this.stringifyTree(treeObj)} }`;
300 return JSON.stringify(identifier);
303 return ` "${identifier}" `;
307 * Return accessible for the given identifier (may be ID attribute or DOM
308 * element or accessible object) or null.
310 * @param accOrElmOrID
311 * identifier to get an accessible implementing the given interfaces
313 * [optional] the interface or an array interfaces to query it/them
314 * from obtained accessible
316 * [optional] object to store DOM element which accessible is obtained
319 * [optional] no error for special cases (see DONOTFAIL_IF_NO_ACC,
320 * DONOTFAIL_IF_NO_INTERFACE)
322 * [optional] document for when accOrElmOrID is an ID.
324 getAccessible(accOrElmOrID, interfaces, elmObj, doNotFailIf, doc) {
330 if (accOrElmOrID instanceof Ci.nsIAccessible) {
332 elm = accOrElmOrID.DOMNode;
334 } else if (Node.isInstance(accOrElmOrID)) {
337 elm = doc.getElementById(accOrElmOrID);
339 Assert.ok(false, `Can't get DOM element for ${accOrElmOrID}`);
344 if (elmObj && typeof elmObj == "object") {
348 let acc = accOrElmOrID instanceof Ci.nsIAccessible ? accOrElmOrID : null;
351 acc = this.accService.getAccessibleFor(elm);
355 if (!(doNotFailIf & this.DONOTFAIL_IF_NO_ACC)) {
358 `Can't get accessible for ${this.prettyName(accOrElmOrID)}`
370 if (!(interfaces instanceof Array)) {
371 interfaces = [interfaces];
374 for (let index = 0; index < interfaces.length; index++) {
375 if (acc instanceof interfaces[index]) {
380 acc.QueryInterface(interfaces[index]);
382 if (!(doNotFailIf & this.DONOTFAIL_IF_NO_INTERFACE)) {
385 `Can't query ${interfaces[index]} for ${accOrElmOrID}`
397 * Return the DOM node by identifier (may be accessible, DOM node or ID).
399 getNode(accOrNodeOrID, doc) {
400 if (!accOrNodeOrID) {
404 if (Node.isInstance(accOrNodeOrID)) {
405 return accOrNodeOrID;
408 if (accOrNodeOrID instanceof Ci.nsIAccessible) {
409 return accOrNodeOrID.DOMNode;
412 const node = doc.getElementById(accOrNodeOrID);
414 Assert.ok(false, `Can't get DOM element for ${accOrNodeOrID}`);
422 * Return root accessible.
424 * @param {DOMNode} doc
427 * @return {nsIAccessible}
428 * Accessible object for chrome window.
430 getRootAccessible(doc) {
431 const acc = this.getAccessible(doc);
432 return acc ? acc.rootDocument.QueryInterface(Ci.nsIAccessible) : null;
436 * Analogy of SimpleTest.is function used to compare objects.
438 isObject(obj, expectedObj, msg) {
439 if (obj == expectedObj) {
440 Assert.ok(true, msg);
446 `${msg} - got "${this.prettyName(obj)}", expected "${this.prettyName(