Commit 83031cb2 authored by Anastasia Helfinstein's avatar Anastasia Helfinstein Committed by Commit Bot

[Switch Access] Avoid recalculating values repeatedly

Switch Access has been experiencing performance issues on moderate to
large webpages. A large portion of this is due to the inefficiencies of
its traversal of the automation tree, recalculating the same values
sometimes hundreds of times for nodes deep in the tree.

This change utilizes techniques from dynamic programming to persist
intermediate values for continued use, reducing the asymptotic worst-
case run time from exponential in the depth of the node to linear in
the number of nodes in the subtree.

AX-Relnotes: n/a.
Bug: 1109970
Change-Id: I61368292f2e731245e4f733c2883b86293d8a74c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2360340Reviewed-by: default avatarAbigail Klein <abigailbklein@google.com>
Commit-Queue: Anastasia Helfinstein <anastasi@google.com>
Cr-Commit-Position: refs/heads/master@{#799856}
parent 6b44cb09
...@@ -27,6 +27,7 @@ run_jsbundler("switch_access_copied_files") { ...@@ -27,6 +27,7 @@ run_jsbundler("switch_access_copied_files") {
sources = [ sources = [
"auto_scan_manager.js", "auto_scan_manager.js",
"background.js", "background.js",
"cache.js",
"commands.js", "commands.js",
"event_helper.js", "event_helper.js",
"focus_ring_manager.js", "focus_ring_manager.js",
...@@ -133,6 +134,7 @@ js_type_check("closure_compile") { ...@@ -133,6 +134,7 @@ js_type_check("closure_compile") {
":auto_scan_manager", ":auto_scan_manager",
":back_button_node", ":back_button_node",
":background", ":background",
":cache",
":combo_box_node", ":combo_box_node",
":commands", ":commands",
":desktop_node", ":desktop_node",
...@@ -188,6 +190,10 @@ js_library("back_button_node") { ...@@ -188,6 +190,10 @@ js_library("back_button_node") {
] ]
} }
js_library("cache") {
externs_list = [ "$externs_path/automation.js" ]
}
js_library("combo_box_node") { js_library("combo_box_node") {
sources = [ "nodes/combo_box_node.js" ] sources = [ "nodes/combo_box_node.js" ]
deps = [ deps = [
...@@ -256,6 +262,7 @@ js_library("group_node") { ...@@ -256,6 +262,7 @@ js_library("group_node") {
js_library("history") { js_library("history") {
deps = [ deps = [
":cache",
":desktop_node", ":desktop_node",
":node_wrapper", ":node_wrapper",
":switch_access_node", ":switch_access_node",
...@@ -338,6 +345,7 @@ js_library("node_wrapper") { ...@@ -338,6 +345,7 @@ js_library("node_wrapper") {
sources = [ "nodes/node_wrapper.js" ] sources = [ "nodes/node_wrapper.js" ]
deps = [ deps = [
":back_button_node", ":back_button_node",
":cache",
":switch_access_constants", ":switch_access_constants",
":switch_access_node", ":switch_access_node",
":switch_access_predicate", ":switch_access_predicate",
...@@ -404,6 +412,7 @@ js_library("switch_access_node") { ...@@ -404,6 +412,7 @@ js_library("switch_access_node") {
js_library("switch_access_predicate") { js_library("switch_access_predicate") {
deps = [ deps = [
":cache",
":switch_access_constants", ":switch_access_constants",
":switch_access_node", ":switch_access_node",
"../common:automation_predicate", "../common:automation_predicate",
......
// Copyright 2020 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.
/**
* Saves computed values to avoid recalculating them repeatedly.
*
* Caches are single-use, and abandoned after the top-level question is answered
* (e.g. what are all the interesting descendants of this node?)
*/
class SACache {
constructor() {
/** @private {!Map<!AutomationNode, boolean>} */
this.isActionableMap_ = new Map();
/** @private {!Map<!AutomationNode, boolean>} */
this.isGroupMap_ = new Map();
/** @private {!Map<!AutomationNode, boolean>} */
this.isInterestingSubtreeMap_ = new Map();
}
/** @return {!Map<!AutomationNode, boolean>} */
get isActionable() {
return this.isActionableMap_;
}
/** @return {!Map<!AutomationNode, boolean>} */
get isGroup() {
return this.isGroupMap_;
}
/** @return {!Map<!AutomationNode, boolean>} */
get isInterestingSubtree() {
return this.isInterestingSubtreeMap_;
}
}
...@@ -45,6 +45,7 @@ class FocusHistory { ...@@ -45,6 +45,7 @@ class FocusHistory {
// No ancestors, cannot create stack. // No ancestors, cannot create stack.
return false; return false;
} }
const cache = new SACache();
// Create a list of ancestors. // Create a list of ancestors.
const ancestorStack = [node]; const ancestorStack = [node];
while (node.parent) { while (node.parent) {
...@@ -54,7 +55,7 @@ class FocusHistory { ...@@ -54,7 +55,7 @@ class FocusHistory {
let group = DesktopNode.build(ancestorStack.pop()); let group = DesktopNode.build(ancestorStack.pop());
const firstAncestor = ancestorStack[ancestorStack.length - 1]; const firstAncestor = ancestorStack[ancestorStack.length - 1];
if (!SwitchAccessPredicate.isInterestingSubtree(firstAncestor)) { if (!SwitchAccessPredicate.isInterestingSubtree(firstAncestor, cache)) {
// If the topmost ancestor (other than the desktop) is entirely // If the topmost ancestor (other than the desktop) is entirely
// uninteresting, we leave the history as is. // uninteresting, we leave the history as is.
return false; return false;
...@@ -63,7 +64,7 @@ class FocusHistory { ...@@ -63,7 +64,7 @@ class FocusHistory {
const newDataStack = []; const newDataStack = [];
while (ancestorStack.length > 0) { while (ancestorStack.length > 0) {
const candidate = ancestorStack.pop(); const candidate = ancestorStack.pop();
if (!SwitchAccessPredicate.isInteresting(candidate, group)) { if (!SwitchAccessPredicate.isInteresting(candidate, group, cache)) {
continue; continue;
} }
......
...@@ -14,12 +14,14 @@ SwitchAccessNavigationManagerTest = class extends SwitchAccessE2ETest { ...@@ -14,12 +14,14 @@ SwitchAccessNavigationManagerTest = class extends SwitchAccessE2ETest {
} }
moveToPageContents(pageContents) { moveToPageContents(pageContents) {
if (!SwitchAccessPredicate.isGroup(pageContents)) { const cache = new SACache();
pageContents = new AutomationTreeWalker( if (!SwitchAccessPredicate.isGroup(pageContents, null, cache)) {
pageContents, constants.Dir.FORWARD, pageContents =
{visit: SwitchAccessPredicate.isGroup}) new AutomationTreeWalker(pageContents, constants.Dir.FORWARD, {
.next() visit: (node) => SwitchAccessPredicate.isGroup(node, null, cache)
.node; })
.next()
.node;
} }
assertNotNullNorUndefined( assertNotNullNorUndefined(
pageContents, 'Could not find group corresponding to page contents'); pageContents, 'Could not find group corresponding to page contents');
......
...@@ -102,7 +102,8 @@ class NodeWrapper extends SAChildNode { ...@@ -102,7 +102,8 @@ class NodeWrapper extends SAChildNode {
/** @override */ /** @override */
isGroup() { isGroup() {
return SwitchAccessPredicate.isGroup(this.baseNode_, this.parent_); const cache = new SACache();
return SwitchAccessPredicate.isGroup(this.baseNode_, this.parent_, cache);
} }
/** @override */ /** @override */
......
...@@ -27,9 +27,14 @@ const SwitchAccessPredicate = { ...@@ -27,9 +27,14 @@ const SwitchAccessPredicate = {
* it in some way. * it in some way.
* *
* @param {!AutomationNode} node * @param {!AutomationNode} node
* @param {!SACache} cache
* @return {boolean} * @return {boolean}
*/ */
isActionable: (node) => { isActionable: (node, cache) => {
if (cache.isActionable.has(node)) {
return cache.isActionable.get(node);
}
const defaultActionVerb = node.defaultActionVerb; const defaultActionVerb = node.defaultActionVerb;
const loc = node.location; const loc = node.location;
const parent = node.parent; const parent = node.parent;
...@@ -39,16 +44,19 @@ const SwitchAccessPredicate = { ...@@ -39,16 +44,19 @@ const SwitchAccessPredicate = {
// Skip things that are offscreen or invisible. // Skip things that are offscreen or invisible.
if (!SwitchAccessPredicate.isVisible(node)) { if (!SwitchAccessPredicate.isVisible(node)) {
cache.isActionable.set(node, false);
return false; return false;
} }
// Skip things that are disabled. // Skip things that are disabled.
if (node.restriction === chrome.automation.Restriction.DISABLED) { if (node.restriction === chrome.automation.Restriction.DISABLED) {
cache.isActionable.set(node, false);
return false; return false;
} }
// These web containers are not directly actionable. // These web containers are not directly actionable.
if (role === RoleType.WEB_VIEW || role === RoleType.ROOT_WEB_AREA) { if (role === RoleType.WEB_VIEW || role === RoleType.ROOT_WEB_AREA) {
cache.isActionable.set(node, false);
return false; return false;
} }
...@@ -57,17 +65,20 @@ const SwitchAccessPredicate = { ...@@ -57,17 +65,20 @@ const SwitchAccessPredicate = {
// Work around for browser tabs. // Work around for browser tabs.
if (role === RoleType.TAB && parent.role === RoleType.TAB_LIST && if (role === RoleType.TAB && parent.role === RoleType.TAB_LIST &&
root.role === RoleType.DESKTOP) { root.role === RoleType.DESKTOP) {
cache.isActionable.set(node, true);
return true; return true;
} }
} }
// Check various indicators that the node is actionable. // Check various indicators that the node is actionable.
if (role === RoleType.BUTTON || role === RoleType.SLIDER) { if (role === RoleType.BUTTON || role === RoleType.SLIDER) {
cache.isActionable.set(node, true);
return true; return true;
} }
if (AutomationPredicate.comboBox(node) || if (AutomationPredicate.comboBox(node) ||
SwitchAccessPredicate.isTextInput(node)) { SwitchAccessPredicate.isTextInput(node)) {
cache.isActionable.set(node, true);
return true; return true;
} }
...@@ -78,11 +89,13 @@ const SwitchAccessPredicate = { ...@@ -78,11 +89,13 @@ const SwitchAccessPredicate = {
defaultActionVerb === DefaultActionVerb.PRESS || defaultActionVerb === DefaultActionVerb.PRESS ||
defaultActionVerb === DefaultActionVerb.SELECT || defaultActionVerb === DefaultActionVerb.SELECT ||
defaultActionVerb === DefaultActionVerb.UNCHECK)) { defaultActionVerb === DefaultActionVerb.UNCHECK)) {
cache.isActionable.set(node, true);
return true; return true;
} }
if (role === RoleType.LIST_ITEM && if (role === RoleType.LIST_ITEM &&
defaultActionVerb === DefaultActionVerb.CLICK) { defaultActionVerb === DefaultActionVerb.CLICK) {
cache.isActionable.set(node, true);
return true; return true;
} }
...@@ -91,7 +104,10 @@ const SwitchAccessPredicate = { ...@@ -91,7 +104,10 @@ const SwitchAccessPredicate = {
// Current heuristic is to show as actionble any focusable item where no // Current heuristic is to show as actionble any focusable item where no
// child is an interesting subtree. // child is an interesting subtree.
if (state[StateType.FOCUSABLE] || role === RoleType.MENU_ITEM) { if (state[StateType.FOCUSABLE] || role === RoleType.MENU_ITEM) {
return !node.children.some(SwitchAccessPredicate.isInterestingSubtree); const result = !node.children.some(
(child) => SwitchAccessPredicate.isInterestingSubtree(child, cache));
cache.isActionable.set(node, result);
return result;
} }
return false; return false;
}, },
...@@ -105,38 +121,48 @@ const SwitchAccessPredicate = { ...@@ -105,38 +121,48 @@ const SwitchAccessPredicate = {
* box as its scope. * box as its scope.
* *
* @param {!AutomationNode} node * @param {!AutomationNode} node
* @param {AutomationNode|SARootNode} scope * @param {!AutomationNode|!SARootNode|null} scope
* @param {!SACache} cache
* @return {boolean} * @return {boolean}
*/ */
isGroup: (node, scope) => { isGroup: (node, scope, cache) => {
if (cache.isGroup.has(node)) {
return cache.isGroup.get(node);
}
const scopeEqualsNode = scope && const scopeEqualsNode = scope &&
(scope instanceof SARootNode ? scope.isEquivalentTo(node) : (scope instanceof SARootNode ? scope.isEquivalentTo(node) :
scope === node); scope === node);
if (scope && !scopeEqualsNode && if (scope && !scopeEqualsNode &&
RectHelper.areEqual(node.location, scope.location)) { RectHelper.areEqual(node.location, scope.location)) {
cache.isGroup.set(node, false);
return false; return false;
} }
if (node.state[StateType.INVISIBLE]) { if (node.state[StateType.INVISIBLE]) {
cache.isGroup.set(node, false);
return false; return false;
} }
if (node.role === chrome.automation.RoleType.KEYBOARD) { if (node.role === chrome.automation.RoleType.KEYBOARD) {
cache.isGroup.set(node, true);
return true; return true;
} }
let interestingBranchesCount = let interestingBranchesCount =
SwitchAccessPredicate.isActionable(node) ? 1 : 0; SwitchAccessPredicate.isActionable(node, cache) ? 1 : 0;
let child = node.firstChild; let child = node.firstChild;
while (child) { while (child) {
if (SwitchAccessPredicate.isInterestingSubtree(child)) { if (SwitchAccessPredicate.isInterestingSubtree(child, cache)) {
interestingBranchesCount += 1; interestingBranchesCount += 1;
} }
if (interestingBranchesCount >= if (interestingBranchesCount >=
SwitchAccessPredicate.GROUP_INTERESTING_CHILD_THRESHOLD) { SwitchAccessPredicate.GROUP_INTERESTING_CHILD_THRESHOLD) {
cache.isGroup.set(node, true);
return true; return true;
} }
child = child.nextSibling; child = child.nextSibling;
} }
cache.isGroup.set(node, false);
return false; return false;
}, },
...@@ -146,10 +172,12 @@ const SwitchAccessPredicate = { ...@@ -146,10 +172,12 @@ const SwitchAccessPredicate = {
* *
* @param {!AutomationNode} node * @param {!AutomationNode} node
* @param {!AutomationNode|!SARootNode} scope * @param {!AutomationNode|!SARootNode} scope
* @param {!SACache} cache
* @return {boolean} * @return {boolean}
*/ */
isInteresting: (node, scope) => SwitchAccessPredicate.isActionable(node) || isInteresting: (node, scope, cache) =>
SwitchAccessPredicate.isGroup(node, scope), SwitchAccessPredicate.isActionable(node, cache) ||
SwitchAccessPredicate.isGroup(node, scope, cache),
/** /**
* Returns true if the element is visible to the user for any reason. * Returns true if the element is visible to the user for any reason.
...@@ -170,10 +198,20 @@ const SwitchAccessPredicate = { ...@@ -170,10 +198,20 @@ const SwitchAccessPredicate = {
* isInterestingSubtree). * isInterestingSubtree).
* *
* @param {!AutomationNode} node * @param {!AutomationNode} node
* @param {!SACache} cache
* @return {boolean} * @return {boolean}
*/ */
isInterestingSubtree: (node) => SwitchAccessPredicate.isActionable(node) || isInterestingSubtree: (node, cache) => {
node.children.some(SwitchAccessPredicate.isInterestingSubtree), if (cache.isInterestingSubtree.has(node)) {
return cache.isInterestingSubtree.get(node);
}
const result = SwitchAccessPredicate.isActionable(node, cache) ||
node.children.some(
(child) =>
SwitchAccessPredicate.isInterestingSubtree(child, cache));
cache.isInterestingSubtree.set(node, result);
return result;
},
/** /**
* Returns true if |node| is an element that contains editable text. * Returns true if |node| is an element that contains editable text.
...@@ -199,10 +237,11 @@ const SwitchAccessPredicate = { ...@@ -199,10 +237,11 @@ const SwitchAccessPredicate = {
* @return {!AutomationTreeWalkerRestriction} * @return {!AutomationTreeWalkerRestriction}
*/ */
restrictions: (scope) => { restrictions: (scope) => {
const cache = new SACache();
return { return {
leaf: SwitchAccessPredicate.leaf(scope), leaf: SwitchAccessPredicate.leaf(scope, cache),
root: SwitchAccessPredicate.root(scope), root: SwitchAccessPredicate.root(scope),
visit: SwitchAccessPredicate.visit(scope) visit: SwitchAccessPredicate.visit(scope, cache)
}; };
}, },
...@@ -211,12 +250,14 @@ const SwitchAccessPredicate = { ...@@ -211,12 +250,14 @@ const SwitchAccessPredicate = {
* SwitchAccess scope tree when |scope| is the root. * SwitchAccess scope tree when |scope| is the root.
* *
* @param {!AutomationNode} scope * @param {!AutomationNode} scope
* @param {!SACache} cache
* @return {function(!AutomationNode): boolean} * @return {function(!AutomationNode): boolean}
*/ */
leaf(scope) { leaf(scope, cache) {
return (node) => node.state[StateType.INVISIBLE] || return (node) => node.state[StateType.INVISIBLE] ||
!SwitchAccessPredicate.isInterestingSubtree(node) || !SwitchAccessPredicate.isInterestingSubtree(node, cache) ||
(scope !== node && SwitchAccessPredicate.isInteresting(node, scope)); (scope !== node &&
SwitchAccessPredicate.isInteresting(node, scope, cache));
}, },
/** /**
...@@ -235,10 +276,11 @@ const SwitchAccessPredicate = { ...@@ -235,10 +276,11 @@ const SwitchAccessPredicate = {
* SwitchAccess scope tree with |scope| as the root. * SwitchAccess scope tree with |scope| as the root.
* *
* @param {!AutomationNode} scope * @param {!AutomationNode} scope
* @param {!SACache} cache
* @return {function(!AutomationNode): boolean} * @return {function(!AutomationNode): boolean}
*/ */
visit(scope) { visit(scope, cache) {
return (node) => node.role !== RoleType.DESKTOP && return (node) => node.role !== RoleType.DESKTOP &&
SwitchAccessPredicate.isInteresting(node, scope); SwitchAccessPredicate.isInteresting(node, scope, cache);
} }
}; };
...@@ -84,48 +84,49 @@ function getTree(loadedPage) { ...@@ -84,48 +84,49 @@ function getTree(loadedPage) {
TEST_F('SwitchAccessPredicateTest', 'IsInteresting', function() { TEST_F('SwitchAccessPredicateTest', 'IsInteresting', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => { this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage); const t = getTree(loadedPage);
const cache = new SACache();
// The scope is only used to verify the locations are not the same, and // The scope is only used to verify the locations are not the same, and
// since the buildTree function depends on isInteresting, pass in null // since the buildTree function depends on isInteresting, pass in null
// for the scope. // for the scope.
assertTrue( assertTrue(
SwitchAccessPredicate.isInteresting(t.root, null), SwitchAccessPredicate.isInteresting(t.root, null, cache),
'Root should be interesting'); 'Root should be interesting');
assertTrue( assertTrue(
SwitchAccessPredicate.isInteresting(t.upper1, null), SwitchAccessPredicate.isInteresting(t.upper1, null, cache),
'Upper1 should be interesting'); 'Upper1 should be interesting');
assertTrue( assertTrue(
SwitchAccessPredicate.isInteresting(t.upper2, null), SwitchAccessPredicate.isInteresting(t.upper2, null, cache),
'Upper2 should be interesting'); 'Upper2 should be interesting');
assertTrue( assertTrue(
SwitchAccessPredicate.isInteresting(t.lower1, null), SwitchAccessPredicate.isInteresting(t.lower1, null, cache),
'Lower1 should be interesting'); 'Lower1 should be interesting');
assertFalse( assertFalse(
SwitchAccessPredicate.isInteresting(t.lower2, null), SwitchAccessPredicate.isInteresting(t.lower2, null, cache),
'Lower2 should not be interesting'); 'Lower2 should not be interesting');
assertFalse( assertFalse(
SwitchAccessPredicate.isInteresting(t.lower3, null), SwitchAccessPredicate.isInteresting(t.lower3, null, cache),
'Lower3 should not be interesting'); 'Lower3 should not be interesting');
assertTrue( assertTrue(
SwitchAccessPredicate.isInteresting(t.leaf1, null), SwitchAccessPredicate.isInteresting(t.leaf1, null, cache),
'Leaf1 should be interesting'); 'Leaf1 should be interesting');
assertFalse( assertFalse(
SwitchAccessPredicate.isInteresting(t.leaf2, null), SwitchAccessPredicate.isInteresting(t.leaf2, null, cache),
'Leaf2 should not be interesting'); 'Leaf2 should not be interesting');
assertTrue( assertTrue(
SwitchAccessPredicate.isInteresting(t.leaf3, null), SwitchAccessPredicate.isInteresting(t.leaf3, null, cache),
'Leaf3 should be interesting'); 'Leaf3 should be interesting');
assertFalse( assertFalse(
SwitchAccessPredicate.isInteresting(t.leaf4, null), SwitchAccessPredicate.isInteresting(t.leaf4, null, cache),
'Leaf4 should not be interesting'); 'Leaf4 should not be interesting');
assertTrue( assertTrue(
SwitchAccessPredicate.isInteresting(t.leaf5, null), SwitchAccessPredicate.isInteresting(t.leaf5, null, cache),
'Leaf5 should be interesting'); 'Leaf5 should be interesting');
assertFalse( assertFalse(
SwitchAccessPredicate.isInteresting(t.leaf6, null), SwitchAccessPredicate.isInteresting(t.leaf6, null, cache),
'Leaf6 should not be interesting'); 'Leaf6 should not be interesting');
assertFalse( assertFalse(
SwitchAccessPredicate.isInteresting(t.leaf7, null), SwitchAccessPredicate.isInteresting(t.leaf7, null, cache),
'Leaf7 should not be interesting'); 'Leaf7 should not be interesting');
}, {returnPage: true}); }, {returnPage: true});
}); });
...@@ -133,47 +134,49 @@ TEST_F('SwitchAccessPredicateTest', 'IsInteresting', function() { ...@@ -133,47 +134,49 @@ TEST_F('SwitchAccessPredicateTest', 'IsInteresting', function() {
TEST_F('SwitchAccessPredicateTest', 'IsGroup', function() { TEST_F('SwitchAccessPredicateTest', 'IsGroup', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => { this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage); const t = getTree(loadedPage);
const cache = new SACache();
// The scope is only used to verify the locations are not the same, and // The scope is only used to verify the locations are not the same, and
// since the buildTree function depends on isGroup, pass in null for // since the buildTree function depends on isGroup, pass in null for
// the scope. // the scope.
assertTrue( assertTrue(
SwitchAccessPredicate.isGroup(t.root, null), 'Root should be a group'); SwitchAccessPredicate.isGroup(t.root, null, cache),
'Root should be a group');
assertTrue( assertTrue(
SwitchAccessPredicate.isGroup(t.upper1, null), SwitchAccessPredicate.isGroup(t.upper1, null, cache),
'Upper1 should be a group'); 'Upper1 should be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.upper2, null), SwitchAccessPredicate.isGroup(t.upper2, null, cache),
'Upper2 should not be a group'); 'Upper2 should not be a group');
assertTrue( assertTrue(
SwitchAccessPredicate.isGroup(t.lower1, null), SwitchAccessPredicate.isGroup(t.lower1, null, cache),
'Lower1 should be a group'); 'Lower1 should be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.lower2, null), SwitchAccessPredicate.isGroup(t.lower2, null, cache),
'Lower2 should not be a group'); 'Lower2 should not be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.lower3, null), SwitchAccessPredicate.isGroup(t.lower3, null, cache),
'Lower3 should not be a group'); 'Lower3 should not be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.leaf1, null), SwitchAccessPredicate.isGroup(t.leaf1, null, cache),
'Leaf1 should not be a group'); 'Leaf1 should not be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.leaf2, null), SwitchAccessPredicate.isGroup(t.leaf2, null, cache),
'Leaf2 should not be a group'); 'Leaf2 should not be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.leaf3, null), SwitchAccessPredicate.isGroup(t.leaf3, null, cache),
'Leaf3 should not be a group'); 'Leaf3 should not be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.leaf4, null), SwitchAccessPredicate.isGroup(t.leaf4, null, cache),
'Leaf4 should not be a group'); 'Leaf4 should not be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.leaf5, null), SwitchAccessPredicate.isGroup(t.leaf5, null, cache),
'Leaf5 should not be a group'); 'Leaf5 should not be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.leaf6, null), SwitchAccessPredicate.isGroup(t.leaf6, null, cache),
'Leaf6 should not be a group'); 'Leaf6 should not be a group');
assertFalse( assertFalse(
SwitchAccessPredicate.isGroup(t.leaf7, null), SwitchAccessPredicate.isGroup(t.leaf7, null, cache),
'Leaf7 should not be a group'); 'Leaf7 should not be a group');
}, {returnPage: true}); }, {returnPage: true});
}); });
...@@ -181,45 +184,46 @@ TEST_F('SwitchAccessPredicateTest', 'IsGroup', function() { ...@@ -181,45 +184,46 @@ TEST_F('SwitchAccessPredicateTest', 'IsGroup', function() {
TEST_F('SwitchAccessPredicateTest', 'IsInterestingSubtree', function() { TEST_F('SwitchAccessPredicateTest', 'IsInterestingSubtree', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => { this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage); const t = getTree(loadedPage);
const cache = new SACache();
assertTrue( assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.root), SwitchAccessPredicate.isInterestingSubtree(t.root, cache),
'Root should be an interesting subtree'); 'Root should be an interesting subtree');
assertTrue( assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.upper1), SwitchAccessPredicate.isInterestingSubtree(t.upper1, cache),
'Upper1 should be an interesting subtree'); 'Upper1 should be an interesting subtree');
assertTrue( assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.upper2), SwitchAccessPredicate.isInterestingSubtree(t.upper2, cache),
'Upper2 should be an interesting subtree'); 'Upper2 should be an interesting subtree');
assertTrue( assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.lower1), SwitchAccessPredicate.isInterestingSubtree(t.lower1, cache),
'Lower1 should be an interesting subtree'); 'Lower1 should be an interesting subtree');
assertTrue( assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.lower2), SwitchAccessPredicate.isInterestingSubtree(t.lower2, cache),
'Lower2 should be an interesting subtree'); 'Lower2 should be an interesting subtree');
assertFalse( assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.lower3), SwitchAccessPredicate.isInterestingSubtree(t.lower3, cache),
'Lower3 should not be an interesting subtree'); 'Lower3 should not be an interesting subtree');
assertTrue( assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.leaf1), SwitchAccessPredicate.isInterestingSubtree(t.leaf1, cache),
'Leaf1 should be an interesting subtree'); 'Leaf1 should be an interesting subtree');
assertFalse( assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.leaf2), SwitchAccessPredicate.isInterestingSubtree(t.leaf2, cache),
'Leaf2 should not be an interesting subtree'); 'Leaf2 should not be an interesting subtree');
assertTrue( assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.leaf3), SwitchAccessPredicate.isInterestingSubtree(t.leaf3, cache),
'Leaf3 should be an interesting subtree'); 'Leaf3 should be an interesting subtree');
assertFalse( assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.leaf4), SwitchAccessPredicate.isInterestingSubtree(t.leaf4, cache),
'Leaf4 should not be an interesting subtree'); 'Leaf4 should not be an interesting subtree');
assertTrue( assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.leaf5), SwitchAccessPredicate.isInterestingSubtree(t.leaf5, cache),
'Leaf5 should be an interesting subtree'); 'Leaf5 should be an interesting subtree');
assertFalse( assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.leaf6), SwitchAccessPredicate.isInterestingSubtree(t.leaf6, cache),
'Leaf6 should not be an interesting subtree'); 'Leaf6 should not be an interesting subtree');
assertFalse( assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.leaf7), SwitchAccessPredicate.isInterestingSubtree(t.leaf7, cache),
'Leaf7 should not be an interesting subtree'); 'Leaf7 should not be an interesting subtree');
}, {returnPage: true}); }, {returnPage: true});
}); });
...@@ -235,57 +239,59 @@ TEST_F('SwitchAccessPredicateTest', 'IsActionable', function() { ...@@ -235,57 +239,59 @@ TEST_F('SwitchAccessPredicateTest', 'IsActionable', function() {
<div id="clickable" role="listitem" onclick="2+2"></div> <div id="clickable" role="listitem" onclick="2+2"></div>
<div aria-label="div1"><p>p1</p></div>`; <div aria-label="div1"><p>p1</p></div>`;
this.runWithLoadedTree(treeString, (desktop) => { this.runWithLoadedTree(treeString, (desktop) => {
const cache = new SACache();
const offscreenButton = this.findNodeByNameAndRole('offscreen', 'button'); const offscreenButton = this.findNodeByNameAndRole('offscreen', 'button');
assertFalse( assertFalse(
SwitchAccessPredicate.isActionable(offscreenButton), SwitchAccessPredicate.isActionable(offscreenButton, cache),
'Offscreen objects should not be actionable'); 'Offscreen objects should not be actionable');
const disabledButton = this.findNodeByNameAndRole('disabled', 'button'); const disabledButton = this.findNodeByNameAndRole('disabled', 'button');
assertFalse( assertFalse(
SwitchAccessPredicate.isActionable(disabledButton), SwitchAccessPredicate.isActionable(disabledButton, cache),
'Disabled objects should not be actionable'); 'Disabled objects should not be actionable');
const rwas = const rwas =
desktop.findAll({role: chrome.automation.RoleType.ROOT_WEB_AREA}); desktop.findAll({role: chrome.automation.RoleType.ROOT_WEB_AREA});
for (const node of rwas) { for (const node of rwas) {
assertFalse( assertFalse(
SwitchAccessPredicate.isActionable(node), SwitchAccessPredicate.isActionable(node, cache),
'Root web area should not be directly actionable'); 'Root web area should not be directly actionable');
} }
const link1 = this.findNodeByNameAndRole('link1', 'link'); const link1 = this.findNodeByNameAndRole('link1', 'link');
assertTrue( assertTrue(
SwitchAccessPredicate.isActionable(link1), SwitchAccessPredicate.isActionable(link1, cache),
'Links should be actionable'); 'Links should be actionable');
const input1 = this.findNodeByNameAndRole('input1', 'textField'); const input1 = this.findNodeByNameAndRole('input1', 'textField');
assertTrue( assertTrue(
SwitchAccessPredicate.isActionable(input1), SwitchAccessPredicate.isActionable(input1, cache),
'Inputs should be actionable'); 'Inputs should be actionable');
const button3 = this.findNodeByNameAndRole('button3', 'button'); const button3 = this.findNodeByNameAndRole('button3', 'button');
assertTrue( assertTrue(
SwitchAccessPredicate.isActionable(button3), SwitchAccessPredicate.isActionable(button3, cache),
'Buttons should be actionable'); 'Buttons should be actionable');
const slider = this.findNodeByNameAndRole('slider', 'slider'); const slider = this.findNodeByNameAndRole('slider', 'slider');
assertTrue( assertTrue(
SwitchAccessPredicate.isActionable(slider), SwitchAccessPredicate.isActionable(slider, cache),
'Sliders should be actionable'); 'Sliders should be actionable');
const clickable = this.findNodeById('clickable'); const clickable = this.findNodeById('clickable');
assertTrue( assertTrue(
SwitchAccessPredicate.isActionable(clickable), SwitchAccessPredicate.isActionable(clickable, cache),
'Clickable list items should be actionable'); 'Clickable list items should be actionable');
const div1 = this.findNodeByNameAndRole('div1', 'genericContainer'); const div1 = this.findNodeByNameAndRole('div1', 'genericContainer');
assertFalse( assertFalse(
SwitchAccessPredicate.isActionable(div1), SwitchAccessPredicate.isActionable(div1, cache),
'Divs should not generally be actionable'); 'Divs should not generally be actionable');
const p1 = this.findNodeByNameAndRole('p1', 'staticText'); const p1 = this.findNodeByNameAndRole('p1', 'staticText');
assertFalse( assertFalse(
SwitchAccessPredicate.isActionable(p1), SwitchAccessPredicate.isActionable(p1, cache),
'Static text should not generally be actionable'); 'Static text should not generally be actionable');
}); });
}); });
...@@ -313,29 +319,31 @@ TEST_F('SwitchAccessPredicateTest', 'IsActionableFocusableElements', function() ...@@ -313,29 +319,31 @@ TEST_F('SwitchAccessPredicateTest', 'IsActionableFocusableElements', function()
<p>p3</p> <p>p3</p>
</div>`; </div>`;
this.runWithLoadedTree(treeString, (desktop) => { this.runWithLoadedTree(treeString, (desktop) => {
const cache = new SACache();
const noChildren = this.findNodeById('noChildren'); const noChildren = this.findNodeById('noChildren');
assertTrue( assertTrue(
SwitchAccessPredicate.isActionable(noChildren), SwitchAccessPredicate.isActionable(noChildren, cache),
'Focusable element with no children should be actionable'); 'Focusable element with no children should be actionable');
const oneInterestingChild = this.findNodeById('oneInterestingChild'); const oneInterestingChild = this.findNodeById('oneInterestingChild');
assertFalse( assertFalse(
SwitchAccessPredicate.isActionable(oneInterestingChild), SwitchAccessPredicate.isActionable(oneInterestingChild, cache),
'Focusable element with an interesting child should not be actionable'); 'Focusable element with an interesting child should not be actionable');
const interestingChildren = this.findNodeById('interestingChildren'); const interestingChildren = this.findNodeById('interestingChildren');
assertFalse( assertFalse(
SwitchAccessPredicate.isActionable(interestingChildren), SwitchAccessPredicate.isActionable(interestingChildren, cache),
'Focusable element with interesting children should not be actionable'); 'Focusable element with interesting children should not be actionable');
const oneUninterestingChild = this.findNodeById('oneUninterestingChild'); const oneUninterestingChild = this.findNodeById('oneUninterestingChild');
assertTrue( assertTrue(
SwitchAccessPredicate.isActionable(oneUninterestingChild), SwitchAccessPredicate.isActionable(oneUninterestingChild, cache),
'Focusable element with one uninteresting child should be actionable'); 'Focusable element with one uninteresting child should be actionable');
const uninterestingChildren = this.findNodeById('uninterestingChildren'); const uninterestingChildren = this.findNodeById('uninterestingChildren');
assertTrue( assertTrue(
SwitchAccessPredicate.isActionable(uninterestingChildren), SwitchAccessPredicate.isActionable(uninterestingChildren, cache),
'Focusable element with uninteresting children should be actionable'); 'Focusable element with uninteresting children should be actionable');
}); });
}); });
...@@ -343,22 +351,23 @@ TEST_F('SwitchAccessPredicateTest', 'IsActionableFocusableElements', function() ...@@ -343,22 +351,23 @@ TEST_F('SwitchAccessPredicateTest', 'IsActionableFocusableElements', function()
TEST_F('SwitchAccessPredicateTest', 'LeafPredicate', function() { TEST_F('SwitchAccessPredicateTest', 'LeafPredicate', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => { this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage); const t = getTree(loadedPage);
const cache = new SACache();
// Start with root as scope // Start with root as scope
let leaf = SwitchAccessPredicate.leaf(t.root); let leaf = SwitchAccessPredicate.leaf(t.root, cache);
assertFalse(leaf(t.root), 'Root should not be a leaf node'); assertFalse(leaf(t.root), 'Root should not be a leaf node');
assertTrue(leaf(t.upper1), 'Upper1 should be a leaf node for root tree'); assertTrue(leaf(t.upper1), 'Upper1 should be a leaf node for root tree');
assertTrue(leaf(t.upper2), 'Upper2 should be a leaf node for root tree'); assertTrue(leaf(t.upper2), 'Upper2 should be a leaf node for root tree');
// Set upper1 as scope // Set upper1 as scope
leaf = SwitchAccessPredicate.leaf(t.upper1); leaf = SwitchAccessPredicate.leaf(t.upper1, cache);
assertFalse(leaf(t.upper1), 'Upper1 should not be a leaf for upper1 tree'); assertFalse(leaf(t.upper1), 'Upper1 should not be a leaf for upper1 tree');
assertTrue(leaf(t.lower1), 'Lower1 should be a leaf for upper1 tree'); assertTrue(leaf(t.lower1), 'Lower1 should be a leaf for upper1 tree');
assertTrue(leaf(t.leaf4), 'leaf4 should be a leaf for upper1 tree'); assertTrue(leaf(t.leaf4), 'leaf4 should be a leaf for upper1 tree');
assertTrue(leaf(t.leaf5), 'leaf5 should be a leaf for upper1 tree'); assertTrue(leaf(t.leaf5), 'leaf5 should be a leaf for upper1 tree');
// Set lower1 as scope // Set lower1 as scope
leaf = SwitchAccessPredicate.leaf(t.lower1); leaf = SwitchAccessPredicate.leaf(t.lower1, cache);
assertFalse(leaf(t.lower1), 'Lower1 should not be a leaf for lower1 tree'); assertFalse(leaf(t.lower1), 'Lower1 should not be a leaf for lower1 tree');
assertTrue(leaf(t.leaf1), 'Leaf1 should be a leaf for lower1 tree'); assertTrue(leaf(t.leaf1), 'Leaf1 should be a leaf for lower1 tree');
assertTrue(leaf(t.leaf2), 'Leaf2 should be a leaf for lower1 tree'); assertTrue(leaf(t.leaf2), 'Leaf2 should be a leaf for lower1 tree');
...@@ -396,15 +405,16 @@ TEST_F('SwitchAccessPredicateTest', 'RootPredicate', function() { ...@@ -396,15 +405,16 @@ TEST_F('SwitchAccessPredicateTest', 'RootPredicate', function() {
TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', function() { TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => { this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage); const t = getTree(loadedPage);
const cache = new SACache();
// Start with root as scope // Start with root as scope
let visit = SwitchAccessPredicate.visit(t.root); let visit = SwitchAccessPredicate.visit(t.root, cache);
assertTrue(visit(t.root), 'Root should be visited in root tree'); assertTrue(visit(t.root), 'Root should be visited in root tree');
assertTrue(visit(t.upper1), 'Upper1 should be visited in root tree'); assertTrue(visit(t.upper1), 'Upper1 should be visited in root tree');
assertTrue(visit(t.upper2), 'Upper2 should be visited in root tree'); assertTrue(visit(t.upper2), 'Upper2 should be visited in root tree');
// Set upper1 as scope // Set upper1 as scope
visit = SwitchAccessPredicate.visit(t.upper1); visit = SwitchAccessPredicate.visit(t.upper1, cache);
assertTrue(visit(t.upper1), 'Upper1 should be visited in upper1 tree'); assertTrue(visit(t.upper1), 'Upper1 should be visited in upper1 tree');
assertTrue(visit(t.lower1), 'Lower1 should be visited in upper1 tree'); assertTrue(visit(t.lower1), 'Lower1 should be visited in upper1 tree');
assertFalse(visit(t.lower2), 'Lower2 should not be visited in upper1 tree'); assertFalse(visit(t.lower2), 'Lower2 should not be visited in upper1 tree');
...@@ -412,7 +422,7 @@ TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', function() { ...@@ -412,7 +422,7 @@ TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', function() {
assertTrue(visit(t.leaf5), 'Leaf5 should be visited in upper1 tree'); assertTrue(visit(t.leaf5), 'Leaf5 should be visited in upper1 tree');
// Set lower1 as scope // Set lower1 as scope
visit = SwitchAccessPredicate.visit(t.lower1); visit = SwitchAccessPredicate.visit(t.lower1, cache);
assertTrue(visit(t.lower1), 'Lower1 should be visited in lower1 tree'); assertTrue(visit(t.lower1), 'Lower1 should be visited in lower1 tree');
assertTrue(visit(t.leaf1), 'Leaf1 should be visited in lower1 tree'); assertTrue(visit(t.leaf1), 'Leaf1 should be visited in lower1 tree');
assertFalse(visit(t.leaf2), 'Leaf2 should not be visited in lower1 tree'); assertFalse(visit(t.leaf2), 'Leaf2 should not be visited in lower1 tree');
...@@ -424,3 +434,47 @@ TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', function() { ...@@ -424,3 +434,47 @@ TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', function() {
assertFalse(visit(t.leaf7), 'Leaf7 should not be visited in lower1 tree'); assertFalse(visit(t.leaf7), 'Leaf7 should not be visited in lower1 tree');
}, {returnPage: true}); }, {returnPage: true});
}); });
TEST_F('SwitchAccessPredicateTest', 'Cache', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage);
const cache = new SACache();
let locationAccessCount = 0;
class TestRoot extends SARootNode {
/** @override */
get location() {
locationAccessCount++;
return null;
}
}
const group = new TestRoot();
assertTrue(
SwitchAccessPredicate.isGroup(t.root, group, cache),
'Root should be a group');
assertEquals(
locationAccessCount, 1,
'Location should have been accessed to calculate isGroup');
assertTrue(
SwitchAccessPredicate.isGroup(t.root, group, cache),
'isGroup value should not change');
assertEquals(
locationAccessCount, 1,
'Cache should have been used, avoiding second location access');
locationAccessCount = 0;
assertFalse(
SwitchAccessPredicate.isGroup(t.leaf1, group, cache),
'Leaf should not be a group');
assertEquals(
locationAccessCount, 1,
'Location should have been accessed to calculate isGroup');
assertFalse(
SwitchAccessPredicate.isGroup(t.leaf1, group, cache),
'isGroup value should not change');
assertEquals(
locationAccessCount, 1,
'Cache should have been used, avoiding second location access');
}, {returnPage: true});
});
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
"common/repeated_tree_change_handler.js", "common/repeated_tree_change_handler.js",
"common/tree_walker.js", "common/tree_walker.js",
"switch_access/auto_scan_manager.js", "switch_access/auto_scan_manager.js",
"switch_access/cache.js",
"switch_access/commands.js", "switch_access/commands.js",
"switch_access/event_helper.js", "switch_access/event_helper.js",
"switch_access/focus_ring_manager.js", "switch_access/focus_ring_manager.js",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment