| // 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 "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(base::StringPiece id) { |
| // Distance to click from the top/left extents of an input field. |
| constexpr int kInputFieldClickInset = 8; |
| |
| absl::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_; |
| |
| absl::optional<EnsureConnectedChecker<fuchsia_ui_input3::Keyboard>> |
| keyboard_input_checker_; |
| |
| // Fake virtual keyboard services for the InputMethod to use. |
| absl::optional<base::TestComponentContextForProcess> component_context_; |
| absl::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<base::StringPiece, 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 |