diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /accessible/tests/mochitest/common.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | accessible/tests/mochitest/common.js | 1048 |
1 files changed, 1048 insertions, 0 deletions
diff --git a/accessible/tests/mochitest/common.js b/accessible/tests/mochitest/common.js new file mode 100644 index 0000000000..f0ee117452 --- /dev/null +++ b/accessible/tests/mochitest/common.js @@ -0,0 +1,1048 @@ +// This file has circular dependencies that may require other files. Rather +// than use import-globals-from, we list the globals individually here to save +// confusing ESLint. +// actions.js +/* globals testActionNames */ +// attributes.js +/* globals testAttrs, testAbsentAttrs, testTextAttrs */ +// relations.js +/* globals testRelation */ +// role.js +/* globals isRole */ +// state.js +/* globals testStates */ + +// ////////////////////////////////////////////////////////////////////////////// +// Interfaces + +const nsIAccessibilityService = Ci.nsIAccessibilityService; + +const nsIAccessibleEvent = Ci.nsIAccessibleEvent; +const nsIAccessibleStateChangeEvent = Ci.nsIAccessibleStateChangeEvent; +const nsIAccessibleCaretMoveEvent = Ci.nsIAccessibleCaretMoveEvent; +const nsIAccessibleScrollingEvent = Ci.nsIAccessibleScrollingEvent; +const nsIAccessibleTextChangeEvent = Ci.nsIAccessibleTextChangeEvent; +const nsIAccessibleTextSelectionChangeEvent = + Ci.nsIAccessibleTextSelectionChangeEvent; +const nsIAccessibleObjectAttributeChangedEvent = + Ci.nsIAccessibleObjectAttributeChangedEvent; +const nsIAccessibleAnnouncementEvent = Ci.nsIAccessibleAnnouncementEvent; + +const nsIAccessibleStates = Ci.nsIAccessibleStates; +const nsIAccessibleRole = Ci.nsIAccessibleRole; +const nsIAccessibleScrollType = Ci.nsIAccessibleScrollType; +const nsIAccessibleCoordinateType = Ci.nsIAccessibleCoordinateType; + +const nsIAccessibleRelation = Ci.nsIAccessibleRelation; +const nsIAccessibleTextRange = Ci.nsIAccessibleTextRange; + +const nsIAccessible = Ci.nsIAccessible; + +const nsIAccessibleDocument = Ci.nsIAccessibleDocument; +const nsIAccessibleApplication = Ci.nsIAccessibleApplication; + +const nsIAccessibleText = Ci.nsIAccessibleText; +const nsIAccessibleEditableText = Ci.nsIAccessibleEditableText; + +const nsIAccessibleHyperLink = Ci.nsIAccessibleHyperLink; +const nsIAccessibleHyperText = Ci.nsIAccessibleHyperText; + +const nsIAccessibleImage = Ci.nsIAccessibleImage; +const nsIAccessiblePivot = Ci.nsIAccessiblePivot; +const nsIAccessibleSelectable = Ci.nsIAccessibleSelectable; +const nsIAccessibleTable = Ci.nsIAccessibleTable; +const nsIAccessibleTableCell = Ci.nsIAccessibleTableCell; +const nsIAccessibleTraversalRule = Ci.nsIAccessibleTraversalRule; +const nsIAccessibleValue = Ci.nsIAccessibleValue; + +const nsIObserverService = Ci.nsIObserverService; + +const nsIDOMWindow = Ci.nsIDOMWindow; + +const nsIPropertyElement = Ci.nsIPropertyElement; + +// ////////////////////////////////////////////////////////////////////////////// +// OS detect + +const MAC = navigator.platform.includes("Mac"); +const LINUX = navigator.platform.includes("Linux"); +const SOLARIS = navigator.platform.includes("SunOS"); +const WIN = navigator.platform.includes("Win"); + +// ////////////////////////////////////////////////////////////////////////////// +// Application detect +// Firefox is assumed by default. + +const SEAMONKEY = navigator.userAgent.match(/ SeaMonkey\//); + +// ////////////////////////////////////////////////////////////////////////////// +// Accessible general + +const STATE_BUSY = nsIAccessibleStates.STATE_BUSY; + +const SCROLL_TYPE_TOP_EDGE = nsIAccessibleScrollType.SCROLL_TYPE_TOP_EDGE; +const SCROLL_TYPE_ANYWHERE = nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE; + +const COORDTYPE_SCREEN_RELATIVE = + nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE; +const COORDTYPE_WINDOW_RELATIVE = + nsIAccessibleCoordinateType.COORDTYPE_WINDOW_RELATIVE; +const COORDTYPE_PARENT_RELATIVE = + nsIAccessibleCoordinateType.COORDTYPE_PARENT_RELATIVE; + +const kEmbedChar = String.fromCharCode(0xfffc); + +const kDiscBulletChar = String.fromCharCode(0x2022); +const kDiscBulletText = kDiscBulletChar + " "; +const kCircleBulletText = String.fromCharCode(0x25e6) + " "; +const kSquareBulletText = String.fromCharCode(0x25aa) + " "; + +const MAX_TRIM_LENGTH = 100; + +/** + * Services to determine if e10s is enabled. + */ + +/** + * nsIAccessibilityService service. + */ +var gAccService = Cc["@mozilla.org/accessibilityService;1"].getService( + nsIAccessibilityService +); + +/** + * Enable/disable logging. + */ +function enableLogging(aModules) { + gAccService.setLogging(aModules); +} +function disableLogging() { + gAccService.setLogging(""); +} +function isLogged(aModule) { + return gAccService.isLogged(aModule); +} + +/** + * Dumps the accessible tree into console. + */ +function dumpTree(aId, aMsg) { + function dumpTreeIntl(acc, indent) { + dump(indent + prettyName(acc) + "\n"); + + var children = acc.children; + for (var i = 0; i < children.length; i++) { + var child = children.queryElementAt(i, nsIAccessible); + dumpTreeIntl(child, indent + " "); + } + } + + function dumpDOMTreeIntl(node, indent) { + dump(indent + prettyName(node) + "\n"); + + var children = node.childNodes; + for (var i = 0; i < children.length; i++) { + var child = children.item(i); + dumpDOMTreeIntl(child, indent + " "); + } + } + + dump(aMsg + "\n"); + var root = getAccessible(aId); + dumpTreeIntl(root, " "); + + dump("DOM tree:\n"); + dumpDOMTreeIntl(getNode(aId), " "); +} + +/** + * Invokes the given function when document is loaded and focused. Preferable + * to mochitests 'addLoadEvent' function -- additionally ensures state of the + * document accessible is not busy. + * + * @param aFunc the function to invoke + */ +function addA11yLoadEvent(aFunc, aWindow) { + function waitForDocLoad() { + window.setTimeout(function () { + var targetDocument = aWindow ? aWindow.document : document; + var accDoc = getAccessible(targetDocument); + var state = {}; + accDoc.getState(state, {}); + if (state.value & STATE_BUSY) { + waitForDocLoad(); + return; + } + + window.setTimeout(aFunc, 0); + }, 0); + } + + if ( + aWindow && + aWindow.document.activeElement && + aWindow.document.activeElement.localName == "browser" + ) { + waitForDocLoad(); + } else { + SimpleTest.waitForFocus(waitForDocLoad, aWindow); + } +} + +/** + * Analogy of SimpleTest.is function used to compare objects. + */ +function isObject(aObj, aExpectedObj, aMsg) { + if (aObj == aExpectedObj) { + ok(true, aMsg); + return; + } + + ok( + false, + aMsg + + " - got '" + + prettyName(aObj) + + "', expected '" + + prettyName(aExpectedObj) + + "'" + ); +} + +/** + * is() function checking the expected value is within the range. + */ +function isWithin(aExpected, aGot, aWithin, aMsg) { + if (Math.abs(aGot - aExpected) <= aWithin) { + ok(true, `${aMsg} - Got ${aGot}`); + } else { + ok( + false, + `${aMsg} - Got ${aGot}, expected ${aExpected} with error of ${aWithin}` + ); + } +} + +// ////////////////////////////////////////////////////////////////////////////// +// Helpers for getting DOM node/accessible + +/** + * Return the DOM node by identifier (may be accessible, DOM node or ID). + */ +function getNode(aAccOrNodeOrID, aDocument) { + if (!aAccOrNodeOrID) { + return null; + } + + if (Node.isInstance(aAccOrNodeOrID)) { + return aAccOrNodeOrID; + } + + if (aAccOrNodeOrID instanceof nsIAccessible) { + return aAccOrNodeOrID.DOMNode; + } + + var node = (aDocument || document).getElementById(aAccOrNodeOrID); + if (!node) { + ok(false, "Can't get DOM element for " + aAccOrNodeOrID); + return null; + } + + return node; +} + +/** + * Constants indicates getAccessible doesn't fail if there is no accessible. + */ +const DONOTFAIL_IF_NO_ACC = 1; + +/** + * Constants indicates getAccessible won't fail if accessible doesn't implement + * the requested interfaces. + */ +const DONOTFAIL_IF_NO_INTERFACE = 2; + +/** + * Return accessible for the given identifier (may be ID attribute or DOM + * element or accessible object) or null. + * + * @param aAccOrElmOrID [in] identifier to get an accessible implementing + * the given interfaces + * @param aInterfaces [in, optional] the interface or an array interfaces + * to query it/them from obtained accessible + * @param aElmObj [out, optional] object to store DOM element which + * accessible is obtained for + * @param aDoNotFailIf [in, optional] no error for special cases (see + * constants above) + */ +function getAccessible(aAccOrElmOrID, aInterfaces, aElmObj, aDoNotFailIf) { + if (!aAccOrElmOrID) { + return null; + } + + var elm = null; + + if (aAccOrElmOrID instanceof nsIAccessible) { + try { + elm = aAccOrElmOrID.DOMNode; + } catch (e) {} + } else if (Node.isInstance(aAccOrElmOrID)) { + elm = aAccOrElmOrID; + } else { + elm = document.getElementById(aAccOrElmOrID); + if (!elm) { + ok(false, "Can't get DOM element for " + aAccOrElmOrID); + return null; + } + } + + if (aElmObj && typeof aElmObj == "object") { + aElmObj.value = elm; + } + + var acc = aAccOrElmOrID instanceof nsIAccessible ? aAccOrElmOrID : null; + if (!acc) { + try { + acc = gAccService.getAccessibleFor(elm); + } catch (e) {} + + if (!acc) { + if (!(aDoNotFailIf & DONOTFAIL_IF_NO_ACC)) { + ok(false, "Can't get accessible for " + prettyName(aAccOrElmOrID)); + } + + return null; + } + } + + if (!aInterfaces) { + return acc; + } + + if (!(aInterfaces instanceof Array)) { + aInterfaces = [aInterfaces]; + } + + for (var index = 0; index < aInterfaces.length; index++) { + if (acc instanceof aInterfaces[index]) { + continue; + } + try { + acc.QueryInterface(aInterfaces[index]); + } catch (e) { + if (!(aDoNotFailIf & DONOTFAIL_IF_NO_INTERFACE)) { + ok( + false, + "Can't query " + aInterfaces[index] + " for " + aAccOrElmOrID + ); + } + + return null; + } + } + + return acc; +} + +/** + * Return true if the given identifier has an accessible, or exposes the wanted + * interfaces. + */ +function isAccessible(aAccOrElmOrID, aInterfaces) { + return !!getAccessible( + aAccOrElmOrID, + aInterfaces, + null, + DONOTFAIL_IF_NO_ACC | DONOTFAIL_IF_NO_INTERFACE + ); +} + +/** + * Return an accessible that contains the DOM node for the given identifier. + */ +function getContainerAccessible(aAccOrElmOrID) { + var node = getNode(aAccOrElmOrID); + if (!node) { + return null; + } + + // eslint-disable-next-line no-empty + while ((node = node.parentNode) && !isAccessible(node)) {} + return node ? getAccessible(node) : null; +} + +/** + * Return root accessible for the given identifier. + */ +function getRootAccessible(aAccOrElmOrID) { + var acc = getAccessible(aAccOrElmOrID ? aAccOrElmOrID : document); + return acc ? acc.rootDocument.QueryInterface(nsIAccessible) : null; +} + +/** + * Return tab document accessible the given accessible is contained by. + */ +function getTabDocAccessible(aAccOrElmOrID) { + var acc = getAccessible(aAccOrElmOrID ? aAccOrElmOrID : document); + + var docAcc = acc.document.QueryInterface(nsIAccessible); + var containerDocAcc = docAcc.parent.document; + + // Test is running is stand-alone mode. + if (acc.rootDocument == containerDocAcc) { + return docAcc; + } + + // In the case of running all tests together. + return containerDocAcc.QueryInterface(nsIAccessible); +} + +/** + * Return application accessible. + */ +function getApplicationAccessible() { + return gAccService + .getApplicationAccessible() + .QueryInterface(nsIAccessibleApplication); +} + +/** + * A version of accessible tree testing, doesn't fail if tree is not complete. + */ +function testElm(aID, aTreeObj) { + testAccessibleTree(aID, aTreeObj, kSkipTreeFullCheck); +} + +/** + * Flags used for testAccessibleTree + */ +const kSkipTreeFullCheck = 1; + +/** + * Compare expected and actual accessibles trees. + * + * @param aAccOrElmOrID [in] accessible identifier + * @param aAccTree [in] JS object, each field corresponds to property of + * accessible object. Additionally special properties + * are presented: + * children - an array of JS objects representing + * children of accessible + * states - an object having states and extraStates + * fields + * @param aFlags [in, optional] flags, see constants above + */ +// eslint-disable-next-line complexity +function testAccessibleTree(aAccOrElmOrID, aAccTree, aFlags) { + var acc = getAccessible(aAccOrElmOrID); + if (!acc) { + return; + } + + var accTree = aAccTree; + + // Support of simplified accessible tree object. + accTree = normalizeAccTreeObj(accTree); + + // Test accessible properties. + for (var prop in accTree) { + var msg = + "Wrong value of property '" + prop + "' for " + prettyName(acc) + "."; + + switch (prop) { + case "actions": { + testActionNames(acc, accTree.actions); + break; + } + + case "attributes": + testAttrs(acc, accTree[prop], true); + break; + + case "absentAttributes": + testAbsentAttrs(acc, accTree[prop]); + break; + + case "interfaces": { + var ifaces = + accTree[prop] instanceof Array ? accTree[prop] : [accTree[prop]]; + for (let i = 0; i < ifaces.length; i++) { + ok( + acc instanceof ifaces[i], + "No " + ifaces[i] + " interface on " + prettyName(acc) + ); + } + break; + } + + case "relations": { + for (var rel in accTree[prop]) { + testRelation(acc, window[rel], accTree[prop][rel]); + } + break; + } + + case "role": + isRole(acc, accTree[prop], msg); + break; + + case "states": + case "extraStates": + case "absentStates": + case "absentExtraStates": { + testStates( + acc, + accTree.states, + accTree.extraStates, + accTree.absentStates, + accTree.absentExtraStates + ); + break; + } + + case "tagName": + is(accTree[prop], acc.DOMNode.tagName, msg); + break; + + case "textAttrs": { + var prevOffset = -1; + for (var offset in accTree[prop]) { + if (prevOffset != -1) { + let attrs = accTree[prop][prevOffset]; + testTextAttrs( + acc, + prevOffset, + attrs, + {}, + prevOffset, + +offset, + true + ); + } + prevOffset = +offset; + } + + if (prevOffset != -1) { + var charCount = getAccessible(acc, [ + nsIAccessibleText, + ]).characterCount; + let attrs = accTree[prop][prevOffset]; + testTextAttrs( + acc, + prevOffset, + attrs, + {}, + prevOffset, + charCount, + true + ); + } + + break; + } + + default: + if (prop.indexOf("todo_") == 0) { + todo(false, msg); + } else if (prop != "children") { + is(acc[prop], accTree[prop], msg); + } + } + } + + // Test children. + if ("children" in accTree && accTree.children instanceof Array) { + var children = acc.children; + var childCount = children.length; + + if (accTree.children.length != childCount) { + for (let i = 0; i < Math.max(accTree.children.length, childCount); i++) { + var accChild = null, + testChild = null; + try { + testChild = accTree.children[i]; + accChild = children.queryElementAt(i, nsIAccessible); + + if (!testChild) { + ok( + false, + prettyName(acc) + + " has an extra child at index " + + i + + " : " + + prettyName(accChild) + ); + continue; + } + + testChild = normalizeAccTreeObj(testChild); + if (accChild.role !== testChild.role) { + ok( + false, + prettyName(accTree) + + " and " + + prettyName(acc) + + " have different children at index " + + i + + " : " + + prettyName(testChild) + + ", " + + prettyName(accChild) + ); + } + info( + "Matching " + + prettyName(accTree) + + " and " + + prettyName(acc) + + " child at index " + + i + + " : " + + prettyName(accChild) + ); + } catch (e) { + ok( + false, + prettyName(accTree) + + " is expected to have a child at index " + + i + + " : " + + prettyName(testChild) + + ", original tested: " + + prettyName(aAccOrElmOrID) + + ", " + + e + ); + } + } + } else { + if (aFlags & kSkipTreeFullCheck) { + for (let i = 0; i < childCount; i++) { + let child = children.queryElementAt(i, nsIAccessible); + testAccessibleTree(child, accTree.children[i], aFlags); + } + return; + } + + // nsIAccessible::firstChild + var expectedFirstChild = + childCount > 0 ? children.queryElementAt(0, nsIAccessible) : null; + var firstChild = null; + try { + firstChild = acc.firstChild; + } catch (e) {} + is( + firstChild, + expectedFirstChild, + "Wrong first child of " + prettyName(acc) + ); + + // nsIAccessible::lastChild + var expectedLastChild = + childCount > 0 + ? children.queryElementAt(childCount - 1, nsIAccessible) + : null; + var lastChild = null; + try { + lastChild = acc.lastChild; + } catch (e) {} + is( + lastChild, + expectedLastChild, + "Wrong last child of " + prettyName(acc) + ); + + for (var i = 0; i < childCount; i++) { + let child = children.queryElementAt(i, nsIAccessible); + + // nsIAccessible::parent + var parent = null; + try { + parent = child.parent; + } catch (e) {} + is(parent, acc, "Wrong parent of " + prettyName(child)); + + // nsIAccessible::indexInParent + var indexInParent = -1; + try { + indexInParent = child.indexInParent; + } catch (e) {} + is(indexInParent, i, "Wrong index in parent of " + prettyName(child)); + + // nsIAccessible::nextSibling + var expectedNextSibling = + i < childCount - 1 + ? children.queryElementAt(i + 1, nsIAccessible) + : null; + var nextSibling = null; + try { + nextSibling = child.nextSibling; + } catch (e) {} + is( + nextSibling, + expectedNextSibling, + "Wrong next sibling of " + prettyName(child) + ); + + // nsIAccessible::previousSibling + var expectedPrevSibling = + i > 0 ? children.queryElementAt(i - 1, nsIAccessible) : null; + var prevSibling = null; + try { + prevSibling = child.previousSibling; + } catch (e) {} + is( + prevSibling, + expectedPrevSibling, + "Wrong previous sibling of " + prettyName(child) + ); + + // Go down through subtree + testAccessibleTree(child, accTree.children[i], aFlags); + } + } + } +} + +/** + * Return true if accessible for the given node is in cache. + */ +function isAccessibleInCache(aNodeOrId) { + var node = getNode(aNodeOrId); + return !!gAccService.getAccessibleFromCache(node); +} + +/** + * Test accessible tree for defunct accessible. + * + * @param aAcc [in] the defunct accessible + * @param aNodeOrId [in] the DOM node identifier for the defunct accessible + */ +function testDefunctAccessible(aAcc, aNodeOrId) { + if (aNodeOrId) { + ok( + !isAccessible(aNodeOrId), + "Accessible for " + aNodeOrId + " wasn't properly shut down!" + ); + } + + var msg = + " doesn't fail for shut down accessible " + prettyName(aNodeOrId) + "!"; + + // firstChild + var success = false; + try { + aAcc.firstChild; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "firstChild" + msg); + + // lastChild + success = false; + try { + aAcc.lastChild; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "lastChild" + msg); + + // childCount + success = false; + try { + aAcc.childCount; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "childCount" + msg); + + // children + success = false; + try { + aAcc.children; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "children" + msg); + + // nextSibling + success = false; + try { + aAcc.nextSibling; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "nextSibling" + msg); + + // previousSibling + success = false; + try { + aAcc.previousSibling; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "previousSibling" + msg); + + // parent + success = false; + try { + aAcc.parent; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "parent" + msg); +} + +/** + * Convert role to human readable string. + */ +function roleToString(aRole) { + return gAccService.getStringRole(aRole); +} + +/** + * Convert states to human readable string. + */ +function statesToString(aStates, aExtraStates) { + var list = gAccService.getStringStates(aStates, aExtraStates); + + var str = ""; + for (var index = 0; index < list.length - 1; index++) { + str += list.item(index) + ", "; + } + + if (list.length) { + str += list.item(index); + } + + return str; +} + +/** + * Convert event type to human readable string. + */ +function eventTypeToString(aEventType) { + return gAccService.getStringEventType(aEventType); +} + +/** + * Convert relation type to human readable string. + */ +function relationTypeToString(aRelationType) { + return gAccService.getStringRelationType(aRelationType); +} + +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +/** + * Return text from clipboard. + */ +function getTextFromClipboard() { + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(getLoadContext()); + if (!trans) { + return ""; + } + + trans.addDataFlavor("text/plain"); + Services.clipboard.getData( + trans, + Services.clipboard.kGlobalClipboard, + SpecialPowers.wrap(window).browsingContext.currentWindowContext + ); + + var str = {}; + trans.getTransferData("text/plain", str); + + if (str) { + str = str.value.QueryInterface(Ci.nsISupportsString); + } + if (str) { + return str.data; + } + + return ""; +} + +/** + * Extract DOMNode id from an accessible. If e10s is enabled, DOMNode is not + * present in parent process but, if available, DOMNode id is attached to an + * accessible object. + * @param {nsIAccessible} accessible accessible + * @return {String?} DOMNode id if available + */ +function getAccessibleDOMNodeID(accessible) { + if (accessible instanceof 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; +} + +/** + * Return pretty name for identifier, it may be ID, DOM node or accessible. + */ +function prettyName(aIdentifier) { + if (aIdentifier instanceof Array) { + let msg = ""; + for (var idx = 0; idx < aIdentifier.length; idx++) { + if (msg != "") { + msg += ", "; + } + + msg += prettyName(aIdentifier[idx]); + } + return msg; + } + + if (aIdentifier instanceof nsIAccessible) { + var acc = getAccessible(aIdentifier); + var domID = getAccessibleDOMNodeID(acc); + let msg = "["; + try { + if (Services.appinfo.browserTabsRemoteAutostart) { + if (domID) { + msg += `DOM node id: ${domID}, `; + } + } else { + msg += `${getNodePrettyName(acc.DOMNode)}, `; + } + msg += "role: " + roleToString(acc.role); + if (acc.name) { + msg += ", name: '" + shortenString(acc.name) + "'"; + } + } catch (e) { + msg += "defunct"; + } + + if (acc) { + msg += ", address: " + getObjAddress(acc); + } + msg += "]"; + + return msg; + } + + if (Node.isInstance(aIdentifier)) { + return "[ " + getNodePrettyName(aIdentifier) + " ]"; + } + + if (aIdentifier && typeof aIdentifier === "object") { + var treeObj = normalizeAccTreeObj(aIdentifier); + if ("role" in treeObj) { + function stringifyTree(aObj) { + var text = roleToString(aObj.role) + ": [ "; + if ("children" in aObj) { + for (var i = 0; i < aObj.children.length; i++) { + var c = normalizeAccTreeObj(aObj.children[i]); + text += stringifyTree(c); + if (i < aObj.children.length - 1) { + text += ", "; + } + } + } + return text + "] "; + } + return `{ ${stringifyTree(treeObj)} }`; + } + return JSON.stringify(aIdentifier); + } + + return " '" + aIdentifier + "' "; +} + +/** + * Shorten a long string if it exceeds MAX_TRIM_LENGTH. + * @param aString the string to shorten. + * @returns the shortened string. + */ +function shortenString(aString, aMaxLength) { + if (aString.length <= MAX_TRIM_LENGTH) { + return aString; + } + + // Trim the string if its length is > MAX_TRIM_LENGTH characters. + var trimOffset = MAX_TRIM_LENGTH / 2; + return ( + aString.substring(0, trimOffset - 1) + + "..." + + aString.substring(aString.length - trimOffset, aString.length) + ); +} + +// ////////////////////////////////////////////////////////////////////////////// +// General Utils +// ////////////////////////////////////////////////////////////////////////////// +/** + * Return main chrome window (crosses chrome boundary) + */ +function getMainChromeWindow(aWindow) { + return aWindow.browsingContext.topChromeWindow; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private +// ////////////////////////////////////////////////////////////////////////////// + +// ////////////////////////////////////////////////////////////////////////////// +// Accessible general + +function getNodePrettyName(aNode) { + try { + var tag = ""; + if (aNode.nodeType == Node.DOCUMENT_NODE) { + tag = "document"; + } else { + tag = aNode.localName; + if (aNode.nodeType == Node.ELEMENT_NODE && aNode.hasAttribute("id")) { + tag += '@id="' + aNode.getAttribute("id") + '"'; + } + } + + return "'" + tag + " node', address: " + getObjAddress(aNode); + } catch (e) { + return "' no node info '"; + } +} + +function getObjAddress(aObj) { + var exp = /native\s*@\s*(0x[a-f0-9]+)/g; + var match = exp.exec(aObj.toString()); + if (match) { + return match[1]; + } + + return aObj.toString(); +} + +function normalizeAccTreeObj(aObj) { + var key = Object.keys(aObj)[0]; + var roleName = "ROLE_" + key; + if (roleName in nsIAccessibleRole) { + return { + role: nsIAccessibleRole[roleName], + children: aObj[key], + }; + } + return aObj; +} |