// 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);
}