blob: 23292f80fe7f1e55c6f3291f657ecff06d752078 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Utilities for the automation extension API.
*/
import {AutomationPredicate} from './automation_predicate.js';
import {constants} from './constants.js';
import {AutomationTreeWalker, AutomationTreeWalkerRestriction} from './tree_walker.js';
const AutomationNode = chrome.automation.AutomationNode;
const RoleType = chrome.automation.RoleType;
export class AutomationUtil {
/**
* Find a node in subtree of |cur| satisfying |pred| using pre-order
* traversal.
* @param {AutomationNode} cur Node to begin the search
* from.
* @param {constants.Dir} dir
* @param {AutomationPredicate.Unary} pred A predicate to apply
* to a candidate node.
* @return {AutomationNode}
*/
static findNodePre(cur, dir, pred) {
if (!cur) {
return null;
}
if (pred(cur) && !AutomationPredicate.shouldIgnoreNode(cur)) {
return cur;
}
let child = dir === constants.Dir.BACKWARD ? cur.lastChild : cur.firstChild;
while (child) {
const ret = AutomationUtil.findNodePre(child, dir, pred);
if (ret) {
return ret;
}
child = dir === constants.Dir.BACKWARD ? child.previousSibling :
child.nextSibling;
}
return null;
}
/**
* Find a node in subtree of |cur| satisfying |pred| using post-order
* traversal.
* @param {AutomationNode} cur Node to begin the search
* from.
* @param {constants.Dir} dir
* @param {AutomationPredicate.Unary} pred A predicate to apply
* to a candidate node.
* @return {AutomationNode}
*/
static findNodePost(cur, dir, pred) {
if (!cur) {
return null;
}
let child = dir === constants.Dir.BACKWARD ? cur.lastChild : cur.firstChild;
while (child) {
const ret = AutomationUtil.findNodePost(child, dir, pred);
if (ret) {
return ret;
}
child = dir === constants.Dir.BACKWARD ? child.previousSibling :
child.nextSibling;
}
if (pred(cur) && !AutomationPredicate.shouldIgnoreNode(cur)) {
return cur;
}
return null;
}
/**
* Find the next node in the given direction in depth first order.
*
* Let D be the dfs linearization of |cur.root|. Then, let F be the list after
* applying |pred| as a filter to D. This method will return the directed next
* node of |cur| in F.
* The restrictions option will further filter F. For example,
* |skipInitialSubtree| will remove any |pred| matches in the subtree of |cur|
* from F.
* @param {!AutomationNode} cur Node to begin the search
* from.
* @param {constants.Dir} dir
* @param {AutomationPredicate.Unary} pred A predicate to apply
* to a candidate node.
* @param {AutomationTreeWalkerRestriction=} opt_restrictions |leaf|, |root|,
* |skipInitialAncestry|, and |skipInitialSubtree| are valid restrictions
* used when finding the next node.
* By default:
* the root predicate gets set to |AutomationPredicate.root|.
* |skipInitialSubtree| is false if |cur| is a container or matches
* |pred|. This alleviates the caller from syncing forwards.
* Leaves are nodes matched by |pred| which are not also containers.
* This takes care of syncing backwards.
* @return {AutomationNode}
*/
static findNextNode(cur, dir, pred, opt_restrictions) {
const walker = createWalker(cur, dir, pred, opt_restrictions);
return walker.next().node;
}
/**
* Finds all nodes in the given direction in depth first order.
*
* Let D be the dfs linearization of |cur.root|. Then, let F be the list after
* applying |pred| as a filter to D. This method will return the directed next
* node of |cur| in F.
* The restrictions option will further filter F. For example,
* |skipInitialSubtree| will remove any |pred| matches in the subtree of |cur|
* from F.
* @param {!AutomationNode} cur Node to begin the search
* from.
* @param {constants.Dir} dir
* @param {AutomationPredicate.Unary} pred A predicate to apply
* to a candidate node.
* @param {AutomationTreeWalkerRestriction=} opt_restrictions |leaf|, |root|,
* |skipInitialAncestry|, and |skipInitialSubtree| are valid restrictions
* used when finding the next node.
* By default:
* the root predicate gets set to |AutomationPredicate.root|.
* |skipInitialSubtree| is false if |cur| is a container or matches
* |pred|. This alleviates the caller from syncing forwards.
* Leaves are nodes matched by |pred| which are not also containers.
* This takes care of syncing backwards.
* @return {!Array<!AutomationNode>}
*/
static findAllNodes(cur, dir, pred, opt_restrictions) {
const walker = createWalker(cur, dir, pred, opt_restrictions);
const nodes = [];
let currentNode = walker.next().node;
while (currentNode) {
nodes.push(currentNode);
currentNode = walker.next().node;
}
return nodes;
}
/**
* Given nodes a_1, ..., a_n starting at |cur| in pre order traversal, apply
* |pred| to a_i and a_(i - 1) until |pred| is satisfied. Returns a_(i - 1)
* or a_i (depending on opt_before) or null if no match was found.
* @param {!AutomationNode} cur
* @param {constants.Dir} dir
* @param {AutomationPredicate.Binary} pred
* @param {boolean=} opt_before True to return a_(i - 1); a_i otherwise.
* Defaults to false.
* @return {AutomationNode}
*/
static findNodeUntil(cur, dir, pred, opt_before) {
let before = cur;
let after = before;
do {
before = after;
after =
AutomationUtil.findNextNode(before, dir, AutomationPredicate.leaf);
} while (after && !pred(before, after));
return opt_before ? before : after;
}
/**
* Returns an array containing ancestors of node starting at root down to
* node.
* @param {!AutomationNode} node
* @return {!Array<AutomationNode>}
*/
static getAncestors(node) {
const ret = [];
let candidate = node;
while (candidate) {
ret.push(candidate);
candidate = candidate.parent;
}
return ret.reverse();
}
/**
* Finds the lowest ancestor with a given role.
* @param {!AutomationNode} node
* @param {!RoleType} role
*/
static getFirstAncestorWithRole(node, role) {
if (!node.parent) {
return null;
}
if (node.parent.role === role) {
return node.parent;
}
return AutomationUtil.getFirstAncestorWithRole(node.parent, role);
}
/**
* Gets the first index where the two input arrays differ. Returns -1 if they
* do not.
* @param {!Array<AutomationNode>} ancestorsA
* @param {!Array<AutomationNode>} ancestorsB
* @return {number}
*/
static getDivergence(ancestorsA, ancestorsB) {
for (let i = 0; i < ancestorsA.length; i++) {
if (ancestorsA[i] !== ancestorsB[i]) {
return i;
}
}
if (ancestorsA.length === ancestorsB.length) {
return -1;
}
return ancestorsA.length;
}
/**
* Returns ancestors of |node| that are not also ancestors of |prevNode|.
* @param {!AutomationNode} prevNode
* @param {!AutomationNode} node
* @return {!Array<!AutomationNode>}
*/
static getUniqueAncestors(prevNode, node) {
const prevAncestors = AutomationUtil.getAncestors(prevNode);
const ancestors = AutomationUtil.getAncestors(node);
const divergence = AutomationUtil.getDivergence(prevAncestors, ancestors);
return ancestors.slice(divergence);
}
/**
* Given |nodeA| and |nodeB| in that order, determines their ordering in the
* document.
* @param {!AutomationNode} nodeA
* @param {!AutomationNode} nodeB
* @return {constants.Dir}
*/
static getDirection(nodeA, nodeB) {
const ancestorsA = AutomationUtil.getAncestors(nodeA);
const ancestorsB = AutomationUtil.getAncestors(nodeB);
const divergence = AutomationUtil.getDivergence(ancestorsA, ancestorsB);
// Default to constants.Dir.FORWARD.
if (divergence === -1) {
return constants.Dir.FORWARD;
}
const divA = ancestorsA[divergence];
const divB = ancestorsB[divergence];
// One of the nodes is an ancestor of the other. Order this relationship in
// the same way dfs would. nodeA <= nodeB if nodeA is a descendant of
// nodeB. nodeA > nodeB if nodeB is a descendant of nodeA.
if (!divA) {
return constants.Dir.FORWARD;
}
if (!divB) {
return constants.Dir.BACKWARD;
}
if (divA.parent === nodeB) {
return constants.Dir.BACKWARD;
}
if (divB.parent === nodeA) {
return constants.Dir.FORWARD;
}
return divA.indexInParent <= divB.indexInParent ? constants.Dir.FORWARD :
constants.Dir.BACKWARD;
}
/**
* Determines whether the two given nodes come from the same tree source.
* @param {AutomationNode} a
* @param {AutomationNode} b
* @return {boolean}
*/
static isInSameTree(a, b) {
if (!a || !b) {
return true;
}
// Given two non-desktop roots, consider them in the "same" tree.
return a.root === b.root ||
(a.root.role === b.root.role && a.root.role === RoleType.ROOT_WEB_AREA);
}
/**
* Determines whether or not a node is or is the descendant of another node.
* @param {!AutomationNode} node
* @param {!AutomationNode} ancestor
* @return {boolean}
*/
static isDescendantOf(node, ancestor) {
let testNode = node;
while (testNode && testNode !== ancestor) {
testNode = testNode.parent;
}
return testNode === ancestor;
}
/**
* Finds the deepest node containing point. Since the automation tree does not
* maintain a containment invariant when considering child node bounding rects
* with respect to their parents, the hit test considers all children before
* their parents when looking for a matching node.
* @param {AutomationNode} node Subtree to search.
* @param {constants.Point} point
* @return {AutomationNode}
*/
static hitTest(node, point) {
let child = node.firstChild;
while (child) {
const hit = AutomationUtil.hitTest(child, point);
if (hit) {
return hit;
}
child = child.nextSibling;
}
const loc = node.unclippedLocation;
// When |node| is partially or fully offscreen, try to find a better match.
if (loc.left < 0 || loc.top < 0) {
return null;
}
if (point.x <= (loc.left + loc.width) && point.x >= loc.left &&
point.y <= (loc.top + loc.height) && point.y >= loc.top) {
return node;
}
return null;
}
/**
* Gets a top level root.
* @param {!AutomationNode} node
* @return {AutomationNode}
*/
static getTopLevelRoot(node) {
let root = node.root;
if (!root || root.role === RoleType.DESKTOP) {
return null;
}
while (root && root.parent && root.parent.root &&
root.parent.root.role !== RoleType.DESKTOP) {
root = root.parent.root;
}
return root;
}
/**
* @param {!AutomationNode} prevNode
* @param {!AutomationNode} node
* @return {AutomationNode}
*/
static getLeastCommonAncestor(prevNode, node) {
if (prevNode === node) {
return node;
}
const prevAncestors = AutomationUtil.getAncestors(prevNode);
const ancestors = AutomationUtil.getAncestors(node);
const divergence = AutomationUtil.getDivergence(prevAncestors, ancestors);
return ancestors[divergence - 1];
}
/**
* Gets the accessible text for this node based on its role.
* This text is suitable for caret navigation and selection in the node.
* @param {AutomationNode} node
* @return {string}
*/
static getText(node) {
if (!node) {
return '';
}
if (node.role === RoleType.TEXT_FIELD) {
return node.value || '';
}
return node.name || '';
}
/**
* Gets the root of editable node.
* @param {!AutomationNode} node
* @return {!AutomationNode|undefined}
*/
static getEditableRoot(node) {
let testNode = node;
let rootEditable;
do {
if (testNode.state.editable && testNode.state.focused) {
rootEditable = testNode;
}
testNode = testNode.parent;
} while (testNode);
return rootEditable;
}
/**
* Gets the last (DFS) ordered node matched by a predicate assuming a
* preference for ancestors.
*
* In detail:
* Given a DFS ordering on nodes a_1, ..., a_n, applying a predicate
* from 1 to n yields a different set of nodes from that when applying
* a predicate from n to 1 if we skip the remaining descendants of a
* successfully matched node when moving forward. To recover the same
* nodes when applying the predicate from n to 1, we make the
* observation that we want the shallowest node that matches the
* predicate in a successfully matched node's ancestry chain.
* Note that container nodes should only be considered if there are no current
* matches.
* @param {!AutomationNode} root Tree to search.
* @param {AutomationPredicate.Unary} pred A predicate to apply
* @return {AutomationNode}
*/
static findLastNode(root, pred) {
let node = root;
while (node.lastChild) {
node = node.lastChild;
}
do {
if (AutomationPredicate.shouldIgnoreNode(node)) {
continue;
}
// Get the shallowest node matching the predicate.
let walker = node;
let shallowest = null;
while (walker) {
if (walker === root) {
break;
}
if (pred(walker) && !AutomationPredicate.shouldIgnoreNode(walker) &&
(!shallowest || !AutomationPredicate.container(walker))) {
shallowest = walker;
}
walker = walker.parent;
}
if (shallowest) {
return shallowest;
}
} while (
node = AutomationUtil.findNextNode(node, constants.Dir.BACKWARD, pred));
return null;
}
}
/**
* @param {!AutomationNode} cur Node to begin the search
* from.
* @param {constants.Dir} dir
* @param {AutomationPredicate.Unary} pred A predicate to apply
* to a candidate node.
* @param {AutomationTreeWalkerRestriction=} opt_restrictions |leaf|, |root|,
* |skipInitialAncestry|, and |skipInitialSubtree| are valid restrictions
* used when finding the next node.
* @return {!AutomationTreeWalker} Instance of tree walker initialized with
* given parameters.
*/
function createWalker(cur, dir, pred, opt_restrictions) {
const restrictions = {};
opt_restrictions = opt_restrictions || {
leaf: undefined,
root: undefined,
visit: undefined,
skipInitialSubtree: !AutomationPredicate.container(cur) && pred(cur),
};
restrictions.root = opt_restrictions.root || AutomationPredicate.root;
restrictions.leaf = opt_restrictions.leaf || function(node) {
// Treat nodes matched by |pred| as leaves except for containers.
return !AutomationPredicate.container(node) && pred(node);
};
restrictions.skipInitialSubtree = opt_restrictions.skipInitialSubtree;
restrictions.skipInitialAncestry = opt_restrictions.skipInitialAncestry;
restrictions.visit = function(node) {
return pred(node) && !AutomationPredicate.shouldIgnoreNode(node);
};
return new AutomationTreeWalker(cur, dir, restrictions);
}