blob: bec85137d6a695e748a3fc101a1f730cbb800f68 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview A tree walker over the automation tree.
*/
goog.provide('AutomationTreeWalker');
goog.provide('AutomationTreeWalkerPhase');
goog.provide('AutomationTreeWalkerRestriction');
goog.require('constants');
/**
* Defined phases of traversal from the initial node passed to an
* AutomationTreeWalker instance.
* @enum {string}
*/
AutomationTreeWalkerPhase = {
/** Walker is on the initial node. */
INITIAL: 'initial',
/** Walker is on an ancestor of initial node. */
ANCESTOR: 'ancestor',
/** Walker is on a descendant of initial node. */
DESCENDANT: 'descendant',
/** Walker is on a node not covered by any other phase. */
OTHER: 'other'
};
/**
* @typedef {{leaf: (AutomationPredicate.Unary|undefined),
* root: (AutomationPredicate.Unary|undefined),
* visit: (AutomationPredicate.Unary|undefined),
* skipInitialAncestry: (boolean|undefined),
* skipInitialSubtree: (boolean|undefined)}}
*/
let AutomationTreeWalkerRestriction;
/**
* An AutomationTreeWalker provides an incremental pre order traversal of the
* automation tree starting at a particular node.
* Given a flat list of nodes in pre order, the walker moves forward or backward
* a node at a time on each call of |next|.
* A caller can visit a subset of this list by supplying restricting
* predicates. There are three such restricting predicates allowed:
* visit: this predicate determines if a given node should be returned when
* moving to a node in the flattened pre-order list. If not, this walker will
* continue to the next (directed) node in the list, looking for a predicate
* match.
* root: this predicate determines if a node should end upward movement in the
* tree.
* leaf: this predicate determines if a node should end downward movement in the
* tree.
* |skipInitialAncestry| skips visiting ancestor nodes of the start node for
* multiple invokations of next when moving backward.
* Finally, a boolean, |skipInitialSubtree|, makes the first invocation of
* |next| skip the initial node's subtree when finding a match. This is useful
* to establish a known initial state when the initial node may not match any of
* the given predicates.
* Given the above definitions, if supplied with a root and leaf predicate that
* always returns false, and a visit predicate that always returns true, the
* walker would visit all nodes in pre order. If a caller does not supply a
* particular predicate, it will default to these "identity" predicates.
*/
AutomationTreeWalker = class {
/**
* @param {!chrome.automation.AutomationNode} node
* @param {constants.Dir} dir
* @param {AutomationTreeWalkerRestriction=}
* opt_restrictions
*/
constructor(node, dir, opt_restrictions) {
/** @type {chrome.automation.AutomationNode} @private */
this.node_ = node;
/** @type {AutomationTreeWalkerPhase} @private */
this.phase_ = AutomationTreeWalkerPhase.INITIAL;
/** @const {constants.Dir} @private */
this.dir_ = dir;
/** @const {!chrome.automation.AutomationNode} @private */
this.initialNode_ = node;
/**
* Deepest common ancestor of initialNode and node. Valid only when moving
* backward.
* @type {chrome.automation.AutomationNode} @private
*/
this.backwardAncestor_ = node.parent || null;
const restrictions = opt_restrictions || {};
this.visitPred_ = function(node) {
if (this.skipInitialAncestry_ &&
this.phase_ === AutomationTreeWalkerPhase.ANCESTOR) {
return false;
}
if (this.skipInitialSubtree_ &&
this.phase_ !== AutomationTreeWalkerPhase.ANCESTOR &&
this.phase_ !== AutomationTreeWalkerPhase.OTHER) {
return false;
}
if (restrictions.visit) {
return restrictions.visit(node);
}
return true;
};
/** @type {AutomationPredicate.Unary} @private */
this.leafPred_ = restrictions.leaf ? restrictions.leaf :
AutomationTreeWalker.falsePredicate_;
/** @type {AutomationPredicate.Unary} @private */
this.rootPred_ = restrictions.root ? restrictions.root :
AutomationTreeWalker.falsePredicate_;
/** @const {boolean} @private */
this.skipInitialAncestry_ = restrictions.skipInitialAncestry || false;
/** @const {boolean} @private */
this.skipInitialSubtree_ = restrictions.skipInitialSubtree || false;
}
/**
* @param {!chrome.automation.AutomationNode} node
* @return {boolean}
* @private
*/
static falsePredicate_(node) {
return false;
}
/** @type {chrome.automation.AutomationNode} */
get node() {
return this.node_;
}
/** @type {AutomationTreeWalkerPhase} */
get phase() {
return this.phase_;
}
/**
* Moves this walker to the next node.
* @return {!AutomationTreeWalker} The called AutomationTreeWalker, for
* chaining.
*/
next() {
if (!this.node_) {
return this;
}
do {
if (this.rootPred_(this.node_) && this.dir_ === constants.Dir.BACKWARD) {
this.node_ = null;
return this;
}
if (this.dir_ === constants.Dir.FORWARD) {
this.forward_(this.node_);
} else {
this.backward_(this.node_);
}
} while (this.node_ && !this.visitPred_(this.node_));
return this;
}
/**
* @param {!chrome.automation.AutomationNode} node
* @private
*/
forward_(node) {
if (!this.leafPred_(node) && node.firstChild) {
if (this.phase_ === AutomationTreeWalkerPhase.INITIAL) {
this.phase_ = AutomationTreeWalkerPhase.DESCENDANT;
}
if (!this.skipInitialSubtree_ ||
this.phase_ !== AutomationTreeWalkerPhase.DESCENDANT) {
this.node_ = node.firstChild;
return;
}
}
let searchNode = node;
while (searchNode) {
// We have crossed out of the initial node's subtree for either a
// sibling or parent move.
if (searchNode === this.initialNode_) {
this.phase_ = AutomationTreeWalkerPhase.OTHER;
}
if (searchNode.nextSibling) {
this.node_ = searchNode.nextSibling;
return;
}
// Update the phase based on the parent if needed since we may exit below.
if (searchNode.parent === this.initialNode_) {
this.phase_ = AutomationTreeWalkerPhase.OTHER;
}
// Exit if we encounter a root-like node and are not searching descendants
// of the initial node.
if (searchNode.parent && this.rootPred_(searchNode.parent) &&
this.phase_ !== AutomationTreeWalkerPhase.DESCENDANT) {
break;
}
searchNode = searchNode.parent;
}
this.node_ = null;
}
/**
* @param {!chrome.automation.AutomationNode} node
* @private
*/
backward_(node) {
if (node.previousSibling) {
this.phase_ = AutomationTreeWalkerPhase.OTHER;
node = node.previousSibling;
while (!this.leafPred_(node) && node.lastChild) {
node = node.lastChild;
}
this.node_ = node;
return;
}
if (node.parent && this.backwardAncestor_ === node.parent) {
this.phase_ = AutomationTreeWalkerPhase.ANCESTOR;
this.backwardAncestor_ = node.parent.parent || null;
}
this.node_ = node.parent || null;
}
};