// 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 Predicates for the automation extension API.
*/
import {constants} from './constants.js';
import {TestImportManager} from './testing/test_import_manager.js';
import ActionType = chrome.automation.ActionType;
import AutomationNode = chrome.automation.AutomationNode;
import DefaultActionVerb = chrome.automation.DefaultActionVerb;
import Dir = constants.Dir;
import InvalidState = chrome.automation.InvalidState;
import MarkerType = chrome.automation.MarkerType;
import Restriction = chrome.automation.Restriction;
import Role = chrome.automation.RoleType;
import State = chrome.automation.StateType;
interface MatchParams {
anyRole?: Role[];
anyPredicate?: AutomationPredicate.Unary[];
}
interface TableCellPredicateOptions {
dir?: Dir;
end?: boolean;
row?: boolean;
col?: boolean;
}
/**
* A helper to check if |node| or any descendant is actionable.
* @param sawClickAncestorAction A node during this search has a
* default action verb involving click ancestor or none.
*/
const isActionableOrHasActionableDescendant = function(
node: AutomationNode, sawClickAncestorAction: boolean = false): boolean {
// 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 !== DefaultActionVerb.CLICK_ANCESTOR ||
sawClickAncestorAction)) {
return true;
}
if (node.clickable) {
return true;
}
sawClickAncestorAction = sawClickAncestorAction || !node.defaultActionVerb ||
node.defaultActionVerb === 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. */
const hasActionableDescendant = function(node: AutomationNode): boolean {
const sawClickAncestorAction = !node.defaultActionVerb ||
node.defaultActionVerb === 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.
*/
const nodeNameContainedInStaticTextChildren = function(
node: AutomationNode): boolean {
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 (child.name === undefined) {
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 namespace AutomationPredicate {
/** Constructs a predicate given a list of roles. */
export function roles(roles: Role[]): AutomationPredicate.Unary {
return AutomationPredicate.match({anyRole: roles});
}
/** Constructs a predicate given a list of roles or predicates. */
export function match(params: MatchParams): AutomationPredicate.Unary {
const anyRole = params.anyRole || [];
const anyPredicate = params.anyPredicate || [];
return function(node: AutomationNode): boolean {
return anyRole.some(role => role === node.role) ||
anyPredicate.some(p => p(node));
};
}
export function button(node: AutomationNode): boolean {
return node.isButton;
}
export function comboBox(node: AutomationNode): boolean {
return node.isComboBox;
}
export function checkBox(node: AutomationNode): boolean {
return node.isCheckBox;
}
export function editText(node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
return node.role === Role.TEXT_FIELD ||
(node.state![State.EDITABLE] && Boolean(node.parent) &&
!node.parent!.state![State.EDITABLE]);
}
export function image(node: AutomationNode): boolean {
return node.isImage && Boolean(node.name || node.url);
}
export function visitedLink(node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
return node.state![State.VISITED];
}
export function focused(node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
return node.state![State.FOCUSED];
}
/**
* Returns true if this node should be considered a leaf for touch
* exploration.
*/
export function touchLeaf(node: AutomationNode): boolean {
return Boolean(!node.firstChild && node.name) ||
node.role === Role.BUTTON || node.role === Role.CHECK_BOX ||
node.role === Role.POP_UP_BUTTON || 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. */
export function isInvalid(node: AutomationNode): boolean {
return node.invalidState === InvalidState.TRUE ||
AutomationPredicate.hasInvalidGrammarMarker(node) ||
AutomationPredicate.hasInvalidSpellingMarker(node);
}
/** Returns true if this node has an invalid grammar marker. */
export function hasInvalidGrammarMarker(node: AutomationNode): boolean {
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. */
export function hasInvalidSpellingMarker(node: AutomationNode): boolean {
const markers = node.markers;
if (!markers) {
return false;
}
return markers.some(function(marker) {
return marker.flags[MarkerType.SPELLING];
});
}
export function leaf(node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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((n: AutomationNode) => n.state![State.INVISIBLE]) ||
AutomationPredicate.math(node));
}
export function leafWithText(node: AutomationNode): boolean {
return AutomationPredicate.leaf(node) && Boolean(node.name || node.value);
}
export function leafWithWordStop(node: AutomationNode): boolean {
function hasWordStop(node: AutomationNode): boolean {
if (node.role === Role.INLINE_TEXT_BOX) {
return Boolean(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.
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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.
*/
export function leafOrStaticText(node: AutomationNode): boolean {
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.
*/
export function object(node: AutomationNode): boolean {
// 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.
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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, we want to visit focusable
// (e.g. tabindex=0) nodes only when it has a name or is a control.
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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 ' '.
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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. */
export function touchObject(node: AutomationNode): boolean {
// 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. */
export function gestureObject(node: AutomationNode): boolean {
if (node.role === Role.LIST_BOX) {
return false;
}
return AutomationPredicate.object(node);
}
export function linebreak(
first: AutomationNode, second: AutomationNode): boolean {
if (first.nextOnLine === second) {
return false;
}
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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.
*/
export function container(node: AutomationNode): boolean {
// 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.
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
if (node.state![State.FOCUSABLE] &&
nodeNameContainedInStaticTextChildren(node)) {
return false;
}
// Do not consider containers that are clickable containers, unless they
// also contain actionable nodes.
if (node.clickable && !hasActionableDescendant(node)) {
return false;
}
// Always try to dive into subtrees with actionable descendants for some
// roles even if these roles are not naturally containers.
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
if ([
Role.BUTTON,
Role.CELL,
Role.CHECK_BOX,
Role.GRID_CELL,
Role.RADIO_BUTTON,
Role.SWITCH,
].includes(node.role!) &&
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.PDF_ROOT,
Role.LIST,
Role.LIST_ITEM,
Role.TAB,
Role.TAB_PANEL,
Role.TOOLBAR,
Role.WINDOW,
],
anyPredicate: [
AutomationPredicate.landmark,
AutomationPredicate.structuralContainer,
(node: AutomationNode) => {
// For example, crosh.
return node.role === Role.TEXT_FIELD &&
node.restriction === Restriction.READ_ONLY;
},
// TODO(b/314203187): Not null asserted, check to make sure it's
// correct.
(node: AutomationNode) => (
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.
*/
export function root(node: AutomationNode): boolean {
if (node.modal) {
return true;
}
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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 node.parent !== undefined && node.parent.role === Role.WINDOW &&
node.parent.children.every((_child: AutomationNode) =>
// TODO(b/322191528): This should be |child|, not |node|.
node.role === Role.WINDOW || node.role === Role.DIALOG);
case Role.TOOLBAR:
return node.root!.role === Role.DESKTOP &&
!(node.nextWindowFocus || !node.previousWindowFocus);
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.
*/
export function rootOrEditableRoot(node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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.
*/
export function shouldIgnoreNode(node: AutomationNode): boolean {
// Ignore invisible nodes.
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
if (node.state![State.INVISIBLE] ||
(node.location.height === 0 && node.location.width === 0)) {
return true;
}
// Ignore structural containers.
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;
}
// AXTreeSourceAndroid computes names for clickables.
// Ignore nodes for which this computation is not done
if (node.clickable && !node.name && !node.value && !node.description) {
return true;
}
// Ignore some roles.
return AutomationPredicate.leaf(node) && (AutomationPredicate.roles([
Role.CLIENT,
Role.COLUMN,
Role.GENERIC_CONTAINER,
Role.GROUP,
Role.IMAGE,
Role.PARAGRAPH,
Role.SCROLL_VIEW,
Role.STATIC_TEXT,
Role.SVG_ROOT,
Role.TABLE_HEADER_CONTAINER,
Role.UNKNOWN,
])(node));
}
/** Returns if the node has a meaningful checked state. */
export function checkable(node: AutomationNode): boolean {
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 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 Returns null if not in a table.
*/
export function makeTableCellPredicate(
start: AutomationNode,
opts: TableCellPredicateOptions): AutomationPredicate.Unary | null {
if (!opts.row && !opts.col) {
throw new Error('You must set either row or col to true');
}
const dir = opts.dir || Dir.FORWARD;
// Compute the row/col index defaulting to 0.
let rowIndex = 0;
let colIndex = 0;
let tableNode: AutomationNode | undefined = start;
while (tableNode) {
if (AutomationPredicate.table(tableNode)) {
break;
}
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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.';
}
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
if (dir === Dir.FORWARD) {
return (node: AutomationNode) => AutomationPredicate.cellLike(node) &&
node.tableCellColumnIndex === colIndex &&
node.tableCellRowIndex! >= 0;
} else {
return (node: AutomationNode) => AutomationPredicate.cellLike(node) &&
node.tableCellColumnIndex === colIndex &&
node.tableCellRowIndex! < tableNode!.tableRowCount!;
}
}
// Adjust for the next/previous row/col.
if (opts.row) {
rowIndex = dir === Dir.FORWARD ? rowIndex + 1 : rowIndex - 1;
}
if (opts.col) {
colIndex = dir === Dir.FORWARD ? colIndex + 1 : colIndex - 1;
}
return (node: AutomationNode) => AutomationPredicate.cellLike(node) &&
node.tableCellColumnIndex === colIndex &&
node.tableCellRowIndex === rowIndex;
}
/**
* Returns a predicate that will match against a heading with a specific
* hierarchical level.
* @param level 1-6
*/
export function makeHeadingPredicate(
level: number): AutomationPredicate.Unary {
return function(node: AutomationNode) {
return node.role === Role.HEADING && node.hierarchicalLevel === level;
};
}
/**
* Matches against a node that forces showing surrounding contextual
* information for braille.
*/
export function contextualBraille(node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
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.
*/
export function multiline(node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
return node.state![State.MULTILINE] || node.state![State.RICHLY_EDITABLE];
}
/** Matches against a node that should be auto-scrolled during navigation. */
export function autoScrollable(node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
return Boolean(node.scrollable) &&
(node.standardActions!.includes(ActionType.SCROLL_FORWARD) ||
node.standardActions!.includes(ActionType.SCROLL_BACKWARD)) &&
(node.role === Role.GRID || node.role === Role.LIST ||
node.role === Role.POP_UP_BUTTON || node.role === Role.SCROLL_VIEW);
}
export function math(node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
return node.role === Role.MATH ||
Boolean(node.htmlAttributes!['data-mathml']);
}
/** Matches against nodes visited during group navigation. */
export function group(node: AutomationNode): boolean {
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.
*/
export function shouldOnlyOutputSelectionChangeInBraille(
node: AutomationNode): boolean {
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
return node.state![State.RICHLY_EDITABLE] && node.state![State.FOCUSED] &&
node.role === Role.LOG;
}
/** Matches against nodes we should ignore in a jump command. */
export function ignoreDuringJump(node: AutomationNode): boolean {
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 |avoidThis| (or
* |avoidThis| itself, if it is list-like).
*/
export function makeListPredicate(
avoidThis: AutomationNode): AutomationPredicate.Unary {
// Scan upward for a list-like ancestor. We do not want to match against
// this node.
let avoidNode: AutomationNode | undefined = avoidThis;
while (avoidNode && !AutomationPredicate.listLike(avoidNode)) {
avoidNode = avoidNode.parent;
}
return function(node: AutomationNode): boolean {
return AutomationPredicate.listLike(node) && (node !== avoidNode);
};
}
export type Unary = (node: AutomationNode) => boolean;
export type Binary =
(first: AutomationNode, second: AutomationNode) => boolean;
export const heading = AutomationPredicate.roles([Role.HEADING]);
export const inlineTextBox =
AutomationPredicate.roles([Role.INLINE_TEXT_BOX]);
export const link = AutomationPredicate.roles([Role.LINK]);
export const row = AutomationPredicate.roles([Role.ROW]);
export const table =
AutomationPredicate.roles([Role.GRID, Role.LIST_GRID, Role.TABLE]);
export const listLike =
AutomationPredicate.roles([Role.LIST, Role.DESCRIPTION_LIST]);
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
export const 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],
});
export const 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,
],
});
export const control = AutomationPredicate.match({
anyPredicate: [
AutomationPredicate.formField,
],
anyRole: [
Role.DISCLOSURE_TRIANGLE,
Role.MENU_ITEM,
Role.MENU_ITEM_CHECK_BOX,
Role.MENU_ITEM_RADIO,
Role.SCROLL_BAR,
],
});
export const linkOrControl = AutomationPredicate.match(
{anyPredicate: [AutomationPredicate.control], anyRole: [Role.LINK]});
export const 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.
*/
export const structuralContainer = AutomationPredicate.roles([
Role.ALERT_DIALOG,
Role.CLIENT,
Role.DIALOG,
Role.LAYOUT_TABLE,
Role.LAYOUT_TABLE_CELL,
Role.LAYOUT_TABLE_ROW,
Role.MENU_LIST_POPUP,
Role.ROOT_WEB_AREA,
Role.WEB_VIEW,
Role.WINDOW,
Role.EMBEDDED_OBJECT,
Role.IFRAME,
Role.IFRAME_PRESENTATIONAL,
Role.PLUGIN_OBJECT,
Role.UNKNOWN,
Role.PANE,
Role.SCROLL_VIEW,
]);
export const clickable = AutomationPredicate.match({
anyPredicate: [
AutomationPredicate.button,
AutomationPredicate.link,
node => node.defaultActionVerb === DefaultActionVerb.CLICK,
node => node.clickable === true,
],
});
// TODO(b/314203187): Not null asserted, check to make sure it's correct.
export const longClickable = AutomationPredicate.match({
anyPredicate: [
node => node.standardActions!.includes(
chrome.automation.ActionType.LONG_CLICK),
// @ts-ignore Long clickable doesn't seem to be a property?
node => node.longClickable === true,
],
});
/** Returns if the node is a list option, either in a menu or a listbox. */
export const listOption =
AutomationPredicate.roles([Role.LIST_BOX_OPTION, Role.MENU_LIST_OPTION]);
// Table related predicates.
/** Returns if the node has a cell like role. */
export const cellLike = AutomationPredicate.roles(
[Role.CELL, Role.GRID_CELL, Role.ROW_HEADER, Role.COLUMN_HEADER]);
/** Returns if the node is a table header. */
export const tableHeader =
AutomationPredicate.roles([Role.ROW_HEADER, Role.COLUMN_HEADER]);
/** Matches against nodes that we may be able to retrieve image data from. */
export const supportsImageData =
AutomationPredicate.roles([Role.CANVAS, Role.IMAGE, Role.VIDEO]);
/** Matches against menu item like nodes. */
export const menuItem = AutomationPredicate.roles(
[Role.MENU_ITEM, Role.MENU_ITEM_CHECK_BOX, Role.MENU_ITEM_RADIO]);
/** Matches against text like nodes. */
export const text = AutomationPredicate.roles(
[Role.STATIC_TEXT, Role.INLINE_TEXT_BOX, Role.LINE_BREAK]);
/** Matches against selecteable text like nodes. */
export const selectableText = AutomationPredicate.roles([
Role.STATIC_TEXT,
Role.INLINE_TEXT_BOX,
Role.LINE_BREAK,
Role.LIST_MARKER,
]);
/**
* Matches against pop-up button like nodes.
* Historically, single value <select> controls were represented as a
* popup button, but they are distinct from <button aria-haspopup='menu'>.
*/
export const popUpButton = AutomationPredicate.roles([
Role.COMBO_BOX_SELECT,
Role.POP_UP_BUTTON,
]);
}
TestImportManager.exportForTesting(
['AutomationPredicate', AutomationPredicate]);