chromium/chrome/browser/ash/accessibility/autoclick_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 "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