| // 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 <memory> |
| |
| #include "ash/accessibility/ui/accessibility_cursor_ring_layer.h" |
| #include "ash/accessibility/ui/accessibility_focus_ring_controller_impl.h" |
| #include "ash/accessibility/ui/accessibility_focus_ring_layer.h" |
| #include "ash/accessibility/ui/accessibility_highlight_layer.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/shell.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/automation_test_utils.h" |
| #include "chrome/browser/ash/accessibility/select_to_speak_test_utils.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/ui_test_utils.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/test/accessibility_notification_waiter.h" |
| #include "content/public/test/browser_test.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/events/test/event_generator.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| |
| namespace ash { |
| |
| class AccessibilityHighlightsBrowserTest |
| : public AccessibilityFeatureBrowserTest { |
| public: |
| AccessibilityHighlightsBrowserTest( |
| const AccessibilityHighlightsBrowserTest&) = delete; |
| AccessibilityHighlightsBrowserTest& operator=( |
| const AccessibilityHighlightsBrowserTest&) = delete; |
| |
| protected: |
| AccessibilityHighlightsBrowserTest() = default; |
| ~AccessibilityHighlightsBrowserTest() override = default; |
| |
| // InProcessBrowserTest: |
| void SetUpOnMainThread() override { |
| aura::Window* root_window = Shell::Get()->GetPrimaryRootWindow(); |
| generator_ = std::make_unique<ui::test::EventGenerator>(root_window); |
| AccessibilityManager::Get()->SetFocusRingObserverForTest( |
| base::BindRepeating( |
| &AccessibilityHighlightsBrowserTest::OnFocusRingsChanged, |
| base::Unretained(this))); |
| Shell::Get()->accessibility_focus_ring_controller()->SetNoFadeForTesting(); |
| AccessibilityFeatureBrowserTest::SetUpOnMainThread(); |
| |
| // Load Select to Speak so we have a Javascript context with access to the |
| // Automation API to inject AutomationTestUtils. Select to Speak doesn't do |
| // any work unless it is triggered, so this does not impact the test. |
| sts_test_utils::TurnOnSelectToSpeakForTest(GetProfile()); |
| utils_ = std::make_unique<AutomationTestUtils>( |
| extension_misc::kSelectToSpeakExtensionId); |
| utils_->SetUpTestSupport(); |
| |
| NavigateToUrl(GURL(url::kAboutBlankURL)); |
| } |
| |
| void OnFocusRingsChanged() { |
| if (focus_ring_waiter_) { |
| std::move(focus_ring_waiter_).Run(); |
| } |
| } |
| |
| void WaitForFocusRingsChanged() { |
| base::RunLoop run_loop; |
| focus_ring_waiter_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| std::unique_ptr<ui::test::EventGenerator> generator_; |
| std::unique_ptr<AutomationTestUtils> utils_; |
| |
| private: |
| base::OnceClosure focus_ring_waiter_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(AccessibilityHighlightsBrowserTest, |
| CursorHighlightAddsFocusRing) { |
| AccessibilityFocusRingControllerImpl* controller = |
| Shell::Get()->accessibility_focus_ring_controller(); |
| EXPECT_FALSE(controller->cursor_layer_for_testing()); |
| |
| PrefService* prefs = GetProfile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kAccessibilityCursorHighlightEnabled, true); |
| |
| gfx::Point mouse_location(100, 100); |
| generator_->MoveMouseTo(mouse_location); |
| AccessibilityCursorRingLayer* cursor_layer = |
| controller->cursor_layer_for_testing(); |
| ASSERT_TRUE(cursor_layer); |
| gfx::Rect bounds = cursor_layer->layer()->GetTargetBounds(); |
| EXPECT_EQ(bounds.CenterPoint(), mouse_location); |
| |
| mouse_location = gfx::Point(200, 100); |
| generator_->MoveMouseTo(mouse_location); |
| bounds = cursor_layer->layer()->GetTargetBounds(); |
| EXPECT_EQ(bounds.CenterPoint(), mouse_location); |
| |
| // Turns off again. |
| prefs->SetBoolean(prefs::kAccessibilityCursorHighlightEnabled, false); |
| EXPECT_FALSE(controller->cursor_layer_for_testing()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AccessibilityHighlightsBrowserTest, |
| CaretHighlightWebContents) { |
| AccessibilityFocusRingControllerImpl* controller = |
| Shell::Get()->accessibility_focus_ring_controller(); |
| EXPECT_FALSE(controller->caret_layer_for_testing()); |
| |
| PrefService* prefs = GetProfile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kAccessibilityCaretHighlightEnabled, true); |
| |
| // Still doesn't exist because no input text area is focused. |
| EXPECT_FALSE(controller->caret_layer_for_testing()); |
| |
| const struct { |
| std::string url; |
| std::string name; |
| std::string role; |
| } kTestCases[] = {{"data:text/html;charset=utf-8," |
| "<textarea>Hello there</textarea>", |
| "Hello there", "staticText"}, |
| {"data:text/html;charset=utf-8," |
| "<input type=\"text\" value=\"Hows it going?\">", |
| "Hows it going?", "staticText"}, |
| {"data:text/html;charset=utf-8," |
| "<div contenteditable=\"true\">" |
| "<p>Not bad, and <b>you</b>?</p>" |
| "</div>", |
| "Not bad, and ", "staticText"}}; |
| |
| for (const auto& testCase : kTestCases) { |
| NavigateToUrl(GURL(testCase.url)); |
| gfx::Rect element_bounds = |
| utils_->GetNodeBoundsInRoot(testCase.name, testCase.role); |
| |
| generator_->PressAndReleaseKey(ui::KeyboardCode::VKEY_TAB); |
| WaitForFocusRingsChanged(); |
| AccessibilityCursorRingLayer* caret_layer = |
| controller->caret_layer_for_testing(); |
| while (caret_layer == nullptr) { |
| WaitForFocusRingsChanged(); |
| caret_layer = controller->caret_layer_for_testing(); |
| } |
| ASSERT_TRUE(caret_layer); |
| gfx::Rect initial_bounds = caret_layer->layer()->GetTargetBounds(); |
| EXPECT_TRUE(element_bounds.Contains(initial_bounds.CenterPoint())); |
| |
| // Right arrow shifts the bounds to the right slightly. |
| generator_->PressAndReleaseKey(ui::KeyboardCode::VKEY_RIGHT); |
| WaitForFocusRingsChanged(); |
| gfx::Rect new_bounds = caret_layer->layer()->GetTargetBounds(); |
| EXPECT_EQ(initial_bounds.y(), new_bounds.y()); |
| EXPECT_LT(initial_bounds.x(), new_bounds.x()); |
| |
| // Typing something shifts the bounds to the right also. |
| generator_->PressAndReleaseKey(ui::KeyboardCode::VKEY_A); |
| WaitForFocusRingsChanged(); |
| initial_bounds = new_bounds; |
| new_bounds = caret_layer->layer()->GetTargetBounds(); |
| EXPECT_EQ(initial_bounds.y(), new_bounds.y()); |
| EXPECT_LT(initial_bounds.x(), new_bounds.x()); |
| } |
| |
| // Turns off again. |
| prefs->SetBoolean(prefs::kAccessibilityCaretHighlightEnabled, false); |
| EXPECT_FALSE(controller->caret_layer_for_testing()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AccessibilityHighlightsBrowserTest, |
| CaretHighlightOmnibox) { |
| AccessibilityFocusRingControllerImpl* controller = |
| Shell::Get()->accessibility_focus_ring_controller(); |
| PrefService* prefs = GetProfile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kAccessibilityCaretHighlightEnabled, true); |
| |
| // Will wait for the omnibox to be shown. Note in Lacros this might take |
| // a little time. |
| const gfx::Rect omnibox_bounds = |
| utils_->GetBoundsForNodeInRootByClassName("OmniboxViewViews"); |
| |
| // Jump to the omnibox. |
| generator_->PressAndReleaseKeyAndModifierKeys(ui::KeyboardCode::VKEY_L, |
| ui::EF_CONTROL_DOWN); |
| WaitForFocusRingsChanged(); |
| AccessibilityCursorRingLayer* caret_layer = |
| controller->caret_layer_for_testing(); |
| ASSERT_TRUE(caret_layer); |
| gfx::Rect bounds = caret_layer->layer()->GetTargetBounds(); |
| EXPECT_EQ(bounds.CenterPoint().y(), omnibox_bounds.CenterPoint().y()); |
| |
| // On the left edge of the omnibox. |
| EXPECT_LT(bounds.x(), omnibox_bounds.x()); |
| EXPECT_GT(bounds.right(), omnibox_bounds.x()); |
| |
| // Typing something shifts the bounds to the right. |
| generator_->PressAndReleaseKey(ui::KeyboardCode::VKEY_K); |
| gfx::Rect new_bounds = caret_layer->layer()->GetTargetBounds(); |
| if (new_bounds == bounds) { |
| // In Ash this happens immediately, while in Lacros it takes some |
| // time for focus ring changes to propagate. |
| WaitForFocusRingsChanged(); |
| new_bounds = caret_layer->layer()->GetTargetBounds(); |
| } |
| EXPECT_EQ(bounds.y(), new_bounds.y()); |
| EXPECT_LT(bounds.x(), new_bounds.x()); |
| |
| prefs->SetBoolean(prefs::kAccessibilityCaretHighlightEnabled, false); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AccessibilityHighlightsBrowserTest, FocusHighlight) { |
| AccessibilityFocusRingControllerImpl* controller = |
| Shell::Get()->accessibility_focus_ring_controller(); |
| PrefService* prefs = GetProfile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kAccessibilityFocusHighlightEnabled, true); |
| |
| const std::string url = |
| "data:text/html;charset=utf-8," |
| "<input type=\"text\" value=\"long enough text to fill a whole textbox\">" |
| "<input type=\"checkbox\" id=\"focus2\"><label for=\"focus2\">pick " |
| "me</label>" |
| "<input type=\"radio\" id=\"focus3\"><label for=\"focus3\">radio " |
| "me</label>" |
| "<input type=\"submit\">" |
| "<a href=\"\">link</a>"; |
| NavigateToUrl(GURL(url)); |
| |
| const struct { |
| std::string name; |
| std::string role; |
| } kTestCases[] = {{"long enough text to fill a whole textbox", "staticText"}, |
| {"pick me", "checkBox"}, |
| {"radio me", "radioButton"}, |
| {"Submit", "button"}, |
| {"link", "link"}}; |
| |
| for (const auto& testCase : kTestCases) { |
| // Waits for the page to be loaded. |
| gfx::Rect element_bounds = |
| utils_->GetNodeBoundsInRoot(testCase.name, testCase.role); |
| |
| generator_->PressAndReleaseKey(ui::KeyboardCode::VKEY_TAB); |
| WaitForFocusRingsChanged(); |
| |
| const AccessibilityFocusRingGroup* highlights = |
| controller->GetFocusRingGroupForTesting("HighlightController"); |
| ASSERT_TRUE(highlights); |
| auto& focus_rings = highlights->focus_layers_for_testing(); |
| EXPECT_EQ(focus_rings.size(), 1u); |
| gfx::Rect focus_bounds = focus_rings.at(0)->layer()->GetTargetBounds(); |
| if (testCase.role == "staticText") { |
| // In the case of the text input field, the static text node we've |
| // found bounds for within the input field is slightly shorter than its |
| // parent to visually show that it will scroll. Check the center points |
| // are within one pixel of each other. |
| EXPECT_LT( |
| (element_bounds.CenterPoint() - focus_bounds.CenterPoint()).Length(), |
| 2); |
| } else { |
| EXPECT_EQ(element_bounds.CenterPoint(), focus_bounds.CenterPoint()); |
| } |
| EXPECT_TRUE(focus_bounds.Contains(element_bounds)); |
| } |
| |
| prefs->SetBoolean(prefs::kAccessibilityFocusHighlightEnabled, false); |
| } |
| |
| } // namespace ash |