blob: 32e8f215dc21b5ccd0049d1f56f4d0b8319e5253 [file] [log] [blame]
// Copyright 2014 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 ChromeVox predicates for the automation extension API.
*/
const AutomationNode = chrome.automation.AutomationNode;
const InvalidState = chrome.automation.InvalidState;
const MarkerType = chrome.automation.MarkerType;
const Restriction = chrome.automation.Restriction;
const Role = chrome.automation.RoleType;
const State = chrome.automation.StateType;
/**
* A helper to check if |node| or any descendant is actionable.
* @param {!AutomationNode} node
* @param {boolean} sawClickAncestorAction A node during this search has a
* default action verb involving click ancestor or none.
* @return {boolean}
*/
const isActionableOrHasActionableDescendant = function(
node, sawClickAncestorAction = false) {
// Static text nodes are never actionable for the purposes of navigation even
// if they have default action verb set.
if (node.role !== Role.STATIC_TEXT && node.defaultActionVerb &&
(node.defaultActionVerb !==
chrome.automation.DefaultActionVerb.CLICK_ANCESTOR ||
sawClickAncestorAction)) {
return true;
}
if (node.clickable) {
return true;
}
sawClickAncestorAction = sawClickAncestorAction || !node.defaultActionVerb ||
node.defaultActionVerb ===
chrome.automation.DefaultActionVerb.CLICK_ANCESTOR;
for (let i = 0; i < node.children.length; i++) {
if (isActionableOrHasActionableDescendant(
node.children[i], sawClickAncestorAction)) {
return true;
}
}
return false;
};
/**
* A helper to check if any descendants of |node| are actionable.
* @param {!AutomationNode} node
* @return {boolean}
*/
const hasActionableDescendant = function(node) {
const sawClickAncestorAction = !node.defaultActionVerb ||
node.defaultActionVerb ===
chrome.automation.DefaultActionVerb.CLICK_ANCESTOR;
for (let i = 0; i < node.children.length; i++) {
if (isActionableOrHasActionableDescendant(
node.children[i], sawClickAncestorAction)) {
return true;
}
}
return false;
};
/**
* A helper to determine whether the children of a node are all
* STATIC_TEXT, and whether the joined names of such children nodes are equal to
* the current nodes name.
* @param {!AutomationNode} node
* @return {boolean}
* @private
*/
const nodeNameContainedInStaticTextChildren = function(node) {
const name = node.name;
let child = node.firstChild;
if (name === undefined || !child) {
return false;
}
let nameIndex = 0;
do {
if (child.role !== Role.STATIC_TEXT) {
return false;
}
if (name.substring(nameIndex, nameIndex + child.name.length) !==
child.name) {
return false;
}
nameIndex += child.name.length;
// Either space or empty (i.e. end of string).
const char = name.substring(nameIndex, nameIndex + 1);
child = child.nextSibling;
if ((child && char !== ' ') || char !== '') {
return false;
}
nameIndex++;
} while (child);
return true;
};
export class AutomationPredicate {
constructor() {}
/**
* Constructs a predicate given a list of roles.
* @param {!Array<Role>} roles
* @return {!AutomationPredicate.Unary}
*/
static roles(roles) {
return AutomationPredicate.match({anyRole: roles});
}
/**
* Constructs a predicate given a list of roles or predicates.
* @param {{anyRole: (Array<Role>|undefined),
* anyPredicate: (Array<AutomationPredicate.Unary>|undefined),
* anyAttribute: (Object|undefined)}} params
* @return {!AutomationPredicate.Unary}
*/
static match(params) {
const anyRole = params.anyRole || [];
const anyPredicate = params.anyPredicate || [];
const anyAttribute = params.anyAttribute || {};
return function(node) {
return anyRole.some(function(role) {
return role === node.role;
}) ||
anyPredicate.some(function(p) {
return p(node);
}) ||
Object.keys(anyAttribute).some(function(key) {
return node[key] === anyAttribute[key];
});
};
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static button(node) {
return node.isButton;
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static comboBox(node) {
return node.isComboBox;
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static checkBox(node) {
return node.isCheckBox;
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static editText(node) {
return node.role === Role.TEXT_FIELD ||
(node.state[State.EDITABLE] && Boolean(node.parent) &&
!node.parent.state[State.EDITABLE]);
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static image(node) {
return node.isImage && Boolean(node.name || node.url);
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static visitedLink(node) {
return node.state[State.VISITED];
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static focused(node) {
return node.state[State.FOCUSED];
}
/**
* Returns true if this node should be considered a leaf for touch
* exploration.
* @param {!AutomationNode} node
* @return {boolean}
*/
static touchLeaf(node) {
return Boolean(!node.firstChild && node.name) ||
node.role === Role.BUTTON || node.role === Role.CHECK_BOX ||
node.role === Role.POP_UP_BUTTON || node.role === Role.PORTAL ||
node.role === Role.RADIO_BUTTON || node.role === Role.SLIDER ||
node.role === Role.SWITCH || node.role === Role.TEXT_FIELD ||
node.role === Role.TEXT_FIELD_WITH_COMBO_BOX ||
(node.role === Role.MENU_ITEM && !hasActionableDescendant(node)) ||
AutomationPredicate.image(node) ||
// Simple list items should be leaves.
AutomationPredicate.simpleListItem(node);
}
/**
* Returns true if this node is marked as invalid.
* @param {!AutomationNode} node
* @return {boolean}
*/
static isInvalid(node) {
return node.invalidState === InvalidState.TRUE ||
AutomationPredicate.hasInvalidGrammarMarker(node) ||
AutomationPredicate.hasInvalidSpellingMarker(node);
}
/**
* Returns true if this node has an invalid grammar marker.
* @param {!AutomationNode} node
* @return {boolean}
*/
static hasInvalidGrammarMarker(node) {
const markers = node.markers;
if (!markers) {
return false;
}
return markers.some(function(marker) {
return marker.flags[MarkerType.GRAMMAR];
});
}
/**
* Returns true if this node has an invalid spelling marker.
* @param {!AutomationNode} node
* @return {boolean}
*/
static hasInvalidSpellingMarker(node) {
const markers = node.markers;
if (!markers) {
return false;
}
return markers.some(function(marker) {
return marker.flags[MarkerType.SPELLING];
});
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static leaf(node) {
return Boolean(
AutomationPredicate.touchLeaf(node) || node.role === Role.LIST_BOX ||
// A node acting as a label should be a leaf if it has no actionable
// controls.
(node.labelFor && node.labelFor.length > 0 &&
!isActionableOrHasActionableDescendant(node)) ||
(node.descriptionFor && node.descriptionFor.length > 0 &&
!isActionableOrHasActionableDescendant(node)) ||
(node.activeDescendantFor && node.activeDescendantFor.length > 0) ||
node.state[State.INVISIBLE] || node.children.every(function(n) {
return n.state[State.INVISIBLE];
}) ||
AutomationPredicate.math(node));
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static leafWithText(node) {
return AutomationPredicate.leaf(node) && Boolean(node.name || node.value);
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static leafWithWordStop(node) {
function hasWordStop(node) {
if (node.role === Role.INLINE_TEXT_BOX) {
return node.wordStarts && node.wordStarts.length;
}
// Non-text objects are treated as having a single word stop.
return true;
}
// Do not include static text leaves, which occur for an en end-of-line.
return AutomationPredicate.leaf(node) && !node.state[State.INVISIBLE] &&
node.role !== Role.STATIC_TEXT && hasWordStop(node);
}
/**
* Matches against leaves or static text nodes. Useful when restricting
* traversal to non-inline textboxes while still allowing them if navigation
* already entered into an inline textbox.
* @param {!AutomationNode} node
* @return {boolean}
*/
static leafOrStaticText(node) {
return AutomationPredicate.leaf(node) || node.role === Role.STATIC_TEXT;
}
/**
* Matches against nodes visited during object navigation. An object as
* defined below, are all nodes that are focusable or static text. When used
* in tree walking, it should visit all nodes that tab traversal would as well
* as non-focusable static text.
* @param {!AutomationNode} node
* @return {boolean}
*/
static object(node) {
// Editable nodes are within a text-like field and don't make sense when
// performing object navigation. Users should use line, word, or character
// navigation. Only navigate to the top level node.
if (node.parent && node.parent.state[State.EDITABLE] &&
!node.parent.state[State.RICHLY_EDITABLE]) {
return false;
}
// Things explicitly marked clickable (used only on ARC++) should be
// visited.
if (node.clickable) {
return true;
}
// Given no other information, ChromeVox wants to visit focusable
// (e.g. tabindex=0) nodes only when it has a name or is a control.
if (node.state[State.FOCUSABLE] &&
(node.name || node.state[State.EDITABLE] ||
AutomationPredicate.formField(node))) {
return true;
}
// Containers who have name from contents should be treated like objects if
// the contents is all static text and not large.
if (node.name && node.nameFrom === 'contents') {
let onlyStaticText = true;
let textLength = 0;
for (let i = 0, child; child = node.children[i]; i++) {
if (child.role !== Role.STATIC_TEXT) {
onlyStaticText = false;
break;
}
textLength += child.name ? child.name.length : 0;
}
if (onlyStaticText && textLength > 0 &&
textLength < constants.OBJECT_MAX_CHARCOUNT) {
return true;
}
}
// Otherwise, leaf or static text nodes that don't contain only whitespace
// should be visited with the exception of non-text only nodes. This covers
// cases where an author might make a link with a name of ' '.
return AutomationPredicate.leafOrStaticText(node) &&
(/\S+/.test(node.name) ||
(node.role !== Role.LINE_BREAK && node.role !== Role.STATIC_TEXT &&
node.role !== Role.INLINE_TEXT_BOX));
}
/**
* Matches against nodes visited during touch exploration.
* @param {!AutomationNode} node
* @return {boolean}
*/
static touchObject(node) {
// Exclude large objects such as containers.
if (AutomationPredicate.container(node)) {
return false;
}
return AutomationPredicate.object(node);
}
/**
* Matches against nodes visited during object navigation with a gesture.
* @param {!AutomationNode} node
* @return {boolean}
*/
static gestureObject(node) {
if (node.role === Role.LIST_BOX) {
return false;
}
return AutomationPredicate.object(node);
}
/**
* @param {!AutomationNode} first
* @param {!AutomationNode} second
* @return {boolean}
*/
static linebreak(first, second) {
if (first.nextOnLine === second) {
return false;
}
const fl = first.unclippedLocation;
const sl = second.unclippedLocation;
return fl.top !== sl.top || (fl.top + fl.height !== sl.top + sl.height);
}
/**
* Matches against a node that contains other interesting nodes.
* These nodes should always have their subtrees scanned when navigating.
* @param {!AutomationNode} node
* @return {boolean}
*/
static container(node) {
// Math is never a container.
if (AutomationPredicate.math(node)) {
return false;
}
// Sometimes a focusable node will have a static text child with the same
// name. During object navigation, the child will receive focus, resulting
// in the name being read out twice.
if (node.state[State.FOCUSABLE] &&
nodeNameContainedInStaticTextChildren(node)) {
return false;
}
// Always try to dive into subtrees with actionable descendants for some
// roles even if these roles are not naturally containers.
if ((node.role === Role.BUTTON || node.role === Role.CHECK_BOX ||
node.role === Role.RADIO_BUTTON || node.role === Role.SWITCH) &&
hasActionableDescendant(node)) {
return true;
}
// Simple list items are not containers.
if (AutomationPredicate.simpleListItem(node)) {
return false;
}
return AutomationPredicate.match({
anyRole: [
Role.GENERIC_CONTAINER,
Role.DOCUMENT,
Role.GROUP,
Role.LIST,
Role.LIST_ITEM,
Role.TAB,
Role.TAB_PANEL,
Role.TOOLBAR,
Role.WINDOW,
],
anyPredicate: [
AutomationPredicate.landmark,
AutomationPredicate.structuralContainer,
function(node) {
// For example, crosh.
return node.role === Role.TEXT_FIELD &&
node.restriction === Restriction.READ_ONLY;
},
function(node) {
return (
node.state[State.EDITABLE] && node.parent &&
!node.parent.state[State.EDITABLE]);
},
],
})(node);
}
/**
* Returns whether the given node should not be crossed when performing
* traversals up the ancestry chain.
* @param {AutomationNode} node
* @return {boolean}
*/
static root(node) {
if (node.modal) {
return true;
}
switch (node.role) {
case Role.WINDOW:
return true;
case Role.DIALOG:
if (node.root.role !== Role.DESKTOP) {
return Boolean(node.modal);
}
// The below logic handles nested dialogs properly in the desktop tree
// like that found in a bubble view.
return Boolean(node.parent) && node.parent.role === Role.WINDOW &&
node.parent.children.every(function(child) {
return node.role === Role.WINDOW || node.role === Role.DIALOG;
});
case Role.TOOLBAR:
return node.root.role === Role.DESKTOP &&
!(node.nextFocus || !node.previousFocus);
case Role.ROOT_WEB_AREA:
if (node.parent && node.parent.role === Role.WEB_VIEW &&
!node.parent.state[State.FOCUSED]) {
// If parent web view is not focused, we should allow this root web
// area to be crossed when performing traversals up the ancestry
// chain.
return false;
}
return !node.parent || !node.parent.root ||
(node.parent.root.role === Role.DESKTOP &&
node.parent.role === Role.WEB_VIEW);
default:
return false;
}
}
/**
* Returns whether the given node should not be crossed when performing
* traversal inside of an editable. Note that this predicate should not be
* applied everywhere since there would be no way for a user to exit the
* editable.
* @param {AutomationNode} node
* @return {boolean}
*/
static rootOrEditableRoot(node) {
return AutomationPredicate.root(node) ||
(node.state[State.RICHLY_EDITABLE] && node.state[State.FOCUSED] &&
node.children.length > 0);
}
/**
* Nodes that should be ignored while traversing the automation tree. For
* example, apply this predicate when moving to the next object.
* @param {!AutomationNode} node
* @return {boolean}
*/
static shouldIgnoreNode(node) {
// Ignore invisible nodes.
if (node.state[State.INVISIBLE] ||
(node.location.height === 0 && node.location.width === 0)) {
return true;
}
// Ignore structural containres.
if (AutomationPredicate.structuralContainer(node)) {
return true;
}
// Ignore nodes acting as labels for another control, that are unambiguously
// labels.
if (node.labelFor && node.labelFor.length > 0 &&
node.role === Role.LABEL_TEXT) {
return true;
}
// Similarly, ignore nodes acting as descriptions.
if (node.descriptionFor && node.descriptionFor.length > 0 &&
node.role === Role.LABEL_TEXT) {
return true;
}
// Ignore list markers that are followed by a static text.
// The bullet will be added before the static text (or static text's inline
// text box) in output.js.
if (node.role === Role.LIST_MARKER && node.nextSibling &&
node.nextSibling.role === Role.STATIC_TEXT) {
return true;
}
// Don't ignore nodes with names or name-like attribute.
if (node.name || node.value || node.description || node.url) {
return false;
}
// Don't ignore math nodes.
if (AutomationPredicate.math(node)) {
return false;
}
// Ignore some roles.
return AutomationPredicate.leaf(node) && (AutomationPredicate.roles([
Role.CLIENT,
Role.COLUMN,
Role.GENERIC_CONTAINER,
Role.GROUP,
Role.IMAGE,
Role.PARAGRAPH,
Role.STATIC_TEXT,
Role.SVG_ROOT,
Role.TABLE_HEADER_CONTAINER,
Role.UNKNOWN,
])(node));
}
/**
* Returns if the node has a meaningful checked state.
* @param {!AutomationNode} node
* @return {boolean}
*/
static checkable(node) {
return Boolean(node.checked);
}
/**
* Returns a predicate that will match against the directed next cell taking
* into account the current ancestor cell's position in the table.
* @param {AutomationNode} start
* @param {{dir: (constants.Dir|undefined),
* row: (boolean|undefined),
* col: (boolean|undefined)}} opts
* |dir|, specifies direction for |row or/and |col| movement by one cell.
* |dir| defaults to forward.
* |row| and |col| are both false by default.
* |end| defaults to false. If set to true, |col| must also be set to
* true. It will then return the first or last cell in the current column.
* @return {?AutomationPredicate.Unary} Returns null if not in a table.
*/
static makeTableCellPredicate(start, opts) {
if (!opts.row && !opts.col) {
throw new Error('You must set either row or col to true');
}
const dir = opts.dir || constants.Dir.FORWARD;
// Compute the row/col index defaulting to 0.
let rowIndex = 0;
let colIndex = 0;
let tableNode = start;
while (tableNode) {
if (AutomationPredicate.table(tableNode)) {
break;
}
if (AutomationPredicate.cellLike(tableNode)) {
rowIndex = tableNode.tableCellRowIndex;
colIndex = tableNode.tableCellColumnIndex;
}
tableNode = tableNode.parent;
}
if (!tableNode) {
return null;
}
// Only support making a predicate for column ends.
if (opts.end) {
if (!opts.col) {
throw 'Unsupported option.';
}
if (dir === constants.Dir.FORWARD) {
return function(node) {
return AutomationPredicate.cellLike(node) &&
node.tableCellColumnIndex === colIndex &&
node.tableCellRowIndex >= 0;
};
} else {
return function(node) {
return AutomationPredicate.cellLike(node) &&
node.tableCellColumnIndex === colIndex &&
node.tableCellRowIndex < tableNode.tableRowCount;
};
}
}
// Adjust for the next/previous row/col.
if (opts.row) {
rowIndex = dir === constants.Dir.FORWARD ? rowIndex + 1 : rowIndex - 1;
}
if (opts.col) {
colIndex = dir === constants.Dir.FORWARD ? colIndex + 1 : colIndex - 1;
}
return function(node) {
return AutomationPredicate.cellLike(node) &&
node.tableCellColumnIndex === colIndex &&
node.tableCellRowIndex === rowIndex;
};
}
/**
* Returns a predicate that will match against a heading with a specific
* hierarchical level.
* @param {number} level 1-6
* @return {AutomationPredicate.Unary}
*/
static makeHeadingPredicate(level) {
return function(node) {
return node.role === Role.HEADING && node.hierarchicalLevel === level;
};
}
/**
* Matches against a node that forces showing surrounding contextual
* information for braille.
* @param {!AutomationNode} node
* @return {boolean}
*/
static contextualBraille(node) {
return node.parent != null &&
((node.parent.role === Role.ROW &&
AutomationPredicate.cellLike(node)) ||
(node.parent.role === Role.TREE &&
node.parent.state[State.HORIZONTAL]));
}
/**
* Matches against a node that handles multi line key commands.
* @param {!AutomationNode} node
* @return {boolean}
*/
static multiline(node) {
return node.state[State.MULTILINE] || node.state[State.RICHLY_EDITABLE];
}
/**
* Matches against a node that should be auto-scrolled during navigation.
* @param {!AutomationNode} node
* @return {boolean}
*/
static autoScrollable(node) {
return Boolean(node.scrollable) &&
(node.standardActions.includes(
chrome.automation.ActionType.SCROLL_FORWARD) ||
node.standardActions.includes(
chrome.automation.ActionType.SCROLL_BACKWARD)) &&
(node.role === Role.GRID || node.role === Role.LIST ||
node.role === Role.POP_UP_BUTTON || node.role === Role.SCROLL_VIEW);
}
/**
* @param {!AutomationNode} node
* @return {boolean}
*/
static math(node) {
return node.role === Role.MATH ||
Boolean(node.htmlAttributes['data-mathml']);
}
/**
* Matches against nodes visited during group navigation.
* @param {!AutomationNode} node
* @return {boolean}
*/
static group(node) {
if (AutomationPredicate.text(node) || node.display === 'inline') {
return false;
}
return AutomationPredicate.match({
anyRole: [Role.HEADING, Role.LIST, Role.PARAGRAPH],
anyPredicate: [
AutomationPredicate.editText,
AutomationPredicate.formField,
AutomationPredicate.object,
AutomationPredicate.table,
],
})(node);
}
/**
* Matches against editable nodes, that should not be treated in the usual
* fashion.
* Instead, only output the contents around the selection in braille.
* @param {!AutomationNode} node
* @return {boolean}
*/
static shouldOnlyOutputSelectionChangeInBraille(node) {
return node.state[State.RICHLY_EDITABLE] && node.state[State.FOCUSED] &&
node.role === Role.LOG;
}
/**
* Matches against nodes we should ignore in a jump command.
* @param {!AutomationNode} node
* @return {boolean}
*/
static ignoreDuringJump(node) {
return node.role === Role.GENERIC_CONTAINER ||
node.role === Role.STATIC_TEXT || node.role === Role.INLINE_TEXT_BOX;
}
/**
* Returns a predicate that will match against a list-like node. The returned
* predicate should not match the first list-like ancestor of |node| (or
* |node| itself, if it is list-like).
* @param {AutomationNode} node
* @return {AutomationPredicate.Unary}
*/
static makeListPredicate(node) {
// Scan upward for a list-like ancestor. We do not want to match against
// this node.
let avoidNode = node;
while (avoidNode && !AutomationPredicate.listLike(avoidNode)) {
avoidNode = avoidNode.parent;
}
return function(autoNode) {
return AutomationPredicate.listLike(autoNode) && (autoNode !== avoidNode);
};
}
}
/**
* @typedef {function(!AutomationNode) : boolean}
*/
AutomationPredicate.Unary;
/**
* @typedef {function(!AutomationNode,
* !AutomationNode) : boolean}
*/
AutomationPredicate.Binary;
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.heading = AutomationPredicate.roles([Role.HEADING]);
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.inlineTextBox =
AutomationPredicate.roles([Role.INLINE_TEXT_BOX]);
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.link = AutomationPredicate.roles([Role.LINK]);
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.row = AutomationPredicate.roles([Role.ROW]);
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.table =
AutomationPredicate.roles([Role.GRID, Role.LIST_GRID, Role.TABLE]);
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.listLike =
AutomationPredicate.roles([Role.LIST, Role.DESCRIPTION_LIST]);
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.simpleListItem = AutomationPredicate.match({
anyPredicate:
[node => node.role === Role.LIST_ITEM && node.children.length === 2 &&
node.firstChild.role === Role.LIST_MARKER &&
node.lastChild.role === Role.STATIC_TEXT],
});
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.formField = AutomationPredicate.match({
anyPredicate: [
AutomationPredicate.button,
AutomationPredicate.comboBox,
AutomationPredicate.editText,
],
anyRole: [
Role.CHECK_BOX,
Role.COLOR_WELL,
Role.LIST_BOX,
Role.SLIDER,
Role.SWITCH,
Role.TAB,
Role.TREE,
],
});
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.control = AutomationPredicate.match({
anyPredicate: [
AutomationPredicate.formField,
],
anyRole: [
Role.DISCLOSURE_TRIANGLE,
Role.MENU_ITEM,
Role.MENU_ITEM_CHECK_BOX,
Role.MENU_ITEM_RADIO,
Role.MENU_LIST_OPTION,
Role.SCROLL_BAR,
],
});
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.linkOrControl = AutomationPredicate.match(
{anyPredicate: [AutomationPredicate.control], anyRole: [Role.LINK]});
/** @type {AutomationPredicate.Unary} */
AutomationPredicate.landmark = AutomationPredicate.roles([
Role.APPLICATION,
Role.BANNER,
Role.COMPLEMENTARY,
Role.CONTENT_INFO,
Role.FORM,
Role.MAIN,
Role.NAVIGATION,
Role.REGION,
Role.SEARCH,
]);
/**
* Matches against nodes that contain interesting nodes, but should never be
* visited.
* @param {!AutomationNode} node
* @return {boolean}
*/
AutomationPredicate.structuralContainer = AutomationPredicate.roles([
Role.ALERT_DIALOG,
Role.CLIENT,
Role.DIALOG,
Role.LAYOUT_TABLE,
Role.LAYOUT_TABLE_CELL,
Role.LAYOUT_TABLE_ROW,
Role.ROOT_WEB_AREA,
Role.WEB_VIEW,
Role.WINDOW,
Role.EMBEDDED_OBJECT,
Role.IFRAME,
Role.IFRAME_PRESENTATIONAL,
Role.PLUGIN_OBJECT,
Role.UNKNOWN,
Role.PANE,
]);
/**
* Returns if the node is clickable.
* @param {!AutomationNode} node
* @return {boolean}
*/
AutomationPredicate.clickable = AutomationPredicate.match({
anyPredicate: [
AutomationPredicate.button,
AutomationPredicate.link,
node => {
return node.defaultActionVerb ===
chrome.automation.DefaultActionVerb.CLICK;
},
],
anyAttribute: {clickable: true},
});
// Table related predicates.
/**
* Returns if the node has a cell like role.
* @param {!AutomationNode} node
* @return {boolean}
*/
AutomationPredicate.cellLike =
AutomationPredicate.roles([Role.CELL, Role.ROW_HEADER, Role.COLUMN_HEADER]);
/**
* Matches against nodes that we may be able to retrieve image data from.
* @param {!AutomationNode} node
* @return {boolean}
*/
AutomationPredicate.supportsImageData =
AutomationPredicate.roles([Role.CANVAS, Role.IMAGE, Role.VIDEO]);
/**
* Matches against menu item like nodes.
* @param {!AutomationNode} node
* @return {boolean}
*/
AutomationPredicate.menuItem = AutomationPredicate.roles(
[Role.MENU_ITEM, Role.MENU_ITEM_CHECK_BOX, Role.MENU_ITEM_RADIO]);
/**
* Matches against text like nodes.
* @param {!AutomationNode} node
* @return {boolean}
*/
AutomationPredicate.text = AutomationPredicate.roles(
[Role.STATIC_TEXT, Role.INLINE_TEXT_BOX, Role.LINE_BREAK]);
/**
* Matches against selecteable text like nodes.
* @param {!AutomationNode} node
* @return {boolean}
*/
AutomationPredicate.selectableText = AutomationPredicate.roles([
Role.STATIC_TEXT,
Role.INLINE_TEXT_BOX,
Role.LINE_BREAK,
Role.LIST_MARKER,
]);