chromium/chrome/browser/ui/ash/keyboard/keyboard_end_to_end_browsertest.cc

// Copyright 2017 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/keyboard/ui/resources/keyboard_resource_util.h"
#include "ash/public/cpp/keyboard/keyboard_switches.h"
#include "base/command_line.h"
#include "base/files/file.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "chrome/browser/ash/input_method/textinput_test_helper.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "ui/aura/window_tree_host.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"

namespace {

class KeyboardVisibleWaiter : public ChromeKeyboardControllerClient::Observer {
 public:
  explicit KeyboardVisibleWaiter(bool visible) : visible_(visible) {
    ChromeKeyboardControllerClient::Get()->AddObserver(this);
  }

  KeyboardVisibleWaiter(const KeyboardVisibleWaiter&) = delete;
  KeyboardVisibleWaiter& operator=(const KeyboardVisibleWaiter&) = delete;

  ~KeyboardVisibleWaiter() override {
    ChromeKeyboardControllerClient::Get()->RemoveObserver(this);
  }

  void Wait() { run_loop_.Run(); }

  // ChromeKeyboardControllerClient::Observer
  void OnKeyboardVisibilityChanged(bool visible) override {
    if (visible == visible_)
      run_loop_.QuitWhenIdle();
  }

 private:
  base::RunLoop run_loop_;
  const bool visible_;
};  // namespace

bool WaitUntilShown() {
  if (ChromeKeyboardControllerClient::Get()->is_keyboard_visible()) {
    base::RunLoop().RunUntilIdle();  // Allow async operations to complete.
    return true;
  }
  KeyboardVisibleWaiter(true).Wait();
  return ChromeKeyboardControllerClient::Get()->is_keyboard_visible();
}

bool WaitUntilHidden() {
  if (!ChromeKeyboardControllerClient::Get()->is_keyboard_visible()) {
    base::RunLoop().RunUntilIdle();  // Allow async operations to complete.
    return true;
  }
  KeyboardVisibleWaiter(false).Wait();
  return !ChromeKeyboardControllerClient::Get()->is_keyboard_visible();
}

gfx::Size GetScreenBounds() {
  return display::Screen::GetScreen()->GetPrimaryDisplay().GetSizeInPixel();
}

}  // namespace

class KeyboardEndToEndTest : public InProcessBrowserTest {
 public:
  KeyboardEndToEndTest(const KeyboardEndToEndTest&) = delete;
  KeyboardEndToEndTest& operator=(const KeyboardEndToEndTest&) = delete;

  // Ensure that the virtual keyboard is enabled.
  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(keyboard::switches::kEnableVirtualKeyboard);
  }

  void SetUpOnMainThread() override {
    GURL test_url = ui_test_utils::GetTestUrl(
        base::FilePath("chromeos/virtual_keyboard"), test_file_);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_url));
    web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
    ASSERT_TRUE(web_contents_);

    base::RunLoop().RunUntilIdle();

    auto* client = ChromeKeyboardControllerClient::Get();
    ASSERT_TRUE(client);
    ASSERT_TRUE(client->is_keyboard_enabled());
    EXPECT_FALSE(client->is_keyboard_visible());
  }

 protected:
  // Initialized in |SetUpOnMainThread|.
  raw_ptr<content::WebContents, DanglingUntriaged> web_contents_;

  explicit KeyboardEndToEndTest(const base::FilePath& test_file)
      : test_file_(test_file) {}
  ~KeyboardEndToEndTest() override {}

  // Get the value of the attribute attribute |attribute| on the DOM element
  // with the given |id|.
  std::string GetElementAttribute(content::WebContents* web_contents,
                                  const std::string& id,
                                  const std::string& attribute) {
    return content::EvalJs(web_contents,
                           content::JsReplace(
                               "document.getElementById($1).getAttribute($2)",
                               id, attribute))
        .ExtractString();
  }

  // Simulates a click on the middle of the DOM element with the given |id|.
  void ClickElementWithId(content::WebContents* web_contents,
                          const std::string& id) {
    // Get the center coordinates of the DOM element.
    const int x =
        content::EvalJs(
            web_contents,
            content::JsReplace("const bounds = "
                               "document.getElementById($1)."
                               "getBoundingClientRect();"
                               "Math.floor(bounds.left + bounds.width / 2)",
                               id))
            .ExtractInt();
    const int y =
        content::EvalJs(
            web_contents,
            content::JsReplace("const bounds = "
                               "document.getElementById($1)."
                               "getBoundingClientRect();"
                               "Math.floor(bounds.top + bounds.height / 2)",
                               id))
            .ExtractInt();

    content::SimulateMouseClickAt(
        web_contents, 0, blink::WebMouseEvent::Button::kLeft, gfx::Point(x, y));
  }

 private:
  base::FilePath test_file_;
};

class KeyboardEndToEndFormTest : public KeyboardEndToEndTest {
 public:
  KeyboardEndToEndFormTest()
      : KeyboardEndToEndTest(base::FilePath("form.html")) {}

  KeyboardEndToEndFormTest(const KeyboardEndToEndFormTest&) = delete;
  KeyboardEndToEndFormTest& operator=(const KeyboardEndToEndFormTest&) = delete;

  ~KeyboardEndToEndFormTest() override {}

 protected:
};

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFormTest, ClickTextFieldShowsKeyboard) {
  ClickElementWithId(web_contents_, "username");
  ASSERT_TRUE(WaitUntilShown());
}

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFormTest, ClickBodyHidesKeyboard) {
  ClickElementWithId(web_contents_, "username");
  ASSERT_TRUE(WaitUntilShown());

  content::SimulateMouseClickAt(
      web_contents_, 0, blink::WebMouseEvent::Button::kLeft, gfx::Point(0, 0));
  ASSERT_TRUE(WaitUntilHidden());
}

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFormTest,
                       ChangeInputTypeToTextDoesNotHideKeyboard) {
  ClickElementWithId(web_contents_, "username");
  ASSERT_TRUE(WaitUntilShown());

  ASSERT_TRUE(
      content::EvalJs(web_contents_.get(),
                      "document.getElementById('username').type = 'password'")
          .error.empty());

  base::RunLoop().RunUntilIdle();  // Allow async operations to complete.
  EXPECT_TRUE(ChromeKeyboardControllerClient::Get()->is_keyboard_visible());
}

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFormTest,
                       ChangeInputTypeToNonTextHidesKeyboard) {
  ClickElementWithId(web_contents_, "username");
  ASSERT_TRUE(WaitUntilShown());

  ASSERT_TRUE(
      content::EvalJs(web_contents_.get(),
                      "document.getElementById('username').type = 'submit'")
          .error.empty());

  ASSERT_TRUE(WaitUntilHidden());
}

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFormTest,
                       ChangeInputToReadOnlyHidesKeyboard) {
  ClickElementWithId(web_contents_, "username");
  ASSERT_TRUE(WaitUntilShown());

  ASSERT_TRUE(
      content::EvalJs(web_contents_.get(),
                      "document.getElementById('username').readOnly = true")
          .error.empty());

  ASSERT_TRUE(WaitUntilHidden());
}

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFormTest,
                       ChangeInputModeToNumericDoesNotHideKeyboard) {
  ClickElementWithId(web_contents_, "username");
  ASSERT_TRUE(WaitUntilShown());

  ASSERT_TRUE(content::EvalJs(web_contents_.get(),
                              "document.getElementById('username')."
                              "setAttribute('inputmode', 'numeric')")
                  .error.empty());

  base::RunLoop().RunUntilIdle();  // Allow async operations to complete.
  EXPECT_TRUE(ChromeKeyboardControllerClient::Get()->is_keyboard_visible());
}

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFormTest,
                       ChangeInputModeToNoneHidesKeyboard) {
  ClickElementWithId(web_contents_, "username");
  ASSERT_TRUE(WaitUntilShown());

  ASSERT_TRUE(content::EvalJs(web_contents_.get(),
                              "document.getElementById('username')."
                              "setAttribute('inputmode', 'none')")
                  .error.empty());

  ASSERT_TRUE(WaitUntilHidden());
}

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFormTest, DeleteInputHidesKeyboard) {
  ClickElementWithId(web_contents_, "username");
  ASSERT_TRUE(WaitUntilShown());

  ASSERT_TRUE(content::EvalJs(web_contents_.get(),
                              "document.getElementById('username').remove()")
                  .error.empty());

  ASSERT_TRUE(WaitUntilHidden());
}

class KeyboardEndToEndFocusTest : public KeyboardEndToEndTest {
 public:
  KeyboardEndToEndFocusTest()
      : KeyboardEndToEndTest(base::FilePath("focus.html")) {}

  KeyboardEndToEndFocusTest(const KeyboardEndToEndFocusTest&) = delete;
  KeyboardEndToEndFocusTest& operator=(const KeyboardEndToEndFocusTest&) =
      delete;

  ~KeyboardEndToEndFocusTest() override {}

 protected:
};

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFocusTest,
                       TriggerInputFocusWithoutUserGestureDoesNotShowKeyboard) {
  ASSERT_TRUE(content::EvalJs(web_contents_.get(),
                              "document.getElementById('text').focus()")
                  .error.empty());

  base::RunLoop().RunUntilIdle();  // Allow async operations to complete.
  EXPECT_FALSE(ChromeKeyboardControllerClient::Get()->is_keyboard_visible());
}

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndFocusTest,
                       TriggerInputFocusFromUserGestureShowsKeyboard) {
  ClickElementWithId(web_contents_, "sync");

  ASSERT_TRUE(WaitUntilShown());
}

IN_PROC_BROWSER_TEST_F(
    KeyboardEndToEndFocusTest,
    TriggerAsyncInputFocusFromUserGestureDoesNotShowKeyboard) {
  ClickElementWithId(web_contents_, "async");

  base::RunLoop().RunUntilIdle();  // Allow async operations to complete.
  EXPECT_FALSE(ChromeKeyboardControllerClient::Get()->is_keyboard_visible());
}

IN_PROC_BROWSER_TEST_F(
    KeyboardEndToEndFocusTest,
    TriggerAsyncInputFocusFromUserGestureAfterBlurShowsKeyboard) {
  // If async focus occurs quickly after blur, then it should still invoke the
  // keyboard.
  ClickElementWithId(web_contents_, "text");
  ASSERT_TRUE(WaitUntilShown());

  ClickElementWithId(web_contents_, "blur");
  ASSERT_TRUE(WaitUntilHidden());

  ClickElementWithId(web_contents_, "async");
  ASSERT_TRUE(WaitUntilShown());
}

IN_PROC_BROWSER_TEST_F(
    KeyboardEndToEndFocusTest,
    TriggerAsyncInputFocusFromUserGestureAfterBlurTimeoutDoesNotShowKeyboard) {
  ClickElementWithId(web_contents_, "text");
  ASSERT_TRUE(WaitUntilShown());

  ClickElementWithId(web_contents_, "blur");
  ASSERT_TRUE(WaitUntilHidden());

  // Wait until the transient blur threshold (3500ms) passes.
  // TODO(crbug.com/41392988): Find a way to accelerate the clock without
  // actually waiting in real time.
  base::PlatformThread::Sleep(base::Milliseconds(3501));

  ClickElementWithId(web_contents_, "async");
  base::RunLoop().RunUntilIdle();  // Allow async operations to complete.
  EXPECT_FALSE(ChromeKeyboardControllerClient::Get()->is_keyboard_visible());
}

class KeyboardEndToEndOverscrollTest : public KeyboardEndToEndTest {
 public:
  KeyboardEndToEndOverscrollTest()
      : KeyboardEndToEndTest(base::FilePath("form.html")) {}

  KeyboardEndToEndOverscrollTest(const KeyboardEndToEndOverscrollTest&) =
      delete;
  KeyboardEndToEndOverscrollTest& operator=(
      const KeyboardEndToEndOverscrollTest&) = delete;

  ~KeyboardEndToEndOverscrollTest() override {}

  void FocusAndShowKeyboard() { ClickElementWithId(web_contents_, "username"); }

  void HideKeyboard() {
    auto* controller = ChromeKeyboardControllerClient::Get();
    controller->HideKeyboard(ash::HideReason::kUser);
  }

 protected:
  int GetViewportHeight(content::WebContents* web_contents) {
    return web_contents->GetRenderWidgetHostView()
        ->GetVisibleViewportSize()
        .height();
  }
};

IN_PROC_BROWSER_TEST_F(KeyboardEndToEndOverscrollTest,
                       ToggleKeyboardOnMaximizedWindowAffectsViewport) {
  browser()->window()->Maximize();

  const int old_height = GetViewportHeight(web_contents_);

  FocusAndShowKeyboard();
  ASSERT_TRUE(WaitUntilShown());

  EXPECT_LT(GetViewportHeight(web_contents_), old_height);

  HideKeyboard();
  ASSERT_TRUE(WaitUntilHidden());

  EXPECT_EQ(GetViewportHeight(web_contents_), old_height);
}

IN_PROC_BROWSER_TEST_F(
    KeyboardEndToEndOverscrollTest,
    ToggleKeyboardOnNonOverlappingWindowDoesNotAffectViewport) {
  // Set the window bounds so that it does not overlap with the keyboard.
  // The virtual keyboard takes up no more than half the screen height.
  gfx::Size screen_bounds = GetScreenBounds();
  browser()->window()->SetBounds(
      gfx::Rect(0, 0, screen_bounds.width(), screen_bounds.height() / 2));

  const int old_height = GetViewportHeight(web_contents_);

  FocusAndShowKeyboard();
  ASSERT_TRUE(WaitUntilShown());

  EXPECT_EQ(GetViewportHeight(web_contents_), old_height);

  HideKeyboard();
  ASSERT_TRUE(WaitUntilHidden());

  EXPECT_EQ(GetViewportHeight(web_contents_), old_height);
}

IN_PROC_BROWSER_TEST_F(
    KeyboardEndToEndOverscrollTest,
    ToggleKeyboardOnShortOverlappingWindowMovesWindowUpwards) {
  // Shift the window down so that it overlaps with the keyboard, but shrink the
  // window size so that when it moves upwards, it will no longer overlap with
  // the keyboard.
  gfx::Size screen_bounds = GetScreenBounds();
  browser()->window()->SetBounds(gfx::Rect(0, screen_bounds.height() / 2,
                                           screen_bounds.width(),
                                           screen_bounds.height() / 2));
  const auto old_browser_bounds = browser()->window()->GetBounds();
  const int old_height = GetViewportHeight(web_contents_);

  FocusAndShowKeyboard();
  ASSERT_TRUE(WaitUntilShown());

  EXPECT_LT(browser()->window()->GetBounds().y(), old_browser_bounds.y());
  EXPECT_EQ(browser()->window()->GetBounds().height(),
            old_browser_bounds.height());
  EXPECT_EQ(GetViewportHeight(web_contents_), old_height);

  HideKeyboard();
  ASSERT_TRUE(WaitUntilHidden());

  EXPECT_EQ(browser()->window()->GetBounds(), old_browser_bounds);
  EXPECT_EQ(GetViewportHeight(web_contents_), old_height);
}

IN_PROC_BROWSER_TEST_F(
    KeyboardEndToEndOverscrollTest,
    ToggleKeyboardOnTallOverlappingWindowMovesWindowUpwardsAndAffectsViewport) {
  // Shift the window down so that it overlaps with the keyboard, and expand the
  // window size so that when it moves upwards, it will still overlap with
  // the keyboard.
  gfx::Size screen_bounds = GetScreenBounds();
  browser()->window()->SetBounds(gfx::Rect(0, screen_bounds.height() / 3,
                                           screen_bounds.width(),
                                           screen_bounds.height() / 3 * 2));
  const auto old_browser_bounds = browser()->window()->GetBounds();
  const int old_height = GetViewportHeight(web_contents_);

  FocusAndShowKeyboard();
  ASSERT_TRUE(WaitUntilShown());

  EXPECT_LT(browser()->window()->GetBounds().y(), old_browser_bounds.y());
  EXPECT_EQ(browser()->window()->GetBounds().height(),
            old_browser_bounds.height());
  EXPECT_LT(GetViewportHeight(web_contents_), old_height);

  HideKeyboard();
  ASSERT_TRUE(WaitUntilHidden());

  EXPECT_EQ(browser()->window()->GetBounds(), old_browser_bounds);
  EXPECT_EQ(GetViewportHeight(web_contents_), old_height);
}