| // 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 "ash/accessibility/autoclick/autoclick_controller.h" |
| #include "ash/accessibility/ui/accessibility_focus_ring_controller_impl.h" |
| #include "ash/accessibility/ui/accessibility_focus_ring_layer.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/public/cpp/accessibility_controller_enums.h" |
| #include "ash/shell.h" |
| #include "base/test/bind.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/browser/accessibility/service/accessibility_service_router_factory.h" |
| #include "chrome/browser/ash/accessibility/accessibility_feature_browsertest.h" |
| #include "chrome/browser/ash/accessibility/accessibility_manager.h" |
| #include "chrome/browser/ash/accessibility/accessibility_test_utils.h" |
| #include "chrome/browser/ash/accessibility/autoclick_test_utils.h" |
| #include "chrome/browser/ash/accessibility/service/fake_accessibility_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.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 "components/prefs/pref_service.h" |
| #include "content/public/test/accessibility_notification_waiter.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "ui/accessibility/accessibility_features.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/events/test/event_generator.h" |
| #include "url/url_constants.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| const char* kShowButtonOnClickUrl = |
| "data:text/html," |
| "<input type='button' value='click me'" |
| "onclick=\"document.getElementById('result').removeAttribute('hidden')\">" |
| "<input type='button' id='result' hidden value='show me'>"; |
| |
| } // namespace |
| |
| // Tests that Automatic clicks works with elements in the browser. |
| class AutoclickBrowserTest : public AccessibilityFeatureBrowserTest { |
| public: |
| AutoclickBrowserTest(const AutoclickBrowserTest&) = delete; |
| AutoclickBrowserTest& operator=(const AutoclickBrowserTest&) = delete; |
| |
| protected: |
| AutoclickBrowserTest() = default; |
| ~AutoclickBrowserTest() override = default; |
| |
| // InProcessBrowserTest: |
| void SetUpOnMainThread() override { |
| aura::Window* root_window = Shell::Get()->GetPrimaryRootWindow(); |
| generator_ = std::make_unique<ui::test::EventGenerator>(root_window); |
| autoclick_test_utils_ = std::make_unique<AutoclickTestUtils>(GetProfile()); |
| AccessibilityFeatureBrowserTest::SetUpOnMainThread(); |
| NavigateToUrl(GURL(url::kAboutBlankURL)); |
| } |
| |
| void TearDownOnMainThread() override { autoclick_test_utils_.reset(); } |
| |
| PrefService* GetPrefs() { return GetProfile()->GetPrefs(); } |
| |
| // Loads a page with the given URL and then starts up Autoclick. |
| void LoadURLAndAutoclick(const std::string& url) { |
| NavigateToUrl(GURL(url)); |
| autoclick_test_utils_->LoadAutoclick(); |
| autoclick_test_utils_->WaitForPageLoad(url); |
| } |
| |
| ui::test::EventGenerator* generator() { return generator_.get(); } |
| AutoclickTestUtils* utils() { return autoclick_test_utils_.get(); } |
| |
| private: |
| std::unique_ptr<ui::test::EventGenerator> generator_; |
| std::unique_ptr<AutoclickTestUtils> autoclick_test_utils_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(AutoclickBrowserTest, LeftClickButtonOnHover) { |
| LoadURLAndAutoclick(kShowButtonOnClickUrl); |
| // No need to change click type: Default should be right-click. |
| utils()->HoverOverHtmlElement(generator(), "click me", "button"); |
| |
| // Wait for button to be shown. |
| utils()->GetNodeBoundsInRoot("show me", "button"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AutoclickBrowserTest, DoubleClickHover) { |
| LoadURLAndAutoclick( |
| "data:text/html;charset=utf-8," |
| "<input type='text' id='text_field'" |
| "value='peanutbuttersandwichmadewithjam'>"); |
| utils()->SetAutoclickEventTypeWithHover(generator(), |
| AutoclickEventType::kDoubleClick); |
| |
| // Double-clicking over the text field should result in the text being |
| // selected. |
| utils()->HoverOverHtmlElement(generator(), "peanutbuttersandwichmadewithjam", |
| "staticText"); |
| |
| utils()->WaitForTextSelectionChangedEvent(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AutoclickBrowserTest, ClickAndDrag) { |
| LoadURLAndAutoclick( |
| "data:text/html;charset=utf-8," |
| "<input type='text' id='text_field'" |
| "value='peanutbuttersandwichmadewithjam'>"); |
| utils()->SetAutoclickEventTypeWithHover(generator(), |
| AutoclickEventType::kDragAndDrop); |
| |
| gfx::Rect bounds = utils()->GetNodeBoundsInRoot( |
| "peanutbuttersandwichmadewithjam", "staticText"); |
| |
| // First hover causes a down click even that changes the caret. |
| generator()->MoveMouseTo( |
| gfx::Point(bounds.left_center().y(), bounds.x() + 10)); |
| utils()->WaitForTextSelectionChangedEvent(); |
| |
| // Second hover causes a selection. |
| generator()->MoveMouseTo(bounds.right_center()); |
| utils()->WaitForTextSelectionChangedEvent(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AutoclickBrowserTest, |
| RightClickOnHoverOpensContextMenu) { |
| LoadURLAndAutoclick( |
| "data:text/html;charset=utf-8," |
| "<input type='text' id='text_field' value='stop copying me'>"); |
| utils()->SetAutoclickEventTypeWithHover(generator(), |
| AutoclickEventType::kRightClick); |
| |
| // Right clicking over the text field should result in a context menu. |
| utils()->HoverOverHtmlElement(generator(), "stop copying me", "staticText"); |
| |
| // When the context menu is shown, it has options for copy/paste |
| // because this is a textarea. |
| utils()->GetNodeBoundsInRoot("Copy Ctrl+C", "menuItem"); |
| utils()->GetNodeBoundsInRoot("Paste Ctrl+V", "menuItem"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AutoclickBrowserTest, |
| ScrollHoverHighlightsScrollableArea) { |
| utils()->ObserveFocusRings(); |
| |
| const std::string kQuoteText = |
| "'Whatever you choose to do, leave tracks. That means don't do it just " |
| "for yourself. You will want to leave the world a little better for your " |
| "having lived.'"; |
| |
| LoadURLAndAutoclick( |
| "data:text/html;charset=utf-8," |
| "<textarea id='test_textarea' class='scrollableField' rows='2'' " |
| "cols='20'>" + |
| kQuoteText + "</textarea>"); |
| |
| gfx::Rect bounds = |
| utils()->GetBoundsForNodeInRootByClassName("scrollableField"); |
| gfx::Rect found_bounds; |
| base::RunLoop waiter; |
| Shell::Get()->autoclick_controller()->SetScrollableBoundsCallbackForTesting( |
| base::BindLambdaForTesting([&waiter, &bounds, &found_bounds]( |
| const gfx::Rect& scrollable_bounds) { |
| found_bounds = scrollable_bounds; |
| if (scrollable_bounds == bounds && waiter.running()) { |
| waiter.Quit(); |
| } |
| })); |
| |
| AccessibilityFocusRingControllerImpl* controller = |
| Shell::Get()->accessibility_focus_ring_controller(); |
| std::string focus_ring_id = AccessibilityManager::Get()->GetFocusRingId( |
| ax::mojom::AssistiveTechnologyType::kAutoClick, ""); |
| const AccessibilityFocusRingGroup* focus_ring_group = |
| controller->GetFocusRingGroupForTesting(focus_ring_id); |
| // No focus rings to start. |
| EXPECT_EQ(nullptr, focus_ring_group); |
| |
| utils()->SetAutoclickEventTypeWithHover(generator(), |
| AutoclickEventType::kScroll); |
| |
| utils()->HoverOverHtmlElement(generator(), kQuoteText, "staticText"); |
| utils()->WaitForFocusRingChanged(); |
| |
| focus_ring_group = controller->GetFocusRingGroupForTesting(focus_ring_id); |
| ASSERT_NE(nullptr, focus_ring_group); |
| std::vector<std::unique_ptr<AccessibilityFocusRingLayer>> const& focus_rings = |
| focus_ring_group->focus_layers_for_testing(); |
| ASSERT_EQ(focus_rings.size(), 1u); |
| |
| if (found_bounds != bounds) { |
| // Wait for bounds changed. |
| waiter.Run(); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AutoclickBrowserTest, LongDelay) { |
| utils()->SetAutoclickDelayMs(500); |
| LoadURLAndAutoclick(kShowButtonOnClickUrl); |
| |
| base::ElapsedTimer timer; |
| utils()->HoverOverHtmlElement(generator(), "click me", "button"); |
| utils()->GetNodeBoundsInRoot("show me", "button"); |
| EXPECT_GT(timer.Elapsed().InMilliseconds(), 500); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AutoclickBrowserTest, PauseAutoclick) { |
| utils()->SetAutoclickDelayMs(5); |
| LoadURLAndAutoclick( |
| "data:text/html," |
| "<input type='button' value='click me'" |
| "onclick='window.close()'>"); |
| utils()->SetAutoclickEventTypeWithHover(generator(), |
| AutoclickEventType::kNoAction); |
| |
| base::OneShotTimer timer; |
| base::RunLoop runner; |
| utils()->HoverOverHtmlElement(generator(), "click me", "button"); |
| timer.Start(FROM_HERE, base::Milliseconds(2000), |
| base::BindLambdaForTesting([&runner, this]() { |
| runner.Quit(); |
| // If autoclick was enabled, the webpage would have |
| // been closed, and this would fail. |
| utils()->GetNodeBoundsInRoot("click me", "button"); |
| })); |
| runner.Run(); |
| } |
| |
| class AutoclickWithAccessibilityServiceTest : public AutoclickBrowserTest { |
| public: |
| AutoclickWithAccessibilityServiceTest() = default; |
| ~AutoclickWithAccessibilityServiceTest() override = default; |
| AutoclickWithAccessibilityServiceTest( |
| const AutoclickWithAccessibilityServiceTest&) = delete; |
| AutoclickWithAccessibilityServiceTest& operator=( |
| const AutoclickWithAccessibilityServiceTest&) = delete; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| scoped_feature_list_.InitAndEnableFeature( |
| ::features::kAccessibilityService); |
| } |
| |
| void SetUpOnMainThread() override { |
| AutoclickBrowserTest::SetUpOnMainThread(); |
| // Replaces normal AccessibilityService with a fake one. |
| ax::AccessibilityServiceRouterFactory::GetInstanceForTest() |
| ->SetTestingFactoryAndUse( |
| ash::AccessibilityManager::Get()->profile(), |
| base::BindRepeating(&AutoclickWithAccessibilityServiceTest:: |
| CreateTestAccessibilityService, |
| base::Unretained(this))); |
| } |
| |
| protected: |
| // Unowned. |
| raw_ptr<FakeAccessibilityService, DanglingUntriaged> fake_service_ = nullptr; |
| |
| private: |
| std::unique_ptr<KeyedService> CreateTestAccessibilityService( |
| content::BrowserContext* context) { |
| std::unique_ptr<FakeAccessibilityService> fake_service = |
| std::make_unique<FakeAccessibilityService>(); |
| fake_service_ = fake_service.get(); |
| return std::move(fake_service); |
| } |
| |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // TODO(b/262637071): When the AccessibilityService is on (instead of a fake), |
| // check the focus ring bounds too, as autoclick JS should set these. |
| IN_PROC_BROWSER_TEST_F(AutoclickWithAccessibilityServiceTest, |
| ScrollableBoundsPlumbing) { |
| const std::string kQuoteText = |
| "'Whatever you choose to do, leave tracks. That means don't do it just " |
| "for yourself. You will want to leave the world a little better for your " |
| "having lived.'"; |
| |
| LoadURLAndAutoclick( |
| "data:text/html;charset=utf-8," |
| "<textarea id='test_textarea' class='scrollableField' rows='2'' " |
| "cols='20'>" + |
| kQuoteText + "</textarea>"); |
| gfx::Rect bounds = |
| utils()->GetBoundsForNodeInRootByClassName("scrollableField"); |
| |
| fake_service_->BindAnotherAutoclickClient(); |
| |
| utils()->SetAutoclickEventTypeWithHover(generator(), |
| AutoclickEventType::kScroll); |
| |
| fake_service_->set_autoclick_scrollable_bounds(bounds); |
| base::RunLoop waiter; |
| Shell::Get()->autoclick_controller()->SetScrollableBoundsCallbackForTesting( |
| base::BindLambdaForTesting( |
| [&waiter, &bounds](const gfx::Rect& scrollable_bounds) { |
| if (scrollable_bounds == bounds) { |
| waiter.Quit(); |
| } |
| })); |
| utils()->HoverOverHtmlElement(generator(), kQuoteText, "staticText"); |
| waiter.Run(); |
| } |
| |
| } // namespace ash |