Skip to content

Commit

Permalink
Introduce AXPlatformNodeBase::CompareTo.
Browse files Browse the repository at this point in the history
Introduced CompareTo function for AXPlatformNodes to compare the
relative positions of two nodes.

AX-Relnotes: n/a

Bug: 1098160
Change-Id: Ia52977e10ba60988ae2384ad6f13eafc65e11264
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2265552
Commit-Queue: Victor Fei <vicfei@microsoft.com>
Reviewed-by: Aaron Leventhal <aleventhal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#785149}
  • Loading branch information
vicfei-ms authored and Commit Bot committed Jul 4, 2020
1 parent fa66724 commit da90d49
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 0 deletions.
89 changes: 89 additions & 0 deletions ui/accessibility/platform/ax_platform_node_base.cc
Expand Up @@ -187,6 +187,95 @@ base::Optional<int> AXPlatformNodeBase::GetIndexInParent() {
return base::nullopt;
}

base::stack<gfx::NativeViewAccessible> AXPlatformNodeBase::GetAncestors() {
base::stack<gfx::NativeViewAccessible> ancestors;
gfx::NativeViewAccessible current_node = GetNativeViewAccessible();
while (current_node) {
ancestors.push(current_node);
current_node = FromNativeViewAccessible(current_node)->GetParent();
}

return ancestors;
}

base::Optional<int> AXPlatformNodeBase::CompareTo(AXPlatformNodeBase& other) {
// We define two node's relative positions in the following way:
// 1. this->CompareTo(other) == 0:
// - |this| and |other| are the same node.
// 2. this->CompareTo(other) < 0:
// - |this| is an ancestor of |other|.
// - |this|'s first uncommon ancestor comes before |other|'s first uncommon
// ancestor. The first uncommon ancestor is defined as the immediate child
// of the lowest common anestor of the two nodes. The first uncommon
// ancestor of |this| and |other| share the same parent (i.e. lowest common
// ancestor), so we can just compare the first uncommon ancestors' child
// indices to determine their relative positions.
// 3. this->CompareTo(other) == nullopt:
// - |this| and |other| are not comparable. E.g. they do not have a common
// ancestor.
//
// Another way to look at the nodes' relative positions/logical orders is that
// they are equivalent to pre-order traversal of the tree. If we pre-order
// traverse from the root, the node that we visited earlier is always going to
// be before (logically less) the node we visit later.

if (this == &other)
return base::Optional<int>(0);

// Compute the ancestor stacks of both positions and traverse them from the
// top most ancestor down, so we can discover the first uncommon ancestors.
// The first uncommon ancestor is the immediate child of the lowest common
// ancestor.
gfx::NativeViewAccessible common_ancestor = nullptr;
base::stack<gfx::NativeViewAccessible> our_ancestors = GetAncestors();
base::stack<gfx::NativeViewAccessible> other_ancestors = other.GetAncestors();

// Start at the root and traverse down. Keep going until the |this|'s ancestor
// chain and |other|'s ancestor chain disagree. The last node before they
// disagree is the lowest common ancestor.
while (!our_ancestors.empty() && !other_ancestors.empty() &&
our_ancestors.top() == other_ancestors.top()) {
common_ancestor = our_ancestors.top();
our_ancestors.pop();
other_ancestors.pop();
}

// Nodes do not have a common ancestor, they are not comparable.
if (!common_ancestor)
return base::nullopt;

// Compute the logical order when the common ancestor is |this| or |other|.
auto* common_ancestor_platform_node =
FromNativeViewAccessible(common_ancestor);
if (common_ancestor_platform_node == this)
return base::Optional<int>(-1);
if (common_ancestor_platform_node == &other)
return base::Optional<int>(1);

// Compute the logical order of |this| and |other| by using their first
// uncommon ancestors.
if (!our_ancestors.empty() && !other_ancestors.empty()) {
base::Optional<int> this_index_in_parent =
FromNativeViewAccessible(our_ancestors.top())->GetIndexInParent();
base::Optional<int> other_index_in_parent =
FromNativeViewAccessible(other_ancestors.top())->GetIndexInParent();

if (!this_index_in_parent || !other_index_in_parent)
return base::nullopt;

int this_uncommon_ancestor_index = this_index_in_parent.value();
int other_uncommon_ancestor_index = other_index_in_parent.value();
DCHECK_NE(this_uncommon_ancestor_index, other_uncommon_ancestor_index)
<< "Deepest uncommon ancestors should truly be uncommon, i.e. not "
"the same.";

return base::Optional<int>(this_uncommon_ancestor_index -
other_uncommon_ancestor_index);
}

return base::nullopt;
}

// AXPlatformNode overrides.

void AXPlatformNodeBase::Destroy() {
Expand Down
12 changes: 12 additions & 0 deletions ui/accessibility/platform/ax_platform_node_base.h
Expand Up @@ -74,6 +74,18 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode {
// the list of its parent's children, or its parent doesn't have children.
virtual base::Optional<int> GetIndexInParent();

// Returns a stack of ancestors of this node. The node at the top of the stack
// is the top most ancestor.
base::stack<gfx::NativeViewAccessible> GetAncestors();

// Returns an optional integer indicating the logical order of this node
// compared to another node or returns an empty optional if the nodes
// are not comparable.
// 0: if this position is logically equivalent to the other node
// <0: if this position is logically less than (before) the other node
// >0: if this position is logically greater than (after) the other node
base::Optional<int> CompareTo(AXPlatformNodeBase& other);

// AXPlatformNode.
void Destroy() override;
gfx::NativeViewAccessible GetNativeViewAccessible() override;
Expand Down
127 changes: 127 additions & 0 deletions ui/accessibility/platform/ax_platform_node_base_unittest.cc
Expand Up @@ -400,4 +400,131 @@ TEST(AXPlatformNodeBaseTest, TestSelectedChildrenMixed) {
EXPECT_EQ(fourth_child, fourth_selected_node->GetNativeViewAccessible());
}

TEST(AXPlatformNodeBaseTest, CompareTo) {
// Compare the nodes' logical orders for the following tree. Node name is
// denoted according to its id (i.e. "n#" is id#). Nodes that have smaller ids
// are always logically less than nodes with bigger ids.
//
// n1
// |
// __ n2 ___
// / \ \
// n3 _ n8 n9
// / \ \ \
// n4 n5 n6 n10
// /
// n7
AXPlatformNode::NotifyAddAXModeFlags(kAXModeComplete);
AXNodeData node1;
node1.id = 1;
node1.role = ax::mojom::Role::kWebArea;
node1.child_ids = {2};

AXNodeData node2;
node2.id = 2;
node2.role = ax::mojom::Role::kStaticText;
node2.child_ids = {3, 8, 9};

AXNodeData node3;
node3.id = 3;
node3.role = ax::mojom::Role::kStaticText;
node3.child_ids = {4, 5, 6};

AXNodeData node4;
node4.id = 4;
node4.role = ax::mojom::Role::kStaticText;

AXNodeData node5;
node5.id = 5;
node5.role = ax::mojom::Role::kStaticText;

AXNodeData node6;
node6.id = 6;
node6.role = ax::mojom::Role::kStaticText;
node6.child_ids = {7};

AXNodeData node7;
node7.id = 7;
node7.role = ax::mojom::Role::kStaticText;

AXNodeData node8;
node8.id = 8;
node8.role = ax::mojom::Role::kStaticText;

AXNodeData node9;
node9.id = 9;
node9.role = ax::mojom::Role::kStaticText;
node9.child_ids = {10};

AXNodeData node10;
node10.id = 10;
node10.role = ax::mojom::Role::kStaticText;

AXTreeUpdate update;
update.root_id = 1;
update.nodes = {node1, node2, node3, node4, node5,
node6, node7, node8, node9, node10};

AXTree tree(update);

// Retrieve the nodes in a level-order traversal way.
auto* n1 = static_cast<AXPlatformNodeBase*>(
TestAXNodeWrapper::GetOrCreate(&tree, tree.root())->ax_platform_node());
auto* n2 = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(n1->ChildAtIndex(0)));
auto* n3 = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(n2->ChildAtIndex(0)));
auto* n8 = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(n2->ChildAtIndex(1)));
auto* n9 = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(n2->ChildAtIndex(2)));
auto* n4 = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(n3->ChildAtIndex(0)));
auto* n5 = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(n3->ChildAtIndex(1)));
auto* n6 = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(n3->ChildAtIndex(2)));
auto* n10 = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(n9->ChildAtIndex(0)));
auto* n7 = static_cast<AXPlatformNodeBase*>(
AXPlatformNode::FromNativeViewAccessible(n6->ChildAtIndex(0)));

// Test for two nodes that do not share the same root. They should not be
// comparable.
AXPlatformNodeBase detached_node;
EXPECT_EQ(base::nullopt, n1->CompareTo(detached_node));

// Create a test vector of all the tree nodes arranged in a pre-order
// traversal way. The node that has a smaller index in the vector should also
// be logically less (comes before) the nodes with bigger index.
std::vector<AXPlatformNodeBase*> preorder_tree_nodes = {n1, n2, n3, n4, n5,
n6, n7, n8, n9, n10};
// Test through all permutations of lhs/rhs comparisons of nodes from
// |preorder_tree_nodes|.
for (auto* lhs : preorder_tree_nodes) {
for (auto* rhs : preorder_tree_nodes) {
int expected_result = 0;
if (lhs->GetData().id < rhs->GetData().id)
expected_result = -1;
else if (lhs->GetData().id > rhs->GetData().id)
expected_result = 1;

EXPECT_NE(base::nullopt, lhs->CompareTo(*rhs));
int actual_result = 0;
if (lhs->CompareTo(*rhs) < 0)
actual_result = -1;
else if (lhs->CompareTo(*rhs) > 0)
actual_result = 1;

SCOPED_TRACE(testing::Message()
<< "lhs.id=" << base::NumberToString(lhs->GetData().id)
<< ", rhs.id=" << base::NumberToString(rhs->GetData().id)
<< ", lhs->CompareTo(*rhs)={actual:"
<< base::NumberToString(actual_result) << ", expected:"
<< base::NumberToString(expected_result) << "}");

EXPECT_EQ(expected_result, actual_result);
}
}
}
} // namespace ui

0 comments on commit da90d49

Please sign in to comment.