1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
fuchsia_web / webengine / browser / virtual_keyboard_browsertest.cc [blame]
// 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/memory/raw_ptr.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_;
raw_ptr<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