blob: 027d842811a4884b5e4b0b2310362e707dc37758 [file] [log] [blame]
// Copyright 2019 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.
#include "chromecast/browser/accessibility/flutter/ax_tree_source_flutter.h"
#include <string>
#include "base/json/values_util.h"
#include "base/unguessable_token.h"
#include "chromecast/browser/accessibility/flutter/flutter_semantics_node_wrapper.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/tts_controller.h"
#include "content/public/browser/tts_platform.h"
#include "content/public/common/content_client.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "extensions/browser/api/automation_internal/automation_event_router_interface.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_action_data.h"
using ::testing::StrictMock;
using ::gallium::castos::ActionProperties;
using ::gallium::castos::BooleanProperties;
using ::gallium::castos::OnAccessibilityEventRequest;
using ::gallium::castos::OnAccessibilityEventRequest_EventType_ANNOUNCEMENT;
using ::gallium::castos::OnAccessibilityEventRequest_EventType_CONTENT_CHANGED;
using ::gallium::castos::OnAccessibilityEventRequest_EventType_FOCUSED;
using ::gallium::castos::Rect;
namespace content {
class MockTtsPlatformImpl : public TtsPlatform {
public:
MockTtsPlatformImpl() = default;
virtual ~MockTtsPlatformImpl() = default;
void set_voices(const std::vector<VoiceData>& voices) { voices_ = voices; }
void set_run_speak_callback(bool value) { run_speak_callback_ = value; }
void set_is_speaking(bool value) { is_speaking_ = value; }
// TtsPlatform:
bool PlatformImplSupported() override { return true; }
bool PlatformImplInitialized() override { return true; }
void Speak(int utterance_id,
const std::string& utterance,
const std::string& lang,
const VoiceData& voice,
const UtteranceContinuousParameters& params,
base::OnceCallback<void(bool)> on_speak_finished) override {
if (run_speak_callback_)
std::move(on_speak_finished).Run(true);
last_spoken_utterance_ = utterance;
}
bool IsSpeaking() override { return is_speaking_; }
bool StopSpeaking() override { return true; }
void Pause() override {}
void Resume() override {}
void GetVoices(std::vector<VoiceData>* out_voices) override {
*out_voices = voices_;
}
void GetVoicesForBrowserContext(
content::BrowserContext* browser_context,
const GURL& source_url,
std::vector<content::VoiceData>* out_voices) override {}
void LoadBuiltInTtsEngine(BrowserContext* browser_context) override {}
void WillSpeakUtteranceWithVoice(TtsUtterance* utterance,
const VoiceData& voice_data) override {}
void SetError(const std::string& error) override {}
std::string GetError() override { return std::string(); }
void ClearError() override {}
const std::string& GetLastSpokenUtterance() { return last_spoken_utterance_; }
void ClearLastSpokenUtterance() { last_spoken_utterance_ = ""; }
void Shutdown() override {}
bool PreferEngineDelegateVoices() override { return false; }
private:
std::vector<VoiceData> voices_;
bool run_speak_callback_ = true;
bool is_speaking_ = false;
std::string last_spoken_utterance_;
};
class MockContentBrowserClient : public ContentBrowserClient {
public:
~MockContentBrowserClient() override {}
};
class MockContentClient : public ContentClient {
public:
~MockContentClient() override {}
};
} // namespace content
namespace chromecast {
namespace accessibility {
class MockAutomationEventRouter
: public extensions::AutomationEventRouterInterface {
public:
MockAutomationEventRouter() {}
virtual ~MockAutomationEventRouter() = default;
void DispatchAccessibilityEvents(const ui::AXTreeID& tree_id,
std::vector<ui::AXTreeUpdate> updates,
const gfx::Point& mouse_location,
std::vector<ui::AXEvent> events) {
for (const auto& event : events)
event_count_[event.event_type]++;
last_updates_ = updates;
}
MOCK_METHOD(void,
DispatchAccessibilityLocationChange,
(const ExtensionMsg_AccessibilityLocationChangeParams&),
(override));
MOCK_METHOD(void,
DispatchTreeDestroyedEvent,
(ui::AXTreeID, content::BrowserContext*),
(override));
MOCK_METHOD(void,
DispatchActionResult,
(const ui::AXActionData&, bool, content::BrowserContext*),
(override));
void DispatchGetTextLocationDataResult(
const ui::AXActionData& data,
const absl::optional<gfx::Rect>& rect) override {}
std::map<ax::mojom::Event, int> event_count_;
std::vector<ui::AXTreeUpdate> last_updates_;
};
class AXTreeSourceFlutterTest : public testing::Test,
public AXTreeSourceFlutter::Delegate {
public:
AXTreeSourceFlutterTest()
: tree_(std::make_unique<AXTreeSourceFlutter>(this,
&browser_context_,
&router_)) {
// Enable by default.
tree_->SetAccessibilityEnabled(true);
// Setup some mocks required for tts platform impl
content::SetContentClient(&client_);
content::SetBrowserClientForTesting(&mock_content_browser_client_);
}
AXTreeSourceFlutterTest(const AXTreeSourceFlutterTest&) = delete;
~AXTreeSourceFlutterTest() override {
EXPECT_CALL(router_, DispatchTreeDestroyedEvent).Times(1);
content::SetBrowserClientForTesting(nullptr);
content::SetContentClient(nullptr);
}
AXTreeSourceFlutterTest& operator=(const AXTreeSourceFlutterTest&) = delete;
protected:
void CallNotifyAccessibilityEvent(OnAccessibilityEventRequest* event_data) {
tree_->NotifyAccessibilityEvent(event_data);
}
void SetAccessibilityEnabled(bool value) {
tree_->SetAccessibilityEnabled(value);
}
void CallGetChildren(SemanticsNode* node,
std::vector<FlutterSemanticsNode*>* out_children) const {
FlutterSemanticsNodeWrapper node_data(tree_.get(), node);
tree_->GetChildren(&node_data, out_children);
}
void CallSerializeNode(SemanticsNode* node,
std::unique_ptr<ui::AXNodeData>* out_data) const {
ASSERT_TRUE(out_data);
FlutterSemanticsNodeWrapper node_data(tree_.get(), node);
*out_data = std::make_unique<ui::AXNodeData>();
tree_->SerializeNode(&node_data, out_data->get());
}
FlutterSemanticsNode* CallGetFromId(int32_t id) const {
return tree_->GetFromId(id);
}
bool CallGetTreeData(ui::AXTreeData* data) {
return tree_->GetTreeData(data);
}
int GetDispatchedEventCount(ax::mojom::Event type) {
return router_.event_count_[type];
}
const std::vector<ui::AXTreeUpdate>& GetLastUpdates() {
return router_.last_updates_;
}
void SetRect(Rect* rect, int left, int top, int right, int bottom) {
rect->set_left(left);
rect->set_top(top);
rect->set_right(right);
rect->set_bottom(bottom);
}
void ReparentHelperInitial() {
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
// 0
// / \
// 1 2
// \
// 3
SemanticsNode* root = event.add_node_data();
SemanticsNode* child1 = event.add_node_data();
SemanticsNode* child2 = event.add_node_data();
SemanticsNode* child3 = event.add_node_data();
root->set_node_id(0);
root->add_child_node_ids(1);
root->add_child_node_ids(2);
child1->set_node_id(1);
child2->set_node_id(2);
child2->add_child_node_ids(3);
child3->set_node_id(3);
// Populate the tree source with the data.
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
void ReparentHelperUpdate() {
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
// We are going to reparent child 3
// 0
// / \
// 1 4
// \
// 3
SemanticsNode* root = event.add_node_data();
SemanticsNode* child1 = event.add_node_data();
SemanticsNode* child4 = event.add_node_data();
SemanticsNode* child3 = event.add_node_data();
root->set_node_id(0);
root->add_child_node_ids(1);
root->add_child_node_ids(4);
child1->set_node_id(1);
child4->set_node_id(4);
child4->add_child_node_ids(3);
child3->set_node_id(3);
// Populate the tree source with the data.
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(3, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
SemanticsNode* AddChild(OnAccessibilityEventRequest* event,
SemanticsNode* parent,
int id,
int x,
int y,
int w,
int h,
bool click) {
BooleanProperties* boolean_properties;
ActionProperties* action_properties;
SemanticsNode* new_node = event->add_node_data();
new_node->set_node_id(id);
Rect* bounds = new_node->mutable_bounds_in_screen();
SetRect(bounds, x, y, x + w, y + h);
boolean_properties = new_node->mutable_boolean_properties();
boolean_properties->set_is_button(click);
if (click) {
new_node->set_label("Button");
}
action_properties = new_node->mutable_action_properties();
action_properties->set_tap(click);
parent->add_child_node_ids(id);
return new_node;
}
gfx::Rect GetVirtualKeyboardBounds() const {
return virtual_keyboard_bounds_;
}
private:
void OnAction(const ui::AXActionData& data) override {}
void OnVirtualKeyboardBoundsChange(const gfx::Rect& bounds) override {
virtual_keyboard_bounds_ = bounds;
}
// Required for the TestBrowserContext.
content::BrowserTaskEnvironment task_environment_;
content::TestBrowserContext browser_context_;
MockAutomationEventRouter router_;
std::unique_ptr<AXTreeSourceFlutter> tree_;
content::MockContentBrowserClient mock_content_browser_client_;
content::MockContentClient client_;
gfx::Rect virtual_keyboard_bounds_;
};
TEST_F(AXTreeSourceFlutterTest, AccessibleNameComputation) {
ActionProperties* action_properties;
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
// 0
// / \
// 1 2
SemanticsNode* root = event.add_node_data();
root->set_node_id(0);
root->add_child_node_ids(1);
root->add_child_node_ids(2);
// Child.
SemanticsNode* child1 = event.add_node_data();
child1->set_node_id(1);
// Another child.
SemanticsNode* child2 = event.add_node_data();
child2->set_node_id(2);
// Populate the tree source with the data.
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
// No attributes.
std::unique_ptr<ui::AXNodeData> data;
CallSerializeNode(root, &data);
std::string name;
ASSERT_FALSE(
data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("", name);
// Label (empty).
root->set_label("");
CallSerializeNode(root, &data);
ASSERT_TRUE(
data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("", name);
// Label (non-empty).
root->set_label("label text");
CallSerializeNode(root, &data);
ASSERT_TRUE(
data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("label text", name);
// Hint (empty), Label (non-empty).
root->set_hint("");
CallSerializeNode(root, &data);
ASSERT_TRUE(
data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("label text", name);
// Hint (non-empty), Label (empty).
root->set_hint("hint");
root->clear_label();
CallSerializeNode(root, &data);
ASSERT_TRUE(
data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("hint", name);
// Name from contents.
// Root node has no name, but has descendants with name.
root->clear_hint();
root->clear_label();
// Name from contents only happens if a node is clickable.
action_properties = root->mutable_action_properties();
action_properties->set_tap(true);
child1->set_label("child1 label text");
child2->set_label("child2 label text");
CallSerializeNode(root, &data);
ASSERT_TRUE(
data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
ASSERT_EQ("child1 label text child2 label text", name);
// If the node has a name, it should override the contents.
root->set_label("root label text");
CallSerializeNode(root, &data);
ASSERT_TRUE(
data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
ASSERT_EQ("root label text", name);
// Clearing both clickable and name from root, the name should not be
// populated.
root->clear_label();
action_properties->clear_tap();
CallSerializeNode(root, &data);
ASSERT_FALSE(
data->GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
}
// Flutter's 'hidden' attribute should not translate to the node being
// ignored or in any way not a11y focusable.
TEST_F(AXTreeSourceFlutterTest, NeverHidden) {
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
BooleanProperties* boolean_properties;
SemanticsNode* root = event.add_node_data();
root->add_child_node_ids(1);
SemanticsNode* child1 = event.add_node_data();
child1->set_node_id(1);
child1->set_label("some label text");
boolean_properties = child1->mutable_boolean_properties();
boolean_properties->set_is_hidden(true);
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
std::unique_ptr<ui::AXNodeData> data;
CallSerializeNode(child1, &data);
ASSERT_TRUE(data->role == ax::mojom::Role::kStaticText);
ASSERT_FALSE(data->HasState(ax::mojom::State::kInvisible));
}
TEST_F(AXTreeSourceFlutterTest, GetTreeDataAppliesFocus) {
OnAccessibilityEventRequest event;
event.set_source_id(5);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
SemanticsNode* root = event.add_node_data();
root->set_node_id(5);
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
ui::AXTreeData data;
// If no node claimed focus, the root node should get it.
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(5, data.focus_id);
// Add a child node with focus.
root->add_child_node_ids(6);
SemanticsNode* child = event.add_node_data();
child->set_node_id(6);
child->mutable_boolean_properties()->set_is_focused(true);
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(2, GetDispatchedEventCount(ax::mojom::Event::kFocus));
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(6, data.focus_id);
}
TEST_F(AXTreeSourceFlutterTest, LiveRegion) {
OnAccessibilityEventRequest event;
event.set_source_id(1);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
SemanticsNode* root = event.add_node_data();
root->set_node_id(10);
root->add_child_node_ids(1);
root->add_child_node_ids(2);
BooleanProperties* boolean_properties = root->mutable_boolean_properties();
boolean_properties->set_is_live_region(true);
// Add child nodes.
SemanticsNode* node1 = event.add_node_data();
node1->set_node_id(1);
node1->set_label("text 1");
SemanticsNode* node2 = event.add_node_data();
node2->set_node_id(2);
node2->set_label("text 2");
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
std::unique_ptr<ui::AXNodeData> data;
CallSerializeNode(root, &data);
std::string status;
ASSERT_TRUE(data->GetStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus, &status));
ASSERT_EQ(status, "polite");
EXPECT_EQ(0, GetDispatchedEventCount(ax::mojom::Event::kLiveRegionChanged));
// Modify text of node1.
node1->set_label("modified text 1");
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kLiveRegionChanged));
}
TEST_F(AXTreeSourceFlutterTest, ResetFocus) {
//
// tree 1: no child tree
//
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
SemanticsNode* root = event.add_node_data();
root->set_node_id(0);
Rect* bounds = root->mutable_bounds_in_screen();
SetRect(bounds, 0, 0, 1280, 800);
SemanticsNode* child;
AddChild(&event, root, 1, 0, 0, 800, 600, false);
child = AddChild(&event, root, 2, 0, 0, 400, 600, false);
child = AddChild(&event, root, 3, 400, 0, 400, 600, false);
CallNotifyAccessibilityEvent(&event);
// initial focus on root
ui::AXTreeData tree_data;
CallGetTreeData(&tree_data);
ASSERT_EQ(0, tree_data.focus_id);
//
// tree 2: add a node with a child tree id
//
OnAccessibilityEventRequest event2;
event2.set_source_id(0);
event2.set_window_id(1);
event2.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
SemanticsNode* root2 = event2.add_node_data();
root2->set_node_id(0);
Rect* bounds2 = root2->mutable_bounds_in_screen();
SetRect(bounds2, 0, 0, 1280, 800);
AddChild(&event2, root2, 1, 0, 0, 800, 600, false);
child = AddChild(&event2, root2, 2, 0, 0, 400, 600, false);
child = AddChild(&event2, root2, 3, 400, 0, 400, 600, false);
child = AddChild(&event2, root2, 4, 0, 0, 200, 200, false);
// We need a plugin id that is the right length. Here we use
base::UnguessableToken token = base::UnguessableToken::Create();
std::string token_to_string =
base::UnguessableTokenToValue(token).GetString();
child->set_plugin_id(token_to_string);
// focus should move to node with child tree
CallNotifyAccessibilityEvent(&event2);
CallGetTreeData(&tree_data);
ASSERT_EQ(4, tree_data.focus_id);
//
// tree 2: back to initial tree
//
CallNotifyAccessibilityEvent(&event);
CallGetTreeData(&tree_data);
// focus back to root
ASSERT_EQ(0, tree_data.focus_id);
}
TEST_F(AXTreeSourceFlutterTest, NoClickable) {
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
SemanticsNode* root = event.add_node_data();
root->set_node_id(0);
ActionProperties* action_properties = root->mutable_action_properties();
action_properties->set_tap(true);
Rect* bounds = root->mutable_bounds_in_screen();
SetRect(bounds, 0, 0, 1280, 800);
CallNotifyAccessibilityEvent(&event);
std::unique_ptr<ui::AXNodeData> data;
CallSerializeNode(root, &data);
// No node should get the clickable attribute.
ASSERT_FALSE(data->GetBoolAttribute(ax::mojom::BoolAttribute::kClickable));
}
// Tests a new node with scopes route will focus and speak
// a child with names route set.
TEST_F(AXTreeSourceFlutterTest, ScopesRoute) {
// Add node with scopes route and child with names route. Focus
// should move to node with names route.
//
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_CONTENT_CHANGED);
SemanticsNode* root = event.add_node_data();
root->set_node_id(0);
SemanticsNode* child1;
SemanticsNode* child2;
SemanticsNode* child3;
SemanticsNode* child4;
SemanticsNode* child5;
SemanticsNode* child6;
child1 = AddChild(&event, root, 1, 0, 0, 1, 1, false);
child2 = AddChild(&event, child1, 2, 0, 0, 1, 1, false);
child3 = AddChild(&event, child2, 3, 0, 0, 1, 1, false);
std::string child_3_label = "Speak This";
child3->set_label(child_3_label);
child4 = AddChild(&event, root, 4, 0, 0, 1, 1, false);
child5 = AddChild(&event, child4, 5, 0, 0, 1, 1, false);
child6 = AddChild(&event, child5, 6, 0, 0, 1, 1, false);
std::string child_6_label = "Speak That";
child6->set_label(child_6_label);
BooleanProperties* boolean_properties;
// Set scopes on child2, names on child3
boolean_properties = child2->mutable_boolean_properties();
boolean_properties->set_scopes_route(true);
boolean_properties = child3->mutable_boolean_properties();
boolean_properties->set_names_route(true);
// Set scopes on child5, names on child6
boolean_properties = child5->mutable_boolean_properties();
boolean_properties->set_scopes_route(true);
boolean_properties = child6->mutable_boolean_properties();
boolean_properties->set_names_route(true);
CallNotifyAccessibilityEvent(&event);
// focus should have moved to child 6 since that is the first
// node that will be found with scopes
ui::AXTreeData tree_data;
CallGetTreeData(&tree_data);
ASSERT_EQ(6, tree_data.focus_id);
// Same tree should not speak the same scopes_route/names_route
CallNotifyAccessibilityEvent(&event);
// Now setup another tree but with 5&6 removed. This should
// make the tree source focus (but not speak) 3
OnAccessibilityEventRequest event2;
event2.set_source_id(0);
event2.set_window_id(1);
event2.set_event_type(OnAccessibilityEventRequest_EventType_CONTENT_CHANGED);
root = event2.add_node_data();
root->set_node_id(0);
child1 = AddChild(&event2, root, 1, 0, 0, 1, 1, false);
child2 = AddChild(&event2, child1, 2, 0, 0, 1, 1, false);
child3 = AddChild(&event2, child2, 3, 0, 0, 1, 1, false);
child3->set_label(child_3_label);
// Set scopes on child2, names on child3
boolean_properties = child2->mutable_boolean_properties();
boolean_properties->set_scopes_route(true);
boolean_properties = child3->mutable_boolean_properties();
boolean_properties->set_names_route(true);
CallNotifyAccessibilityEvent(&event2);
// Focus moves to 3
CallGetTreeData(&tree_data);
ASSERT_EQ(3, tree_data.focus_id);
// Now step to have removed node 3 and re-add node 3 back in the tree. In this
// case, node 3 should be refocused and spoken.
OnAccessibilityEventRequest event3;
event3.set_source_id(0);
event3.set_window_id(1);
event3.set_event_type(OnAccessibilityEventRequest_EventType_CONTENT_CHANGED);
root = event3.add_node_data();
root->set_node_id(0);
child1 = AddChild(&event3, root, 1, 0, 0, 1, 1, false);
child2 = AddChild(&event3, child1, 2, 0, 0, 1, 1, false);
// Set scopes on child2
boolean_properties = child2->mutable_boolean_properties();
boolean_properties->set_scopes_route(true);
CallNotifyAccessibilityEvent(&event3);
OnAccessibilityEventRequest event4;
event4.set_source_id(0);
event4.set_window_id(1);
event4.set_event_type(OnAccessibilityEventRequest_EventType_CONTENT_CHANGED);
root = event4.add_node_data();
root->set_node_id(0);
child1 = AddChild(&event4, root, 1, 0, 0, 1, 1, false);
child2 = AddChild(&event4, child1, 2, 0, 0, 1, 1, false);
child3 = AddChild(&event4, child2, 3, 0, 0, 1, 1, false);
child3->set_label(child_3_label);
// Set scopes on child2, names on child3
boolean_properties = child2->mutable_boolean_properties();
boolean_properties->set_scopes_route(true);
boolean_properties = child3->mutable_boolean_properties();
boolean_properties->set_names_route(true);
CallNotifyAccessibilityEvent(&event4);
// Focus moves to 3
CallGetTreeData(&tree_data);
ASSERT_EQ(3, tree_data.focus_id);
// Finally, remove 2
//
OnAccessibilityEventRequest event5;
event5.set_source_id(0);
event5.set_window_id(1);
event5.set_event_type(OnAccessibilityEventRequest_EventType_CONTENT_CHANGED);
root = event5.add_node_data();
root->set_node_id(0);
CallNotifyAccessibilityEvent(&event5);
CallGetTreeData(&tree_data);
ASSERT_EQ(0, tree_data.focus_id);
}
// A test to ensure a node that had scopes route but never
// had any names route descendant does not cause a refocus.
TEST_F(AXTreeSourceFlutterTest, ScopesRouteNoNames) {
// Install a mock tts platform
auto* tts_controller = content::TtsController::GetInstance();
content::MockTtsPlatformImpl mock_tts_platform;
tts_controller->SetTtsPlatform(&mock_tts_platform);
// Add node with scopes route but no names route descendant.
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_CONTENT_CHANGED);
SemanticsNode* root = event.add_node_data();
root->set_node_id(0);
SemanticsNode* child1;
SemanticsNode* child2;
child1 = AddChild(&event, root, 1, 0, 0, 1, 1, false);
child2 = AddChild(&event, child1, 2, 0, 0, 1, 1, false);
std::string child_2_label = "Don't Speak This";
child2->set_label(child_2_label);
BooleanProperties* boolean_properties;
// Set scopes on child1 but no names on child 2
boolean_properties = child1->mutable_boolean_properties();
boolean_properties->set_scopes_route(true);
mock_tts_platform.ClearLastSpokenUtterance();
CallNotifyAccessibilityEvent(&event);
// focus should remain on root
ui::AXTreeData tree_data;
CallGetTreeData(&tree_data);
// Confirm no spoken TTS or focus change
ASSERT_TRUE(mock_tts_platform.GetLastSpokenUtterance() == "");
ASSERT_EQ(0, tree_data.focus_id);
EXPECT_EQ(0, GetDispatchedEventCount(ax::mojom::Event::kFocus));
// Remove the node with scopes route. Again, no focus or spoken
// tts is expected.
OnAccessibilityEventRequest event2;
event2.set_source_id(0);
event2.set_window_id(1);
event2.set_event_type(OnAccessibilityEventRequest_EventType_CONTENT_CHANGED);
SemanticsNode* root2 = event2.add_node_data();
root2->set_node_id(0);
CallNotifyAccessibilityEvent(&event2);
// Again, confirm no spoken TTS or focus change
ASSERT_EQ(0, tree_data.focus_id);
ASSERT_TRUE(mock_tts_platform.GetLastSpokenUtterance() == "");
EXPECT_EQ(0, GetDispatchedEventCount(ax::mojom::Event::kFocus));
// Cleanup since the mock will expire at the end of this test.
tts_controller->SetTtsPlatform(content::TtsPlatform::GetInstance());
}
TEST_F(AXTreeSourceFlutterTest, LeaveTheChildrenAlone) {
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
SemanticsNode* root = event.add_node_data();
root->set_node_id(0);
Rect* bounds = root->mutable_bounds_in_screen();
SetRect(bounds, 0, 0, 1280, 800);
SemanticsNode* child;
AddChild(&event, root, 17, 0, 0, 422, 800, true);
child = AddChild(&event, root, 29, 1022, 0, 257, 800, false);
child = AddChild(&event, child, 30, 1022, 55, 257, 690, true);
AddChild(&event, child, 31, 1232, 165, 47, 150, false);
AddChild(&event, child, 32, 1232, 165, 47, 150, false);
child = AddChild(&event, root, 18, 422, 0, 600, 800, false);
child = AddChild(&event, child, 19, 422, 55, 570, 690, false);
AddChild(&event, child, 20, 507, 95, 445, 40, false);
AddChild(&event, child, 21, 463, 186, 488, 70, true);
AddChild(&event, child, 22, 463, 283, 488, 70, true);
AddChild(&event, child, 23, 463, 381, 488, 70, true);
AddChild(&event, child, 24, 463, 478, 488, 70, true);
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
std::vector<FlutterSemanticsNode*> ordered;
CallGetChildren(root, &ordered);
ASSERT_EQ(3U, ordered.size());
EXPECT_EQ(17, ordered[0]->GetId());
EXPECT_EQ(29, ordered[1]->GetId());
EXPECT_EQ(18, ordered[2]->GetId());
}
TEST_F(AXTreeSourceFlutterTest, Announce) {
// Install a mock tts platform
auto* tts_controller = content::TtsController::GetInstance();
content::MockTtsPlatformImpl mock_tts_platform;
tts_controller->SetTtsPlatform(&mock_tts_platform);
// Setup an announcement event
OnAccessibilityEventRequest event;
SetAccessibilityEnabled(false);
event.set_event_type(OnAccessibilityEventRequest_EventType_ANNOUNCEMENT);
event.set_text("Say this please");
CallNotifyAccessibilityEvent(&event);
// Child 3 should NOT have been spoken
ASSERT_EQ(mock_tts_platform.GetLastSpokenUtterance(), "");
// This time with a11y enabled.
SetAccessibilityEnabled(true);
CallNotifyAccessibilityEvent(&event);
// Child 3 should have been spoken
ASSERT_EQ(mock_tts_platform.GetLastSpokenUtterance(), "Say this please");
// Cleanup since the mock will expire at the end of this test.
tts_controller->SetTtsPlatform(content::TtsPlatform::GetInstance());
}
TEST_F(AXTreeSourceFlutterTest, KeyboardBounds) {
OnAccessibilityEventRequest event;
event.set_source_id(1);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
SemanticsNode* root = event.add_node_data();
root->set_node_id(10);
root->add_child_node_ids(1);
root->add_child_node_ids(2);
// Add child nodes.
SemanticsNode* node1 = event.add_node_data();
node1->set_node_id(1);
node1->set_label("text 1");
Rect* bounds1 = node1->mutable_bounds_in_screen();
SetRect(bounds1, 0, 0, 320, 200);
SemanticsNode* node2 = event.add_node_data();
node2->set_node_id(2);
node2->set_label("text 2");
Rect* bounds2 = node2->mutable_bounds_in_screen();
SetRect(bounds2, 320, 200, 640, 400);
// No keyboard nodes.
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(gfx::Rect(), GetVirtualKeyboardBounds());
// node 1 and node 2 are keyboard nodes.
BooleanProperties* boolean_properties;
boolean_properties = node1->mutable_boolean_properties();
boolean_properties->set_is_lift_to_type(true);
boolean_properties = node2->mutable_boolean_properties();
boolean_properties->set_is_lift_to_type(true);
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(gfx::Rect(0, 0, 640, 400), GetVirtualKeyboardBounds());
}
// b/190749275 - Repro crash due to pointers to event
// data that went out of scope still held on to by
// the tree source. Triggered by reparenting children with
// empty update in between updates.
TEST_F(AXTreeSourceFlutterTest, ReparentedChildren) {
// Send in the initial tree.
ReparentHelperInitial();
// Send an empty update.
OnAccessibilityEventRequest event;
event.set_source_id(0);
event.set_window_id(1);
event.set_event_type(OnAccessibilityEventRequest_EventType_FOCUSED);
CallNotifyAccessibilityEvent(&event);
EXPECT_EQ(2, GetDispatchedEventCount(ax::mojom::Event::kFocus));
// Reparent node 3.
ReparentHelperUpdate();
// There should be an extra update from the one notification
// we just did in which child 3 should not appear.
const std::vector<ui::AXTreeUpdate>& last_updates = GetLastUpdates();
EXPECT_EQ(2ul, last_updates.size());
// Make sure child 3 has disappeared in the first update.
const ui::AXTreeUpdate& first_update = last_updates[0];
for (const auto& node : first_update.nodes) {
EXPECT_NE(3, node.id);
}
}
} // namespace accessibility
} // namespace chromecast