Files
sousa-gecko/accessible/tests/browser/Common.sys.mjs
T
James Teh e09ce72ec6 Bug 2021820 part 11: Several changes to facilitate automated testing of the ePdfOutput-only mode. r=eeejay
For these tests, we explicitly want to be able to test with the accessibility engine disabled.
That means we don't want to eagerly create the accessibility service, nor can we use addAccessibleTask.
However, we still want to be able to use some of the common functionality in our test harness.

1. Provide a way to disable eager instantiation of the accessibility service by common.js.
2. Export the id we assign to the body for top level document snippets so we can test for it outside of shared-head.js.
3. Make it possible to use snippetToURL outside of addAccessibleTask by using arguments instead of global state.
4. Make it possible to use CommonUtils.addAccServiceInitializedObserver and .addAccServiceShutdownObserver simultaneously for cases where the test can't precisely control when the service is shut down.
5. Don't prevent the accessibility service from shutting down due to XPCOM observers when in ePdfOutput-only mode.

Differential Revision: https://phabricator.services.mozilla.com/D299963
2026-05-21 01:59:38 +00:00

438 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 other
* than "0" 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 !== "0") {
Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
deferred.resolve();
} else if (data === "0") {
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 if (!this._accServiceInitialized) {
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 or was constructed from a pseudoelement
* like ::details-content.
*
* @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, or if
// the accessible was constructed from a pseudoelement
// like ::details-content.
}
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
)}"`
);
},
};