Backed out 2 changesets (bug 1900622) for causing Bug 1908553 and ktlint failure...
[gecko.git] / accessible / tests / browser / Common.sys.mjs
blob466a0d2b990323a4f69f2bb12eab1a70cb90fc83
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 = {
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,
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,
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     }
32     return this._accService;
33   },
35   clearAccService() {
36     this._accService = null;
37     Cu.forceGC();
38   },
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   },
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   },
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   },
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   },
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   },
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   },
128   /**
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.
132    *
133    * @param  {nsIAccessible} accessible  accessible
134    * @return {String?}                   DOMNode id if available
135    */
136   getAccessibleDOMNodeID(accessible) {
137     if (accessible instanceof Ci.nsIAccessibleDocument) {
138       // If accessible is a document, trying to find its document body id.
139       try {
140         return accessible.DOMNode.body.id;
141       } catch (e) {
142         /* This only works if accessible is not a proxy. */
143       }
144     }
145     try {
146       return accessible.DOMNode.id;
147     } catch (e) {
148       /* This will fail if DOMNode is in different process. */
149     }
150     try {
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;
155     } catch (e) {
156       /* This will fail if accessible is not a proxy. */
157     }
159     return null;
160   },
162   getObjAddress(obj) {
163     const exp = /native\s*@\s*(0x[a-f0-9]+)/g;
164     const match = exp.exec(obj.toString());
165     if (match) {
166       return match[1];
167     }
169     return obj.toString();
170   },
172   getNodePrettyName(node) {
173     try {
174       let tag = "";
175       if (node.nodeType == Node.DOCUMENT_NODE) {
176         tag = "document";
177       } else {
178         tag = node.localName;
179         if (node.nodeType == Node.ELEMENT_NODE && node.hasAttribute("id")) {
180           tag += `@id="${node.getAttribute("id")}"`;
181         }
182       }
184       return `"${tag} node", address: ${this.getObjAddress(node)}`;
185     } catch (e) {
186       return `" no node info "`;
187     }
188   },
190   /**
191    * Convert role to human readable string.
192    */
193   roleToString(role) {
194     return this.accService.getStringRole(role);
195   },
197   /**
198    * Shorten a long string if it exceeds MAX_TRIM_LENGTH.
199    *
200    * @param aString the string to shorten.
201    *
202    * @returns the shortened string.
203    */
204   shortenString(str) {
205     if (str.length <= MAX_TRIM_LENGTH) {
206       return str;
207     }
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,
214       str.length
215     )}`;
216   },
218   normalizeAccTreeObj(obj) {
219     const key = Object.keys(obj)[0];
220     const roleName = `ROLE_${key}`;
221     if (roleName in Ci.nsIAccessibleRole) {
222       return {
223         role: Ci.nsIAccessibleRole[roleName],
224         children: obj[key],
225       };
226     }
228     return obj;
229   },
231   stringifyTree(obj) {
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) {
238           text += ", ";
239         }
240       }
241     }
243     return `${text}] `;
244   },
246   /**
247    * Return pretty name for identifier, it may be ID, DOM node or accessible.
248    */
249   prettyName(identifier) {
250     if (identifier instanceof Array) {
251       let msg = "";
252       for (let idx = 0; idx < identifier.length; idx++) {
253         if (msg != "") {
254           msg += ", ";
255         }
257         msg += this.prettyName(identifier[idx]);
258       }
259       return msg;
260     }
262     if (identifier instanceof Ci.nsIAccessible) {
263       const acc = this.getAccessible(identifier);
264       const domID = this.getAccessibleDOMNodeID(acc);
265       let msg = "[";
266       try {
267         if (Services.appinfo.browserTabsRemoteAutostart) {
268           if (domID) {
269             msg += `DOM node id: ${domID}, `;
270           }
271         } else {
272           msg += `${this.getNodePrettyName(acc.DOMNode)}, `;
273         }
274         msg += `role: ${this.roleToString(acc.role)}`;
275         if (acc.name) {
276           msg += `, name: "${this.shortenString(acc.name)}"`;
277         }
278       } catch (e) {
279         msg += "defunct";
280       }
282       if (acc) {
283         msg += `, address: ${this.getObjAddress(acc)}`;
284       }
285       msg += "]";
287       return msg;
288     }
290     if (Node.isInstance(identifier)) {
291       return `[ ${this.getNodePrettyName(identifier)} ]`;
292     }
294     if (identifier && typeof identifier === "object") {
295       const treeObj = this.normalizeAccTreeObj(identifier);
296       if ("role" in treeObj) {
297         return `{ ${this.stringifyTree(treeObj)} }`;
298       }
300       return JSON.stringify(identifier);
301     }
303     return ` "${identifier}" `;
304   },
306   /**
307    * Return accessible for the given identifier (may be ID attribute or DOM
308    * element or accessible object) or null.
309    *
310    * @param accOrElmOrID
311    *        identifier to get an accessible implementing the given interfaces
312    * @param aInterfaces
313    *        [optional] the interface or an array interfaces to query it/them
314    *        from obtained accessible
315    * @param elmObj
316    *        [optional] object to store DOM element which accessible is obtained
317    *        for
318    * @param doNotFailIf
319    *        [optional] no error for special cases (see DONOTFAIL_IF_NO_ACC,
320    *        DONOTFAIL_IF_NO_INTERFACE)
321    * @param doc
322    *        [optional] document for when accOrElmOrID is an ID.
323    */
324   getAccessible(accOrElmOrID, interfaces, elmObj, doNotFailIf, doc) {
325     if (!accOrElmOrID) {
326       return null;
327     }
329     let elm = null;
330     if (accOrElmOrID instanceof Ci.nsIAccessible) {
331       try {
332         elm = accOrElmOrID.DOMNode;
333       } catch (e) {}
334     } else if (Node.isInstance(accOrElmOrID)) {
335       elm = accOrElmOrID;
336     } else {
337       elm = doc.getElementById(accOrElmOrID);
338       if (!elm) {
339         Assert.ok(false, `Can't get DOM element for ${accOrElmOrID}`);
340         return null;
341       }
342     }
344     if (elmObj && typeof elmObj == "object") {
345       elmObj.value = elm;
346     }
348     let acc = accOrElmOrID instanceof Ci.nsIAccessible ? accOrElmOrID : null;
349     if (!acc) {
350       try {
351         acc = this.accService.getAccessibleFor(elm);
352       } catch (e) {}
354       if (!acc) {
355         if (!(doNotFailIf & this.DONOTFAIL_IF_NO_ACC)) {
356           Assert.ok(
357             false,
358             `Can't get accessible for ${this.prettyName(accOrElmOrID)}`
359           );
360         }
362         return null;
363       }
364     }
366     if (!interfaces) {
367       return acc;
368     }
370     if (!(interfaces instanceof Array)) {
371       interfaces = [interfaces];
372     }
374     for (let index = 0; index < interfaces.length; index++) {
375       if (acc instanceof interfaces[index]) {
376         continue;
377       }
379       try {
380         acc.QueryInterface(interfaces[index]);
381       } catch (e) {
382         if (!(doNotFailIf & this.DONOTFAIL_IF_NO_INTERFACE)) {
383           Assert.ok(
384             false,
385             `Can't query ${interfaces[index]} for ${accOrElmOrID}`
386           );
387         }
389         return null;
390       }
391     }
393     return acc;
394   },
396   /**
397    * Return the DOM node by identifier (may be accessible, DOM node or ID).
398    */
399   getNode(accOrNodeOrID, doc) {
400     if (!accOrNodeOrID) {
401       return null;
402     }
404     if (Node.isInstance(accOrNodeOrID)) {
405       return accOrNodeOrID;
406     }
408     if (accOrNodeOrID instanceof Ci.nsIAccessible) {
409       return accOrNodeOrID.DOMNode;
410     }
412     const node = doc.getElementById(accOrNodeOrID);
413     if (!node) {
414       Assert.ok(false, `Can't get DOM element for ${accOrNodeOrID}`);
415       return null;
416     }
418     return node;
419   },
421   /**
422    * Return root accessible.
423    *
424    * @param  {DOMNode} doc
425    *         Chrome document.
426    *
427    * @return {nsIAccessible}
428    *         Accessible object for chrome window.
429    */
430   getRootAccessible(doc) {
431     const acc = this.getAccessible(doc);
432     return acc ? acc.rootDocument.QueryInterface(Ci.nsIAccessible) : null;
433   },
435   /**
436    * Analogy of SimpleTest.is function used to compare objects.
437    */
438   isObject(obj, expectedObj, msg) {
439     if (obj == expectedObj) {
440       Assert.ok(true, msg);
441       return;
442     }
444     Assert.ok(
445       false,
446       `${msg} - got "${this.prettyName(obj)}", expected "${this.prettyName(
447         expectedObj
448       )}"`
449     );
450   },