blob: 6e71310ed0c09404f3b2410401f7efe8cc9d5810 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/lacros/embedded_a11y_manager_lacros.h"
#include <memory>
#include <optional>
#include <string>
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/profiles/profile_window.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/crosapi/mojom/test_controller.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_host_test_helper.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/service_worker/service_worker_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/window.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/widget/widget.h"
// Tests for EmbeddedA11yManagerLacros, ensuring it can install
// the correct accessibility helper extensions on all the profiles
// and responds to the state of the ash accessibility prefs.
//
// NOTE: Tests in this file modify Ash accessibility features. That is
// potentially a lasting side effect that can affect other tests.
// * To prevent interference with tests that are run in parallel, these tests
// are a part of lacros_chrome_browsertests test suite.
// * To prevent interference with following tests, they try to clean up all the
// side effects themselves, e.g. if a test sets a pref, it is also responsible
// for unsetting it.
namespace {
constexpr const char16_t kMenuItemName[] = u"Listen to selected text";
using AssistiveTechnologyType = crosapi::mojom::AssistiveTechnologyType;
gfx::Rect GetControlBoundsInRoot(content::WebContents* web_contents,
const std::string& field_id) {
// Use var instead of const or let so that this can be executed many
// times within a context on different elements without Javascript
// variable redeclaration errors.
// TODO: handle return value.
std::ignore =
content::ExecJs(web_contents, base::StringPrintf(R"(
var element = document.getElementById('%s');
var bounds = element.getBoundingClientRect();
)",
field_id.c_str()));
int top = content::EvalJs(web_contents, "bounds.top").ExtractInt();
int left = content::EvalJs(web_contents, "bounds.left").ExtractInt();
int width = content::EvalJs(web_contents, "bounds.width").ExtractInt();
int height = content::EvalJs(web_contents, "bounds.height").ExtractInt();
gfx::Rect rect(left, top, width, height);
content::RenderWidgetHostView* view = web_contents->GetRenderWidgetHostView();
gfx::Rect view_bounds_in_screen = view->GetViewBounds();
gfx::Point origin = rect.origin();
origin.Offset(view_bounds_in_screen.x(), view_bounds_in_screen.y());
gfx::Rect rect_in_screen(origin.x(), origin.y(), rect.width(), rect.height());
return rect_in_screen;
}
// Waits for a context menu to be shown.
class ContextMenuWaiter {
public:
ContextMenuWaiter() {
RenderViewContextMenu::RegisterMenuShownCallbackForTesting(
base::BindOnce(&ContextMenuWaiter::MenuShown, base::Unretained(this)));
}
ContextMenuWaiter(const ContextMenuWaiter&) = delete;
ContextMenuWaiter& operator=(const ContextMenuWaiter&) = delete;
~ContextMenuWaiter() = default;
RenderViewContextMenu* WaitForMenuShown() {
waiter_.Run();
return context_menu_;
}
private:
void MenuShown(RenderViewContextMenu* context_menu) {
waiter_.Quit();
context_menu_ = context_menu;
}
base::RunLoop waiter_;
raw_ptr<RenderViewContextMenu> context_menu_ = nullptr;
};
} // namespace
class EmbeddedA11yManagerLacrosTest : public InProcessBrowserTest {
public:
EmbeddedA11yManagerLacrosTest() = default;
~EmbeddedA11yManagerLacrosTest() override = default;
EmbeddedA11yManagerLacrosTest(const EmbeddedA11yManagerLacrosTest&) = delete;
EmbeddedA11yManagerLacrosTest& operator=(
const EmbeddedA11yManagerLacrosTest&) = delete;
void SetUp() override {
// This test has conflicts with some other tests(crbug/1501236).
StartUniqueAshChrome(
/*enabled_features=*/{},
/*disabled_features=*/{}, /*additional_cmdline_switches=*/{},
"crbug/1501236 Switch to shared ash once the bug is fixed.");
InProcessBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
auto* embedded_a11y_manager = EmbeddedA11yManagerLacros::GetInstance();
embedded_a11y_manager->AddExtensionChangedCallbackForTest(
base::BindRepeating(&EmbeddedA11yManagerLacrosTest::OnExtensionChanged,
base::Unretained(this)));
embedded_a11y_manager->AddFocusChangedCallbackForTest(
base::BindRepeating(&EmbeddedA11yManagerLacrosTest::OnFocusChanged,
base::Unretained(this)));
auto* lacros_service = chromeos::LacrosService::Get();
if (!lacros_service ||
!lacros_service->IsAvailable<crosapi::mojom::TestController>() ||
lacros_service->GetInterfaceVersion<crosapi::mojom::TestController>() <
static_cast<int>(crosapi::mojom::TestController::MethodMinVersions::
kSetAssistiveTechnologyEnabledMinVersion)) {
GTEST_SKIP() << "Ash version doesn't have required test API";
}
}
void TearDownOnMainThread() override {
CloseAllAshBrowserWindows();
InProcessBrowserTest::TearDownOnMainThread();
}
void SetFeatureEnabled(AssistiveTechnologyType at_type, bool enabled) {
chromeos::LacrosService::Get()
->GetRemote<crosapi::mojom::TestController>()
->SetAssistiveTechnologyEnabled(at_type, enabled);
}
void WaitForExtensionLoaded(Profile* profile,
const std::string& extension_id) {
extensions::ComponentLoader* component_loader =
extensions::ExtensionSystem::Get(profile)
->extension_service()
->component_loader();
while (!component_loader->Exists(extension_id)) {
waiter_ = std::make_unique<base::RunLoop>();
waiter_->Run();
}
EXPECT_TRUE(component_loader->Exists(extension_id));
}
void WaitForExtensionUnloaded(Profile* profile,
const std::string& extension_id) {
extensions::ComponentLoader* component_loader =
extensions::ExtensionSystem::Get(profile)
->extension_service()
->component_loader();
while (component_loader->Exists(extension_id)) {
waiter_ = std::make_unique<base::RunLoop>();
waiter_->Run();
}
EXPECT_FALSE(component_loader->Exists(extension_id));
}
void SetEnabledAndWaitForExtensionLoaded(Profile* profile,
AssistiveTechnologyType at_type,
const std::string& extension_id) {
SetFeatureEnabled(at_type, true);
WaitForExtensionLoaded(profile, extension_id);
}
void SetDisabledAndWaitForExtensionUnloaded(Profile* profile,
AssistiveTechnologyType at_type,
const std::string& extension_id) {
SetFeatureEnabled(at_type, false);
WaitForExtensionUnloaded(profile, extension_id);
}
void WaitForFocusRingsChangedTo(gfx::Point center_point) {
while (center_point != last_focus_bounds_.CenterPoint()) {
focus_waiter_ = std::make_unique<base::RunLoop>();
focus_waiter_->Run();
}
}
RenderViewContextMenu* LoadTestPageAndSelectTextAndRightClick() {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
CHECK(ui_test_utils::NavigateToURL(
browser(), GURL(("data:text/html;charset=utf-8,<p "
"id='selected'>This is some selected text</p>"))));
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
content::BoundingBoxUpdateWaiter bounding_box_waiter(web_contents);
BrowserView* browser_view =
BrowserView::GetBrowserViewForBrowser(browser());
views::Widget* widget = browser_view->GetWidget();
aura::Window* window = widget->GetNativeWindow();
ui::test::EventGenerator generator(window->GetRootWindow());
const gfx::Rect text_bounds =
GetControlBoundsInRoot(web_contents, "selected");
generator.MoveMouseTo(text_bounds.CenterPoint());
generator.ClickLeftButton();
// Set selection with ctrl+a.
generator.PressAndReleaseKey(ui::VKEY_A, ui::EF_CONTROL_DOWN);
bounding_box_waiter.Wait();
ContextMenuWaiter menu_waiter;
generator.PressRightButton();
RenderViewContextMenu* menu = menu_waiter.WaitForMenuShown();
CHECK(menu);
return menu;
}
int num_context_clicks() const { return num_context_clicks_; }
private:
void OnExtensionChanged() {
if (waiter_ && waiter_->running()) {
waiter_->Quit();
}
}
void OnFocusChanged(gfx::Rect bounds) {
last_focus_bounds_ = bounds;
if (focus_waiter_ && focus_waiter_->running()) {
focus_waiter_->Quit();
}
}
std::unique_ptr<base::RunLoop> waiter_;
std::unique_ptr<base::RunLoop> focus_waiter_;
int num_context_clicks_ = 0;
gfx::Rect last_focus_bounds_;
};
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
AddsAndRemovesHelperForChromeVox) {
ProfileManager* profile_manager = g_browser_process->profile_manager();
const auto& profiles = profile_manager->GetLoadedProfiles();
ASSERT_GT(profiles.size(), 0u);
Profile* profile = profiles[0];
SetEnabledAndWaitForExtensionLoaded(
profile, AssistiveTechnologyType::kChromeVox,
extension_misc::kChromeVoxHelperExtensionId);
SetDisabledAndWaitForExtensionUnloaded(
profile, AssistiveTechnologyType::kChromeVox,
extension_misc::kChromeVoxHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
AddsAndRemovesHelperForSelectToSpeak) {
ProfileManager* profile_manager = g_browser_process->profile_manager();
const auto& profiles = profile_manager->GetLoadedProfiles();
ASSERT_GT(profiles.size(), 0u);
Profile* profile = profiles[0];
SetEnabledAndWaitForExtensionLoaded(
profile, AssistiveTechnologyType::kSelectToSpeak,
extension_misc::kEmbeddedA11yHelperExtensionId);
SetDisabledAndWaitForExtensionUnloaded(
profile, AssistiveTechnologyType::kSelectToSpeak,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
AddsAndRemovesHelperForSwitchAccess) {
ProfileManager* profile_manager = g_browser_process->profile_manager();
const auto& profiles = profile_manager->GetLoadedProfiles();
ASSERT_GT(profiles.size(), 0u);
Profile* profile = profiles[0];
SetEnabledAndWaitForExtensionLoaded(
profile, AssistiveTechnologyType::kSwitchAccess,
extension_misc::kEmbeddedA11yHelperExtensionId);
SetDisabledAndWaitForExtensionUnloaded(
profile, AssistiveTechnologyType::kSwitchAccess,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
AddsAndRemovesHelperForReadingMode) {
ProfileManager* profile_manager = g_browser_process->profile_manager();
const auto& profiles = profile_manager->GetLoadedProfiles();
ASSERT_GT(profiles.size(), 0u);
Profile* profile = profiles[0];
auto* embedded_a11y_manager = EmbeddedA11yManagerLacros::GetInstance();
embedded_a11y_manager->SetReadingModeEnabled(true);
WaitForExtensionLoaded(profile,
extension_misc::kEmbeddedA11yHelperExtensionId);
embedded_a11y_manager->SetReadingModeEnabled(false);
WaitForExtensionUnloaded(profile,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
SwitchAccessAndSelectToSpeak) {
ProfileManager* profile_manager = g_browser_process->profile_manager();
const auto& profiles = profile_manager->GetLoadedProfiles();
ASSERT_GT(profiles.size(), 0u);
Profile* profile = profiles[0];
// Installed with first feature enabled.
SetEnabledAndWaitForExtensionLoaded(
profile, AssistiveTechnologyType::kSwitchAccess,
extension_misc::kEmbeddedA11yHelperExtensionId);
// Still installed with second feature enabled.
SetFeatureEnabled(AssistiveTechnologyType::kSelectToSpeak, true);
extensions::ComponentLoader* component_loader =
extensions::ExtensionSystem::Get(profile)
->extension_service()
->component_loader();
EXPECT_TRUE(
component_loader->Exists(extension_misc::kEmbeddedA11yHelperExtensionId));
// Not unloaded if one of the two features is still enabled.
SetFeatureEnabled(AssistiveTechnologyType::kSwitchAccess, false);
EXPECT_TRUE(
component_loader->Exists(extension_misc::kEmbeddedA11yHelperExtensionId));
// Unloads after Select to Speak is also disabled.
SetDisabledAndWaitForExtensionUnloaded(
profile, AssistiveTechnologyType::kSelectToSpeak,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
InstallsOnMultipleProfiles) {
ProfileManager* profile_manager = g_browser_process->profile_manager();
size_t num_extra_profiles = 2;
for (size_t i = 0; i < num_extra_profiles; i++) {
// Create an additional profile.
base::FilePath path_profile =
profile_manager->GenerateNextProfileDirectoryPath();
profiles::testing::CreateProfileSync(profile_manager, path_profile);
// Open a browser window for the profile.
profiles::SwitchToProfile(path_profile, false);
content::RunAllTasksUntilIdle();
}
EXPECT_EQ(profile_manager->GetNumberOfProfiles(), num_extra_profiles + 1);
const auto& profiles = profile_manager->GetLoadedProfiles();
SetFeatureEnabled(AssistiveTechnologyType::kSwitchAccess, true);
for (auto* const profile : profiles) {
WaitForExtensionLoaded(profile,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
// Turn off switch access.
SetFeatureEnabled(AssistiveTechnologyType::kSwitchAccess, false);
for (auto* const profile : profiles) {
WaitForExtensionUnloaded(profile,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
IncognitoProfileA11yLoadedFirst) {
SetFeatureEnabled(AssistiveTechnologyType::kSelectToSpeak, true);
ProfileManager* profile_manager = g_browser_process->profile_manager();
Browser* incognito =
CreateIncognitoBrowser(profile_manager->GetPrimaryUserProfile());
content::RunAllTasksUntilIdle();
WaitForExtensionLoaded(incognito->profile(),
extension_misc::kEmbeddedA11yHelperExtensionId);
SetDisabledAndWaitForExtensionUnloaded(
incognito->profile(), AssistiveTechnologyType::kSelectToSpeak,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
IncognitoProfileA11yLoadedSecond) {
ProfileManager* profile_manager = g_browser_process->profile_manager();
Browser* incognito =
CreateIncognitoBrowser(profile_manager->GetPrimaryUserProfile());
content::RunAllTasksUntilIdle();
SetEnabledAndWaitForExtensionLoaded(
incognito->profile(), AssistiveTechnologyType::kSelectToSpeak,
extension_misc::kEmbeddedA11yHelperExtensionId);
SetDisabledAndWaitForExtensionUnloaded(
incognito->profile(), AssistiveTechnologyType::kSelectToSpeak,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
GuestProfileA11yLoadedFirst) {
SetFeatureEnabled(AssistiveTechnologyType::kSwitchAccess, true);
Browser* guest_browser = CreateGuestBrowser();
content::RunAllTasksUntilIdle();
WaitForExtensionLoaded(guest_browser->profile(),
extension_misc::kEmbeddedA11yHelperExtensionId);
SetDisabledAndWaitForExtensionUnloaded(
guest_browser->profile(), AssistiveTechnologyType::kSwitchAccess,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
GuestProfileA11yLoadedSecond) {
Browser* guest_browser = CreateGuestBrowser();
content::RunAllTasksUntilIdle();
SetDisabledAndWaitForExtensionUnloaded(
guest_browser->profile(), AssistiveTechnologyType::kChromeVox,
extension_misc::kChromeVoxHelperExtensionId);
SetDisabledAndWaitForExtensionUnloaded(
guest_browser->profile(), AssistiveTechnologyType::kChromeVox,
extension_misc::kChromeVoxHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
DoesNotShowContextMenuWhenSelectToSpeakDisabled) {
Profile* profile = ProfileManager::GetPrimaryUserProfile();
ASSERT_TRUE(profile);
extensions::service_worker_test_utils::TestRegistrationObserver
service_worker_observer(profile);
SetEnabledAndWaitForExtensionLoaded(
profile, AssistiveTechnologyType::kSwitchAccess,
extension_misc::kEmbeddedA11yHelperExtensionId);
service_worker_observer.WaitForWorkerStart();
RenderViewContextMenu* menu = LoadTestPageAndSelectTextAndRightClick();
const ui::SimpleMenuModel& menu_model = menu->menu_model();
bool found = false;
for (size_t i = 0; i < menu_model.GetItemCount(); i++) {
if (menu_model.GetLabelAt(i) == kMenuItemName) {
found = true;
break;
}
}
ASSERT_FALSE(found);
SetDisabledAndWaitForExtensionUnloaded(
profile, AssistiveTechnologyType::kSwitchAccess,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest,
TriggerSelectToSpeakFromContextMenu) {
Profile* profile = ProfileManager::GetPrimaryUserProfile();
ASSERT_TRUE(profile);
extensions::service_worker_test_utils::TestRegistrationObserver
service_worker_observer(profile);
SetEnabledAndWaitForExtensionLoaded(
profile, AssistiveTechnologyType::kSelectToSpeak,
extension_misc::kEmbeddedA11yHelperExtensionId);
service_worker_observer.WaitForWorkerStart();
RenderViewContextMenu* menu = LoadTestPageAndSelectTextAndRightClick();
const ui::SimpleMenuModel& menu_model = menu->menu_model();
int found_index = -1;
for (size_t i = 0; i < menu_model.GetItemCount(); i++) {
if (menu_model.GetLabelAt(i) == kMenuItemName) {
found_index = i;
break;
}
}
ASSERT_GE(found_index, 0);
base::RunLoop run_loop;
EmbeddedA11yManagerLacros::GetInstance()->AddSpeakSelectedTextCallbackForTest(
run_loop.QuitClosure());
int command_id = menu_model.GetCommandIdAt(found_index);
menu->ExecuteCommand(command_id, /*flags=*/0);
// Block until the callback is received.
run_loop.Run();
SetDisabledAndWaitForExtensionUnloaded(
profile, AssistiveTechnologyType::kSelectToSpeak,
extension_misc::kEmbeddedA11yHelperExtensionId);
}
IN_PROC_BROWSER_TEST_F(EmbeddedA11yManagerLacrosTest, FocusHighlightSent) {
SetFeatureEnabled(crosapi::mojom::AssistiveTechnologyType::kFocusHighlight,
true);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
CHECK(ui_test_utils::NavigateToURL(
browser(),
GURL(("data:text/html;charset=utf-8,<input type='button' value='first' "
"id='first' autofocus><input type='submit' id='second'>"))));
EXPECT_TRUE(content::WaitForLoadStop(web_contents));
auto* lacros_service = chromeos::LacrosService::Get();
if (lacros_service->GetInterfaceVersion<
crosapi::mojom::EmbeddedAccessibilityHelperClient>() <
static_cast<int>(crosapi::mojom::EmbeddedAccessibilityHelperClient::
MethodMinVersions::kFocusChangedMinVersion)) {
LOG(ERROR) << "Ash version doesn't have required API, skipping test.";
// Nothing should explode when the page loads, we just aren't passing
// focus changes along to Ash because we never listened to them.
// Clean up.
SetFeatureEnabled(crosapi::mojom::AssistiveTechnologyType::kFocusHighlight,
false);
return;
}
gfx::Rect first_button_bounds = GetControlBoundsInRoot(web_contents, "first");
gfx::Rect second_button_bounds =
GetControlBoundsInRoot(web_contents, "second");
WaitForFocusRingsChangedTo(first_button_bounds.CenterPoint());
// Focus the second button with tab.
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
views::Widget* widget = browser_view->GetWidget();
aura::Window* window = widget->GetNativeWindow();
ui::test::EventGenerator generator(window->GetRootWindow());
generator.PressAndReleaseKey(ui::VKEY_TAB);
WaitForFocusRingsChangedTo(second_button_bounds.CenterPoint());
// Clean up.
SetFeatureEnabled(crosapi::mojom::AssistiveTechnologyType::kFocusHighlight,
false);
}