// Copyright 2014 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/keyboard_ui_controller.h"
#include "ash/keyboard/ui/resources/keyboard_resource_util.h"
#include "ash/public/cpp/keyboard/keyboard_switches.h"
#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/values.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_ui.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ime/dummy_text_input_client.h"
#include "ui/base/ime/init/input_method_factory.h"
#include "ui/base/ime/input_method.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/test/event_generator.h"
namespace {
const int kKeyboardHeightForTest = 100;
// TODO(shend): Remove this since all calls are synchronous now.
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() {
if (ChromeKeyboardControllerClient::Get()->is_keyboard_visible() ==
visible_) {
return;
}
run_loop_.Run();
}
// ChromeKeyboardControllerClient::Observer
void OnKeyboardVisibilityChanged(bool visible) override {
if (visible == visible_)
run_loop_.QuitWhenIdle();
}
private:
base::RunLoop run_loop_;
const bool visible_;
};
class KeyboardLoadedWaiter : public ChromeKeyboardControllerClient::Observer {
public:
KeyboardLoadedWaiter() {
ChromeKeyboardControllerClient::Get()->AddObserver(this);
}
KeyboardLoadedWaiter(const KeyboardLoadedWaiter&) = delete;
KeyboardLoadedWaiter& operator=(const KeyboardLoadedWaiter&) = delete;
~KeyboardLoadedWaiter() override {
ChromeKeyboardControllerClient::Get()->RemoveObserver(this);
}
void Wait() {
if (ChromeKeyboardControllerClient::Get()->is_keyboard_loaded())
return;
run_loop_.Run();
}
// ChromeKeyboardControllerClient::Observer
void OnKeyboardLoaded() override { run_loop_.QuitWhenIdle(); }
private:
base::RunLoop run_loop_;
};
class KeyboardOccludedBoundsChangeWaiter
: public ChromeKeyboardControllerClient::Observer {
public:
KeyboardOccludedBoundsChangeWaiter() {
ChromeKeyboardControllerClient::Get()->AddObserver(this);
}
KeyboardOccludedBoundsChangeWaiter(
const KeyboardOccludedBoundsChangeWaiter&) = delete;
KeyboardOccludedBoundsChangeWaiter& operator=(
const KeyboardOccludedBoundsChangeWaiter&) = delete;
~KeyboardOccludedBoundsChangeWaiter() override {
ChromeKeyboardControllerClient::Get()->RemoveObserver(this);
}
void Wait() { run_loop_.Run(); }
// ChromeKeyboardControllerClient::Observer
void OnKeyboardOccludedBoundsChanged(const gfx::Rect& bounds) override {
run_loop_.QuitWhenIdle();
}
private:
base::RunLoop run_loop_;
};
ui::InputMethod* GetInputMethod() {
aura::Window* root_window = ChromeKeyboardControllerClient::Get()
->GetKeyboardWindow()
->GetRootWindow();
return root_window ? root_window->GetHost()->GetInputMethod() : nullptr;
}
} // namespace
class KeyboardControllerWebContentTest : public InProcessBrowserTest {
public:
KeyboardControllerWebContentTest() {}
KeyboardControllerWebContentTest(const KeyboardControllerWebContentTest&) =
delete;
KeyboardControllerWebContentTest& operator=(
const KeyboardControllerWebContentTest&) = delete;
~KeyboardControllerWebContentTest() override {}
void SetUp() override {
InProcessBrowserTest::SetUp();
}
void TearDown() override { InProcessBrowserTest::TearDown(); }
// Ensure that the virtual keyboard is enabled.
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(keyboard::switches::kEnableVirtualKeyboard);
}
protected:
void FocusEditableNodeAndShowKeyboard(const gfx::Rect& init_bounds) {
client =
std::make_unique<ui::DummyTextInputClient>(ui::TEXT_INPUT_TYPE_TEXT);
ui::InputMethod* input_method = GetInputMethod();
ASSERT_TRUE(input_method);
input_method->SetFocusedTextInputClient(client.get());
input_method->SetVirtualKeyboardVisibilityIfEnabled(true);
// Mock window.resizeTo that is expected to be called after navigate to a
// new virtual keyboard.
auto* keyboard_controller = ChromeKeyboardControllerClient::Get();
keyboard_controller->GetKeyboardWindow()->SetBounds(init_bounds);
}
void FocusNonEditableNode() {
client =
std::make_unique<ui::DummyTextInputClient>(ui::TEXT_INPUT_TYPE_NONE);
GetInputMethod()->SetFocusedTextInputClient(client.get());
}
void MockEnableIMEInDifferentExtension(const std::string& url,
const gfx::Rect& init_bounds) {
DCHECK(!url.empty());
auto* keyboard_controller = ChromeKeyboardControllerClient::Get();
keyboard_controller->set_virtual_keyboard_url_for_test(GURL(url));
keyboard_controller->ReloadKeyboardIfNeeded();
// Mock window.resizeTo that is expected to be called after navigate to a
// new virtual keyboard.
keyboard_controller->GetKeyboardWindow()->SetBounds(init_bounds);
}
private:
std::unique_ptr<ui::DummyTextInputClient> client;
ui::ScopedTestInputMethodFactory scoped_test_input_method_factory_;
};
// Test for crbug.com/404340. After enabling an IME in a different extension,
// its virtual keyboard should not become visible if previous one is not.
IN_PROC_BROWSER_TEST_F(KeyboardControllerWebContentTest,
EnableIMEInDifferentExtension) {
KeyboardLoadedWaiter().Wait();
gfx::Rect test_bounds(0, 0, 0, kKeyboardHeightForTest);
FocusEditableNodeAndShowKeyboard(test_bounds);
KeyboardVisibleWaiter(true).Wait();
FocusNonEditableNode();
KeyboardVisibleWaiter(false).Wait();
MockEnableIMEInDifferentExtension("chrome-extension://domain-1", test_bounds);
// Keyboard should not become visible if previous keyboard is not.
EXPECT_FALSE(ChromeKeyboardControllerClient::Get()->is_keyboard_visible());
FocusEditableNodeAndShowKeyboard(test_bounds);
// Keyboard should become visible after focus on an editable node.
KeyboardVisibleWaiter(true).Wait();
// Simulate hide keyboard by pressing hide key on the virtual keyboard.
ChromeKeyboardControllerClient::Get()->HideKeyboard(ash::HideReason::kUser);
KeyboardVisibleWaiter(false).Wait();
MockEnableIMEInDifferentExtension("chrome-extension://domain-2", test_bounds);
// Keyboard should not become visible if previous keyboard is not, even if it
// is currently focused on an editable node.
EXPECT_FALSE(ChromeKeyboardControllerClient::Get()->is_keyboard_visible());
}
// This test requires using the Ash keyboard window for EventGenerator to work.
// TODO(stevenjb/shend): Investigate/fix.
IN_PROC_BROWSER_TEST_F(KeyboardControllerWebContentTest,
CanDragFloatingKeyboardWithMouse) {
ChromeKeyboardControllerClient::Get()->SetContainerType(
keyboard::ContainerType::kFloating, gfx::Rect(0, 0, 400, 200),
base::DoNothing());
auto* controller = keyboard::KeyboardUIController::Get();
controller->ShowKeyboard(false);
KeyboardVisibleWaiter(true).Wait();
aura::Window* keyboard_window = controller->GetKeyboardWindow();
keyboard_window->SetBounds(gfx::Rect(0, 0, 100, 100));
EXPECT_EQ(gfx::Point(0, 0), keyboard_window->bounds().origin());
controller->SetDraggableArea(keyboard_window->bounds());
// Drag the top left corner of the keyboard to move it.
ui::test::EventGenerator event_generator(keyboard_window->GetRootWindow());
event_generator.MoveMouseTo(gfx::Point(0, 0));
event_generator.PressLeftButton();
event_generator.MoveMouseTo(gfx::Point(50, 50));
event_generator.ReleaseLeftButton();
event_generator.MoveMouseTo(gfx::Point(100, 100));
EXPECT_EQ(gfx::Point(50, 50), keyboard_window->bounds().origin());
}
class KeyboardControllerAppWindowTest
: public extensions::PlatformAppBrowserTest {
public:
KeyboardControllerAppWindowTest() {}
KeyboardControllerAppWindowTest(const KeyboardControllerAppWindowTest&) =
delete;
KeyboardControllerAppWindowTest& operator=(
const KeyboardControllerAppWindowTest&) = delete;
~KeyboardControllerAppWindowTest() override {}
// Ensure that the virtual keyboard is enabled.
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(keyboard::switches::kEnableVirtualKeyboard);
}
scoped_refptr<const extensions::Extension> CreateDummyExtension() {
auto extension =
extensions::ExtensionBuilder()
.SetManifest(base::Value::Dict()
.Set("name", "test extension")
.Set("version", "1")
.Set("manifest_version", 2)
.Set("background",
base::Value::Dict().Set(
"scripts", base::Value::List().Append(
"background.js"))))
.Build();
extension_service()->AddExtension(extension.get());
return extension;
}
};
IN_PROC_BROWSER_TEST_F(KeyboardControllerAppWindowTest,
ShowingKeyboardChangesViewport) {
auto* client = ChromeKeyboardControllerClient::Get();
auto extension = CreateDummyExtension();
extensions::AppWindow::CreateParams params;
params.frame = extensions::AppWindow::FRAME_NONE;
params.state = ui::SHOW_STATE_MAXIMIZED;
extensions::AppWindow* app_window =
CreateAppWindowFromParams(browser()->profile(), extension.get(), params);
// Wait until the keyboard is shown.
KeyboardLoadedWaiter().Wait();
client->ShowKeyboard();
KeyboardVisibleWaiter(true).Wait();
const int new_viewport_height = app_window->web_contents()
->GetRenderWidgetHostView()
->GetVisibleViewportSize()
.height();
const int screen_height = display::Screen::GetScreen()
->GetPrimaryDisplay()
.GetSizeInPixel()
.height();
EXPECT_EQ(new_viewport_height,
screen_height - client->GetKeyboardWindow()->bounds().height());
}
IN_PROC_BROWSER_TEST_F(KeyboardControllerAppWindowTest,
ShowingStickyKeyboardChangesViewport) {
auto extension = CreateDummyExtension();
extensions::AppWindow::CreateParams params;
params.frame = extensions::AppWindow::FRAME_NONE;
params.state = ui::SHOW_STATE_MAXIMIZED;
extensions::AppWindow* app_window =
CreateAppWindowFromParams(browser()->profile(), extension.get(), params);
// Wait until the keyboard is shown.
KeyboardLoadedWaiter().Wait();
keyboard::KeyboardUIController::Get()->ShowKeyboard(/*locked*/ true);
KeyboardVisibleWaiter(true).Wait();
const int new_viewport_height = app_window->web_contents()
->GetRenderWidgetHostView()
->GetVisibleViewportSize()
.height();
const int screen_height = display::Screen::GetScreen()
->GetPrimaryDisplay()
.GetSizeInPixel()
.height();
EXPECT_EQ(new_viewport_height,
screen_height - ChromeKeyboardControllerClient::Get()
->GetKeyboardWindow()
->bounds()
.height());
}
// Tests that ime window won't overscroll. See crbug.com/529880.
IN_PROC_BROWSER_TEST_F(KeyboardControllerAppWindowTest,
DisableOverscrollForImeWindow) {
auto extension = CreateDummyExtension();
extensions::AppWindow::CreateParams non_ime_params;
non_ime_params.frame = extensions::AppWindow::FRAME_NONE;
extensions::AppWindow* non_ime_app_window = CreateAppWindowFromParams(
browser()->profile(), extension.get(), non_ime_params);
int non_ime_window_visible_height = non_ime_app_window->web_contents()
->GetRenderWidgetHostView()
->GetVisibleViewportSize()
.height();
extensions::AppWindow::CreateParams ime_params;
ime_params.frame = extensions::AppWindow::FRAME_NONE;
ime_params.is_ime_window = true;
extensions::AppWindow* ime_app_window = CreateAppWindowFromParams(
browser()->profile(), extension.get(), ime_params);
int ime_window_visible_height = ime_app_window->web_contents()
->GetRenderWidgetHostView()
->GetVisibleViewportSize()
.height();
ASSERT_EQ(non_ime_window_visible_height, ime_window_visible_height);
ASSERT_TRUE(ime_window_visible_height > 0);
// Make sure the keyboard has loaded before showing.
KeyboardLoadedWaiter().Wait();
auto* controller = ChromeKeyboardControllerClient::Get();
controller->ShowKeyboard();
KeyboardVisibleWaiter(true).Wait();
int screen_height = display::Screen::GetScreen()
->GetPrimaryDisplay()
.GetSizeInPixel()
.height();
int keyboard_height = screen_height - ime_window_visible_height + 1;
ASSERT_GT(keyboard_height, 0);
gfx::Rect test_bounds = controller->GetKeyboardWindow()->bounds();
test_bounds.set_height(keyboard_height);
{
// Waiter needs to be created before SetBounds() is invoked so that it can
// catch OnOccludedBoundsChanged event even before it starts waiting.
KeyboardOccludedBoundsChangeWaiter waiter;
controller->GetKeyboardWindow()->SetBounds(test_bounds);
// Wait for the keyboard bounds change has been processed.
waiter.Wait();
}
// Non ime window should have smaller visible view port due to overlap with
// virtual keyboard.
EXPECT_LT(non_ime_app_window->web_contents()
->GetRenderWidgetHostView()
->GetVisibleViewportSize()
.height(),
non_ime_window_visible_height);
// Ime window should have not be affected by virtual keyboard.
EXPECT_EQ(ime_app_window->web_contents()
->GetRenderWidgetHostView()
->GetVisibleViewportSize()
.height(),
ime_window_visible_height);
}
class KeyboardControllerStateTest : public InProcessBrowserTest {
public:
KeyboardControllerStateTest() {}
KeyboardControllerStateTest(const KeyboardControllerStateTest&) = delete;
KeyboardControllerStateTest& operator=(const KeyboardControllerStateTest&) =
delete;
~KeyboardControllerStateTest() override {}
// Ensure that the virtual keyboard is enabled.
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(keyboard::switches::kEnableVirtualKeyboard);
}
};
IN_PROC_BROWSER_TEST_F(KeyboardControllerStateTest, OpenTwice) {
auto* controller = ChromeKeyboardControllerClient::Get();
EXPECT_FALSE(controller->is_keyboard_visible());
// Call ShowKeyboard twice, the keyboard should become visible.
controller->ShowKeyboard();
controller->ShowKeyboard();
KeyboardVisibleWaiter(true).Wait();
EXPECT_TRUE(controller->is_keyboard_visible());
// Ensure the keyboard remains visible. Note: we call RunUntilIdle to at least
// ensure no other messages are pending instead of relying on a timeout that
// will slow down tests and potentially be flakey.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(controller->is_keyboard_visible());
}
IN_PROC_BROWSER_TEST_F(KeyboardControllerStateTest, OpenAndCloseAndOpen) {
auto* controller = ChromeKeyboardControllerClient::Get();
controller->ShowKeyboard();
KeyboardVisibleWaiter(true).Wait();
controller->HideKeyboard(ash::HideReason::kSystem);
KeyboardVisibleWaiter(false).Wait();
controller->ShowKeyboard();
KeyboardVisibleWaiter(true).Wait();
}
// NOTE: The following tests test internal state of keyboard::KeyboardController
// and will not work in Multi Process Mash. TODO(stevenjb/shend): Determine
// whether this needs to be tested in a keyboard::KeyboardController unit test.
IN_PROC_BROWSER_TEST_F(KeyboardControllerStateTest, StateResolvesAfterPreload) {
auto* controller = keyboard::KeyboardUIController::Get();
EXPECT_EQ(controller->GetStateForTest(), keyboard::KeyboardUIState::kLoading);
KeyboardLoadedWaiter().Wait();
EXPECT_EQ(controller->GetStateForTest(), keyboard::KeyboardUIState::kHidden);
}
IN_PROC_BROWSER_TEST_F(KeyboardControllerStateTest,
OpenAndCloseAndOpenInternal) {
auto* controller = keyboard::KeyboardUIController::Get();
controller->ShowKeyboard(false);
// Need to wait the extension to be loaded. Hence LOADING_EXTENSION.
EXPECT_EQ(controller->GetStateForTest(), keyboard::KeyboardUIState::kLoading);
KeyboardVisibleWaiter(true).Wait();
controller->HideKeyboardExplicitlyBySystem();
EXPECT_EQ(controller->GetStateForTest(), keyboard::KeyboardUIState::kHidden);
controller->ShowKeyboard(false);
// The extension already has been loaded. Hence SHOWING.
EXPECT_EQ(controller->GetStateForTest(), keyboard::KeyboardUIState::kShown);
}
// See crbug.com/755354.
IN_PROC_BROWSER_TEST_F(KeyboardControllerStateTest,
DisablingKeyboardGoesToInitialState) {
auto* controller = keyboard::KeyboardUIController::Get();
EXPECT_EQ(controller->GetStateForTest(), keyboard::KeyboardUIState::kLoading);
controller->Shutdown();
EXPECT_EQ(controller->GetStateForTest(), keyboard::KeyboardUIState::kInitial);
}