blob: 0bcbf8e4e5f196c583988595dac35e2cad776ab9 [file] [log] [blame]
// Copyright 2018 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 "ui/views/mus/ax_remote_host.h"
#include "base/macros.h"
#include "base/no_destructor.h"
#include "base/run_loop.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/mojom/ax_host.mojom.h"
#include "ui/accessibility/platform/aura_window_properties.h"
#include "ui/display/display.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/gfx/transform.h"
#include "ui/views/accessibility/ax_aura_obj_cache.h"
#include "ui/views/mus/mus_client_test_api.h"
#include "ui/views/mus/screen_mus.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
namespace views {
namespace {
// Returns a well-known tree ID for the test widget.
const ui::AXTreeID& TestAXTreeID() {
static const base::NoDestructor<ui::AXTreeID> test_ax_tree_id(
ui::AXTreeID::CreateNewAXTreeID());
return *test_ax_tree_id;
}
// Simulates the AXHostService in the browser.
class TestAXHostService : public ax::mojom::AXHost {
public:
explicit TestAXHostService(bool automation_enabled)
: automation_enabled_(automation_enabled) {}
~TestAXHostService() override = default;
ax::mojom::AXHostPtr CreateInterfacePtr() {
ax::mojom::AXHostPtr ptr;
binding_.Bind(mojo::MakeRequest(&ptr));
return ptr;
}
void ResetCounts() {
remote_host_count_ = 0;
event_count_ = 0;
last_tree_id_ = ui::AXTreeIDUnknown();
last_updates_.clear();
last_event_ = ui::AXEvent();
}
// ax::mojom::AXHost:
void RegisterRemoteHost(ax::mojom::AXRemoteHostPtr client,
RegisterRemoteHostCallback cb) override {
++remote_host_count_;
std::move(cb).Run(TestAXTreeID(), automation_enabled_);
client.FlushForTesting();
}
void HandleAccessibilityEvent(const ui::AXTreeID& tree_id,
const std::vector<ui::AXTreeUpdate>& updates,
const ui::AXEvent& event) override {
++event_count_;
last_tree_id_ = tree_id;
last_updates_ = updates;
last_event_ = event;
}
mojo::Binding<ax::mojom::AXHost> binding_{this};
bool automation_enabled_ = false;
int remote_host_count_ = 0;
int event_count_ = 0;
ui::AXTreeID last_tree_id_ = ui::AXTreeIDUnknown();
std::vector<ui::AXTreeUpdate> last_updates_;
ui::AXEvent last_event_;
private:
DISALLOW_COPY_AND_ASSIGN(TestAXHostService);
};
// TestView senses accessibility actions.
class TestView : public View {
public:
TestView() { set_owned_by_client(); }
~TestView() override = default;
void ResetCounts() {
action_count_ = 0;
last_action_ = {};
event_count_ = 0;
last_event_type_ = ax::mojom::Event::kNone;
}
// View:
bool HandleAccessibleAction(const ui::AXActionData& action) override {
++action_count_;
last_action_ = action;
return true;
}
void OnAccessibilityEvent(ax::mojom::Event event_type) override {
++event_count_;
last_event_type_ = event_type;
}
int action_count_ = 0;
ui::AXActionData last_action_;
int event_count_ = 0;
ax::mojom::Event last_event_type_ = ax::mojom::Event::kNone;
private:
DISALLOW_COPY_AND_ASSIGN(TestView);
};
AXRemoteHost* CreateRemote(TestAXHostService* service) {
std::unique_ptr<AXRemoteHost> remote = std::make_unique<AXRemoteHost>();
remote->InitForTesting(service->CreateInterfacePtr());
remote->FlushForTesting();
// Install the AXRemoteHost on MusClient so it monitors Widget creation.
AXRemoteHost* remote_raw = remote.get();
MusClientTestApi::SetAXRemoteHost(std::move(remote));
return remote_raw;
}
std::unique_ptr<Widget> CreateTestWidget() {
std::unique_ptr<Widget> widget = std::make_unique<Widget>();
Widget::InitParams params;
params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.bounds = gfx::Rect(1, 2, 333, 444);
widget->Init(params);
return widget;
}
using AXRemoteHostTest = ViewsTestWithDesktopNativeWidget;
TEST_F(AXRemoteHostTest, CreateRemote) {
TestAXHostService service(false /*automation_enabled*/);
CreateRemote(&service);
// Client registered itself with service.
EXPECT_EQ(1, service.remote_host_count_);
}
TEST_F(AXRemoteHostTest, AutomationEnabled) {
TestAXHostService service(true /*automation_enabled*/);
AXRemoteHost* remote = CreateRemote(&service);
std::unique_ptr<Widget> widget = CreateTestWidget();
remote->FlushForTesting();
// Tree ID is assigned.
std::string* tree_id_ptr =
widget->GetNativeWindow()->GetProperty(ui::kChildAXTreeID);
ASSERT_TRUE(tree_id_ptr);
ui::AXTreeID tree_id = ui::AXTreeID::FromString(*tree_id_ptr);
EXPECT_EQ(TestAXTreeID(), tree_id);
// Event was sent with initial hierarchy.
EXPECT_EQ(TestAXTreeID(), service.last_tree_id_);
EXPECT_EQ(ax::mojom::Event::kLoadComplete, service.last_event_.event_type);
EXPECT_EQ(AXAuraObjCache::GetInstance()->GetID(
widget->widget_delegate()->GetContentsView()),
service.last_event_.id);
}
// Regression test for https://crbug.com/876407
TEST_F(AXRemoteHostTest, AutomationEnabledTwice) {
// If ChromeVox has ever been open during the user session then the remote app
// can see automation enabled at startup.
TestAXHostService service(true /*automation_enabled*/);
AXRemoteHost* remote = CreateRemote(&service);
std::unique_ptr<Widget> widget = CreateTestWidget();
remote->FlushForTesting();
// Remote host sent load complete.
EXPECT_EQ(ax::mojom::Event::kLoadComplete, service.last_event_.event_type);
service.ResetCounts();
// Simulate host service asking to enable again. This happens if ChromeVox is
// re-enabled after the remote app is open.
remote->OnAutomationEnabled(true);
remote->FlushForTesting();
// Load complete was sent again after the second enable.
EXPECT_EQ(ax::mojom::Event::kLoadComplete, service.last_event_.event_type);
}
// Verifies that a remote app doesn't crash if a View triggers an accessibility
// event before it is attached to a Widget. https://crbug.com/889121
TEST_F(AXRemoteHostTest, SendEventOnViewWithNoWidget) {
TestAXHostService service(true /*automation_enabled*/);
AXRemoteHost* remote = CreateRemote(&service);
std::unique_ptr<Widget> widget = CreateTestWidget();
remote->FlushForTesting();
// Create a view that is not yet associated with the widget.
views::View view;
remote->OnViewEvent(&view, ax::mojom::Event::kLocationChanged);
// No crash.
}
// Verifies that the AXRemoteHost stops monitoring widgets that are closed
// asynchronously, like when ash requests close via DesktopWindowTreeHostMus.
// https://crbug.com/869608
TEST_F(AXRemoteHostTest, AsyncWidgetClose) {
TestAXHostService service(true /*automation_enabled*/);
AXRemoteHost* remote = CreateRemote(&service);
remote->FlushForTesting();
Widget* widget = new Widget(); // Owned by native widget.
Widget::InitParams params;
params.bounds = gfx::Rect(1, 2, 333, 444);
widget->Init(params);
widget->Show();
// AXRemoteHost is tracking the widget.
EXPECT_TRUE(remote->widget_for_testing());
// Asynchronously close the widget using Close() instead of CloseNow().
widget->Close();
// AXRemoteHost stops tracking the widget, even though it isn't destroyed yet.
EXPECT_FALSE(remote->widget_for_testing());
// Widget finishes closing.
base::RunLoop().RunUntilIdle();
// No crash.
}
TEST_F(AXRemoteHostTest, CreateWidgetThenEnableAutomation) {
TestAXHostService service(false /*automation_enabled*/);
AXRemoteHost* remote = CreateRemote(&service);
std::unique_ptr<Widget> widget = CreateTestWidget();
remote->FlushForTesting();
// No events were sent because automation isn't enabled.
EXPECT_EQ(0, service.event_count_);
remote->OnAutomationEnabled(true);
remote->FlushForTesting();
// Event was sent with initial hierarchy.
EXPECT_EQ(ax::mojom::Event::kLoadComplete, service.last_event_.event_type);
EXPECT_EQ(AXAuraObjCache::GetInstance()->GetID(
widget->widget_delegate()->GetContentsView()),
service.last_event_.id);
}
TEST_F(AXRemoteHostTest, PerformAction) {
TestAXHostService service(true /*automation_enabled*/);
AXRemoteHost* remote = CreateRemote(&service);
// Create a view to sense the action.
TestView view;
view.SetBounds(0, 0, 100, 100);
std::unique_ptr<Widget> widget = CreateTestWidget();
widget->GetRootView()->AddChildView(&view);
AXAuraObjCache::GetInstance()->GetOrCreate(&view);
// Request an action on the view.
ui::AXActionData action;
action.action = ax::mojom::Action::kScrollDown;
action.target_node_id = AXAuraObjCache::GetInstance()->GetID(&view);
remote->PerformAction(action);
// View received the action.
EXPECT_EQ(1, view.action_count_);
EXPECT_EQ(ax::mojom::Action::kScrollDown, view.last_action_.action);
}
TEST_F(AXRemoteHostTest, PerformHitTest) {
TestAXHostService service(true /*automation_enabled*/);
AXRemoteHost* remote = CreateRemote(&service);
// Create a view to sense the action.
TestView view;
view.SetBounds(0, 0, 100, 100);
std::unique_ptr<Widget> widget = CreateTestWidget();
widget->GetRootView()->AddChildView(&view);
// Clear event counts triggered by view creation and bounds set.
view.ResetCounts();
// Request a hit test.
ui::AXActionData action;
action.action = ax::mojom::Action::kHitTest;
action.hit_test_event_to_fire = ax::mojom::Event::kHitTestResult;
action.target_point = gfx::Point(5, 5);
remote->PerformAction(action);
// View sensed a hit.
EXPECT_EQ(1, view.event_count_);
EXPECT_EQ(ax::mojom::Event::kHitTestResult, view.last_event_type_);
view.ResetCounts();
// Try to hit a point outside the view.
action.target_point = gfx::Point(101, 101);
remote->PerformAction(action);
// View wasn't hit.
EXPECT_EQ(0, view.event_count_);
EXPECT_EQ(ax::mojom::Event::kNone, view.last_event_type_);
}
TEST_F(AXRemoteHostTest, ScaleFactor) {
// Set the primary display to high DPI.
ScreenMus* screen = static_cast<ScreenMus*>(display::Screen::GetScreen());
display::Display display = screen->GetPrimaryDisplay();
display.set_device_scale_factor(2.f);
screen->display_list().UpdateDisplay(display);
// Create a widget.
TestAXHostService service(true /*automation_enabled*/);
AXRemoteHost* remote = CreateRemote(&service);
std::unique_ptr<Widget> widget = CreateTestWidget();
remote->FlushForTesting();
// Widget transform is scaled by a factor of 2.
ASSERT_FALSE(service.last_updates_.empty());
gfx::Transform* transform =
service.last_updates_[0].nodes[0].relative_bounds.transform.get();
ASSERT_TRUE(transform);
EXPECT_TRUE(transform->IsScale2d());
EXPECT_EQ(gfx::Vector2dF(2.f, 2.f), transform->Scale2d());
}
} // namespace
} // namespace views