| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/accessibility/features/automation_internal_bindings.h" |
| |
| #include "base/check.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/path_service.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/task_environment.h" |
| #include "gin/public/context_holder.h" |
| #include "gin/public/isolate_holder.h" |
| #include "services/accessibility/fake_service_client.h" |
| #include "services/accessibility/features/bindings_isolate_holder.h" |
| #include "services/accessibility/features/v8_manager.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "v8/include/v8-context.h" |
| #include "v8/include/v8-template.h" |
| |
| namespace ax { |
| |
| namespace { |
| |
| // Single-threaded IsolateHolder for use in bindings tests. |
| // TODO(crbug.com/1357889): This is somewhat similar to |
| // chrome/test/base/v8_unit_test.h which is used by js2gtest, which would be a |
| // good test framework to use for all the bindings tests eventually, depending |
| // on Chrome dependencies. |
| class TestIsolateHolder : public BindingsIsolateHolder { |
| public: |
| TestIsolateHolder() = default; |
| TestIsolateHolder(const TestIsolateHolder&) = delete; |
| TestIsolateHolder& operator=(const TestIsolateHolder&) = delete; |
| virtual ~TestIsolateHolder() = default; |
| |
| // BindingsIsolateHolder: |
| v8::Isolate* GetIsolate() const override { |
| return isolate_holder_->isolate(); |
| } |
| v8::Local<v8::Context> GetContext() const override { |
| return context_holder_->context(); |
| } |
| void HandleError(const std::string& message) override { |
| LOG(ERROR) << message; |
| error_count_++; |
| } |
| |
| void InstallAutomation( |
| mojo::PendingAssociatedReceiver<mojom::Automation> automation) { |
| automation_bindings_ = std::make_unique<AutomationInternalBindings>( |
| this, std::move(automation)); |
| } |
| |
| AutomationInternalBindings* GetAutomationBindings() { |
| return automation_bindings_.get(); |
| } |
| |
| void StartTestV8AndBindAutomation() { |
| CHECK(automation_bindings_); |
| BindingsIsolateHolder::InitializeV8(); |
| |
| // Test isolate uses the test main thread and will block. |
| isolate_holder_ = std::make_unique<gin::IsolateHolder>( |
| base::SingleThreadTaskRunner::GetCurrentDefault(), |
| gin::IsolateHolder::kSingleThread, |
| gin::IsolateHolder::IsolateType::kUtility); |
| |
| v8::Isolate::Scope isolate_scope(isolate_holder_->isolate()); |
| v8::HandleScope handle_scope(isolate_holder_->isolate()); |
| v8::Local<v8::ObjectTemplate> global_template = |
| v8::ObjectTemplate::New(isolate_holder_->isolate()); |
| |
| v8::Local<v8::ObjectTemplate> automation_template = |
| v8::ObjectTemplate::New(isolate_holder_->isolate()); |
| automation_bindings_->AddAutomationRoutesToTemplate(&automation_template); |
| global_template->Set(isolate_holder_->isolate(), "nativeAutomationInternal", |
| automation_template); |
| |
| v8::Local<v8::Context> context = |
| v8::Context::New(isolate_holder_->isolate(), |
| /*extensions=*/nullptr, global_template); |
| context_holder_ = |
| std::make_unique<gin::ContextHolder>(isolate_holder_->isolate()); |
| context_holder_->SetContext(context); |
| } |
| |
| base::WeakPtr<TestIsolateHolder> GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| int ErrorCount() const { return error_count_; } |
| |
| private: |
| // automation_bindings_ must outlive the isolate, as the isolate's heap holds |
| // references to automation_bindings_. However, the BindingsIsolateHolder must |
| // outlive automation_bindings_, so the bindings are a field in this class. |
| std::unique_ptr<AutomationInternalBindings> automation_bindings_; |
| std::unique_ptr<gin::IsolateHolder> isolate_holder_; |
| std::unique_ptr<gin::ContextHolder> context_holder_; |
| int error_count_ = 0; |
| |
| base::WeakPtrFactory<TestIsolateHolder> weak_ptr_factory_{this}; |
| }; |
| |
| } // namespace |
| |
| class AutomationInternalBindingsTest : public testing::Test { |
| public: |
| AutomationInternalBindingsTest() = default; |
| AutomationInternalBindingsTest(const AutomationInternalBindingsTest&) = |
| delete; |
| AutomationInternalBindingsTest& operator=( |
| const AutomationInternalBindingsTest&) = delete; |
| ~AutomationInternalBindingsTest() override = default; |
| |
| void SetUp() override { |
| service_client_ = std::make_unique<FakeServiceClient>(nullptr); |
| test_isolate_holder_ = std::make_unique<TestIsolateHolder>(); |
| mojo::PendingAssociatedReceiver<mojom::Automation> automation; |
| service_client_->BindAutomation( |
| automation.InitWithNewEndpointAndPassRemote()); |
| test_isolate_holder_->InstallAutomation(std::move(automation)); |
| test_isolate_holder_->StartTestV8AndBindAutomation(); |
| |
| // Many automation functions, when called, cause events to be dispatched. |
| // Here, because we are not loading the full automation API (just the |
| // bindings), we load only what is necessary to dispatch the events to |
| // listeners (AutomationInternal and its dependencies). |
| ASSERT_TRUE(test_isolate_holder_->ExecuteScriptInContext(LoadScriptFromFile( |
| "services/accessibility/features/javascript/chrome_event.js"))); |
| ASSERT_TRUE(test_isolate_holder_->ExecuteScriptInContext(LoadScriptFromFile( |
| "services/accessibility/features/javascript/automation_internal.js"))); |
| } |
| |
| void DispatchAccessibilityEvents(const ui::AXTreeID& tree_id, |
| const std::vector<ui::AXTreeUpdate>& updates, |
| const gfx::Point& mouse_location, |
| const std::vector<ui::AXEvent>& events) { |
| test_isolate_holder_->GetAutomationBindings()->DispatchAccessibilityEvents( |
| tree_id, updates, mouse_location, events); |
| } |
| |
| void DispatchLocationChange(const ui::AXTreeID& tree_id, |
| int node_id, |
| const ui::AXRelativeBounds& bounds) { |
| test_isolate_holder_->GetAutomationBindings() |
| ->DispatchAccessibilityLocationChange(tree_id, node_id, bounds); |
| } |
| |
| std::string LoadScriptFromFile(const std::string& file_path) { |
| base::FilePath gen_test_data_root; |
| base::PathService::Get(base::DIR_GEN_TEST_DATA_ROOT, &gen_test_data_root); |
| base::FilePath source_path = |
| gen_test_data_root.Append(FILE_PATH_LITERAL(file_path)); |
| std::string script; |
| EXPECT_TRUE(base::ReadFileToString(source_path, &script)) |
| << "Could not load script from " << file_path; |
| return script; |
| } |
| |
| protected: |
| base::test::TaskEnvironment task_environment_; |
| std::unique_ptr<FakeServiceClient> service_client_; |
| std::unique_ptr<TestIsolateHolder> test_isolate_holder_; |
| }; |
| |
| // A test to ensure that the testing framework can catch exceptions. |
| TEST_F(AutomationInternalBindingsTest, CatchesExceptions) { |
| std::string script = R"JS( |
| throw 'catch me if you can'; |
| )JS"; |
| EXPECT_FALSE(test_isolate_holder_->ExecuteScriptInContext(script)); |
| EXPECT_EQ(1, test_isolate_holder_->ErrorCount()); |
| } |
| |
| // Spot checks that bindings were added to the correct namespace. |
| TEST_F(AutomationInternalBindingsTest, SpotCheckBindingsAdded) { |
| // This should succeed because automation and nativeAutomationInternal |
| // bindings were added. |
| std::string script = |
| base::StringPrintf(R"JS( |
| nativeAutomationInternal.GetFocus(); |
| nativeAutomationInternal.SetDesktopID('%s'); |
| nativeAutomationInternal.StartCachingAccessibilityTrees(); |
| )JS", |
| ui::AXTreeID::CreateNewAXTreeID().ToString().c_str()); |
| EXPECT_TRUE(test_isolate_holder_->ExecuteScriptInContext(script)); |
| EXPECT_EQ(0, test_isolate_holder_->ErrorCount()); |
| |
| // Lowercase getFocus does not exist on nativeAutomationInternal. |
| script = R"JS( |
| nativeAutomationInternal.getFocus(); |
| )JS"; |
| EXPECT_FALSE(test_isolate_holder_->ExecuteScriptInContext(script)); |
| EXPECT_EQ(1, test_isolate_holder_->ErrorCount()); |
| } |
| |
| TEST_F(AutomationInternalBindingsTest, GetsFocusAndNodeData) { |
| // A desktop tree with focus on a button. |
| std::vector<ui::AXTreeUpdate> updates; |
| updates.emplace_back(); |
| auto& tree_update = updates.back(); |
| tree_update.has_tree_data = true; |
| tree_update.root_id = 1; |
| auto& tree_data = tree_update.tree_data; |
| tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); |
| tree_data.focus_id = 2; |
| tree_update.nodes.emplace_back(); |
| auto& node_data1 = tree_update.nodes.back(); |
| node_data1.id = 1; |
| node_data1.role = ax::mojom::Role::kDesktop; |
| node_data1.child_ids.push_back(2); |
| tree_update.nodes.emplace_back(); |
| auto& node_data2 = tree_update.nodes.back(); |
| node_data2.id = 2; |
| node_data2.role = ax::mojom::Role::kButton; |
| std::vector<ui::AXEvent> events; |
| DispatchAccessibilityEvents(tree_data.tree_id, updates, gfx::Point(), events); |
| |
| // TODO(crbug.com/1357889): Use JS test framework assert/expect instead of |
| // throwing exceptions in these tests. |
| std::string script = base::StringPrintf(R"JS( |
| const treeId = '%s'; |
| nativeAutomationInternal.SetDesktopID(treeId); |
| const focusedNodeInfo = nativeAutomationInternal.GetFocus(); |
| if (!focusedNodeInfo) { |
| throw 'Expected to find a focused object'; |
| } |
| if (focusedNodeInfo.nodeId !== 2) { |
| throw 'Expected focused node ID 2, got ' + focusedNodeInfo.nodeId; |
| } |
| const role = nativeAutomationInternal.GetRole(treeId, |
| focusedNodeInfo.nodeId); |
| if (role !== 'button') { |
| throw 'Expected role button, got ' + role; |
| } |
| )JS", |
| tree_data.tree_id.ToString().c_str()); |
| EXPECT_TRUE(test_isolate_holder_->ExecuteScriptInContext(script)); |
| EXPECT_EQ(0, test_isolate_holder_->ErrorCount()); |
| } |
| |
| TEST_F(AutomationInternalBindingsTest, GetsLocation) { |
| std::vector<ui::AXTreeUpdate> updates; |
| updates.emplace_back(); |
| auto& tree_update = updates.back(); |
| tree_update.has_tree_data = true; |
| tree_update.root_id = 1; |
| auto& tree_data = tree_update.tree_data; |
| tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); |
| tree_data.focus_id = 2; |
| tree_update.nodes.emplace_back(); |
| auto& node_data1 = tree_update.nodes.back(); |
| node_data1.id = 1; |
| node_data1.role = ax::mojom::Role::kDesktop; |
| node_data1.relative_bounds.bounds = gfx::RectF(100, 100, 200, 200); |
| node_data1.child_ids.push_back(2); |
| tree_update.nodes.emplace_back(); |
| auto& node_data2 = tree_update.nodes.back(); |
| node_data2.id = 2; |
| node_data2.role = ax::mojom::Role::kImage; |
| node_data2.relative_bounds.bounds = gfx::RectF(10, 10, 50, 50); |
| std::vector<ui::AXEvent> events; |
| DispatchAccessibilityEvents(tree_data.tree_id, updates, gfx::Point(), events); |
| |
| // TODO(crbug.com/1357889): Use JS test framework assert/expect instead of |
| // throwing exceptions in these tests. |
| std::string script = base::StringPrintf(R"JS( |
| const treeId = '%s'; |
| nativeAutomationInternal.SetDesktopID(treeId); |
| let bounds = nativeAutomationInternal.GetLocation(treeId, 2); |
| if (!bounds) { |
| throw 'Could not get bounds for node'; |
| } |
| if (bounds.left !== 110 || bounds.top !== 110 || |
| bounds.width !== 50 || bounds.height !== 50) { |
| throw 'Bounds should be (110, 110, 50, 50), got (' + |
| bounds.left + ', ' + bounds.top + ', ' + bounds.width + |
| ', ' + bounds.height + ')'; |
| } |
| )JS", |
| tree_data.tree_id.ToString().c_str()); |
| EXPECT_TRUE(test_isolate_holder_->ExecuteScriptInContext(script)); |
| EXPECT_EQ(0, test_isolate_holder_->ErrorCount()); |
| |
| ui::AXRelativeBounds new_bounds; |
| new_bounds.bounds = gfx::RectF(32, 42, 200, 200); |
| DispatchLocationChange(tree_data.tree_id, 1, new_bounds); |
| |
| script = R"JS( |
| bounds = nativeAutomationInternal.GetLocation(treeId, 2); |
| if (!bounds) { |
| throw 'Could not get bounds for node'; |
| } |
| if (bounds.left !== 42 || bounds.top !== 52 || |
| bounds.width !== 50 || bounds.height !== 50) { |
| throw 'Bounds should be (42, 52, 50, 50), got (' + |
| bounds.left + ', ' + bounds.top + ', ' + bounds.width + |
| ', ' + bounds.height + ')'; |
| } |
| )JS"; |
| EXPECT_TRUE(test_isolate_holder_->ExecuteScriptInContext(script)); |
| EXPECT_EQ(0, test_isolate_holder_->ErrorCount()); |
| } |
| |
| } // namespace ax |