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

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