Commit e4cb5326 authored by David Tseng's avatar David Tseng Committed by Commit Bot

Ensure ChromeVox descends into buttons to click on YouTube videos in ARC

R=hirokisato@chromium.org

clickable text to play each video

Bug: none
Change-Id: I9f01f111fdcce6665a4eabd872ad9778d64ed6f2
AX-Relnotes: Within the YouTube Android app, ChromeVox can navigate to the
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2450890
Commit-Queue: David Tseng <dtseng@chromium.org>
Reviewed-by: default avatarHiroki Sato <hirokisato@chromium.org>
Cr-Commit-Position: refs/heads/master@{#814930}
parent e506362c
......@@ -3133,3 +3133,30 @@ TEST_F('ChromeVoxBackgroundTest', 'ReadFromHereBlankNodes', function() {
.replay();
});
});
TEST_F('ChromeVoxBackgroundTest', 'ContainerButtons', function() {
const mockFeedback = this.createMockFeedback();
// This pattern can be found in ARC++/YouTube.
const site = `
<p>videos</p>
<div aria-label="Cat Video" role="button">
<div role="group">4 minutes, Cat Video</div>
</div>
`;
this.runWithLoadedTree(site, function(root) {
const group = root.find({role: RoleType.GROUP});
Object.defineProperty(group, 'clickable', {
get() {
return true;
}
});
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('Cat Video', 'Button')
.call(doCmd('nextObject'))
.expectSpeech('4 minutes, Cat Video')
.replay();
});
});
......@@ -20,22 +20,42 @@ const Role = chrome.automation.RoleType;
const State = chrome.automation.StateType;
/**
* A helper to find any actionable children.
* A helper to check if |node| or any descendant is actionable.
* @param {!AutomationNode} node
* @return {boolean}
*/
const hasActionableDescendant = function(node) {
const isActionableOrHasActionableDescendant = function(node) {
// DefaultActionVerb does not have value 'none' even though it gets set.
if (node.defaultActionVerb != 'none') {
return true;
}
let result = false;
if (node.clickable) {
return true;
}
for (let i = 0; i < node.children.length; i++) {
if (isActionableOrHasActionableDescendant(node.children[i])) {
return true;
}
}
return false;
};
/**
* A helper to check if any descendants of |node| are actionable.
* @param {!AutomationNode} node
* @return {boolean}
*/
const hasActionableDescendant = function(node) {
for (let i = 0; i < node.children.length; i++) {
result = hasActionableDescendant(node.children[i]);
if (isActionableOrHasActionableDescendant(node.children[i])) {
return true;
}
}
return result;
return false;
};
/**
......@@ -173,9 +193,9 @@ AutomationPredicate = class {
// A node acting as a label should be a leaf if it has no actionable
// controls.
(!!node.labelFor && node.labelFor.length > 0 &&
!hasActionableDescendant(node)) ||
!isActionableOrHasActionableDescendant(node)) ||
(!!node.descriptionFor && node.descriptionFor.length > 0 &&
!hasActionableDescendant(node)) ||
!isActionableOrHasActionableDescendant(node)) ||
(node.activeDescendantFor && node.activeDescendantFor.length > 0) ||
node.state[State.INVISIBLE] || node.children.every(function(n) {
return n.state[State.INVISIBLE];
......@@ -237,6 +257,12 @@ AutomationPredicate = class {
return false;
}
// Things explicitly marked clickable (used only on ARC++) should be
// visited.
if (node.clickable) {
return true;
}
// Given no other information, ChromeVox wants to visit focusable
// (e.g. tabindex=0) nodes only when it has a name or is a control.
if (node.state[State.FOCUSABLE] &&
......@@ -322,6 +348,12 @@ AutomationPredicate = class {
return false;
}
// Always try to dive into subtrees with actionable descendants for some
// roles even if these roles are not naturally containers.
if (node.role == Role.BUTTON && hasActionableDescendant(node)) {
return true;
}
return AutomationPredicate.match({
anyRole: [
Role.GENERIC_CONTAINER, Role.DOCUMENT, Role.GROUP, Role.LIST,
......@@ -418,13 +450,13 @@ AutomationPredicate = class {
// Ignore nodes acting as labels for another control, that don't
// have actionable descendants.
if (node.labelFor && node.labelFor.length > 0 &&
!hasActionableDescendant(node)) {
!isActionableOrHasActionableDescendant(node)) {
return true;
}
// Similarly, ignore nodes acting as descriptions.
if (node.descriptionFor && node.descriptionFor.length > 0 &&
!hasActionableDescendant(node)) {
!isActionableOrHasActionableDescendant(node)) {
return true;
}
......
......@@ -228,7 +228,7 @@ TEST_F(
this.runWithLoadedTree(
`
<div role="button" aria-label="outer">
<div role="button" aria-label="inner">
<div role="heading" aria-label="inner">
</div>
</div>
`,
......
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