Files
tubestation/accessible/tests/browser/Common.sys.mjs
James Teh dc02759a3b Bug 1926541 part 2: Change the getAccessibleDOMNodeID test harness functions to simply use the id property. r=morgan
This keeps id retrieval code in one place: the accessibility engine.
It thus fixes some edge cases that were previously broken; e.g. SVG documents.
It would have been nice to remove these functions altogether and have everything use the id property directly.
Unfortunately, some code depends on getAccessibleDOMNodeID returning null when an accessible has died, rather than throwing an exception.
Changing all the impacted callers to catch exceptions is a bit ugly, but having the XPCOM function just return null itself in this case seemed like hiding reality.
This way, callers have a choice: they can use the id property directly if they care about the failure case (or if the failure case isn't relevant for their use case), or they can call getAccessibleDOMNodeID if they explicitly want to ignore the failure.

It turns out that some tests were unwittingly relying on the nsIAccessibleDocument interface having been queried on the document accessible by getAccessibleDOMNodeID when waiting for the doc load complete event.
To fix this, the harness now explicitly queries for nsIAccessibleDocument before passing the document to test tasks.

Differential Revision: https://phabricator.services.mozilla.com/D244708
2025-04-10 00:41:26 +00:00

434 lines
11 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/. */
import { Assert } from "resource://testing-common/Assert.sys.mjs";
const MAX_TRIM_LENGTH = 100;
export 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;
},
/**
* Obtain DOMNode id from an accessible. This simply queries the .id property
* on the accessible, but it catches exceptions which might occur if the
* accessible has died.
* @param {nsIAccessible} accessible accessible
* @return {String?} DOMNode id if available
*/
getAccessibleDOMNodeID(accessible) {
try {
return accessible.id;
} catch (e) {
// This will fail if the accessible has died.
}
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
)}"`
);
},
};