chromium/chrome/browser/ash/accessibility/accessibility_highlights_browsertest.cc

// 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 "build/build_config.h"

#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