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") {
sources = [
"auto_scan_manager.js",
"background.js",
"cache.js",
"commands.js",
"event_helper.js",
"focus_ring_manager.js",
......@@ -133,6 +134,7 @@ js_type_check("closure_compile") {
":auto_scan_manager",
":back_button_node",
":background",
":cache",
":combo_box_node",
":commands",
":desktop_node",
......@@ -188,6 +190,10 @@ js_library("back_button_node") {
]
}
js_library("cache") {
externs_list = [ "$externs_path/automation.js" ]
}
js_library("combo_box_node") {
sources = [ "nodes/combo_box_node.js" ]
deps = [
......@@ -256,6 +262,7 @@ js_library("group_node") {
js_library("history") {
deps = [
":cache",
":desktop_node",
":node_wrapper",
":switch_access_node",
......@@ -338,6 +345,7 @@ js_library("node_wrapper") {
sources = [ "nodes/node_wrapper.js" ]
deps = [
":back_button_node",
":cache",
":switch_access_constants",
":switch_access_node",
":switch_access_predicate",
......@@ -404,6 +412,7 @@ js_library("switch_access_node") {
js_library("switch_access_predicate") {
deps = [
":cache",
":switch_access_constants",
":switch_access_node",
"../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 {
// No ancestors, cannot create stack.
return false;
}
const cache = new SACache();
// Create a list of ancestors.
const ancestorStack = [node];
while (node.parent) {
......@@ -54,7 +55,7 @@ class FocusHistory {
let group = DesktopNode.build(ancestorStack.pop());
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
// uninteresting, we leave the history as is.
return false;
......@@ -63,7 +64,7 @@ class FocusHistory {
const newDataStack = [];
while (ancestorStack.length > 0) {
const candidate = ancestorStack.pop();
if (!SwitchAccessPredicate.isInteresting(candidate, group)) {
if (!SwitchAccessPredicate.isInteresting(candidate, group, cache)) {
continue;
}
......
......@@ -14,12 +14,14 @@ SwitchAccessNavigationManagerTest = class extends SwitchAccessE2ETest {
}
moveToPageContents(pageContents) {
if (!SwitchAccessPredicate.isGroup(pageContents)) {
pageContents = new AutomationTreeWalker(
pageContents, constants.Dir.FORWARD,
{visit: SwitchAccessPredicate.isGroup})
.next()
.node;
const cache = new SACache();
if (!SwitchAccessPredicate.isGroup(pageContents, null, cache)) {
pageContents =
new AutomationTreeWalker(pageContents, constants.Dir.FORWARD, {
visit: (node) => SwitchAccessPredicate.isGroup(node, null, cache)
})
.next()
.node;
}
assertNotNullNorUndefined(
pageContents, 'Could not find group corresponding to page contents');
......
......@@ -102,7 +102,8 @@ class NodeWrapper extends SAChildNode {
/** @override */
isGroup() {
return SwitchAccessPredicate.isGroup(this.baseNode_, this.parent_);
const cache = new SACache();
return SwitchAccessPredicate.isGroup(this.baseNode_, this.parent_, cache);
}
/** @override */
......
......@@ -27,9 +27,14 @@ const SwitchAccessPredicate = {
* it in some way.
*
* @param {!AutomationNode} node
* @param {!SACache} cache
* @return {boolean}
*/
isActionable: (node) => {
isActionable: (node, cache) => {
if (cache.isActionable.has(node)) {
return cache.isActionable.get(node);
}
const defaultActionVerb = node.defaultActionVerb;
const loc = node.location;
const parent = node.parent;
......@@ -39,16 +44,19 @@ const SwitchAccessPredicate = {
// Skip things that are offscreen or invisible.
if (!SwitchAccessPredicate.isVisible(node)) {
cache.isActionable.set(node, false);
return false;
}
// Skip things that are disabled.
if (node.restriction === chrome.automation.Restriction.DISABLED) {
cache.isActionable.set(node, false);
return false;
}
// These web containers are not directly actionable.
if (role === RoleType.WEB_VIEW || role === RoleType.ROOT_WEB_AREA) {
cache.isActionable.set(node, false);
return false;
}
......@@ -57,17 +65,20 @@ const SwitchAccessPredicate = {
// Work around for browser tabs.
if (role === RoleType.TAB && parent.role === RoleType.TAB_LIST &&
root.role === RoleType.DESKTOP) {
cache.isActionable.set(node, true);
return true;
}
}
// Check various indicators that the node is actionable.
if (role === RoleType.BUTTON || role === RoleType.SLIDER) {
cache.isActionable.set(node, true);
return true;
}
if (AutomationPredicate.comboBox(node) ||
SwitchAccessPredicate.isTextInput(node)) {
cache.isActionable.set(node, true);
return true;
}
......@@ -78,11 +89,13 @@ const SwitchAccessPredicate = {
defaultActionVerb === DefaultActionVerb.PRESS ||
defaultActionVerb === DefaultActionVerb.SELECT ||
defaultActionVerb === DefaultActionVerb.UNCHECK)) {
cache.isActionable.set(node, true);
return true;
}
if (role === RoleType.LIST_ITEM &&
defaultActionVerb === DefaultActionVerb.CLICK) {
cache.isActionable.set(node, true);
return true;
}
......@@ -91,7 +104,10 @@ const SwitchAccessPredicate = {
// Current heuristic is to show as actionble any focusable item where no
// child is an interesting subtree.
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;
},
......@@ -105,38 +121,48 @@ const SwitchAccessPredicate = {
* box as its scope.
*
* @param {!AutomationNode} node
* @param {AutomationNode|SARootNode} scope
* @param {!AutomationNode|!SARootNode|null} scope
* @param {!SACache} cache
* @return {boolean}
*/
isGroup: (node, scope) => {
isGroup: (node, scope, cache) => {
if (cache.isGroup.has(node)) {
return cache.isGroup.get(node);
}
const scopeEqualsNode = scope &&
(scope instanceof SARootNode ? scope.isEquivalentTo(node) :
scope === node);
if (scope && !scopeEqualsNode &&
RectHelper.areEqual(node.location, scope.location)) {
cache.isGroup.set(node, false);
return false;
}
if (node.state[StateType.INVISIBLE]) {
cache.isGroup.set(node, false);
return false;
}
if (node.role === chrome.automation.RoleType.KEYBOARD) {
cache.isGroup.set(node, true);
return true;
}
let interestingBranchesCount =
SwitchAccessPredicate.isActionable(node) ? 1 : 0;
SwitchAccessPredicate.isActionable(node, cache) ? 1 : 0;
let child = node.firstChild;
while (child) {
if (SwitchAccessPredicate.isInterestingSubtree(child)) {
if (SwitchAccessPredicate.isInterestingSubtree(child, cache)) {
interestingBranchesCount += 1;
}
if (interestingBranchesCount >=
SwitchAccessPredicate.GROUP_INTERESTING_CHILD_THRESHOLD) {
cache.isGroup.set(node, true);
return true;
}
child = child.nextSibling;
}
cache.isGroup.set(node, false);
return false;
},
......@@ -146,10 +172,12 @@ const SwitchAccessPredicate = {
*
* @param {!AutomationNode} node
* @param {!AutomationNode|!SARootNode} scope
* @param {!SACache} cache
* @return {boolean}
*/
isInteresting: (node, scope) => SwitchAccessPredicate.isActionable(node) ||
SwitchAccessPredicate.isGroup(node, scope),
isInteresting: (node, scope, cache) =>
SwitchAccessPredicate.isActionable(node, cache) ||
SwitchAccessPredicate.isGroup(node, scope, cache),
/**
* Returns true if the element is visible to the user for any reason.
......@@ -170,10 +198,20 @@ const SwitchAccessPredicate = {
* isInterestingSubtree).
*
* @param {!AutomationNode} node
* @param {!SACache} cache
* @return {boolean}
*/
isInterestingSubtree: (node) => SwitchAccessPredicate.isActionable(node) ||
node.children.some(SwitchAccessPredicate.isInterestingSubtree),
isInterestingSubtree: (node, cache) => {
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.
......@@ -199,10 +237,11 @@ const SwitchAccessPredicate = {
* @return {!AutomationTreeWalkerRestriction}
*/
restrictions: (scope) => {
const cache = new SACache();
return {
leaf: SwitchAccessPredicate.leaf(scope),
leaf: SwitchAccessPredicate.leaf(scope, cache),
root: SwitchAccessPredicate.root(scope),
visit: SwitchAccessPredicate.visit(scope)
visit: SwitchAccessPredicate.visit(scope, cache)
};
},
......@@ -211,12 +250,14 @@ const SwitchAccessPredicate = {
* SwitchAccess scope tree when |scope| is the root.
*
* @param {!AutomationNode} scope
* @param {!SACache} cache
* @return {function(!AutomationNode): boolean}
*/
leaf(scope) {
leaf(scope, cache) {
return (node) => node.state[StateType.INVISIBLE] ||
!SwitchAccessPredicate.isInterestingSubtree(node) ||
(scope !== node && SwitchAccessPredicate.isInteresting(node, scope));
!SwitchAccessPredicate.isInterestingSubtree(node, cache) ||
(scope !== node &&
SwitchAccessPredicate.isInteresting(node, scope, cache));
},
/**
......@@ -235,10 +276,11 @@ const SwitchAccessPredicate = {
* SwitchAccess scope tree with |scope| as the root.
*
* @param {!AutomationNode} scope
* @param {!SACache} cache
* @return {function(!AutomationNode): boolean}
*/
visit(scope) {
visit(scope, cache) {
return (node) => node.role !== RoleType.DESKTOP &&
SwitchAccessPredicate.isInteresting(node, scope);
SwitchAccessPredicate.isInteresting(node, scope, cache);
}
};
......@@ -84,48 +84,49 @@ function getTree(loadedPage) {
TEST_F('SwitchAccessPredicateTest', 'IsInteresting', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage);
const cache = new SACache();
// The scope is only used to verify the locations are not the same, and
// since the buildTree function depends on isInteresting, pass in null
// for the scope.
assertTrue(
SwitchAccessPredicate.isInteresting(t.root, null),
SwitchAccessPredicate.isInteresting(t.root, null, cache),
'Root should be interesting');
assertTrue(
SwitchAccessPredicate.isInteresting(t.upper1, null),
SwitchAccessPredicate.isInteresting(t.upper1, null, cache),
'Upper1 should be interesting');
assertTrue(
SwitchAccessPredicate.isInteresting(t.upper2, null),
SwitchAccessPredicate.isInteresting(t.upper2, null, cache),
'Upper2 should be interesting');
assertTrue(
SwitchAccessPredicate.isInteresting(t.lower1, null),
SwitchAccessPredicate.isInteresting(t.lower1, null, cache),
'Lower1 should be interesting');
assertFalse(
SwitchAccessPredicate.isInteresting(t.lower2, null),
SwitchAccessPredicate.isInteresting(t.lower2, null, cache),
'Lower2 should not be interesting');
assertFalse(
SwitchAccessPredicate.isInteresting(t.lower3, null),
SwitchAccessPredicate.isInteresting(t.lower3, null, cache),
'Lower3 should not be interesting');
assertTrue(
SwitchAccessPredicate.isInteresting(t.leaf1, null),
SwitchAccessPredicate.isInteresting(t.leaf1, null, cache),
'Leaf1 should be interesting');
assertFalse(
SwitchAccessPredicate.isInteresting(t.leaf2, null),
SwitchAccessPredicate.isInteresting(t.leaf2, null, cache),
'Leaf2 should not be interesting');
assertTrue(
SwitchAccessPredicate.isInteresting(t.leaf3, null),
SwitchAccessPredicate.isInteresting(t.leaf3, null, cache),
'Leaf3 should be interesting');
assertFalse(
SwitchAccessPredicate.isInteresting(t.leaf4, null),
SwitchAccessPredicate.isInteresting(t.leaf4, null, cache),
'Leaf4 should not be interesting');
assertTrue(
SwitchAccessPredicate.isInteresting(t.leaf5, null),
SwitchAccessPredicate.isInteresting(t.leaf5, null, cache),
'Leaf5 should be interesting');
assertFalse(
SwitchAccessPredicate.isInteresting(t.leaf6, null),
SwitchAccessPredicate.isInteresting(t.leaf6, null, cache),
'Leaf6 should not be interesting');
assertFalse(
SwitchAccessPredicate.isInteresting(t.leaf7, null),
SwitchAccessPredicate.isInteresting(t.leaf7, null, cache),
'Leaf7 should not be interesting');
}, {returnPage: true});
});
......@@ -133,47 +134,49 @@ TEST_F('SwitchAccessPredicateTest', 'IsInteresting', function() {
TEST_F('SwitchAccessPredicateTest', 'IsGroup', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage);
const cache = new SACache();
// 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
// the scope.
assertTrue(
SwitchAccessPredicate.isGroup(t.root, null), 'Root should be a group');
SwitchAccessPredicate.isGroup(t.root, null, cache),
'Root should be a group');
assertTrue(
SwitchAccessPredicate.isGroup(t.upper1, null),
SwitchAccessPredicate.isGroup(t.upper1, null, cache),
'Upper1 should be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.upper2, null),
SwitchAccessPredicate.isGroup(t.upper2, null, cache),
'Upper2 should not be a group');
assertTrue(
SwitchAccessPredicate.isGroup(t.lower1, null),
SwitchAccessPredicate.isGroup(t.lower1, null, cache),
'Lower1 should be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.lower2, null),
SwitchAccessPredicate.isGroup(t.lower2, null, cache),
'Lower2 should not be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.lower3, null),
SwitchAccessPredicate.isGroup(t.lower3, null, cache),
'Lower3 should not be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.leaf1, null),
SwitchAccessPredicate.isGroup(t.leaf1, null, cache),
'Leaf1 should not be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.leaf2, null),
SwitchAccessPredicate.isGroup(t.leaf2, null, cache),
'Leaf2 should not be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.leaf3, null),
SwitchAccessPredicate.isGroup(t.leaf3, null, cache),
'Leaf3 should not be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.leaf4, null),
SwitchAccessPredicate.isGroup(t.leaf4, null, cache),
'Leaf4 should not be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.leaf5, null),
SwitchAccessPredicate.isGroup(t.leaf5, null, cache),
'Leaf5 should not be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.leaf6, null),
SwitchAccessPredicate.isGroup(t.leaf6, null, cache),
'Leaf6 should not be a group');
assertFalse(
SwitchAccessPredicate.isGroup(t.leaf7, null),
SwitchAccessPredicate.isGroup(t.leaf7, null, cache),
'Leaf7 should not be a group');
}, {returnPage: true});
});
......@@ -181,45 +184,46 @@ TEST_F('SwitchAccessPredicateTest', 'IsGroup', function() {
TEST_F('SwitchAccessPredicateTest', 'IsInterestingSubtree', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage);
const cache = new SACache();
assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.root),
SwitchAccessPredicate.isInterestingSubtree(t.root, cache),
'Root should be an interesting subtree');
assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.upper1),
SwitchAccessPredicate.isInterestingSubtree(t.upper1, cache),
'Upper1 should be an interesting subtree');
assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.upper2),
SwitchAccessPredicate.isInterestingSubtree(t.upper2, cache),
'Upper2 should be an interesting subtree');
assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.lower1),
SwitchAccessPredicate.isInterestingSubtree(t.lower1, cache),
'Lower1 should be an interesting subtree');
assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.lower2),
SwitchAccessPredicate.isInterestingSubtree(t.lower2, cache),
'Lower2 should be an interesting subtree');
assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.lower3),
SwitchAccessPredicate.isInterestingSubtree(t.lower3, cache),
'Lower3 should not be an interesting subtree');
assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.leaf1),
SwitchAccessPredicate.isInterestingSubtree(t.leaf1, cache),
'Leaf1 should be an interesting subtree');
assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.leaf2),
SwitchAccessPredicate.isInterestingSubtree(t.leaf2, cache),
'Leaf2 should not be an interesting subtree');
assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.leaf3),
SwitchAccessPredicate.isInterestingSubtree(t.leaf3, cache),
'Leaf3 should be an interesting subtree');
assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.leaf4),
SwitchAccessPredicate.isInterestingSubtree(t.leaf4, cache),
'Leaf4 should not be an interesting subtree');
assertTrue(
SwitchAccessPredicate.isInterestingSubtree(t.leaf5),
SwitchAccessPredicate.isInterestingSubtree(t.leaf5, cache),
'Leaf5 should be an interesting subtree');
assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.leaf6),
SwitchAccessPredicate.isInterestingSubtree(t.leaf6, cache),
'Leaf6 should not be an interesting subtree');
assertFalse(
SwitchAccessPredicate.isInterestingSubtree(t.leaf7),
SwitchAccessPredicate.isInterestingSubtree(t.leaf7, cache),
'Leaf7 should not be an interesting subtree');
}, {returnPage: true});
});
......@@ -235,57 +239,59 @@ TEST_F('SwitchAccessPredicateTest', 'IsActionable', function() {
<div id="clickable" role="listitem" onclick="2+2"></div>
<div aria-label="div1"><p>p1</p></div>`;
this.runWithLoadedTree(treeString, (desktop) => {
const cache = new SACache();
const offscreenButton = this.findNodeByNameAndRole('offscreen', 'button');
assertFalse(
SwitchAccessPredicate.isActionable(offscreenButton),
SwitchAccessPredicate.isActionable(offscreenButton, cache),
'Offscreen objects should not be actionable');
const disabledButton = this.findNodeByNameAndRole('disabled', 'button');
assertFalse(
SwitchAccessPredicate.isActionable(disabledButton),
SwitchAccessPredicate.isActionable(disabledButton, cache),
'Disabled objects should not be actionable');
const rwas =
desktop.findAll({role: chrome.automation.RoleType.ROOT_WEB_AREA});
for (const node of rwas) {
assertFalse(
SwitchAccessPredicate.isActionable(node),
SwitchAccessPredicate.isActionable(node, cache),
'Root web area should not be directly actionable');
}
const link1 = this.findNodeByNameAndRole('link1', 'link');
assertTrue(
SwitchAccessPredicate.isActionable(link1),
SwitchAccessPredicate.isActionable(link1, cache),
'Links should be actionable');
const input1 = this.findNodeByNameAndRole('input1', 'textField');
assertTrue(
SwitchAccessPredicate.isActionable(input1),
SwitchAccessPredicate.isActionable(input1, cache),
'Inputs should be actionable');
const button3 = this.findNodeByNameAndRole('button3', 'button');
assertTrue(
SwitchAccessPredicate.isActionable(button3),
SwitchAccessPredicate.isActionable(button3, cache),
'Buttons should be actionable');
const slider = this.findNodeByNameAndRole('slider', 'slider');
assertTrue(
SwitchAccessPredicate.isActionable(slider),
SwitchAccessPredicate.isActionable(slider, cache),
'Sliders should be actionable');
const clickable = this.findNodeById('clickable');
assertTrue(
SwitchAccessPredicate.isActionable(clickable),
SwitchAccessPredicate.isActionable(clickable, cache),
'Clickable list items should be actionable');
const div1 = this.findNodeByNameAndRole('div1', 'genericContainer');
assertFalse(
SwitchAccessPredicate.isActionable(div1),
SwitchAccessPredicate.isActionable(div1, cache),
'Divs should not generally be actionable');
const p1 = this.findNodeByNameAndRole('p1', 'staticText');
assertFalse(
SwitchAccessPredicate.isActionable(p1),
SwitchAccessPredicate.isActionable(p1, cache),
'Static text should not generally be actionable');
});
});
......@@ -313,29 +319,31 @@ TEST_F('SwitchAccessPredicateTest', 'IsActionableFocusableElements', function()
<p>p3</p>
</div>`;
this.runWithLoadedTree(treeString, (desktop) => {
const cache = new SACache();
const noChildren = this.findNodeById('noChildren');
assertTrue(
SwitchAccessPredicate.isActionable(noChildren),
SwitchAccessPredicate.isActionable(noChildren, cache),
'Focusable element with no children should be actionable');
const oneInterestingChild = this.findNodeById('oneInterestingChild');
assertFalse(
SwitchAccessPredicate.isActionable(oneInterestingChild),
SwitchAccessPredicate.isActionable(oneInterestingChild, cache),
'Focusable element with an interesting child should not be actionable');
const interestingChildren = this.findNodeById('interestingChildren');
assertFalse(
SwitchAccessPredicate.isActionable(interestingChildren),
SwitchAccessPredicate.isActionable(interestingChildren, cache),
'Focusable element with interesting children should not be actionable');
const oneUninterestingChild = this.findNodeById('oneUninterestingChild');
assertTrue(
SwitchAccessPredicate.isActionable(oneUninterestingChild),
SwitchAccessPredicate.isActionable(oneUninterestingChild, cache),
'Focusable element with one uninteresting child should be actionable');
const uninterestingChildren = this.findNodeById('uninterestingChildren');
assertTrue(
SwitchAccessPredicate.isActionable(uninterestingChildren),
SwitchAccessPredicate.isActionable(uninterestingChildren, cache),
'Focusable element with uninteresting children should be actionable');
});
});
......@@ -343,22 +351,23 @@ TEST_F('SwitchAccessPredicateTest', 'IsActionableFocusableElements', function()
TEST_F('SwitchAccessPredicateTest', 'LeafPredicate', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage);
const cache = new SACache();
// 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');
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');
// 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');
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.leaf5), 'leaf5 should be a leaf for upper1 tree');
// 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');
assertTrue(leaf(t.leaf1), 'Leaf1 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() {
TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', function() {
this.runWithLoadedTree(testWebsite(), (loadedPage) => {
const t = getTree(loadedPage);
const cache = new SACache();
// 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.upper1), 'Upper1 should be visited in root tree');
assertTrue(visit(t.upper2), 'Upper2 should be visited in root tree');
// 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.lower1), 'Lower1 should 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() {
assertTrue(visit(t.leaf5), 'Leaf5 should be visited in upper1 tree');
// 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.leaf1), 'Leaf1 should 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() {
assertFalse(visit(t.leaf7), 'Leaf7 should not be visited in lower1 tree');
}, {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 @@
"common/repeated_tree_change_handler.js",
"common/tree_walker.js",
"switch_access/auto_scan_manager.js",
"switch_access/cache.js",
"switch_access/commands.js",
"switch_access/event_helper.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