// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Represents an AutomationEvent. See:
// extensions/renderer/resources/automation/automation_event.js
class AutomationEvent {
constructor(
type, target, eventFrom, eventFromAction, mouseX, mouseY, intents) {
this.propagationStopped_ = false;
this.type_ = type;
this.target_ = target;
this.eventPhase_ = Event.NONE;
this.eventFrom_ = eventFrom;
this.eventFromAction_ = eventFromAction;
this.mouseX_ = mouseX;
this.mouseY_ = mouseY;
this.intents_ = intents;
}
stopPropagation() {
this.propagationStopped_ = true;
}
get propagationStopped() {
return this.propagationStopped_;
}
get type() {
return this.type_;
}
get target() {
return this.target_;
}
get eventPhase() {
return this.eventPhase_;
}
set eventPhase(phase) {
this.eventPhase_ = phase;
}
get eventFrom() {
return this.eventFrom_;
}
get eventFromAction() {
return this.eventFromAction_;
}
get mouseX() {
return this.mouseX_;
}
get mouseY() {
return this.mouseY_;
}
get intents() {
return this.intents_;
}
}
// Caches a mapping from IDs to automation root nodes. See:
// extensions/renderer/resources/automation/automation_tree_cache.js
const AutomationTreeCache = {
idToAutomationRootNode: {},
};
// Shim an exceptionHandler used in extensions.
const exceptionHandler = {
handle: (msg) => atpconsole.error(msg)
}
// Access the native bindings installed on the global template.
const natives = nativeAutomationInternal;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The id of the root node.
*/
const GetRootID = natives.GetRootID;
/**
* Similar to above, but may move to ancestor roots if the current tree
* has multiple roots.
* @param {string} axTreeID The id of the accessibility tree.
* @return {{treeID: string, nodeID: number}}
*/
const GetPublicRoot = natives.GetPublicRoot;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?string} The title of the document.
*/
const GetDocTitle = natives.GetDocTitle;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?string} The url of the document.
*/
const GetDocURL = natives.GetDocURL;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?boolean} True if the document has finished loading.
*/
const GetDocLoaded = natives.GetDocLoaded;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The loading progress, from 0.0 to 1.0 (fully loaded).
*/
const GetDocLoadingProgress = natives.GetDocLoadingProgress;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {boolean} Whether the selection's anchor comes after its focus in the
* accessibility tree.
*/
const GetIsSelectionBackward = natives.GetIsSelectionBackward;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The ID of the selection anchor object.
*/
const GetAnchorObjectID = natives.GetAnchorObjectID;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The selection anchor offset.
*/
const GetAnchorOffset = natives.GetAnchorOffset;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?string} The selection anchor affinity.
*/
const GetAnchorAffinity = natives.GetAnchorAffinity;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The ID of the selection focus object.
*/
const GetFocusObjectID = natives.GetFocusObjectID;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The selection focus offset.
*/
const GetFocusOffset = natives.GetFocusOffset;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @return {?string} The selection focus affinity.
*/
const GetFocusAffinity = natives.GetFocusAffinity;
/**
* The start of the selection always comes before its end in the accessibility
* tree.
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The ID of the object at the start of the
* selection.
*/
const GetSelectionStartObjectID = natives.GetSelectionStartObjectID;
/**
* The start of the selection always comes before its end in the accessibility
* tree.
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The offset at the start of the selection.
*/
const GetSelectionStartOffset = natives.GetSelectionStartOffset;
/**
* The start of the selection always comes before its end in the accessibility
* tree.
* @param {string} axTreeID The id of the accessibility tree.
* @return {?string} The affinity at the start of the selection.
*/
const GetSelectionStartAffinity = natives.GetSelectionStartAffinity;
/**
* The end of the selection always comes after its start in the accessibility
* tree.
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The ID of the object at the end of the selection.
*/
const GetSelectionEndObjectID = natives.GetSelectionEndObjectID;
/**
* The end of the selection always comes after its start in the accessibility
* tree.
* @param {string} axTreeID The id of the accessibility tree.
* @return {?number} The offset at the end of the selection.
*/
const GetSelectionEndOffset = natives.GetSelectionEndOffset;
/**
* The end of the selection always comes after its start in the accessibility
* tree.
* @param {string} axTreeID The id of the accessibility tree.
* @return {?string} The affinity at the end of the selection.
*/
const GetSelectionEndAffinity = natives.GetSelectionEndAffinity;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?number} The id of the node's parent, or undefined if it's the
* root of its tree or if the tree or node wasn't found.
*/
const GetParentID = natives.GetParentID;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?number} The number of children of the node, or undefined if
* the tree or node wasn't found.
*/
const GetChildCount = natives.GetChildCount;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {number} childIndex An index of a child of this node.
* @return {?number} The id of the child at the given index, or undefined
* if the tree or node or child at that index wasn't found.
*/
const GetChildIDAtIndex = natives.GetChildIDAtIndex;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?number} The ids of the children of the node, or undefined
* if the tree or node wasn't found.
*/
const GetChildIds = natives.GetChildIDs;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?Object} An object mapping html attributes to values.
*/
const GetHtmlAttributes = natives.GetHtmlAttributes;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?number} The index of this node in its parent, or undefined if
* the tree or node or node parent wasn't found.
*/
const GetIndexInParent = natives.GetIndexInParent;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?Object} An object with a string key for every state flag set,
* or undefined if the tree or node or node parent wasn't found.
*/
const GetState = natives.GetState;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {string} The restriction, one of
* "disabled", "readOnly" or undefined if enabled or other object not disabled
*/
const GetRestriction = natives.GetRestriction;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {string} The checked state, as undefined, "true", "false" or "mixed".
*/
const GetChecked = natives.GetChecked;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {string} The role of the node, or undefined if the tree or
* node wasn't found.
*/
const GetRole = natives.GetRole;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?automation.Rect} The location of the node, or undefined if
* the tree or node wasn't found.
*/
const GetLocation = natives.GetLocation;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {number} startIndex The start index of the range.
* @param {number} endIndex The end index of the range.
* @param {boolean} clipped Whether the bounds are clipped to ancestors.
* @return {?automation.Rect} The bounding box of the subrange of this node,
* or the location if there are no subranges, or undefined if
* the tree or node wasn't found.
*/
const GetBoundsForRange = natives.GetBoundsForRange;
/**
* @param {number} left The left location of the text range.
* @param {number} top The top location of the text range.
* @param {number} width The width of text range.
* @param {number} height The height of the text range.
* @param {number} requestID The request id associated with the query
* for this range.
* @return {?automation.Rect} The bounding box of the subrange of this node,
* specified by arguments provided to the function.
*/
const ComputeGlobalBounds = natives.ComputeGlobalBounds;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?automation.Rect} The unclipped location of the node, or
* undefined if the tree or node wasn't found.
*/
const GetUnclippedLocation = natives.GetUnclippedLocation;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {!Array<number>} The text offset where each line starts, or an empty
* array if this node has no text content, or undefined if the tree or node
* was not found.
*/
const GetLineStartOffsets = natives.GetLineStartOffsets;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of the node.
* @return {?string} The computed name of this node.
*/
const GetName = natives.GetName;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} attr The name of a string attribute.
* @return {?string} The value of this attribute, or undefined if the tree,
* node, or attribute wasn't found.
*/
const GetStringAttribute = natives.GetStringAttribute;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} attr The name of an attribute.
* @return {?boolean} The value of this attribute, or undefined if the tree,
* node, or attribute wasn't found.
*/
const GetBoolAttribute = natives.GetBoolAttribute;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} attr The name of an attribute.
* @return {?number} The value of this attribute, or undefined if the tree,
* node, or attribute wasn't found.
*/
const GetIntAttribute = natives.GetIntAttribute;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} attr The name of an attribute.
* @return {?Array<number>} The ids of nodes who have a relationship pointing
* to |nodeID| (a reverse relationship).
*/
const GetIntAttributeReverseRelations = natives.GetIntAttributeReverseRelations;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} attr The name of an attribute.
* @return {?number} The value of this attribute, or undefined if the tree,
* node, or attribute wasn't found.
*/
const GetFloatAttribute = natives.GetFloatAttribute;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} attr The name of an attribute.
* @return {?Array<number>} The value of this attribute, or undefined
* if the tree, node, or attribute wasn't found.
*/
const GetIntListAttribute = natives.GetIntListAttribute;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} attr The name of an attribute.
* @return {?Array<number>} The ids of nodes who have a relationship pointing
* to |nodeID| (a reverse relationship).
*/
const GetIntListAttributeReverseRelations =
natives.GetIntListAttributeReverseRelations;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} attr The name of an HTML attribute.
* @return {?string} The value of this attribute, or undefined if the tree,
* node, or attribute wasn't found.
*/
const GetHtmlAttribute = natives.GetHtmlAttribute;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {automation.NameFromType} The source of the node's name.
*/
const GetNameFrom = natives.GetNameFrom;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {automation.DescriptionFromType} The node description source.
*/
const GetDescriptionFrom = natives.GetDescriptionFrom;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?string} The image annotation status, which may
* include the annotation itself if completed successfully.
*/
const GetImageAnnotation = natives.GetImageAnnotation;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {boolean}
*/
const GetBold = natives.GetBold;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {boolean}
*/
const GetItalic = natives.GetItalic;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {boolean}
*/
const GetUnderline = natives.GetUnderline;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {boolean}
*/
const GetLineThrough = natives.GetLineThrough;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {boolean}
*/
const GetIsButton = natives.GetIsButton;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {boolean}
*/
const GetIsCheckBox = natives.GetIsCheckBox;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {boolean}
*/
const GetIsComboBox = natives.GetIsComboBox;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {boolean}
*/
const GetIsImage = natives.GetIsImage;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?Array<automation.CustomAction>} List of custom actions of the
* node.
*/
const GetCustomActions = natives.GetCustomActions;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?Array<string>} List of standard actions of the node.
*/
const GetStandardActions = natives.GetStandardActions;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {automation.NameFromType} The source of the node's name.
*/
const GetDefaultActionVerb = natives.GetDefaultActionVerb;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {automation.HasPopup}
*/
const GetHasPopup = natives.GetHasPopup;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {automation.AriaCurrentState}
*/
const GetAriaCurrentState = natives.GetAriaCurrentState;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {automation.InvalidState}
*/
const GetInvalidState = natives.GetInvalidState;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} searchStr
* @param {boolean} backward
* @return {{treeId: string, nodeId: number}}
*/
const GetNextTextMatch = natives.GetNextTextMatch;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?Array<number>} A list of column header ids.
*/
const GetTableCellColumnHeaders = natives.GetTableCellColumnHeaders;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {?Array<number>} A list of row header ids.
*/
const GetTableCellRowHeaders = natives.GetTableCellRowHeaders;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {number} Column index for this cell.
*/
const GetTableCellColumnIndex = natives.GetTableCellColumnIndex;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {number} Row index for this cell.
*/
const GetTableCellRowIndex = natives.GetTableCellRowIndex;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {number} Column index for this cell.
*/
const GetTableCellAriaColumnIndex = natives.GetTableCellAriaColumnIndex;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {number} Row index for this cell.
*/
const GetTableCellAriaRowIndex = natives.GetTableCellAriaRowIndex;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {number} column count for this cell's table. 0 if not in a table.
*/
const GetTableColumnCount = natives.GetTableColumnCount;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {number} Row count for this cell's table. 0 if not in a table.
*/
const GetTableRowCount = natives.GetTableRowCount;
/**
* @param {string} axTreeId The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {string} Detected language for this node.
*/
const GetDetectedLanguage = natives.GetDetectedLanguage;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {!Array<number>}
*/
const GetWordStartOffsets = natives.GetWordStartOffsets;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {!Array<number>}
*/
const GetWordEndOffsets = natives.GetWordEndOffsets;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {!Array<number>}
*/
const GetSentenceStartOffsets = natives.GetSentenceStartOffsets;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {!Array<number>}
*/
const GetSentenceEndOffsets = natives.GetSentenceEndOffsets;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
*/
const SetAccessibilityFocus = natives.SetAccessibilityFocus;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} eventType
*/
const EventListenerAdded = natives.EventListenerAdded;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {string} eventType
*/
const EventListenerRemoved = natives.EventListenerRemoved;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {Array}
*/
const GetMarkers = natives.GetMarkers;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @param {!automation.PositionType} type
* @param {number} offset
* @param {boolean} isUpstream
* @return {!Object}
*/
const CreateAutomationPosition = natives.CreateAutomationPosition;
/**
* @param {string} axTreeID The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {string} The sort direction.
*/
const GetSortDirection = natives.GetSortDirection;
/**
* @param {string} axTreeId The id of the accessibility tree.
* @param {number} nodeID The id of a node.
* @return {string} .
*/
const GetValue = natives.GetValue;
/**
* A single node in the Automation tree.
* @param {AutomationRootNode} root The root of the tree.
* @constructor
*/
class AutomationNode {
constructor(root) {
this.rootImpl_ = root;
this.listeners_ = {__proto__: null};
/** @private {string} */
this.treeID_ = '';
/** @private {number} */
this.id_ = -1;
/** @private {boolean} */
this.isRootNode_ = false;
}
get treeID() {
return this.treeID_;
}
get id() {
return this.id_;
}
detach() {
this.rootImpl_ = null;
this.listeners_ = {__proto__: null};
}
get isRootNode() {
return this.isRootNode_;
}
get root() {
const info = GetPublicRoot(this.treeID);
if (!info) {
return null;
}
return AutomationRootNode.getNodeFromTree(info.treeId, info.nodeId) || null;
}
get parent() {
const info = GetParentID(this.treeID, this.id);
if (info) {
return AutomationRootNode.getNodeFromTree(info.treeId, info.nodeId);
}
}
get htmlAttributes() {
return GetHtmlAttributes(this.treeID, this.id) || {};
}
get state() {
return GetState(this.treeID, this.id) || {};
}
get role() {
return GetRole(this.treeID, this.id);
}
get restriction() {
return GetRestriction(this.treeID, this.id);
}
get checked() {
return GetChecked(this.treeID, this.id);
}
get caretBounds() {
const data = GetIntListAttribute(this.treeID, this.id, 'caretBounds');
if (!data) {
return;
}
if (data.length !== 4) {
throw Error('Internal encoding error for caret bounds.');
}
return {left: data[0], top: data[1], width: data[2], height: data[3]};
}
get location() {
return GetLocation(this.treeID, this.id);
}
boundsForRange(startIndex, endIndex, callback) {
this.boundsForRangeInternal_(
startIndex, endIndex, true /* clipped */, callback);
}
unclippedBoundsForRange(startIndex, endIndex, callback) {
this.boundsForRangeInternal_(
startIndex, endIndex, false /* clipped */, callback);
}
boundsForRangeInternal_(startIndex, endIndex, clipped, callback) {
const errorMessage = clipped ?
'Error with bounds for range callback' :
'Error with unclipped bounds for range callback';
if (!this.rootImpl_) {
return;
}
// Not yet initialized.
if (this.rootImpl_.treeID === undefined || this.id === undefined) {
return;
}
if (!callback) {
return;
}
if (!GetBoolAttribute(this.treeID, this.id, 'supportsTextLocation')) {
try {
callback(GetBoundsForRange(
this.treeID, this.id, startIndex, endIndex, clipped /* clipped */));
return;
} catch (e) {
console.warn(errorMessage + e);
}
return;
}
this.performAction_(
'getTextLocation', {startIndex: startIndex, endIndex: endIndex},
callback);
return;
}
get sortDirection() {
return GetSortDirection(this.treeID, this.id);
}
get value() {
return GetValue(this.treeID, this.id);
}
get unclippedLocation() {
let result = GetUnclippedLocation(this.treeID, this.id);
if (result === undefined) {
result = GetLocation(this.treeID, this.id);
}
return result;
}
get indexInParent() {
return GetIndexInParent(this.treeID, this.id);
}
get lineStartOffsets() {
return GetLineStartOffsets(this.treeID, this.id);
}
get childTree() {
const childTreeID = GetStringAttribute(this.treeID, this.id, 'childTreeId');
if (childTreeID) {
return AutomationRootNode.get(childTreeID);
}
}
get firstChild() {
if (GetChildCount(this.treeID, this.id) == 0) {
return undefined;
}
const info = GetChildIDAtIndex(this.treeID, this.id, 0);
if (info) {
const child =
AutomationRootNode.getNodeFromTree(info.treeId, info.nodeId);
// A child with an app id should always be in a different tree.
if (child.appId && this.treeID === info.treeId) {
return;
}
return child;
}
}
get lastChild() {
const count = GetChildCount(this.treeID, this.id);
if (count == 0) {
return;
}
const info = GetChildIDAtIndex(this.treeID, this.id, count - 1);
if (info) {
const child =
AutomationRootNode.getNodeFromTree(info.treeId, info.nodeId);
// A child with an app id should always be in a different tree.
if (child.appId && this.treeID === info.treeId) {
return;
}
return child;
}
}
get children() {
const info = GetChildIds(this.treeID, this.id);
if (!info) {
return [];
}
const children = [];
for (let i = 0; i < info.nodeIds.length; ++i) {
const childID = info.nodeIds[i];
const child = AutomationRootNode.getNodeFromTree(info.treeId, childID);
// A child with an app id should always be in a different tree.
if (child.appId && this.treeID === info.treeId) {
continue;
}
if (child) {
Array.prototype.push.call(children, child);
}
}
return children;
}
get previousSibling() {
const parent = this.parent;
if (!parent) {
return undefined;
}
const indexInParent = GetIndexInParent(this.treeID, this.id);
const info = GetChildIDAtIndex(parent.treeID, parent.id, indexInParent - 1);
if (info) {
return AutomationRootNode.getNodeFromTree(info.treeId, info.nodeId);
}
}
get nextSibling() {
const parent = this.parent;
if (!parent) {
return undefined;
}
const indexInParent = GetIndexInParent(this.treeID, this.id);
const info = GetChildIDAtIndex(parent.treeID, parent.id, indexInParent + 1);
if (info) {
return AutomationRootNode.getNodeFromTree(info.treeId, info.nodeId);
}
}
get nameFrom() {
return GetNameFrom(this.treeID, this.id);
}
get name() {
return GetName(this.treeID, this.id);
}
get descriptionFrom() {
return GetDescriptionFrom(this.treeID, this.id);
}
get imageAnnotation() {
return GetImageAnnotation(this.treeID, this.id);
}
get bold() {
return GetBold(this.treeID, this.id);
}
get italic() {
return GetItalic(this.treeID, this.id);
}
get underline() {
return GetUnderline(this.treeID, this.id);
}
get lineThrough() {
return GetLineThrough(this.treeID, this.id);
}
get isButton() {
return GetIsButton(this.treeID, this.id);
}
get isCheckBox() {
return GetIsCheckBox(this.treeID, this.id);
}
get isComboBox() {
return GetIsComboBox(this.treeID, this.id);
}
get isImage() {
return GetIsImage(this.treeID, this.id);
}
get detectedLanguage() {
return GetDetectedLanguage(this.treeID, this.id);
}
get customActions() {
return GetCustomActions(this.treeID, this.id);
}
get standardActions() {
return GetStandardActions(this.treeID, this.id);
}
get defaultActionVerb() {
return GetDefaultActionVerb(this.treeID, this.id);
}
get hasPopup() {
return GetHasPopup(this.treeID, this.id);
}
get ariaCurrentState() {
return GetAriaCurrentState(this.treeID, this.id);
}
get invalidState() {
return GetInvalidState(this.treeID, this.id);
}
get tableCellColumnHeaders() {
const ids = GetTableCellColumnHeaders(this.treeID, this.id);
if (ids && this.rootImpl_) {
const result = [];
for (let i = 0; i < ids.length; i++) {
result.push(this.rootImpl_.get(ids[i]));
}
return result;
}
}
get tableCellRowHeaders() {
const ids = GetTableCellRowHeaders(this.treeID, this.id);
if (ids && this.rootImpl_) {
const result = [];
for (let i = 0; i < ids.length; i++) {
result.push(this.rootImpl_.get(ids[i]));
}
return result;
}
}
get tableCellColumnIndex() {
return GetTableCellColumnIndex(this.treeID, this.id);
}
get tableCellRowIndex() {
return GetTableCellRowIndex(this.treeID, this.id);
}
get tableCellAriaColumnIndex() {
return GetTableCellAriaColumnIndex(this.treeID, this.id);
}
get tableCellAriaRowIndex() {
return GetTableCellAriaRowIndex(this.treeID, this.id);
}
get tableColumnCount() {
return GetTableColumnCount(this.treeID, this.id);
}
get tableRowCount() {
return GetTableRowCount(this.treeID, this.id);
}
get nonInlineTextWordStarts() {
return GetWordStartOffsets(this.treeID, this.id);
}
get nonInlineTextWordEnds() {
return GetWordEndOffsets(this.treeID, this.id);
}
get sentenceStarts() {
return GetSentenceStartOffsets(this.treeID, this.id);
}
get sentenceEnds() {
return GetSentenceEndOffsets(this.treeID, this.id);
}
get markers() {
return GetMarkers(this.treeID, this.id);
}
createPosition(type, offset, opt_isUpstream) {
const nativePosition = CreateAutomationPosition(
this.treeID, this.id, type, offset, Boolean(opt_isUpstream));
// Attach a getter for the node, which is only available in js.
Object.defineProperty(nativePosition, 'node', {
get: function() {
const tree =
AutomationTreeCache.idToAutomationRootNode[nativePosition.treeID];
if (!tree) {
return null;
}
return tree.get(nativePosition.anchorID);
},
});
return nativePosition;
}
doDefault() {
this.performAction_('doDefault');
}
focus() {
this.performAction_('focus');
}
getImageData(maxWidth, maxHeight) {
this.performAction_('getImageData',
{ maxWidth: maxWidth,
maxHeight: maxHeight });
}
hitTest(x, y, eventToFire) {
// Set an empty callback to trigger onActionResult.
const callback = () => {};
this.hitTestInternal(x, y, eventToFire, callback);
}
hitTestWithReply(x, y, opt_callback) {
this.hitTestInternal(x, y, 'hitTestResult', opt_callback);
}
hitTestInternal(x, y, eventToFire, opt_callback) {
// Convert from global to tree-relative coordinates.
const location = GetLocation(this.treeID, GetRootID(this.treeID));
this.performAction_('hitTest',
{ x: Math.floor(x - location.left),
y: Math.floor(y - location.top),
eventToFire: eventToFire },
opt_callback);
}
makeVisible() {
this.performAction_('scrollToMakeVisible');
}
performCustomAction(customActionId) {
this.performAction_('customAction', { customActionID: customActionId });
}
performStandardAction(action) {
const standardActions = GetStandardActions(this.treeID, this.id);
if (!standardActions ||
!standardActions.find(item => action == item)) {
throw Error('Inapplicable action for node: ' + action);
}
this.performAction_(action);
}
replaceSelectedText(value) {
if (this.state.editable) {
this.performAction_('replaceSelectedText', { value: value});
}
}
resumeMedia() {
this.performAction_('resumeMedia');
}
scrollBackward(opt_callback) {
this.performAction_('scrollBackward', {}, opt_callback);
}
scrollForward(opt_callback) {
this.performAction_('scrollForward', {}, opt_callback);
}
scrollUp(opt_callback) {
this.performAction_('scrollUp', {}, opt_callback);
}
scrollDown(opt_callback) {
this.performAction_('scrollDown', {}, opt_callback);
}
scrollLeft(opt_callback) {
this.performAction_('scrollLeft', {}, opt_callback);
}
scrollRight(opt_callback) {
this.performAction_('scrollRight', {}, opt_callback);
}
scrollToPoint(x, y) {
this.performAction_('scrollToPoint', {x, y});
}
scrollToPositionAtRowColumn(row, column) {
this.performAction_('scrollToPositionAtRowColumn', {row, column});
}
setScrollOffset(x, y) {
this.performAction_('setScrollOffset', {x, y});
}
setAccessibilityFocus() {
SetAccessibilityFocus(this.treeID, this.id);
}
setSelection(startIndex, endIndex) {
if (this.state.editable) {
this.performAction_('setSelection', {
focusNodeID: this.id,
anchorOffset: startIndex,
focusOffset: endIndex,
});
}
}
setSequentialFocusNavigationStartingPoint() {
this.performAction_('setSequentialFocusNavigationStartingPoint');
}
setValue(value) {
if (this.state.editable) {
this.performAction_('setValue', { value: value});
}
}
showContextMenu() {
this.performAction_('showContextMenu');
}
startDuckingMedia() {
this.performAction_('startDuckingMedia');
}
stopDuckingMedia() {
this.performAction_('stopDuckingMedia');
}
suspendMedia() {
this.performAction_('suspendMedia');
}
longClick() {
this.performAction_('longClick');
}
find(params) {
return this.findInternal_(params);
}
findAll(params) {
return this.findInternal_(params, []);
}
matches(params) {
return this.matchInternal_(params);
}
getNextTextMatch(searchStr, backward) {
const info = GetNextTextMatch(this.treeID, this.id, searchStr, backward);
if (!info) {
return;
}
const impl = AutomationRootNode.get(info.treeId);
if (impl) {
return impl.get(info.nodeId);
}
}
addEventListener(eventType, callback, capture) {
this.removeEventListener(eventType, callback);
if (!this.listeners_[eventType]) {
this.listeners_[eventType] = [];
}
// Calling EventListenerAdded will also validate the args
// and throw an exception it's not a valid event type, so no invalid event
// type/listener gets enqueued.
EventListenerAdded(this.treeID, this.id, eventType);
Array.prototype.push.call(this.listeners_[eventType], {
__proto__: null,
callback: callback,
capture: !!capture,
});
}
// TODO(dtseng/aboxhall): Check this impl against spec.
removeEventListener(eventType, callback) {
if (this.listeners_[eventType]) {
const listeners = this.listeners_[eventType];
for (let i = 0; i < listeners.length; i++) {
if (callback === listeners[i].callback) {
Array.prototype.splice.call(listeners, i, 1);
}
}
if (listeners.length == 0) {
EventListenerRemoved(this.treeID, this.id, eventType);
}
}
}
toJSON() {
return {
treeID: this.treeID,
id: this.id,
role: this.role,
attributes: this.attributes,
};
}
dispatchEvent(
eventType, eventFrom, eventFromAction, mouseX, mouseY, intents) {
const path = [];
let parent = this.parent;
while (parent) {
Array.prototype.push.call(path, parent);
parent = parent.parent;
}
const event = new AutomationEvent(
eventType, this, eventFrom, eventFromAction, mouseX, mouseY, intents);
// Dispatch the event through the propagation path in three phases:
// - capturing: starting from the root and going down to the target's parent
// - targeting: dispatching the event on the target itself
// - bubbling: starting from the target's parent, going back up to the root.
// At any stage, a listener may call stopPropagation() on the event, which
// will immediately stop event propagation through this path.
if (this.dispatchEventAtCapturing_(event, path)) {
if (this.dispatchEventAtTargeting_(event, path)) {
this.dispatchEventAtBubbling_(event, path);
}
}
}
toString() {
return this.toStringHelper_();
}
toStringHelper_() {
let parentID = GetParentID(this.treeID, this.id);
parentID = parentID ? parentID.nodeId : null;
const childTreeID = GetStringAttribute(this.treeID, this.id, 'childTreeId');
const count = GetChildCount(this.treeID, this.id);
const childIDs = [];
for (let i = 0; i < count; ++i) {
const childID = GetChildIDAtIndex(this.treeID, this.id, i).nodeId;
Array.prototype.push.call(childIDs, childID);
}
const name = GetName(this.treeID, this.id);
let result = 'node id=' + this.id + ' role=' + this.role +
' state=' + JSON.stringify(this.state) + ' parentID=' + parentID +
' childIds=' + JSON.stringify(childIDs);
if (childTreeID) {
result += ' childTreeID=' + childTreeID;
}
if (name) {
result += ' name=' + name;
}
if (this.className) {
result += ' className=' + this.className;
}
return result;
}
dispatchEventAtCapturing_(event, path) {
event.eventPhase = Event.CAPTURING_PHASE;
for (let i = path.length - 1; i >= 0; i--) {
this.fireEventListeners_(path[i], event);
if (event.propagationStopped) {
return false;
}
}
return true;
}
dispatchEventAtTargeting_(event) {
event.eventPhase = Event.AT_TARGET;
this.fireEventListeners_(this, event);
return !event.propagationStopped;
}
dispatchEventAtBubbling_(event, path) {
event.eventPhase = Event.BUBBLING_PHASE;
for (let i = 0; i < path.length; i++) {
this.fireEventListeners_(path[i], event);
if (event.propagationStopped) {
return false;
}
}
return true;
}
fireEventListeners_(node, event) {
if (!node.rootImpl_) {
return;
}
const originalListeners = node.listeners_[event.type];
if (!originalListeners) {
return;
}
// Make a copy of the original listeners since calling any of them can cause
// the list to be modified.
const listeners = [];
for (let i = 0; i < originalListeners.length; i++) {
listeners.push(originalListeners[i]);
}
const eventPhase = event.eventPhase;
for (let i = 0; i < listeners.length; i++) {
if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture) {
continue;
}
if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture) {
continue;
}
try {
listeners[i].callback(event);
} catch (e) {
console.error('Error in event handler for ' + event.type +
' during phase ' + eventPhase, e);
}
}
}
performAction_(actionType, opt_args, opt_callback) {
if (!this.rootImpl_) {
return;
}
// Not yet initialized.
if (this.rootImpl_.treeID === undefined || this.id === undefined) {
return;
}
let requestID = -1;
if (opt_callback) {
requestID = this.rootImpl_.addActionResultCallback(
actionType, opt_args, opt_callback);
}
let actionData = automationUtil.getDefaultAXActionData();
actionData.targetTreeId =
automationUtil.stringAXTreeIDToMojo(this.rootImpl_.treeID);
actionData.targetNodeId = this.id;
actionData.action = automationUtil.StringActionToMojo(actionType);
actionData.requestId = requestID;
// TODO(b:333790806): Convert opt_args to AxActionData format.
chrome.automation.automationClientRemote.performAction(actionData);
}
findInternal_(params, opt_results) {
let result = null;
this.forAllDescendants_(function(node) {
if (node.matchInternal_(params)) {
if (opt_results) {
Array.prototype.push.call(opt_results, node);
} else {
result = node;
}
return !opt_results;
}
});
if (opt_results) {
return opt_results;
}
return result;
}
/**
* Executes a closure for all of this node's descendants, in pre-order.
* Early-outs if the closure returns true.
* @param {Function(AutomationNode):boolean} closure Closure to be executed
* for each node. Return true to early-out the traversal.
*/
forAllDescendants_(closure) {
const stack = this.children.reverse();
while (stack.length > 0) {
const node = stack.pop();
if (closure(node)) {
return;
}
const children = node.children;
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i]);
}
}
}
matchInternal_(params) {
if (Object.keys(params).length === 0) {
return false;
}
if ('role' in params && this.role != params.role) {
return false;
}
if ('state' in params) {
for (const state in params.state) {
if (params.state[state] != (state in this.state)) {
return false;
}
}
}
if ('attributes' in params) {
for (const attribute in params.attributes) {
const attrValue = params.attributes[attribute];
if (typeof attrValue != 'object') {
if (this[attribute] !== attrValue) {
return false;
}
} else if (attrValue instanceof $RegExp.self) {
if (typeof this[attribute] != 'string') {
return false;
}
if (!attrValue.test(this[attribute])) {
return false;
}
} else {
// TODO(aboxhall): handle intlist case.
return false;
}
}
}
return true;
}
}
const stringAttributes = [
'accessKey',
'appId',
'autoComplete',
'checkedStateDescription',
'className',
'containerLiveRelevant',
'containerLiveStatus',
'description',
'display',
'doDefaultLabel',
'fontFamily',
'htmlTag',
'imageDataUrl',
'innerHtml',
'language',
'liveRelevant',
'liveStatus',
'longClickLabel',
'placeholder',
'roleDescription',
'tooltip',
'url',
];
const boolAttributes = [
'busy',
'clickable',
'containerLiveAtomic',
'containerLiveBusy',
'hasHiddenOffscreenNodes',
'nonAtomicTextFieldRoot',
'liveAtomic',
'modal',
'notUserSelectableStyle',
'scrollable',
'selected',
'supportsTextLocation',
];
const intAttributes = [
'backgroundColor',
'color',
'colorValue',
'hierarchicalLevel',
'posInSet',
'scrollX',
'scrollXMax',
'scrollXMin',
'scrollY',
'scrollYMax',
'scrollYMin',
'setSize',
'tableCellColumnSpan',
'tableCellRowSpan',
'ariaColumnCount',
'ariaRowCount',
'textSelEnd',
'textSelStart',
];
// Int attribute, relation property to expose, reverse relation to expose.
const nodeRefAttributes = [
['activedescendantId', 'activeDescendant', 'activeDescendantFor'],
['inPageLinkTargetId', 'inPageLinkTarget', null],
['nextFocusId', 'nextFocus', null],
['nextOnLineId', 'nextOnLine', null],
['nextWindowFocusId', 'nextWindowFocus', null],
['previousFocusId', 'previousFocus', null],
['previousOnLineId', 'previousOnLine', null],
['previousWindowFocusId', 'previousWindowFocus', null],
['tableColumnHeaderId', 'tableColumnHeader', null],
['tableHeaderId', 'tableHeader', null],
['tableRowHeaderId', 'tableRowHeader', null],
];
const intListAttributes = ['wordEnds', 'wordStarts'];
// Intlist attribute, relation property to expose, reverse relation to expose.
const nodeRefListAttributes = [
['controlsIds', 'controls', 'controlledBy'],
['describedbyIds', 'describedBy', 'descriptionFor'],
['detailsIds', 'details', 'detailsFor'],
['errorMessageIds', 'errorMessage', 'errorMessageFor'],
['flowtoIds', 'flowTo', 'flowFrom'],
['labelledbyIds', 'labelledBy', 'labelFor'],
];
const floatAttributes =
['fontSize', 'maxValueForRange', 'minValueForRange', 'valueForRange'];
const htmlAttributes = [['type', 'inputType']];
const publicAttributes = [];
Array.prototype.forEach.call(stringAttributes, function(attributeName) {
Array.prototype.push.call(publicAttributes, attributeName);
Object.defineProperty(AutomationNode.prototype, attributeName, {
__proto__: null,
get: function() {
return GetStringAttribute(this.treeID, this.id, attributeName);
},
});
});
Array.prototype.forEach.call(boolAttributes, function(attributeName) {
Array.prototype.push.call(publicAttributes, attributeName);
Object.defineProperty(AutomationNode.prototype, attributeName, {
__proto__: null,
get: function() {
return GetBoolAttribute(this.treeID, this.id, attributeName);
},
});
});
Array.prototype.forEach.call(intAttributes, function(attributeName) {
Array.prototype.push.call(publicAttributes, attributeName);
Object.defineProperty(AutomationNode.prototype, attributeName, {
__proto__: null,
get: function() {
return GetIntAttribute(this.treeID, this.id, attributeName);
},
});
});
Array.prototype.forEach.call(nodeRefAttributes, function(params) {
const srcAttributeName = params[0];
const dstAttributeName = params[1];
const dstReverseAttributeName = params[2];
Array.prototype.push.call(publicAttributes, dstAttributeName);
Object.defineProperty(AutomationNode.prototype, dstAttributeName, {
__proto__: null,
get: function() {
const id = GetIntAttribute(this.treeID, this.id, srcAttributeName);
if (id && this.rootImpl_) {
return this.rootImpl_.get(id);
} else {
return undefined;
}
},
});
if (dstReverseAttributeName) {
Array.prototype.push.call(publicAttributes, dstReverseAttributeName);
Object.defineProperty(AutomationNode.prototype, dstReverseAttributeName, {
__proto__: null,
get: function() {
const ids = GetIntAttributeReverseRelations(
this.treeID, this.id, srcAttributeName);
if (!ids || !this.rootImpl_) {
return undefined;
}
const result = [];
for (let i = 0; i < ids.length; ++i) {
const node = this.rootImpl_.get(ids[i]);
if (node) {
Array.prototype.push.call(result, node);
}
}
return result;
},
});
}
});
Array.prototype.forEach.call(intListAttributes, function(attributeName) {
Array.prototype.push.call(publicAttributes, attributeName);
Object.defineProperty(AutomationNode.prototype, attributeName, {
__proto__: null,
get: function() {
return GetIntListAttribute(this.treeID, this.id, attributeName);
},
});
});
Array.prototype.forEach.call(nodeRefListAttributes, function(params) {
const srcAttributeName = params[0];
const dstAttributeName = params[1];
const dstReverseAttributeName = params[2];
Array.prototype.push.call(publicAttributes, dstAttributeName);
Object.defineProperty(AutomationNode.prototype, dstAttributeName, {
__proto__: null,
get: function() {
const ids = GetIntListAttribute(this.treeID, this.id, srcAttributeName);
if (!ids || !this.rootImpl_) {
return undefined;
}
const result = [];
for (let i = 0; i < ids.length; ++i) {
const node = this.rootImpl_.get(ids[i]);
if (node) {
Array.prototype.push.call(result, node);
}
}
return result;
},
});
if (dstReverseAttributeName) {
Array.prototype.push.call(publicAttributes, dstReverseAttributeName);
Object.defineProperty(AutomationNode.prototype, dstReverseAttributeName, {
__proto__: null,
get: function() {
const ids = GetIntListAttributeReverseRelations(
this.treeID, this.id, srcAttributeName);
if (!ids || !this.rootImpl_) {
return undefined;
}
const result = [];
for (let i = 0; i < ids.length; ++i) {
const node = this.rootImpl_.get(ids[i]);
if (node) {
Array.prototype.push.call(result, node);
}
}
return result;
},
});
}
});
Array.prototype.forEach.call(floatAttributes, function(attributeName) {
Array.prototype.push.call(publicAttributes, attributeName);
Object.defineProperty(AutomationNode.prototype, attributeName, {
__proto__: null,
get: function() {
return GetFloatAttribute(this.treeID, this.id, attributeName);
},
});
});
Array.prototype.forEach.call(htmlAttributes, function(params) {
const srcAttributeName = params[0];
const dstAttributeName = params[1];
Array.prototype.push.call(publicAttributes, dstAttributeName);
Object.defineProperty(AutomationNode.prototype, dstAttributeName, {
__proto__: null,
get: function() {
return GetHtmlAttribute(this.treeID, this.id, srcAttributeName);
},
});
});
/**
* AutomationRootNode.
*
* An AutomationRootNode is the javascript end of an AXTree living in the
* browser. AutomationRootNode handles unserializing incremental updates from
* the source AXTree. Each update contains node data that form a complete tree
* after applying the update.
*
* A brief note about ids used through this class. The source AXTree assigns
* unique ids per node and we use these ids to build a hash to the actual
* AutomationNode object.
* Thus, tree traversals amount to a lookup in our hash.
*
* The tree itself is identified by the accessibility tree id of the
* renderer widget host.
* @constructor
*/
class AutomationRootNode extends AutomationNode {
constructor(treeID) {
super(null);
this.rootImpl_ = this;
this.treeID_ = treeID;
this.isRootNode_ = true;
/**
* A map from id to AutomationNode.
* @type {Object<number, AutomationNode>}
* @private
*/
this.axNodeDataCache_ = {__proto__: null};
}
get id() {
let id = GetRootID(this.treeID);
// Don't return undefined, because the id is often passed directly
// as an argument to a native binding that expects only a valid number.
if (id === undefined) {
console.warn('id of root node was undefined. Setting to -1.');
id = -1;
}
return id;
}
static get(treeID) {
const result = AutomationTreeCache.idToAutomationRootNode[treeID];
return result || undefined;
}
static getOrCreate(treeID) {
if (AutomationTreeCache.idToAutomationRootNode[treeID]) {
return AutomationTreeCache.idToAutomationRootNode[treeID];
}
const result = new AutomationRootNode(treeID);
AutomationTreeCache.idToAutomationRootNode[treeID] = result;
return result;
}
static getNodeFromTree(treeId, nodeId) {
const tree = AutomationRootNode.get(treeId);
if (!tree) {
return;
}
return tree.get(nodeId);
}
static destroy(treeID) {
delete AutomationTreeCache.idToAutomationRootNode[treeID];
}
static destroyAll() {
AutomationTreeCache.idToAutomationRootNode = {};
}
get docUrl() {
return GetDocURL(this.treeID);
}
get docTitle() {
return GetDocTitle(this.treeID);
}
get docLoaded() {
return GetDocLoaded(this.treeID);
}
get docLoadingProgress() {
return GetDocLoadingProgress(this.treeID);
}
get isSelectionBackward() {
return GetIsSelectionBackward(this.treeID);
}
get anchorObject() {
const id = GetAnchorObjectID(this.treeID);
if (id && id != -1) {
return this.get(id);
}
return undefined;
}
get anchorOffset() {
const id = GetAnchorObjectID(this.treeID);
if (id && id != -1) {
return GetAnchorOffset(this.treeID);
}
return undefined;
}
get anchorAffinity() {
const id = GetAnchorObjectID(this.treeID);
if (id && id != -1) {
return GetAnchorAffinity(this.treeID);
}
return undefined;
}
get focusObject() {
const id = GetFocusObjectID(this.treeID);
if (id && id != -1) {
return this.get(id);
}
return undefined;
}
get focusOffset() {
const id = GetFocusObjectID(this.treeID);
if (id && id != -1) {
return GetFocusOffset(this.treeID);
}
return undefined;
}
get focusAffinity() {
const id = GetFocusObjectID(this.treeID);
if (id && id != -1) {
return GetFocusAffinity(this.treeID);
}
return undefined;
}
get selectionStartObject() {
const id = GetSelectionStartObjectID(this.treeID);
if (id && id != -1) {
return this.get(id);
}
return undefined;
}
get selectionStartOffset() {
const id = GetSelectionStartObjectID(this.treeID);
if (id && id != -1) {
return GetSelectionStartOffset(this.treeID);
}
return undefined;
}
get selectionStartAffinity() {
const id = GetSelectionStartObjectID(this.treeID);
if (id && id != -1) {
return GetSelectionStartAffinity(this.treeID);
}
return undefined;
}
get selectionEndObject() {
const id = GetSelectionEndObjectID(this.treeID);
if (id && id != -1) {
return this.get(id);
}
return undefined;
}
get selectionEndOffset() {
const id = GetSelectionEndObjectID(this.treeID);
if (id && id != -1) {
return GetSelectionEndOffset(this.treeID);
}
return undefined;
}
get selectionEndAffinity() {
const id = GetSelectionEndObjectID(this.treeID);
if (id && id != -1) {
return GetSelectionEndAffinity(this.treeID);
}
return undefined;
}
get(id) {
if (id == undefined) {
return undefined;
}
if (id == this.id) {
return this;
}
let obj = this.axNodeDataCache_[id];
if (obj) {
return obj;
}
// Validate the backing AXTree has the specified node.
if (!GetRole(this.treeID, id)) {
return;
}
obj = new AutomationNode(this);
obj.treeID_ = this.treeID;
obj.id_ = id;
this.axNodeDataCache_[id] = obj;
return obj;
}
remove(id) {
if (this.axNodeDataCache_[id]) {
this.axNodeDataCache_[id].detach();
}
delete this.axNodeDataCache_[id];
}
destroy() {
for (const id in this.axNodeDataCache_) {
this.remove(id);
}
this.detach();
}
onAccessibilityEvent(eventParams) {
const targetNode = this.get(eventParams.targetID);
if (targetNode) {
if (eventParams.actionRequestID != -1 &&
this.onActionResult(eventParams.actionRequestID, targetNode)) {
return;
}
targetNode.dispatchEvent(
eventParams.eventType, eventParams.eventFrom,
eventParams.eventFromAction, eventParams.mouseX, eventParams.mouseY,
eventParams.intents);
} else {
console.warn(
'Got ' + eventParams.eventType + ' event on unknown node: ' +
eventParams.targetID + '; this: ' + this.id);
}
}
addActionResultCallback(actionType, opt_args, callback) {
AutomationRootNode
.actionRequestIDToCallback[++AutomationRootNode.actionRequestCounter] =
{
actionType,
opt_args,
callback,
};
return AutomationRootNode.actionRequestCounter;
}
onGetTextLocationResult(textLocationParams) {
const requestID = textLocationParams.requestID;
if (requestID in AutomationRootNode.actionRequestIDToCallback) {
const callback =
AutomationRootNode.actionRequestIDToCallback[requestID].callback;
try {
if (textLocationParams.result) {
callback(ComputeGlobalBounds(
this.treeID, textLocationParams.nodeID, textLocationParams.left,
textLocationParams.top, textLocationParams.width,
textLocationParams.height));
} else {
callback(undefined);
}
} catch (e) {
logging.WARNING('Error with onGetTextLocationResult callback:' + e);
}
delete AutomationRootNode.actionRequestIDToCallback[requestID];
}
}
onActionResult(requestID, result) {
if (requestID in AutomationRootNode.actionRequestIDToCallback) {
const data = AutomationRootNode.actionRequestIDToCallback[requestID];
if (data.actionType.indexOf('hitTest') === 0 && result &&
result.role === 'window' && result.className &&
result.className.indexOf('ExoSurface') === 0) {
// Search for a node containing app id, which indicates Lacros.
function findApp(node) {
// Exit early if we've crossed roots from |result|.
if (result.root !== node.root) {
return null;
}
// This node is actually in a different backing C++ tree though at
// this internal js layer, we merge the trees so that it is rooted to
// the desktop tree (same as |result|).
if (node.appId) {
return node;
}
for (const child of node.children) {
const found = findApp(child);
if (found) {
return found;
}
}
return null;
}
// The hit test |result| node is not quite what we need to start
// searching in. Find the topmost ExoShell surface.
while (result.parent && result.parent.className &&
result.parent.className.indexOf('ExoShellSurface') === 0) {
result = result.parent;
}
const appNode = findApp(result);
if (appNode) {
delete AutomationRootNode.actionRequestIDToCallback[requestID];
// Repost the hit test on |appNode|.
appNode.performAction_(data.actionType, data.opt_args, data.callback);
return true;
}
}
data.callback(result);
delete AutomationRootNode.actionRequestIDToCallback[requestID];
return false;
}
}
toString() {
function toStringInternal(nodeImpl, indent) {
if (nodeImpl === null || nodeImpl === undefined) {
return '';
}
let output = '';
if (nodeImpl.isRootNode) {
output += indent + 'tree id=' + nodeImpl.treeID + '\n';
}
output += indent + nodeImpl.toStringHelper_() + '\n';
indent += ' ';
const children = nodeImpl.children;
for (let i = 0; i < children.length; ++i) {
output += toStringInternal(children[i], indent);
}
return output;
}
return toStringInternal(this, '');
}
}
/**
* A counter keeping track of IDs to use for mapping action requests to
* their callback function.
*/
AutomationRootNode.actionRequestCounter = 0;
/**
* A map from a request ID to the corresponding callback function to call
* when the action response event is received.
*/
AutomationRootNode.actionRequestIDToCallback = {};
// A class to export utility functions to other files in automation.
class AutomationUtil {
constructor() {
/**
* Global map of tree change observers.
* @public {Object<number, TreeChangeObserver>}
*/
this.treeChangeObserverMap = {};
/**
* The id for the next tree change observer.
* @public
* @type {number}
*/
this.nextTreeChangeObserverId = 1;
this.idToCallback = {};
}
stringAXTreeIDToMojo(stringTreeID) {
let token = natives.StringAXTreeIDToUnguessableToken(stringTreeID);
let mojoTreeID = {
token: {
high: BigInt(token.high),
low: BigInt(token.low),
}
};
return mojoTreeID;
}
storeTreeCallback(id, callback) {
if (!callback) {
throw new Error('callback can not be null');
}
const targetTree = AutomationRootNode.get(id);
if (!targetTree) {
// If we haven't cached the tree, hold the callback until the tree is
// populated by the initial onAccessibilityEvent call.
if (id in this.idToCallback) {
this.idToCallback[id].push(callback);
} else {
this.idToCallback[id] = [callback];
}
} else {
callback(targetTree);
}
}
getDefaultAXActionData() {
let actionData = new ax.mojom.AXActionData();
let treeID = {
unknown: 0,
};
actionData.targetTreeId = treeID;
actionData.sourceExtensionId = '';
actionData.requestId = 0;
actionData.targetNodeId = 0;
actionData.targetRole = ax.mojom.Role.kUnknown;
actionData.flags = 0;
actionData.action = ax.mojom.Action.kNone;
actionData.anchorNodeId = 0;
actionData.focusNodeId = 0;
actionData.anchorOffset = 0;
actionData.focusOffset = 0;
actionData.customActionId = 0;
let rect = new gfx.mojom.Rect();
rect.x = 0;
rect.y = 0;
rect.width = 0;
rect.height = 0;
actionData.targetRect = rect;
let point = new gfx.mojom.Point();
point.x = 0;
point.y = 0;
actionData.targetPoint = point;
actionData.value = '';
actionData.hitTestEventToFire = ax.mojom.Event.kNone;
actionData.horizontalScrollAlignment = ax.mojom.ScrollAlignment.kNone;
actionData.verticalScrollAlignment = ax.mojom.ScrollAlignment.kNone;
actionData.scrollBehavior = ax.mojom.ScrollBehavior.kNone;
return actionData;
}
StringActionToMojo(action) {
// Action types are represented as strings because features are using ATP
// automation and extensions automation (which uses the old IDL formats).
// See ActionType in extensions/common/api/automation.idl.
if (action == 'hitTest') {
return ax.mojom.Action.kHitTest;
}
// TODO(b:327258691): Share const strings between c++ and js for action
// names.
return ax.mojom.Action.kNone;
}
removeTreeChangeObserver(observer) {
for (const id in this.treeChangeObserverMap) {
if (this.treeChangeObserverMap[id] === observer) {
natives.RemoveTreeChangeObserver(id);
delete this.treeChangeObserverMap[id];
return;
}
}
}
addTreeChangeObserver(filter, observer) {
this.removeTreeChangeObserver(observer);
const id = this.nextTreeChangeObserverId++;
natives.AddTreeChangeObserver(id, filter);
this.treeChangeObserverMap[id] = observer;
}
}
automationUtil = new AutomationUtil();
// Shim class for Automation API. Compare to
// extensions/renderer/resources/automation/automation_custom_bindings.js.
class AtpAutomation {
constructor() {
const AutomationClient = ax.mojom.AutomationClient;
this.automationClientRemote_ = AutomationClient.getRemote();
/** @private {?string} */
this.desktopId_ = null;
}
get automationClientRemote() {
return this.automationClientRemote_;
}
get desktopId() {
return this.desktopId_;
}
reset() {
this.automationClientRemote_.disable();
this.desktopId_ = undefined;
this.desktopTree_ = undefined;
automationUtil.idToCallback = {};
AutomationRootNode.destroyAll();
}
get desktopTree() {
return this.desktopTree_;
}
getDesktop(callback) {
if (this.desktopId_) {
this.desktopTree_ = AutomationRootNode.get(this.desktopId_);
}
if (this.desktopTree_) {
callback(this.desktopTree_);
return;
}
return new Promise(async resolve => {
await this.automationClientRemote_.enable().then(enableResult => {
if (enableResult !== null && enableResult.desktopId !== null) {
const high = enableResult.desktopId.token.high;
const low = enableResult.desktopId.token.low;
// BigInt strings in JS are lowercase while C++ does uppercase, and
// strangely in reverse order, so we need to flip every
// two characters around.
let lowString = low.toString(16);
while (lowString.length < 16) {
// Zero-pad.
lowString = "0" + lowString;
}
let highString = high.toString(16);
while (highString.length < 16) {
// Zero-pad.
highString = "0" + highString;
}
const initial = lowString + highString;
this.desktopId_ = '';
for (let i = 0; i < 16; i++) {
this.desktopId_ += initial.substr(32 - i * 2 - 2, 2).toUpperCase();
}
nativeAutomationInternal.SetDesktopID(this.desktopId_);
this.desktopTree_ = AutomationRootNode.getOrCreate(this.desktopId_);
callback(this.desktopTree_);
resolve();
} else {
console.error('Unexpected result from AutomationClient::Enable');
this.desktopId_ = null;
this.desktopTree_ = null;
AutomationRootNode.destroy(treeId);
nativeAutomationInternal.SetDesktopID('');
callback();
resolve();
}
});
});
}
getFocus(callback) {
let focusedNodeInfo = natives.GetFocus();
if (!focusedNodeInfo) {
callback(null);
return;
}
const tree = AutomationRootNode.getOrCreate(focusedNodeInfo.treeId);
if (tree) {
callback(tree.get(focusedNodeInfo.nodeId));
return;
}
}
getAccessibilityFocus(callback) {
let focusedNodeInfo = natives.GetAccessibilityFocus();
if (!focusedNodeInfo) {
callback(null);
return;
}
const tree = AutomationRootNode.getOrCreate(focusedNodeInfo.treeId);
if (tree) {
callback(tree.get(focusedNodeInfo.nodeId));
return;
}
}
removeTreeChangeObserver(observer) {
automationUtil.removeTreeChangeObserver(observer);
}
addTreeChangeObserver(filter, observer) {
automationUtil.addTreeChangeObserver(filter, observer);
}
setDocumentSelection(params) {
const anchorNode = params.anchorObject;
const focusNode = params.focusObject;
if (anchorNode.treeID !== focusNode.treeID) {
console.error('Selection anchor and focus must be in the same tree.');
return;
}
if (anchorNode.treeID === this.desktopId_) {
console.error(
'Use AutomationNode.setSelection to set the selection ' +
'in the desktop tree.');
return;
}
// Note: the AXActionData must have all its fields defined to be sent over
// mojo, so the next call is mandatory.
let actionData = automationUtil.getDefaultAXActionData();
actionData.targetTreeId =
automationUtil.stringAXTreeIDToMojo(anchorNode.treeID);
actionData.targetNodeId = anchorNode.id;
actionData.action = ax.mojom.Action.kSetSelection;
actionData.anchorNodeId = anchorNode.id;
actionData.focusNodeId = focusNode.id;
actionData.anchorOffset = params.anchorOffset;
actionData.focusOffset = params.focusOffset;
this.automationClientRemote_.performAction(actionData);
}
};
automationInternal.onChildTreeID.addListener((childTreeId) => {
const targetTree = AutomationRootNode.get(childTreeId);
// If the tree is already loaded, or if we previously requested it be loaded
// (i.e. have a callback for it), don't try to do so again.
if (targetTree || automationUtil.idToCallback[childTreeId]) {
return;
}
// A WebView in the desktop tree has a different AX tree as its child.
// When we encounter a WebView with a child AX tree id that we don't
// currently have cached, explicitly request that AX tree from the
// browser process and set up a callback when it loads to attach that
// tree as a child of this node and fire appropriate events.
automationUtil.storeTreeCallback(childTreeId, function(root) {
root.dispatchEvent('loadComplete', 'page');
if (root.parent) {
root.parent.dispatchEvent('childrenChanged');
}
}, true);
chrome.automation.automationClientRemote.enableChildTree(
automationUtil.stringAXTreeIDToMojo(childTreeId));
});
automationInternal.onTreeChange.addListener(
(observerID, treeID, nodeID, changeType) => {
const tree = AutomationRootNode.getOrCreate(treeID);
if (!tree) {
return;
}
const node = tree.get(nodeID);
if (!node) {
return;
}
const observer = automationUtil.treeChangeObserverMap[observerID];
if (!observer) {
return;
}
try {
observer({target: node, type: changeType});
} catch (e) {
exceptionHandler.handle(
'Error in tree change observer for ' + changeType, e);
}
});
automationInternal.onNodesRemoved.addListener((treeID, nodeIDs) => {
const tree = AutomationRootNode.getOrCreate(treeID);
if (!tree) {
return;
}
for (let i = 0; i < nodeIDs.length; i++) {
tree.remove(nodeIDs[i]);
}
});
automationInternal.onAllAutomationEventListenersRemoved.addListener(() => {
if (!chrome.automation.desktopId) {
return;
}
chrome.automation.reset();
});
/**
* Dispatch accessibility events fired on individual nodes to its
* corresponding AutomationNode.
*/
automationInternal.onAccessibilityEvent.addListener((eventParams) => {
const id = eventParams.treeID;
const targetTree = AutomationRootNode.getOrCreate(id);
if (eventParams.eventType == 'mediaStartedPlaying' ||
eventParams.eventType == 'mediaStoppedPlaying') {
// These events are global to the tree.
eventParams.targetID = targetTree.id;
}
targetTree.onAccessibilityEvent(eventParams);
// If we're not waiting on a callback, we can early out here.
if (!(id in automationUtil.idToCallback)) {
return;
}
// We usually get a 'placeholder' tree first, which doesn't have any url
// attribute or child nodes. If we've got that, wait for the full tree before
// calling the callback.
if (id != chrome.automation.desktopId && !targetTree.url &&
targetTree.children.length == 0) {
return;
}
// If the tree wasn't available, the callback will have been cached in
// idToCallback, so call and delete it now that we have the complete tree.
for (let i = 0; i < automationUtil.idToCallback[id].length; i++) {
const callback = automationUtil.idToCallback[id][i];
callback(targetTree);
}
delete automationUtil.idToCallback[id];
});
automationInternal.onAccessibilityTreeDestroyed.addListener((id) => {
// Destroy the AutomationRootNode.
const targetTree = AutomationRootNode.get(id);
if (targetTree) {
targetTree.destroy();
AutomationRootNode.destroy(id);
} else {
console.warn('no targetTree to destroy');
}
// Destroy the native cache of the accessibility tree.
natives.DestroyAccessibilityTree(id);
});
automationInternal.onAccessibilityTreeSerializationError.addListener((id) => {
// TODO(b:332975670): Investigate the usage of automationInternal.enableTree
// to reset on serialization problems.
chrome.automation.automationClientRemote.enableChildTree(
automationUtil.stringAXTreeIDToMojo(id));
});
automationInternal.onActionResult.addListener((treeID, requestID, result) => {
const targetTree = AutomationRootNode.get(treeID);
if (!targetTree) {
return;
}
targetTree.onActionResult(requestID, result);
});
automationInternal.onGetTextLocationResult.addListener((textLocationParams) => {
const targetTree = AutomationRootNode.get(textLocationParams.treeID);
if (!targetTree) {
return;
}
targetTree.onGetTextLocationResult(textLocationParams);
});
chrome.automation = new AtpAutomation();