blob: 5dc52393ec737f341962a09bd7c8c8d04130e546 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "accessibility_bridge.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "test_accessibility_bridge.h"
namespace flutter {
namespace testing {
using ::testing::Contains;
FlutterSemanticsNode CreateSemanticsNode(
int32_t id,
const char* label,
const std::vector<int32_t>* children = nullptr) {
return {
.id = id,
.flags = static_cast<FlutterSemanticsFlag>(0),
.actions = static_cast<FlutterSemanticsAction>(0),
.text_selection_base = -1,
.text_selection_extent = -1,
.label = label,
.hint = "",
.value = "",
.increased_value = "",
.decreased_value = "",
.child_count = children ? children->size() : 0,
.children_in_traversal_order = children ? children->data() : nullptr,
.custom_accessibility_actions_count = 0,
.tooltip = "",
};
}
TEST(AccessibilityBridgeTest, basicTest) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
std::vector<int32_t> children{1, 2};
FlutterSemanticsNode root = CreateSemanticsNode(0, "root", &children);
FlutterSemanticsNode child1 = CreateSemanticsNode(1, "child 1");
FlutterSemanticsNode child2 = CreateSemanticsNode(2, "child 2");
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->AddFlutterSemanticsNodeUpdate(&child1);
bridge->AddFlutterSemanticsNodeUpdate(&child2);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
auto child1_node = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
auto child2_node = bridge->GetFlutterPlatformNodeDelegateFromID(2).lock();
EXPECT_EQ(root_node->GetChildCount(), 2);
EXPECT_EQ(root_node->GetData().child_ids[0], 1);
EXPECT_EQ(root_node->GetData().child_ids[1], 2);
EXPECT_EQ(root_node->GetName(), "root");
EXPECT_EQ(child1_node->GetChildCount(), 0);
EXPECT_EQ(child1_node->GetName(), "child 1");
EXPECT_EQ(child2_node->GetChildCount(), 0);
EXPECT_EQ(child2_node->GetName(), "child 2");
}
TEST(AccessibilityBridgeTest, canFireChildrenChangedCorrectly) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
std::vector<int32_t> children{1};
FlutterSemanticsNode root = CreateSemanticsNode(0, "root", &children);
FlutterSemanticsNode child1 = CreateSemanticsNode(1, "child 1");
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->AddFlutterSemanticsNodeUpdate(&child1);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
auto child1_node = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
EXPECT_EQ(root_node->GetChildCount(), 1);
EXPECT_EQ(root_node->GetData().child_ids[0], 1);
EXPECT_EQ(root_node->GetName(), "root");
EXPECT_EQ(child1_node->GetChildCount(), 0);
EXPECT_EQ(child1_node->GetName(), "child 1");
bridge->accessibility_events.clear();
// Add a child to root.
root.child_count = 2;
int32_t new_children[] = {1, 2};
root.children_in_traversal_order = new_children;
FlutterSemanticsNode child2 = CreateSemanticsNode(2, "child 2");
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->AddFlutterSemanticsNodeUpdate(&child2);
bridge->CommitUpdates();
root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
EXPECT_EQ(root_node->GetChildCount(), 2);
EXPECT_EQ(root_node->GetData().child_ids[0], 1);
EXPECT_EQ(root_node->GetData().child_ids[1], 2);
EXPECT_EQ(bridge->accessibility_events.size(), size_t{2});
std::set<ui::AXEventGenerator::Event> actual_event{
bridge->accessibility_events.begin(), bridge->accessibility_events.end()};
EXPECT_THAT(actual_event,
Contains(ui::AXEventGenerator::Event::CHILDREN_CHANGED));
EXPECT_THAT(actual_event,
Contains(ui::AXEventGenerator::Event::SUBTREE_CREATED));
}
TEST(AccessibilityBridgeTest, canRecreateNodeDelegates) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
std::vector<int32_t> children{1};
FlutterSemanticsNode root = CreateSemanticsNode(0, "root", &children);
FlutterSemanticsNode child1 = CreateSemanticsNode(1, "child 1");
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->AddFlutterSemanticsNodeUpdate(&child1);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0);
auto child1_node = bridge->GetFlutterPlatformNodeDelegateFromID(1);
EXPECT_FALSE(root_node.expired());
EXPECT_FALSE(child1_node.expired());
bridge->RecreateNodeDelegates();
// Old tree is destroyed.
EXPECT_TRUE(root_node.expired());
EXPECT_TRUE(child1_node.expired());
// New tree still has the data.
auto new_root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
auto new_child1_node = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
EXPECT_EQ(new_root_node->GetChildCount(), 1);
EXPECT_EQ(new_root_node->GetData().child_ids[0], 1);
EXPECT_EQ(new_root_node->GetName(), "root");
EXPECT_EQ(new_child1_node->GetChildCount(), 0);
EXPECT_EQ(new_child1_node->GetName(), "child 1");
}
TEST(AccessibilityBridgeTest, canHandleSelectionChangeCorrectly) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
FlutterSemanticsNode root = CreateSemanticsNode(0, "root");
root.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField;
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();
const ui::AXTreeData& tree = bridge->GetAXTreeData();
EXPECT_EQ(tree.sel_anchor_object_id, ui::AXNode::kInvalidAXID);
bridge->accessibility_events.clear();
// Update the selection.
root.text_selection_base = 0;
root.text_selection_extent = 5;
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();
EXPECT_EQ(tree.sel_anchor_object_id, 0);
EXPECT_EQ(tree.sel_anchor_offset, 0);
EXPECT_EQ(tree.sel_focus_object_id, 0);
EXPECT_EQ(tree.sel_focus_offset, 5);
ASSERT_EQ(bridge->accessibility_events.size(), size_t{2});
EXPECT_EQ(bridge->accessibility_events[0],
ui::AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED);
EXPECT_EQ(bridge->accessibility_events[1],
ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED);
}
TEST(AccessibilityBridgeTest, doesNotAssignEditableRootToSelectableText) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
FlutterSemanticsNode root = CreateSemanticsNode(0, "root");
root.flags = static_cast<FlutterSemanticsFlag>(
FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField |
FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly);
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
EXPECT_FALSE(root_node->GetData().GetBoolAttribute(
ax::mojom::BoolAttribute::kEditableRoot));
}
TEST(AccessibilityBridgeTest, ToggleHasToggleButtonRole) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
FlutterSemanticsNode root = CreateSemanticsNode(0, "root");
root.flags = static_cast<FlutterSemanticsFlag>(
FlutterSemanticsFlag::kFlutterSemanticsFlagHasToggledState |
FlutterSemanticsFlag::kFlutterSemanticsFlagHasEnabledState |
FlutterSemanticsFlag::kFlutterSemanticsFlagIsEnabled);
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kToggleButton);
}
TEST(AccessibilityBridgeTest, SliderHasSliderRole) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
FlutterSemanticsNode root = CreateSemanticsNode(0, "root");
root.flags = static_cast<FlutterSemanticsFlag>(
FlutterSemanticsFlag::kFlutterSemanticsFlagIsSlider |
FlutterSemanticsFlag::kFlutterSemanticsFlagHasEnabledState |
FlutterSemanticsFlag::kFlutterSemanticsFlagIsEnabled |
FlutterSemanticsFlag::kFlutterSemanticsFlagIsFocusable);
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kSlider);
}
// Ensure that checkboxes have their checked status set apropriately
// Previously, only Radios could have this flag updated
// Resulted in the issue seen at
// https://github.com/flutter/flutter/issues/96218
// As this fix involved code run on all platforms, it is included here.
TEST(AccessibilityBridgeTest, CanSetCheckboxChecked) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
FlutterSemanticsNode root = CreateSemanticsNode(0, "root");
root.flags = static_cast<FlutterSemanticsFlag>(
FlutterSemanticsFlag::kFlutterSemanticsFlagHasCheckedState |
FlutterSemanticsFlag::kFlutterSemanticsFlagIsChecked);
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kCheckBox);
EXPECT_EQ(root_node->GetData().GetCheckedState(),
ax::mojom::CheckedState::kTrue);
}
// Verify that a node can be moved from one parent to another.
TEST(AccessibilityBridgeTest, CanReparentNode) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
std::vector<int32_t> root_children{1};
std::vector<int32_t> child1_children{2};
FlutterSemanticsNode root = CreateSemanticsNode(0, "root", &root_children);
FlutterSemanticsNode child1 =
CreateSemanticsNode(1, "child 1", &child1_children);
FlutterSemanticsNode child2 = CreateSemanticsNode(2, "child 2");
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->AddFlutterSemanticsNodeUpdate(&child1);
bridge->AddFlutterSemanticsNodeUpdate(&child2);
bridge->CommitUpdates();
bridge->accessibility_events.clear();
// Reparent child2 from child1 to the root.
child1.child_count = 0;
child1.children_in_traversal_order = nullptr;
int32_t new_root_children[] = {1, 2};
root.child_count = 2;
root.children_in_traversal_order = new_root_children;
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->AddFlutterSemanticsNodeUpdate(&child1);
bridge->AddFlutterSemanticsNodeUpdate(&child2);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
auto child1_node = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
auto child2_node = bridge->GetFlutterPlatformNodeDelegateFromID(2).lock();
EXPECT_EQ(root_node->GetChildCount(), 2);
EXPECT_EQ(root_node->GetData().child_ids[0], 1);
EXPECT_EQ(root_node->GetData().child_ids[1], 2);
EXPECT_EQ(root_node->GetName(), "root");
EXPECT_EQ(child1_node->GetChildCount(), 0);
EXPECT_EQ(child1_node->GetName(), "child 1");
EXPECT_EQ(child2_node->GetChildCount(), 0);
EXPECT_EQ(child2_node->GetName(), "child 2");
ASSERT_EQ(bridge->accessibility_events.size(), size_t{5});
// Child2 is moved from child1 to root.
EXPECT_THAT(bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::CHILDREN_CHANGED).Times(2));
EXPECT_THAT(bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::SUBTREE_CREATED).Times(1));
// Child1 is no longer a parent. It loses its group role and disables its
// 'clip children' attribute.
EXPECT_THAT(
bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED).Times(1));
EXPECT_THAT(bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::ROLE_CHANGED).Times(1));
}
// Verify that multiple nodes can be moved to new parents.
TEST(AccessibilityBridgeTest, CanReparentMultipleNodes) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
int32_t root_id = 0;
int32_t intermediary1_id = 1;
int32_t intermediary2_id = 2;
int32_t leaf1_id = 3;
int32_t leaf2_id = 4;
int32_t leaf3_id = 5;
std::vector<int32_t> root_children{intermediary1_id, intermediary2_id};
std::vector<int32_t> intermediary1_children{leaf1_id};
std::vector<int32_t> intermediary2_children{leaf2_id, leaf3_id};
FlutterSemanticsNode root =
CreateSemanticsNode(root_id, "root", &root_children);
FlutterSemanticsNode intermediary1 = CreateSemanticsNode(
intermediary1_id, "intermediary 1", &intermediary1_children);
FlutterSemanticsNode intermediary2 = CreateSemanticsNode(
intermediary2_id, "intermediary 2", &intermediary2_children);
FlutterSemanticsNode leaf1 = CreateSemanticsNode(leaf1_id, "leaf 1");
FlutterSemanticsNode leaf2 = CreateSemanticsNode(leaf2_id, "leaf 2");
FlutterSemanticsNode leaf3 = CreateSemanticsNode(leaf3_id, "leaf 3");
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->AddFlutterSemanticsNodeUpdate(&intermediary1);
bridge->AddFlutterSemanticsNodeUpdate(&intermediary2);
bridge->AddFlutterSemanticsNodeUpdate(&leaf1);
bridge->AddFlutterSemanticsNodeUpdate(&leaf2);
bridge->AddFlutterSemanticsNodeUpdate(&leaf3);
bridge->CommitUpdates();
bridge->accessibility_events.clear();
// Swap intermediary 1's and intermediary2's children.
int32_t new_intermediary1_children[] = {leaf2_id, leaf3_id};
intermediary1.child_count = 2;
intermediary1.children_in_traversal_order = new_intermediary1_children;
int32_t new_intermediary2_children[] = {leaf1_id};
intermediary2.child_count = 1;
intermediary2.children_in_traversal_order = new_intermediary2_children;
bridge->AddFlutterSemanticsNodeUpdate(&intermediary1);
bridge->AddFlutterSemanticsNodeUpdate(&intermediary2);
bridge->AddFlutterSemanticsNodeUpdate(&leaf1);
bridge->AddFlutterSemanticsNodeUpdate(&leaf2);
bridge->AddFlutterSemanticsNodeUpdate(&leaf3);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(root_id).lock();
auto intermediary1_node =
bridge->GetFlutterPlatformNodeDelegateFromID(intermediary1_id).lock();
auto intermediary2_node =
bridge->GetFlutterPlatformNodeDelegateFromID(intermediary2_id).lock();
auto leaf1_node =
bridge->GetFlutterPlatformNodeDelegateFromID(leaf1_id).lock();
auto leaf2_node =
bridge->GetFlutterPlatformNodeDelegateFromID(leaf2_id).lock();
auto leaf3_node =
bridge->GetFlutterPlatformNodeDelegateFromID(leaf3_id).lock();
EXPECT_EQ(root_node->GetChildCount(), 2);
EXPECT_EQ(root_node->GetData().child_ids[0], intermediary1_id);
EXPECT_EQ(root_node->GetData().child_ids[1], intermediary2_id);
EXPECT_EQ(root_node->GetName(), "root");
EXPECT_EQ(intermediary1_node->GetChildCount(), 2);
EXPECT_EQ(intermediary1_node->GetData().child_ids[0], leaf2_id);
EXPECT_EQ(intermediary1_node->GetData().child_ids[1], leaf3_id);
EXPECT_EQ(intermediary1_node->GetName(), "intermediary 1");
EXPECT_EQ(intermediary2_node->GetChildCount(), 1);
EXPECT_EQ(intermediary2_node->GetData().child_ids[0], leaf1_id);
EXPECT_EQ(intermediary2_node->GetName(), "intermediary 2");
EXPECT_EQ(leaf1_node->GetChildCount(), 0);
EXPECT_EQ(leaf1_node->GetName(), "leaf 1");
EXPECT_EQ(leaf2_node->GetChildCount(), 0);
EXPECT_EQ(leaf2_node->GetName(), "leaf 2");
EXPECT_EQ(leaf3_node->GetChildCount(), 0);
EXPECT_EQ(leaf3_node->GetName(), "leaf 3");
// Intermediary 1 and intermediary 2 have new children.
// Leaf 1, 2, and 3 are all moved.
ASSERT_EQ(bridge->accessibility_events.size(), size_t{5});
EXPECT_THAT(bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::CHILDREN_CHANGED).Times(2));
EXPECT_THAT(bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::SUBTREE_CREATED).Times(3));
}
// Verify that a node with a child can be moved from one parent to another.
TEST(AccessibilityBridgeTest, CanReparentNodeWithChild) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
int32_t root_id = 0;
int32_t intermediary1_id = 1;
int32_t intermediary2_id = 2;
int32_t leaf1_id = 3;
std::vector<int32_t> root_children{intermediary1_id, intermediary2_id};
std::vector<int32_t> intermediary1_children{leaf1_id};
FlutterSemanticsNode root =
CreateSemanticsNode(root_id, "root", &root_children);
FlutterSemanticsNode intermediary1 = CreateSemanticsNode(
intermediary1_id, "intermediary 1", &intermediary1_children);
FlutterSemanticsNode intermediary2 =
CreateSemanticsNode(intermediary2_id, "intermediary 2");
FlutterSemanticsNode leaf1 = CreateSemanticsNode(leaf1_id, "leaf 1");
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->AddFlutterSemanticsNodeUpdate(&intermediary1);
bridge->AddFlutterSemanticsNodeUpdate(&intermediary2);
bridge->AddFlutterSemanticsNodeUpdate(&leaf1);
bridge->CommitUpdates();
bridge->accessibility_events.clear();
// Move intermediary1 from root to intermediary 2.
int32_t new_root_children[] = {intermediary2_id};
root.child_count = 1;
root.children_in_traversal_order = new_root_children;
int32_t new_intermediary2_children[] = {intermediary1_id};
intermediary2.child_count = 1;
intermediary2.children_in_traversal_order = new_intermediary2_children;
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->AddFlutterSemanticsNodeUpdate(&intermediary1);
bridge->AddFlutterSemanticsNodeUpdate(&intermediary2);
bridge->AddFlutterSemanticsNodeUpdate(&leaf1);
bridge->CommitUpdates();
auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(root_id).lock();
auto intermediary1_node =
bridge->GetFlutterPlatformNodeDelegateFromID(intermediary1_id).lock();
auto intermediary2_node =
bridge->GetFlutterPlatformNodeDelegateFromID(intermediary2_id).lock();
auto leaf1_node =
bridge->GetFlutterPlatformNodeDelegateFromID(leaf1_id).lock();
EXPECT_EQ(root_node->GetChildCount(), 1);
EXPECT_EQ(root_node->GetData().child_ids[0], intermediary2_id);
EXPECT_EQ(root_node->GetName(), "root");
EXPECT_EQ(intermediary2_node->GetChildCount(), 1);
EXPECT_EQ(intermediary2_node->GetData().child_ids[0], intermediary1_id);
EXPECT_EQ(intermediary2_node->GetName(), "intermediary 2");
EXPECT_EQ(intermediary1_node->GetChildCount(), 1);
EXPECT_EQ(intermediary1_node->GetData().child_ids[0], leaf1_id);
EXPECT_EQ(intermediary1_node->GetName(), "intermediary 1");
EXPECT_EQ(leaf1_node->GetChildCount(), 0);
EXPECT_EQ(leaf1_node->GetName(), "leaf 1");
ASSERT_EQ(bridge->accessibility_events.size(), size_t{5});
EXPECT_THAT(bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::CHILDREN_CHANGED).Times(2));
EXPECT_THAT(bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::SUBTREE_CREATED).Times(1));
// Intermediary 2 becomes a parent node. It updates to group role and enables
// its 'clip children' attribute.
EXPECT_THAT(
bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED).Times(1));
EXPECT_THAT(bridge->accessibility_events,
Contains(ui::AXEventGenerator::Event::ROLE_CHANGED).Times(1));
}
} // namespace testing
} // namespace flutter