chromium/fuchsia_web/webengine/browser/virtual_keyboard_browsertest.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/fuchsia.input.virtualkeyboard/cpp/fidl.h>
#include <fidl/fuchsia.ui.input3/cpp/fidl.h>
#include <lib/fit/function.h>

#include <string_view>

#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/koid.h"
#include "base/fuchsia/scoped_service_binding.h"
#include "base/fuchsia/test_component_context_for_process.h"
#include "base/functional/callback.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "content/public/test/browser_test.h"
#include "fuchsia_web/common/test/frame_for_test.h"
#include "fuchsia_web/common/test/frame_test_util.h"
#include "fuchsia_web/common/test/test_navigation_listener.h"
#include "fuchsia_web/webengine/browser/context_impl.h"
#include "fuchsia_web/webengine/browser/frame_impl.h"
#include "fuchsia_web/webengine/browser/mock_virtual_keyboard.h"
#include "fuchsia_web/webengine/features.h"
#include "fuchsia_web/webengine/test/scenic_test_helper.h"
#include "fuchsia_web/webengine/test/scoped_connection_checker.h"
#include "fuchsia_web/webengine/test/test_data.h"
#include "fuchsia_web/webengine/test/web_engine_browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/ozone/public/ozone_platform.h"

namespace virtualkeyboard = fuchsia_input_virtualkeyboard;

namespace {

const gfx::Point kNoTarget = {999, 999};

constexpr char kInputFieldText[] = "input-text";
constexpr char kInputFieldModeTel[] = "input-mode-tel";
constexpr char kInputFieldModeNumeric[] = "input-mode-numeric";
constexpr char kInputFieldModeUrl[] = "input-mode-url";
constexpr char kInputFieldModeEmail[] = "input-mode-email";
constexpr char kInputFieldModeDecimal[] = "input-mode-decimal";
constexpr char kInputFieldModeSearch[] = "input-mode-search";
constexpr char kInputFieldTypeTel[] = "input-type-tel";
constexpr char kInputFieldTypeNumber[] = "input-type-number";
constexpr char kInputFieldTypePassword[] = "input-type-password";

class VirtualKeyboardTest : public WebEngineBrowserTest {
 public:
  VirtualKeyboardTest() {
    set_test_server_root(base::FilePath(kTestServerRoot));
  }
  ~VirtualKeyboardTest() override = default;

  void SetUp() override {
    if (ui::OzonePlatform::GetPlatformNameForTest() == "headless") {
      GTEST_SKIP() << "Keyboard inputs are ignored in headless mode.";
    }

    scoped_feature_list_.InitWithFeatures(
        {features::kVirtualKeyboard, features::kKeyboardInput}, {});
    WebEngineBrowserTest::SetUp();
  }

  void SetUpOnMainThread() override {
    WebEngineBrowserTest::SetUpOnMainThread();
    ASSERT_TRUE(embedded_test_server()->Start());

    fuchsia::web::CreateFrameParams params;
    frame_for_test_ = FrameForTest::Create(context(), std::move(params));

    component_context_.emplace(
        base::TestComponentContextForProcess::InitialState::kCloneAll);
    controller_creator_.emplace(&component_context_.value());

    controller_ = controller_creator_->CreateController();

    // Ensure that the fuchsia.ui.input3.Keyboard service is connected.
    component_context_->additional_services()
        ->RemovePublicService<fuchsia_ui_input3::Keyboard>(
            fidl::DiscoverableProtocolName<fuchsia_ui_input3::Keyboard>);
    keyboard_input_checker_.emplace(component_context_->additional_services());

    fuchsia::web::NavigationControllerPtr controller;
    frame_for_test_.ptr()->GetNavigationController(controller.NewRequest());
    const GURL test_url(embedded_test_server()->GetURL("/input_fields.html"));
    EXPECT_TRUE(LoadUrlAndExpectResponse(
        controller.get(), fuchsia::web::LoadUrlParams(), test_url.spec()));
    frame_for_test_.navigation_listener().RunUntilUrlEquals(test_url);

    fuchsia::web::FramePtr* frame_ptr = &(frame_for_test_.ptr());
    web_contents_ =
        context_impl()->GetFrameImplForTest(frame_ptr)->web_contents();
    scenic_test_helper_.CreateScenicView(
        context_impl()->GetFrameImplForTest(frame_ptr), frame_for_test_.ptr());
    scenic_test_helper_.SetUpViewForInteraction(web_contents_);

    controller_->AwaitWatchAndRespondWith(false);
    ASSERT_EQ(
        base::GetKoid(controller_->view_ref().reference()).value(),
        base::GetKoid(scenic_test_helper_.CloneViewRef().reference).value());
  }

  void TearDownOnMainThread() override {
    frame_for_test_ = {};
    WebEngineBrowserTest::TearDownOnMainThread();
  }

  // The tests expect to have input processed immediately, even if the
  // content has not been displayed yet. That's fine for the test, but
  // we need to explicitly allow it.
  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch("allow-pre-commit-input");
  }

  gfx::Point GetCoordinatesOfInputField(std::string_view id) {
    // Distance to click from the top/left extents of an input field.
    constexpr int kInputFieldClickInset = 8;

    std::optional<base::Value> result = ExecuteJavaScript(
        frame_for_test_.ptr().get(),
        base::StringPrintf("getPointInsideText('%.*s')",
                           base::saturated_cast<int>(id.length()), id.data()));
    if (!result || !result->is_dict()) {
      ADD_FAILURE() << "!result";
      return {};
    }

    // Note that coordinates are floating point and must be retrieved as such
    // from the Value, but we can cast them to integers and disregard the
    // fractional value with no major consequences.
    return gfx::Point(
        *result->GetDict().FindDouble("x") + kInputFieldClickInset,
        *result->GetDict().FindDouble("y") + kInputFieldClickInset);
  }

 protected:
  FrameForTest frame_for_test_;
  ScenicTestHelper scenic_test_helper_;
  base::test::ScopedFeatureList scoped_feature_list_;

  std::optional<EnsureConnectedChecker<fuchsia_ui_input3::Keyboard>>
      keyboard_input_checker_;

  // Fake virtual keyboard services for the InputMethod to use.
  std::optional<base::TestComponentContextForProcess> component_context_;
  std::optional<MockVirtualKeyboardControllerCreator> controller_creator_;
  std::unique_ptr<MockVirtualKeyboardController> controller_;

  content::WebContents* web_contents_ = nullptr;
};

// Verifies that RequestShow() is not called redundantly if the virtual
// keyboard is reported as visible.
IN_PROC_BROWSER_TEST_F(VirtualKeyboardTest, ShowAndHideWithVisibility) {
  testing::InSequence s;

  // Alphanumeric field click.
  base::RunLoop on_show_run_loop;
  EXPECT_CALL(*controller_, RequestShow(testing::_))
      .WillOnce(testing::InvokeWithoutArgs(
          [&on_show_run_loop]() { on_show_run_loop.Quit(); }))
      .RetiresOnSaturation();

  // Numeric field click.
  base::RunLoop click_numeric_run_loop;
  EXPECT_CALL(*controller_, RequestHide(testing::_)).RetiresOnSaturation();
  EXPECT_CALL(
      *controller_,
      SetTextType(testing::Eq(MockVirtualKeyboardController::SetTextTypeRequest{
                      {.text_type = virtualkeyboard::TextType::kNumeric}}),
                  testing::_))
      .RetiresOnSaturation();
  EXPECT_CALL(*controller_, RequestShow(testing::_))
      .WillOnce(testing::InvokeWithoutArgs(
          [&click_numeric_run_loop]() { click_numeric_run_loop.Quit(); }))
      .RetiresOnSaturation();

  // Input blur click.
  base::RunLoop on_hide_run_loop;
  EXPECT_CALL(*controller_, RequestHide(testing::_))
      .WillOnce(testing::InvokeWithoutArgs(
          [&on_hide_run_loop]() { on_hide_run_loop.Quit(); }))
      .RetiresOnSaturation();

  // In some cases, Blink may signal an
  // InputMethodClient::OnTextInputTypeChanged event, which will cause
  // an extra call to VirtualKeyboardController:RequestHide. This is harmless
  // in practice due to RequestHide()'s idempotence, however we still need to
  // anticipate that behavior in the controller mocks.
  EXPECT_CALL(*controller_, RequestHide(testing::_)).Times(testing::AtMost(1));

  // Give focus to an alphanumeric input field, which will result in
  // RequestShow() being called.
  content::SimulateTapAt(web_contents_,
                         GetCoordinatesOfInputField(kInputFieldText));
  on_show_run_loop.Run();
  EXPECT_EQ(controller_->text_type(), virtualkeyboard::TextType::kAlphanumeric);

  // Indicate that the virtual keyboard is now visible.
  controller_->AwaitWatchAndRespondWith(true);
  base::RunLoop().RunUntilIdle();

  // Tap on another text field. RequestShow should not be called a second time
  // since the keyboard is already onscreen.
  content::SimulateTapAt(web_contents_,
                         GetCoordinatesOfInputField(kInputFieldModeNumeric));
  click_numeric_run_loop.Run();

  // Trigger input blur by clicking outside any input element.
  content::SimulateTapAt(web_contents_, kNoTarget);
  on_hide_run_loop.Run();
}

// Gives focus to a sequence of HTML <input> nodes with different InputModes,
// and verifies that the InputMode's FIDL equivalent is sent via SetTextType().
IN_PROC_BROWSER_TEST_F(VirtualKeyboardTest, InputModeMappings) {
  // Note that the service will elide type updates if there is no change,
  // so the array is ordered to produce an update on each entry.
  const std::vector<std::pair<std::string_view, virtualkeyboard::TextType>>
      kInputTypeMappings = {
          {kInputFieldModeTel, virtualkeyboard::TextType::kPhone},
          {kInputFieldModeSearch, virtualkeyboard::TextType::kAlphanumeric},
          {kInputFieldModeNumeric, virtualkeyboard::TextType::kNumeric},
          {kInputFieldModeUrl, virtualkeyboard::TextType::kAlphanumeric},
          {kInputFieldModeDecimal, virtualkeyboard::TextType::kNumeric},
          {kInputFieldModeEmail, virtualkeyboard::TextType::kAlphanumeric},
          {kInputFieldTypeTel, virtualkeyboard::TextType::kPhone},
          {kInputFieldTypeNumber, virtualkeyboard::TextType::kNumeric},
          {kInputFieldTypePassword, virtualkeyboard::TextType::kAlphanumeric},
      };

  // GMock expectations must be set upfront, hence the redundant for-each loop.
  testing::InSequence s;
  virtualkeyboard::TextType previous_text_type =
      virtualkeyboard::TextType::kAlphanumeric;
  std::vector<base::RunLoop> set_type_loops(std::size(kInputTypeMappings));
  for (size_t i = 0; i < std::size(kInputTypeMappings); ++i) {
    const auto& field_type_pair = kInputTypeMappings[i];
    EXPECT_NE(field_type_pair.second, previous_text_type);

    EXPECT_CALL(
        *controller_,
        SetTextType(
            testing::Eq(MockVirtualKeyboardController::SetTextTypeRequest{
                {.text_type = field_type_pair.second}}),
            testing::_))
        .WillOnce(testing::InvokeWithoutArgs(
            [run_loop = &set_type_loops[i]]() mutable { run_loop->Quit(); }))
        .RetiresOnSaturation();
    previous_text_type = field_type_pair.second;
  }

  controller_->AwaitWatchAndRespondWith(false);

  for (size_t i = 0; i < std::size(kInputTypeMappings); ++i) {
    content::SimulateTapAt(
        web_contents_, GetCoordinatesOfInputField(kInputTypeMappings[i].first));

    // Spin the runloop until we've received the type update.
    set_type_loops[i].Run();
  }
}

IN_PROC_BROWSER_TEST_F(VirtualKeyboardTest, Disconnection) {
  testing::InSequence s;
  base::RunLoop on_show_run_loop;
  EXPECT_CALL(*controller_, RequestShow(testing::_))
      .WillOnce([&on_show_run_loop](
                    MockVirtualKeyboardController::RequestShowCompleter::Sync&
                        completer) { on_show_run_loop.Quit(); });

  // Tapping inside the text field should show the IME and signal RequestShow.
  content::SimulateTapAt(web_contents_,
                         GetCoordinatesOfInputField(kInputFieldText));
  on_show_run_loop.Run();

  controller_->AwaitWatchAndRespondWith(true);
  base::RunLoop().RunUntilIdle();

  // Disconnect the FIDL service.
  controller_.reset();
  base::RunLoop().RunUntilIdle();

  // Focus on another text field, then defocus. Nothing should crash.
  content::SimulateTapAt(web_contents_,
                         GetCoordinatesOfInputField(kInputFieldModeNumeric));
  content::SimulateTapAt(web_contents_, kNoTarget);
}

}  // namespace