summaryrefslogtreecommitdiffstats
path: root/accessible/tests/browser/Common.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--accessible/tests/browser/Common.jsm456
1 files changed, 456 insertions, 0 deletions
diff --git a/accessible/tests/browser/Common.jsm b/accessible/tests/browser/Common.jsm
new file mode 100644
index 0000000000..527e301be9
--- /dev/null
+++ b/accessible/tests/browser/Common.jsm
@@ -0,0 +1,456 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["CommonUtils"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+
+const MAX_TRIM_LENGTH = 100;
+
+const CommonUtils = {
+ /**
+ * Constant passed to getAccessible to indicate that it shouldn't fail if
+ * there is no accessible.
+ */
+ DONOTFAIL_IF_NO_ACC: 1,
+
+ /**
+ * Constant passed to getAccessible to indicate that it shouldn't fail if it
+ * does not support an interface.
+ */
+ DONOTFAIL_IF_NO_INTERFACE: 2,
+
+ /**
+ * nsIAccessibilityService service.
+ */
+ get accService() {
+ if (!this._accService) {
+ this._accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ }
+
+ return this._accService;
+ },
+
+ clearAccService() {
+ this._accService = null;
+ Cu.forceGC();
+ },
+
+ /**
+ * Adds an observer for an 'a11y-consumers-changed' event.
+ */
+ addAccConsumersChangedObserver() {
+ const deferred = {};
+ this._accConsumersChanged = new Promise(resolve => {
+ deferred.resolve = resolve;
+ });
+ const observe = (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "a11y-consumers-changed");
+ deferred.resolve(JSON.parse(data));
+ };
+ Services.obs.addObserver(observe, "a11y-consumers-changed");
+ },
+
+ /**
+ * Returns a promise that resolves when 'a11y-consumers-changed' event is
+ * fired.
+ *
+ * @return {Promise}
+ * event promise evaluating to event's data
+ */
+ observeAccConsumersChanged() {
+ return this._accConsumersChanged;
+ },
+
+ /**
+ * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "1"
+ * which indicates that an accessibility service is initialized in the current
+ * process.
+ */
+ addAccServiceInitializedObserver() {
+ const deferred = {};
+ this._accServiceInitialized = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+ const observe = (subject, topic, data) => {
+ if (data === "1") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ deferred.resolve();
+ } else {
+ deferred.reject("Accessibility service is shutdown unexpectedly.");
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ },
+
+ /**
+ * Returns a promise that resolves when an accessibility service is
+ * initialized in the current process. Otherwise (if the service is shutdown)
+ * the promise is rejected.
+ */
+ observeAccServiceInitialized() {
+ return this._accServiceInitialized;
+ },
+
+ /**
+ * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "0"
+ * which indicates that an accessibility service is shutdown in the current
+ * process.
+ */
+ addAccServiceShutdownObserver() {
+ const deferred = {};
+ this._accServiceShutdown = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+ const observe = (subject, topic, data) => {
+ if (data === "0") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ deferred.resolve();
+ } else {
+ deferred.reject("Accessibility service is initialized unexpectedly.");
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ },
+
+ /**
+ * Returns a promise that resolves when an accessibility service is shutdown
+ * in the current process. Otherwise (if the service is initialized) the
+ * promise is rejected.
+ */
+ observeAccServiceShutdown() {
+ return this._accServiceShutdown;
+ },
+
+ /**
+ * Extract DOMNode id from an accessible. If the accessible is in the remote
+ * process, DOMNode is not present in parent process. However, if specified by
+ * the author, DOMNode id will be attached to an accessible object.
+ *
+ * @param {nsIAccessible} accessible accessible
+ * @return {String?} DOMNode id if available
+ */
+ getAccessibleDOMNodeID(accessible) {
+ if (accessible instanceof Ci.nsIAccessibleDocument) {
+ // If accessible is a document, trying to find its document body id.
+ try {
+ return accessible.DOMNode.body.id;
+ } catch (e) {
+ /* This only works if accessible is not a proxy. */
+ }
+ }
+ try {
+ return accessible.DOMNode.id;
+ } catch (e) {
+ /* This will fail if DOMNode is in different process. */
+ }
+ try {
+ // When e10s is enabled, accessible will have an "id" property if its
+ // corresponding DOMNode has an id. If accessible is a document, its "id"
+ // property corresponds to the "id" of its body element.
+ return accessible.id;
+ } catch (e) {
+ /* This will fail if accessible is not a proxy. */
+ }
+
+ return null;
+ },
+
+ getObjAddress(obj) {
+ const exp = /native\s*@\s*(0x[a-f0-9]+)/g;
+ const match = exp.exec(obj.toString());
+ if (match) {
+ return match[1];
+ }
+
+ return obj.toString();
+ },
+
+ getNodePrettyName(node) {
+ try {
+ let tag = "";
+ if (node.nodeType == Node.DOCUMENT_NODE) {
+ tag = "document";
+ } else {
+ tag = node.localName;
+ if (node.nodeType == Node.ELEMENT_NODE && node.hasAttribute("id")) {
+ tag += `@id="${node.getAttribute("id")}"`;
+ }
+ }
+
+ return `"${tag} node", address: ${this.getObjAddress(node)}`;
+ } catch (e) {
+ return `" no node info "`;
+ }
+ },
+
+ /**
+ * Convert role to human readable string.
+ */
+ roleToString(role) {
+ return this.accService.getStringRole(role);
+ },
+
+ /**
+ * Shorten a long string if it exceeds MAX_TRIM_LENGTH.
+ *
+ * @param aString the string to shorten.
+ *
+ * @returns the shortened string.
+ */
+ shortenString(str) {
+ if (str.length <= MAX_TRIM_LENGTH) {
+ return str;
+ }
+
+ // Trim the string if its length is > MAX_TRIM_LENGTH characters.
+ const trimOffset = MAX_TRIM_LENGTH / 2;
+
+ return `${str.substring(0, trimOffset - 1)}…${str.substring(
+ str.length - trimOffset,
+ str.length
+ )}`;
+ },
+
+ normalizeAccTreeObj(obj) {
+ const key = Object.keys(obj)[0];
+ const roleName = `ROLE_${key}`;
+ if (roleName in Ci.nsIAccessibleRole) {
+ return {
+ role: Ci.nsIAccessibleRole[roleName],
+ children: obj[key],
+ };
+ }
+
+ return obj;
+ },
+
+ stringifyTree(obj) {
+ let text = this.roleToString(obj.role) + ": [ ";
+ if ("children" in obj) {
+ for (let i = 0; i < obj.children.length; i++) {
+ const c = this.normalizeAccTreeObj(obj.children[i]);
+ text += this.stringifyTree(c);
+ if (i < obj.children.length - 1) {
+ text += ", ";
+ }
+ }
+ }
+
+ return `${text}] `;
+ },
+
+ /**
+ * Return pretty name for identifier, it may be ID, DOM node or accessible.
+ */
+ prettyName(identifier) {
+ if (identifier instanceof Array) {
+ let msg = "";
+ for (let idx = 0; idx < identifier.length; idx++) {
+ if (msg != "") {
+ msg += ", ";
+ }
+
+ msg += this.prettyName(identifier[idx]);
+ }
+ return msg;
+ }
+
+ if (identifier instanceof Ci.nsIAccessible) {
+ const acc = this.getAccessible(identifier);
+ const domID = this.getAccessibleDOMNodeID(acc);
+ let msg = "[";
+ try {
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ if (domID) {
+ msg += `DOM node id: ${domID}, `;
+ }
+ } else {
+ msg += `${this.getNodePrettyName(acc.DOMNode)}, `;
+ }
+ msg += `role: ${this.roleToString(acc.role)}`;
+ if (acc.name) {
+ msg += `, name: "${this.shortenString(acc.name)}"`;
+ }
+ } catch (e) {
+ msg += "defunct";
+ }
+
+ if (acc) {
+ msg += `, address: ${this.getObjAddress(acc)}`;
+ }
+ msg += "]";
+
+ return msg;
+ }
+
+ if (Node.isInstance(identifier)) {
+ return `[ ${this.getNodePrettyName(identifier)} ]`;
+ }
+
+ if (identifier && typeof identifier === "object") {
+ const treeObj = this.normalizeAccTreeObj(identifier);
+ if ("role" in treeObj) {
+ return `{ ${this.stringifyTree(treeObj)} }`;
+ }
+
+ return JSON.stringify(identifier);
+ }
+
+ return ` "${identifier}" `;
+ },
+
+ /**
+ * Return accessible for the given identifier (may be ID attribute or DOM
+ * element or accessible object) or null.
+ *
+ * @param accOrElmOrID
+ * identifier to get an accessible implementing the given interfaces
+ * @param aInterfaces
+ * [optional] the interface or an array interfaces to query it/them
+ * from obtained accessible
+ * @param elmObj
+ * [optional] object to store DOM element which accessible is obtained
+ * for
+ * @param doNotFailIf
+ * [optional] no error for special cases (see DONOTFAIL_IF_NO_ACC,
+ * DONOTFAIL_IF_NO_INTERFACE)
+ * @param doc
+ * [optional] document for when accOrElmOrID is an ID.
+ */
+ getAccessible(accOrElmOrID, interfaces, elmObj, doNotFailIf, doc) {
+ if (!accOrElmOrID) {
+ return null;
+ }
+
+ let elm = null;
+ if (accOrElmOrID instanceof Ci.nsIAccessible) {
+ try {
+ elm = accOrElmOrID.DOMNode;
+ } catch (e) {}
+ } else if (Node.isInstance(accOrElmOrID)) {
+ elm = accOrElmOrID;
+ } else {
+ elm = doc.getElementById(accOrElmOrID);
+ if (!elm) {
+ Assert.ok(false, `Can't get DOM element for ${accOrElmOrID}`);
+ return null;
+ }
+ }
+
+ if (elmObj && typeof elmObj == "object") {
+ elmObj.value = elm;
+ }
+
+ let acc = accOrElmOrID instanceof Ci.nsIAccessible ? accOrElmOrID : null;
+ if (!acc) {
+ try {
+ acc = this.accService.getAccessibleFor(elm);
+ } catch (e) {}
+
+ if (!acc) {
+ if (!(doNotFailIf & this.DONOTFAIL_IF_NO_ACC)) {
+ Assert.ok(
+ false,
+ `Can't get accessible for ${this.prettyName(accOrElmOrID)}`
+ );
+ }
+
+ return null;
+ }
+ }
+
+ if (!interfaces) {
+ return acc;
+ }
+
+ if (!(interfaces instanceof Array)) {
+ interfaces = [interfaces];
+ }
+
+ for (let index = 0; index < interfaces.length; index++) {
+ if (acc instanceof interfaces[index]) {
+ continue;
+ }
+
+ try {
+ acc.QueryInterface(interfaces[index]);
+ } catch (e) {
+ if (!(doNotFailIf & this.DONOTFAIL_IF_NO_INTERFACE)) {
+ Assert.ok(
+ false,
+ `Can't query ${interfaces[index]} for ${accOrElmOrID}`
+ );
+ }
+
+ return null;
+ }
+ }
+
+ return acc;
+ },
+
+ /**
+ * Return the DOM node by identifier (may be accessible, DOM node or ID).
+ */
+ getNode(accOrNodeOrID, doc) {
+ if (!accOrNodeOrID) {
+ return null;
+ }
+
+ if (Node.isInstance(accOrNodeOrID)) {
+ return accOrNodeOrID;
+ }
+
+ if (accOrNodeOrID instanceof Ci.nsIAccessible) {
+ return accOrNodeOrID.DOMNode;
+ }
+
+ const node = doc.getElementById(accOrNodeOrID);
+ if (!node) {
+ Assert.ok(false, `Can't get DOM element for ${accOrNodeOrID}`);
+ return null;
+ }
+
+ return node;
+ },
+
+ /**
+ * Return root accessible.
+ *
+ * @param {DOMNode} doc
+ * Chrome document.
+ *
+ * @return {nsIAccessible}
+ * Accessible object for chrome window.
+ */
+ getRootAccessible(doc) {
+ const acc = this.getAccessible(doc);
+ return acc ? acc.rootDocument.QueryInterface(Ci.nsIAccessible) : null;
+ },
+
+ /**
+ * Analogy of SimpleTest.is function used to compare objects.
+ */
+ isObject(obj, expectedObj, msg) {
+ if (obj == expectedObj) {
+ Assert.ok(true, msg);
+ return;
+ }
+
+ Assert.ok(
+ false,
+ `${msg} - got "${this.prettyName(obj)}", expected "${this.prettyName(
+ expectedObj
+ )}"`
+ );
+ },
+};