// 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) { 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; }