зеркало из https://github.com/mozilla/gecko-dev.git
457 строки
12 KiB
JavaScript
457 строки
12 KiB
JavaScript
/* 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
|
|
)}"`
|
|
);
|
|
},
|
|
};
|