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

// Copyright 2018 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/public/cpp/window_tree_host_lookup.h"
#include "chrome/browser/ash/accessibility/accessibility_feature_browsertest.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/accessibility/automation_test_utils.h"
#include "chrome/browser/ash/accessibility/switch_access_test_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/test/browser_test.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/window_tree_host.h"
#include "ui/display/screen.h"

namespace ash {

class SwitchAccessTest : public AccessibilityFeatureBrowserTest {
 protected:
  SwitchAccessTest() = default;
  ~SwitchAccessTest() override = default;
  SwitchAccessTest(const SwitchAccessTest&) = delete;
  SwitchAccessTest& operator=(const SwitchAccessTest&) = delete;

  void SetUpOnMainThread() override {
    switch_access_test_utils_ = std::make_unique<SwitchAccessTestUtils>(
        AccessibilityManager::Get()->profile());
  }

  void SendVirtualKeyPress(ui::KeyboardCode key) {
    ASSERT_NO_FATAL_FAILURE(ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
        nullptr, key, false, false, false, false)));
  }

  // Returns cursor client for root window at location (in DIPs) |x| and |y|.
  aura::client::CursorClient* GetCursorClient(const int x, const int y) {
    gfx::Point location_in_screen(x, y);
    const display::Display& display =
        display::Screen::GetScreen()->GetDisplayNearestPoint(
            location_in_screen);
    auto* host = GetWindowTreeHostForDisplay(display.id());
    CHECK(host);

    aura::Window* root_window = host->window();
    CHECK(root_window);

    return aura::client::GetCursorClient(root_window);
  }

  // Enables mouse events for root window at location (in DIPs) |x| and |y|.
  void EnableMouseEvents(const int x, const int y) {
    GetCursorClient(x, y)->EnableMouseEvents();
  }

  // Disables mouse events for root window at location (in DIPs) |x| and |y|.
  void DisableMouseEvents(const int x, const int y) {
    GetCursorClient(x, y)->DisableMouseEvents();
  }

  // Checks if mouse events are enabled for root window at location (in DIPs)
  // |x| and |y|.
  bool IsMouseEventsEnabled(const int x, const int y) {
    return GetCursorClient(x, y)->IsMouseEventsEnabled();
  }

  SwitchAccessTestUtils* utils() { return switch_access_test_utils_.get(); }

 private:
  std::unique_ptr<SwitchAccessTestUtils> switch_access_test_utils_;
};

// Flaky. See https://crbug.com/1224254.
IN_PROC_BROWSER_TEST_F(SwitchAccessTest, DISABLED_ConsumesKeyEvents) {
  utils()->EnableSwitchAccess({'1', 'A'} /* select */, {'2', 'B'} /* next */,
                              {'3', 'C'} /* previous */);
  AutomationTestUtils test_utils(extension_misc::kSwitchAccessExtensionId);
  test_utils.SetUpTestSupport();

  // Load a webpage with a text box.
  NavigateToUrl(GURL(
      "data:text/html;charset=utf-8,<input type='text' class='sa-input'>"));

  // Put focus in the text box.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_TAB);

  // Send a key event for a character consumed by Switch Access.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_1);

  // Check that the text field did not receive the character.
  EXPECT_STREQ("", test_utils.GetValueForNodeWithClassName("sa_input").c_str());

  // Send a key event for a character not consumed by Switch Access.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_X);

  // Check that the text field received the character.
  EXPECT_STREQ("x",
               test_utils.GetValueForNodeWithClassName("sa_input").c_str());
}

IN_PROC_BROWSER_TEST_F(SwitchAccessTest, NavigateGroupings) {
  utils()->EnableSwitchAccess({'1', 'A'} /* select */, {'2', 'B'} /* next */,
                              {'3', 'C'} /* previous */);

  // Load a webpage with two groups of controls.
  NavigateToUrl(GURL(R"HTML(data:text/html,
      <div role="group" aria-label="Top">
        <button autofocus>Northwest</button>
        <button>Northeast</button>
      </div>
      <div role="group" aria-label="Bottom">
        <button>Southwest</button>
        <button>Southeast</button>
      </div>
      )HTML"));

  // Wait for switch access to focus on the first button.
  utils()->WaitForFocusRing("primary", "button", "Northwest");

  // Go to the next element by pressing the next switch.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);
  utils()->WaitForFocusRing("primary", "button", "Northeast");

  // Next is the back button.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);
  utils()->WaitForFocusRing("primary", "back", "");

  // Press the select key to press the back button, which should focus
  // on the Top container, with Northwest as the preview.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_1);
  utils()->WaitForFocusRing("primary", "group", "Top");
  utils()->WaitForFocusRing("preview", "button", "Northwest");

  // Navigate to the next group by pressing the next switch.
  // Now we should be focused on the Bottom container, with
  // Southwest as the preview.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);
  utils()->WaitForFocusRing("primary", "group", "Bottom");
  utils()->WaitForFocusRing("preview", "button", "Southwest");

  // Press the select key to enter the container, which should focus
  // Southwest.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_1);
  utils()->WaitForFocusRing("primary", "button", "Southwest");

  // Go to the next element by pressing the next switch. That should
  // focus Southeast.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);
  utils()->WaitForFocusRing("primary", "button", "Southeast");
}

IN_PROC_BROWSER_TEST_F(SwitchAccessTest, NavigateButtonsInTextFieldMenu) {
  utils()->EnableSwitchAccess({'1', 'A'} /* select */, {'2', 'B'} /* next */,
                              {'3', 'C'} /* previous */);

  // Load a webpage with a text box.
  NavigateToUrl(
      GURL("data:text/html,<input autofocus aria-label=MyTextField>"));

  // Wait for switch access to focus on the text field.
  utils()->WaitForFocusRing("primary", "textField", "MyTextField");

  // TODO(b/301253962): This fails in Lacros because the virtual keyboard is
  // automatically opened when focus reached the text field, so the key press of
  // "select" does not open the switch access menu.

  // Send "select", which opens the switch access menu.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_1);

  // Wait for the switch access menu to appear and for focus to land on
  // the first item, the "keyboard" button.
  //
  // Note that we don't try to also call WaitForSwitchAccessMenuAndGetActions
  // here because by the time it returns, we may have already received the focus
  // ring for the menu and so the following WaitForFocusRing would fail / loop
  // forever.
  utils()->WaitForFocusRing("primary", "button", "Keyboard");

  // Send "next".
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);

  // The next menu item is the "dictation" button.
  utils()->WaitForFocusRing("primary", "button", "Dictation");

  // Send "next".
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);

  // The next menu item is the "enter" button.
  utils()->WaitForFocusRing("primary", "button", "Drill down");

  // Send "next".
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);

  // The next menu item is the "point scanning" button.
  utils()->WaitForFocusRing("primary", "button", "Point scanning");

  // Send "next".
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);

  // The next menu item is the "settings" button.
  utils()->WaitForFocusRing("primary", "button", "Settings");

  // Send "next".
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);

  // Finally is the back button. Note that it has a role of "back" so we
  // can tell it's the special Switch Access back button.
  utils()->WaitForFocusRing("primary", "back", "");

  // Send "next".
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_2);

  // Wrap back around to the "keyboard" button.
  utils()->WaitForFocusRing("primary", "button", "Keyboard");
}

// TODO(crbug.com/40926594): Enable after fixing flakiness.
IN_PROC_BROWSER_TEST_F(SwitchAccessTest, DISABLED_TypeIntoVirtualKeyboard) {
  utils()->EnableSwitchAccess({'1', 'A'} /* select */, {'2', 'B'} /* next */,
                              {'3', 'C'} /* previous */);

  // Load a webpage with a text box.
  NavigateToUrl(
      GURL("data:text/html,<input autofocus aria-label=MyTextField>"));

  // Wait for switch access to focus on the text field.
  utils()->WaitForFocusRing("primary", "textField", "MyTextField");

  // Send "select", which opens the switch access menu.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_1);

  // Wait for the switch access menu to appear and for focus to land on
  // the first item, the "keyboard" button.
  //
  // Note that we don't try to also call WaitForSwitchAccessMenuAndGetActions
  // here because by the time it returns, we may have already received the focus
  // ring for the menu and so the following WaitForFocusRing would fail / loop
  // forever.
  utils()->WaitForFocusRing("primary", "button", "Keyboard");

  // Send "select", which opens the virtual keyboard.
  SendVirtualKeyPress(ui::KeyboardCode::VKEY_1);

  // Finally, we should land on a keyboard key.
  utils()->WaitForFocusRing("primary", "keyboard", "");

  // Actually typing and verifying text field value should be covered by
  // js-based tests that have the ability to ask the text field for its value.
}

#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_PointScanClickWhenMouseEventsEnabled \
  DISABLED_PointScanClickWhenMouseEventsEnabled
#else
#define MAYBE_PointScanClickWhenMouseEventsEnabled \
  PointScanClickWhenMouseEventsEnabled
#endif
IN_PROC_BROWSER_TEST_F(SwitchAccessTest,
                       MAYBE_PointScanClickWhenMouseEventsEnabled) {
  utils()->EnableSwitchAccess({'1', 'A'} /* select */, {'2', 'B'} /* next */,
                              {'3', 'C'} /* previous */);

  // Load a webpage with a checkbox.
  NavigateToUrl(
      GURL("data:text/html,<input autofocus type=checkbox title='checkbox'"
           "style='width: 800px; height: 800px;'>"));

  // Wait for switch access to focus on the checkbox.
  utils()->WaitForFocusRing("primary", "checkBox", "checkbox");

  // Enable mouse events (within root window containing checkbox).
  EnableMouseEvents(600, 600);

  // Perform default action on the checkbox.
  utils()->DoDefault("checkbox");

  // Verify checkbox state changes.
  utils()->WaitForEventOnAutomationNode("checkedStateChanged", "checkbox");

  // Use Point Scan to click on the checkbox.
  utils()->PointScanClick(600, 600);

  // Verify checkbox state changes.
  utils()->WaitForEventOnAutomationNode("checkedStateChanged", "checkbox");

  // Verify mouse events are still enabled.
  ASSERT_TRUE(IsMouseEventsEnabled(600, 600));
}

#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_PointScanClickWhenMouseEventsDisabled \
  DISABLED_PointScanClickWhenMouseEventsDisabled
#else
#define MAYBE_PointScanClickWhenMouseEventsDisabled \
  PointScanClickWhenMouseEventsDisabled
#endif
IN_PROC_BROWSER_TEST_F(SwitchAccessTest,
                       MAYBE_PointScanClickWhenMouseEventsDisabled) {
  utils()->EnableSwitchAccess({'1', 'A'} /* select */, {'2', 'B'} /* next */,
                              {'3', 'C'} /* previous */);

  // Load a webpage with a checkbox.
  NavigateToUrl(
      GURL("data:text/html,<input autofocus type=checkbox title='checkbox'"
           "style='width: 800px; height: 800px;'>"));

  // Wait for switch access to focus on the checkbox.
  utils()->WaitForFocusRing("primary", "checkBox", "checkbox");

  // Disable mouse events (within root window containing checkbox).
  DisableMouseEvents(600, 600);

  // Perform default action on the checkbox.
  utils()->DoDefault("checkbox");

  // Verify checkbox state changes.
  utils()->WaitForEventOnAutomationNode("checkedStateChanged", "checkbox");

  // Use Point Scan to click on the checkbox.
  utils()->PointScanClick(600, 600);

  // Verify checkbox state changes.
  utils()->WaitForEventOnAutomationNode("checkedStateChanged", "checkbox");

  // Verify mouse events are not enabled.
  ASSERT_FALSE(IsMouseEventsEnabled(600, 600));
}

}  // namespace ash