| Flutter Windows Embedder |

text_input_plugin_unittest.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 #include "flutter/shell/platform/windows/text_input_plugin.h"
5
6 #include <rapidjson/document.h>
7 #include <windows.h>
8 #include <memory>
9
10 #include "flutter/fml/macros.h"
11 #include "flutter/shell/platform/common/json_message_codec.h"
12 #include "flutter/shell/platform/common/json_method_codec.h"
13 #include "flutter/shell/platform/windows/flutter_windows_view.h"
14 #include "flutter/shell/platform/windows/testing/engine_modifier.h"
15 #include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h"
16 #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h"
17 #include "flutter/shell/platform/windows/testing/test_binary_messenger.h"
18 #include "flutter/shell/platform/windows/testing/windows_test.h"
19 #include "gmock/gmock.h"
20 #include "gtest/gtest.h"
21
22 namespace flutter {
23
24 class TextInputPluginModifier {
25 public:
26 explicit TextInputPluginModifier(TextInputPlugin* text_input_plugin)
27 : text_input_plugin(text_input_plugin) {}
28
29 void SetViewId(FlutterViewId view_id) {
30 text_input_plugin->view_id_ = view_id;
31 }
32
33 private:
34 TextInputPlugin* text_input_plugin;
35
36 FML_DISALLOW_COPY_AND_ASSIGN(TextInputPluginModifier);
37 };
38
39 namespace testing {
40
41 namespace {
42 using ::testing::Return;
43
44 static constexpr char kScanCodeKey[] = "scanCode";
45 static constexpr int kHandledScanCode = 20;
46 static constexpr int kUnhandledScanCode = 21;
47 static constexpr char kTextPlainFormat[] = "text/plain";
48 static constexpr int kDefaultClientId = 42;
49 // Should be identical to constants in text_input_plugin.cc.
50 static constexpr char kChannelName[] = "flutter/textinput";
51 static constexpr char kEnableDeltaModel[] = "enableDeltaModel";
52 static constexpr char kViewId[] = "viewId";
53 static constexpr char kSetClientMethod[] = "TextInput.setClient";
54 static constexpr char kAffinityDownstream[] = "TextAffinity.downstream";
55 static constexpr char kTextKey[] = "text";
56 static constexpr char kSelectionBaseKey[] = "selectionBase";
57 static constexpr char kSelectionExtentKey[] = "selectionExtent";
58 static constexpr char kSelectionAffinityKey[] = "selectionAffinity";
59 static constexpr char kSelectionIsDirectionalKey[] = "selectionIsDirectional";
60 static constexpr char kComposingBaseKey[] = "composingBase";
61 static constexpr char kComposingExtentKey[] = "composingExtent";
62 static constexpr char kUpdateEditingStateMethod[] =
63 "TextInputClient.updateEditingState";
64
65 static std::unique_ptr<std::vector<uint8_t>> CreateResponse(bool handled) {
66 auto response_doc =
67 std::make_unique<rapidjson::Document>(rapidjson::kObjectType);
68 auto& allocator = response_doc->GetAllocator();
69 response_doc->AddMember("handled", handled, allocator);
70 return JsonMessageCodec::GetInstance().EncodeMessage(*response_doc);
71 }
72
73 static std::unique_ptr<rapidjson::Document> EncodedClientConfig(
74 std::string type_name,
75 std::string input_action) {
76 auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
77 auto& allocator = arguments->GetAllocator();
78 arguments->PushBack(kDefaultClientId, allocator);
79
80 rapidjson::Value config(rapidjson::kObjectType);
81 config.AddMember("inputAction", input_action, allocator);
82 config.AddMember(kEnableDeltaModel, false, allocator);
83 config.AddMember(kViewId, 456, allocator);
84 rapidjson::Value type_info(rapidjson::kObjectType);
85 type_info.AddMember("name", type_name, allocator);
86 config.AddMember("inputType", type_info, allocator);
87 arguments->PushBack(config, allocator);
88
89 return arguments;
90 }
91
92 static std::unique_ptr<rapidjson::Document> EncodedEditingState(
93 std::string text,
94 TextRange selection) {
95 auto model = std::make_unique<TextInputModel>();
96 model->SetText(text);
97 model->SetSelection(selection);
98
99 auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
100 auto& allocator = arguments->GetAllocator();
101 arguments->PushBack(kDefaultClientId, allocator);
102
103 rapidjson::Value editing_state(rapidjson::kObjectType);
104 editing_state.AddMember(kSelectionAffinityKey, kAffinityDownstream,
105 allocator);
106 editing_state.AddMember(kSelectionBaseKey, selection.base(), allocator);
107 editing_state.AddMember(kSelectionExtentKey, selection.extent(), allocator);
108 editing_state.AddMember(kSelectionIsDirectionalKey, false, allocator);
109
110 int composing_base =
111 model->composing() ? model->composing_range().base() : -1;
112 int composing_extent =
113 model->composing() ? model->composing_range().extent() : -1;
114 editing_state.AddMember(kComposingBaseKey, composing_base, allocator);
115 editing_state.AddMember(kComposingExtentKey, composing_extent, allocator);
116 editing_state.AddMember(kTextKey,
117 rapidjson::Value(model->GetText(), allocator).Move(),
118 allocator);
119 arguments->PushBack(editing_state, allocator);
120
121 return arguments;
122 }
123
124 class MockFlutterWindowsView : public FlutterWindowsView {
125 public:
126 MockFlutterWindowsView(FlutterWindowsEngine* engine,
127 std::unique_ptr<WindowBindingHandler> window)
128 : FlutterWindowsView(kImplicitViewId, engine, std::move(window)) {}
129 virtual ~MockFlutterWindowsView() = default;
130
131 MOCK_METHOD(void, OnCursorRectUpdated, (const Rect&), (override));
132 MOCK_METHOD(void, OnResetImeComposing, (), (override));
133
134 private:
135 FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindowsView);
136 };
137
138 } // namespace
139
140 class TextInputPluginTest : public WindowsTest {
141 public:
142 TextInputPluginTest() = default;
143 virtual ~TextInputPluginTest() = default;
144
145 protected:
146 FlutterWindowsEngine* engine() { return engine_.get(); }
147 MockFlutterWindowsView* view() { return view_.get(); }
148 MockWindowBindingHandler* window() { return window_; }
149
150 void UseHeadlessEngine() {
151 FlutterWindowsEngineBuilder builder{GetContext()};
152
153 engine_ = builder.Build();
154 }
155
156 void UseEngineWithView() {
157 FlutterWindowsEngineBuilder builder{GetContext()};
158
159 auto window = std::make_unique<MockWindowBindingHandler>();
160
161 window_ = window.get();
162 EXPECT_CALL(*window_, SetView).Times(1);
163 EXPECT_CALL(*window, GetWindowHandle).WillRepeatedly(Return(nullptr));
164
165 engine_ = builder.Build();
166 view_ = std::make_unique<MockFlutterWindowsView>(engine_.get(),
167 std::move(window));
168
169 EngineModifier modifier{engine_.get()};
170 modifier.SetViewById(view_.get(), 456);
171 }
172
173 std::unique_ptr<MockFlutterWindowsView> AddViewWithId(int view_id) {
174 EXPECT_NE(engine_, nullptr);
175 auto window = std::make_unique<MockWindowBindingHandler>();
176 EXPECT_CALL(*window, SetView).Times(1);
177 EXPECT_CALL(*window, GetWindowHandle).WillRepeatedly(Return(nullptr));
178 auto view = std::make_unique<MockFlutterWindowsView>(engine_.get(),
179 std::move(window));
180
181 EngineModifier modifier{engine_.get()};
182 modifier.SetViewById(view_.get(), view_id);
183 return view;
184 }
185
186 private:
187 std::unique_ptr<FlutterWindowsEngine> engine_;
188 std::unique_ptr<MockFlutterWindowsView> view_;
189 MockWindowBindingHandler* window_;
190
191 FML_DISALLOW_COPY_AND_ASSIGN(TextInputPluginTest);
192 };
193
194 TEST_F(TextInputPluginTest, TextMethodsWorksWithEmptyModel) {
195 UseEngineWithView();
196
197 auto handled_message = CreateResponse(true);
198 auto unhandled_message = CreateResponse(false);
199 int received_scancode = 0;
200
201 TestBinaryMessenger messenger(
202 [&received_scancode, &handled_message, &unhandled_message](
203 const std::string& channel, const uint8_t* message,
204 size_t message_size, BinaryReply reply) {});
205
206 int redispatch_scancode = 0;
207 TextInputPlugin handler(&messenger, engine());
208
209 handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false);
210 handler.ComposeBeginHook();
211 std::u16string text;
212 text.push_back('\n');
213 handler.ComposeChangeHook(text, 1);
214 handler.ComposeEndHook();
215
216 // Passes if it did not crash
217 }
218
219 TEST_F(TextInputPluginTest, ClearClientResetsComposing) {
220 UseEngineWithView();
221
222 TestBinaryMessenger messenger([](const std::string& channel,
223 const uint8_t* message, size_t message_size,
224 BinaryReply reply) {});
225 BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {};
226
227 TextInputPlugin handler(&messenger, engine());
228 TextInputPluginModifier modifier(&handler);
229 modifier.SetViewId(456);
230
231 EXPECT_CALL(*view(), OnResetImeComposing());
232
233 auto& codec = JsonMethodCodec::GetInstance();
234 auto message = codec.EncodeMethodCall({"TextInput.clearClient", nullptr});
235 messenger.SimulateEngineMessage(kChannelName, message->data(),
236 message->size(), reply_handler);
237 }
238
239 // Verify that clear client fails if in headless mode.
240 TEST_F(TextInputPluginTest, ClearClientRequiresView) {
241 UseHeadlessEngine();
242
243 TestBinaryMessenger messenger([](const std::string& channel,
244 const uint8_t* message, size_t message_size,
245 BinaryReply reply) {});
246
247 std::string reply;
248 BinaryReply reply_handler = [&reply](const uint8_t* reply_bytes,
249 size_t reply_size) {
250 reply = std::string(reinterpret_cast<const char*>(reply_bytes), reply_size);
251 };
252
253 TextInputPlugin handler(&messenger, engine());
254
255 auto& codec = JsonMethodCodec::GetInstance();
256 auto message = codec.EncodeMethodCall({"TextInput.clearClient", nullptr});
257 messenger.SimulateEngineMessage(kChannelName, message->data(),
258 message->size(), reply_handler);
259
260 EXPECT_EQ(
261 reply,
262 "[\"Internal Consistency Error\",\"Text input is not available because "
263 "view with view_id=0 cannot be found\",null]");
264 }
265
266 // Verify that the embedder sends state update messages to the framework during
267 // IME composing.
268 TEST_F(TextInputPluginTest, VerifyComposingSendStateUpdate) {
269 UseEngineWithView();
270
271 bool sent_message = false;
272 TestBinaryMessenger messenger(
273 [&sent_message](const std::string& channel, const uint8_t* message,
274 size_t message_size,
275 BinaryReply reply) { sent_message = true; });
276 BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {};
277
278 TextInputPlugin handler(&messenger, engine());
279
280 auto& codec = JsonMethodCodec::GetInstance();
281
282 // Call TextInput.setClient to initialize the TextInputModel.
283 auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
284 auto& allocator = arguments->GetAllocator();
285 arguments->PushBack(kDefaultClientId, allocator);
286 rapidjson::Value config(rapidjson::kObjectType);
287 config.AddMember("inputAction", "done", allocator);
288 config.AddMember("inputType", "text", allocator);
289 config.AddMember(kEnableDeltaModel, false, allocator);
290 config.AddMember(kViewId, 456, allocator);
291 arguments->PushBack(config, allocator);
292 auto message =
293 codec.EncodeMethodCall({"TextInput.setClient", std::move(arguments)});
294 messenger.SimulateEngineMessage("flutter/textinput", message->data(),
295 message->size(), reply_handler);
296
297 // ComposeBeginHook should send state update.
298 sent_message = false;
299 handler.ComposeBeginHook();
300 EXPECT_TRUE(sent_message);
301
302 // ComposeChangeHook should send state update.
303 sent_message = false;
304 handler.ComposeChangeHook(u"4", 1);
305 EXPECT_TRUE(sent_message);
306
307 // ComposeCommitHook should NOT send state update.
308 //
309 // Commit messages are always immediately followed by a change message or an
310 // end message, both of which will send an update. Sending intermediate state
311 // with a collapsed composing region will trigger the framework to assume
312 // composing has ended, which is not the case until a WM_IME_ENDCOMPOSING
313 // event is received in the main event loop, which will trigger a call to
314 // ComposeEndHook.
315 sent_message = false;
316 handler.ComposeCommitHook();
317 EXPECT_FALSE(sent_message);
318
319 // ComposeEndHook should send state update.
320 sent_message = false;
321 handler.ComposeEndHook();
322 EXPECT_TRUE(sent_message);
323 }
324
325 TEST_F(TextInputPluginTest, VerifyInputActionNewlineInsertNewLine) {
326 UseEngineWithView();
327
328 // Store messages as std::string for convenience.
329 std::vector<std::string> messages;
330
331 TestBinaryMessenger messenger(
332 [&messages](const std::string& channel, const uint8_t* message,
333 size_t message_size, BinaryReply reply) {
334 std::string last_message(reinterpret_cast<const char*>(message),
335 message_size);
336 messages.push_back(last_message);
337 });
338 BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {};
339
340 TextInputPlugin handler(&messenger, engine());
341
342 auto& codec = JsonMethodCodec::GetInstance();
343
344 // Call TextInput.setClient to initialize the TextInputModel.
345 auto set_client_arguments =
346 EncodedClientConfig("TextInputType.multiline", "TextInputAction.newline");
347 auto message = codec.EncodeMethodCall(
348 {"TextInput.setClient", std::move(set_client_arguments)});
349 messenger.SimulateEngineMessage("flutter/textinput", message->data(),
350 message->size(), reply_handler);
351
352 // Simulate a key down event for '\n'.
353 handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false);
354
355 // Two messages are expected, the first is TextInput.updateEditingState and
356 // the second is TextInputClient.performAction.
357 EXPECT_EQ(messages.size(), 2);
358
359 // Editing state should have been updated.
360 auto encoded_arguments = EncodedEditingState("\n", TextRange(1));
361 auto update_state_message = codec.EncodeMethodCall(
362 {kUpdateEditingStateMethod, std::move(encoded_arguments)});
363
364 EXPECT_TRUE(std::equal(update_state_message->begin(),
365 update_state_message->end(),
366 messages.front().begin()));
367
368 // TextInputClient.performAction should have been called.
369 auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
370 auto& allocator = arguments->GetAllocator();
371 arguments->PushBack(kDefaultClientId, allocator);
372 arguments->PushBack(
373 rapidjson::Value("TextInputAction.newline", allocator).Move(), allocator);
374 auto invoke_action_message = codec.EncodeMethodCall(
375 {"TextInputClient.performAction", std::move(arguments)});
376
377 EXPECT_TRUE(std::equal(invoke_action_message->begin(),
378 invoke_action_message->end(),
379 messages.back().begin()));
380 }
381
382 // Regression test for https://github.com/flutter/flutter/issues/125879.
383 TEST_F(TextInputPluginTest, VerifyInputActionSendDoesNotInsertNewLine) {
384 UseEngineWithView();
385
386 std::vector<std::vector<uint8_t>> messages;
387
388 TestBinaryMessenger messenger(
389 [&messages](const std::string& channel, const uint8_t* message,
390 size_t message_size, BinaryReply reply) {
391 int length = static_cast<int>(message_size);
392 std::vector<uint8_t> last_message(length);
393 memcpy(&last_message[0], &message[0], length * sizeof(uint8_t));
394 messages.push_back(last_message);
395 });
396 BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {};
397
398 TextInputPlugin handler(&messenger, engine());
399
400 auto& codec = JsonMethodCodec::GetInstance();
401
402 // Call TextInput.setClient to initialize the TextInputModel.
403 auto set_client_arguments =
404 EncodedClientConfig("TextInputType.multiline", "TextInputAction.send");
405 auto message = codec.EncodeMethodCall(
406 {"TextInput.setClient", std::move(set_client_arguments)});
407 messenger.SimulateEngineMessage("flutter/textinput", message->data(),
408 message->size(), reply_handler);
409
410 // Simulate a key down event for '\n'.
411 handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false);
412
413 // Only a call to TextInputClient.performAction is expected.
414 EXPECT_EQ(messages.size(), 1);
415
416 // TextInputClient.performAction should have been called.
417 auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
418 auto& allocator = arguments->GetAllocator();
419 arguments->PushBack(kDefaultClientId, allocator);
420 arguments->PushBack(
421 rapidjson::Value("TextInputAction.send", allocator).Move(), allocator);
422 auto invoke_action_message = codec.EncodeMethodCall(
423 {"TextInputClient.performAction", std::move(arguments)});
424
425 EXPECT_TRUE(std::equal(invoke_action_message->begin(),
426 invoke_action_message->end(),
427 messages.front().begin()));
428 }
429
430 TEST_F(TextInputPluginTest, SetClientRequiresViewId) {
431 UseEngineWithView();
432
433 TestBinaryMessenger messenger([](const std::string& channel,
434 const uint8_t* message, size_t message_size,
435 BinaryReply reply) {});
436
437 TextInputPlugin handler(&messenger, engine());
438
439 auto args = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
440 auto& allocator = args->GetAllocator();
441 args->PushBack(123, allocator); // client_id
442
443 rapidjson::Value client_config(rapidjson::kObjectType);
444
445 args->PushBack(client_config, allocator);
446 auto encoded = JsonMethodCodec::GetInstance().EncodeMethodCall(
447 MethodCall<rapidjson::Document>(kSetClientMethod, std::move(args)));
448
449 std::string reply;
450 BinaryReply reply_handler = [&reply](const uint8_t* reply_bytes,
451 size_t reply_size) {
452 reply = std::string(reinterpret_cast<const char*>(reply_bytes), reply_size);
453 };
454
455 EXPECT_TRUE(messenger.SimulateEngineMessage(kChannelName, encoded->data(),
456 encoded->size(), reply_handler));
457 EXPECT_EQ(
458 reply,
459 "[\"Bad Arguments\",\"Could not set client, view ID is null.\",null]");
460 }
461
462 TEST_F(TextInputPluginTest, SetClientRequiresViewIdToBeInteger) {
463 UseEngineWithView();
464
465 TestBinaryMessenger messenger([](const std::string& channel,
466 const uint8_t* message, size_t message_size,
467 BinaryReply reply) {});
468
469 TextInputPlugin handler(&messenger, engine());
470
471 auto args = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
472 auto& allocator = args->GetAllocator();
473 args->PushBack(123, allocator); // client_id
474
475 rapidjson::Value client_config(rapidjson::kObjectType);
476 client_config.AddMember(kViewId, "Not an integer", allocator); // view_id
477
478 args->PushBack(client_config, allocator);
479 auto encoded = JsonMethodCodec::GetInstance().EncodeMethodCall(
480 MethodCall<rapidjson::Document>(kSetClientMethod, std::move(args)));
481
482 std::string reply;
483 BinaryReply reply_handler = [&reply](const uint8_t* reply_bytes,
484 size_t reply_size) {
485 reply = std::string(reinterpret_cast<const char*>(reply_bytes), reply_size);
486 };
487
488 EXPECT_TRUE(messenger.SimulateEngineMessage(kChannelName, encoded->data(),
489 encoded->size(), reply_handler));
490 EXPECT_EQ(
491 reply,
492 "[\"Bad Arguments\",\"Could not set client, view ID is null.\",null]");
493 }
494
495 TEST_F(TextInputPluginTest, TextEditingWorksWithDeltaModel) {
496 UseEngineWithView();
497
498 auto handled_message = CreateResponse(true);
499 auto unhandled_message = CreateResponse(false);
500 int received_scancode = 0;
501
502 TestBinaryMessenger messenger(
503 [&received_scancode, &handled_message, &unhandled_message](
504 const std::string& channel, const uint8_t* message,
505 size_t message_size, BinaryReply reply) {});
506
507 int redispatch_scancode = 0;
508 TextInputPlugin handler(&messenger, engine());
509
510 auto args = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
511 auto& allocator = args->GetAllocator();
512 args->PushBack(123, allocator); // client_id
513
514 rapidjson::Value client_config(rapidjson::kObjectType);
515 client_config.AddMember(kEnableDeltaModel, true, allocator);
516 client_config.AddMember(kViewId, 456, allocator);
517
518 args->PushBack(client_config, allocator);
519 auto encoded = JsonMethodCodec::GetInstance().EncodeMethodCall(
520 MethodCall<rapidjson::Document>(kSetClientMethod, std::move(args)));
521
522 EXPECT_TRUE(messenger.SimulateEngineMessage(
523 kChannelName, encoded->data(), encoded->size(),
524 [](const uint8_t* reply, size_t reply_size) {}));
525
526 handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false);
527 handler.ComposeBeginHook();
528 std::u16string text;
529 text.push_back('\n');
530 handler.ComposeChangeHook(text, 1);
531 handler.ComposeEndHook();
532
533 handler.KeyboardHook(0x4E, 100, WM_KEYDOWN, 'n', false, false);
534 handler.ComposeBeginHook();
535 std::u16string textN;
536 text.push_back('n');
537 handler.ComposeChangeHook(textN, 1);
538 handler.KeyboardHook(0x49, 100, WM_KEYDOWN, 'i', false, false);
539 std::u16string textNi;
540 text.push_back('n');
541 text.push_back('i');
542 handler.ComposeChangeHook(textNi, 2);
543 handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false);
544 std::u16string textChineseCharacter;
545 text.push_back(u'\u4F60');
546 handler.ComposeChangeHook(textChineseCharacter, 1);
547 handler.ComposeCommitHook();
548 handler.ComposeEndHook();
549
550 // Passes if it did not crash
551 }
552
553 // Regression test for https://github.com/flutter/flutter/issues/123749
554 TEST_F(TextInputPluginTest, CompositionCursorPos) {
555 UseEngineWithView();
556
557 int selection_base = -1;
558 TestBinaryMessenger messenger([&](const std::string& channel,
559 const uint8_t* message, size_t size,
560 BinaryReply reply) {
561 auto method = JsonMethodCodec::GetInstance().DecodeMethodCall(
562 std::vector<uint8_t>(message, message + size));
563 if (method->method_name() == kUpdateEditingStateMethod) {
564 const auto& args = *method->arguments();
565 const auto& editing_state = args[1];
566 auto base = editing_state.FindMember(kSelectionBaseKey);
567 auto extent = editing_state.FindMember(kSelectionExtentKey);
568 ASSERT_NE(base, editing_state.MemberEnd());
569 ASSERT_TRUE(base->value.IsInt());
570 ASSERT_NE(extent, editing_state.MemberEnd());
571 ASSERT_TRUE(extent->value.IsInt());
572 selection_base = base->value.GetInt();
573 EXPECT_EQ(extent->value.GetInt(), selection_base);
574 }
575 });
576
577 TextInputPlugin plugin(&messenger, engine());
578
579 auto args = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
580 auto& allocator = args->GetAllocator();
581 args->PushBack(123, allocator); // client_id
582 rapidjson::Value client_config(rapidjson::kObjectType);
583 client_config.AddMember(kViewId, 456, allocator);
584 args->PushBack(client_config, allocator);
585 auto encoded = JsonMethodCodec::GetInstance().EncodeMethodCall(
586 MethodCall<rapidjson::Document>(kSetClientMethod, std::move(args)));
587 EXPECT_TRUE(messenger.SimulateEngineMessage(
588 kChannelName, encoded->data(), encoded->size(),
589 [](const uint8_t* reply, size_t reply_size) {}));
590
591 plugin.ComposeBeginHook();
592 EXPECT_EQ(selection_base, 0);
593 plugin.ComposeChangeHook(u"abc", 3);
594 EXPECT_EQ(selection_base, 3);
595
596 plugin.ComposeCommitHook();
597 plugin.ComposeEndHook();
598 EXPECT_EQ(selection_base, 3);
599
600 plugin.ComposeBeginHook();
601 plugin.ComposeChangeHook(u"1", 1);
602 EXPECT_EQ(selection_base, 4);
603
604 plugin.ComposeChangeHook(u"12", 2);
605 EXPECT_EQ(selection_base, 5);
606
607 plugin.ComposeChangeHook(u"12", 1);
608 EXPECT_EQ(selection_base, 4);
609
610 plugin.ComposeChangeHook(u"12", 2);
611 EXPECT_EQ(selection_base, 5);
612 }
613
614 TEST_F(TextInputPluginTest, TransformCursorRect) {
615 UseEngineWithView();
616
617 // A position of `EditableText`.
618 double view_x = 100;
619 double view_y = 200;
620
621 // A position and size of marked text, in `EditableText` local coordinates.
622 double ime_x = 3;
623 double ime_y = 4;
624 double ime_width = 50;
625 double ime_height = 60;
626
627 // Transformation matrix.
628 std::array<std::array<double, 4>, 4> editabletext_transform = {
629 1.0, 0.0, 0.0, view_x, //
630 0.0, 1.0, 0.0, view_y, //
631 0.0, 0.0, 0.0, 0.0, //
632 0.0, 0.0, 0.0, 1.0};
633
634 TestBinaryMessenger messenger([](const std::string& channel,
635 const uint8_t* message, size_t message_size,
636 BinaryReply reply) {});
637 BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {};
638
639 TextInputPlugin handler(&messenger, engine());
640 TextInputPluginModifier modifier(&handler);
641 modifier.SetViewId(456);
642
643 auto& codec = JsonMethodCodec::GetInstance();
644
645 EXPECT_CALL(*view(), OnCursorRectUpdated(Rect{{view_x, view_y}, {0, 0}}));
646
647 {
648 auto arguments =
649 std::make_unique<rapidjson::Document>(rapidjson::kObjectType);
650 auto& allocator = arguments->GetAllocator();
651
652 rapidjson::Value transoform(rapidjson::kArrayType);
653 for (int i = 0; i < 4 * 4; i++) {
654 // Pack 2-dimensional array by column-major order.
655 transoform.PushBack(editabletext_transform[i % 4][i / 4], allocator);
656 }
657
658 arguments->AddMember("transform", transoform, allocator);
659
660 auto message = codec.EncodeMethodCall(
661 {"TextInput.setEditableSizeAndTransform", std::move(arguments)});
662 messenger.SimulateEngineMessage(kChannelName, message->data(),
663 message->size(), reply_handler);
664 }
665
666 EXPECT_CALL(*view(),
667 OnCursorRectUpdated(Rect{{view_x + ime_x, view_y + ime_y},
668 {ime_width, ime_height}}));
669
670 {
671 auto arguments =
672 std::make_unique<rapidjson::Document>(rapidjson::kObjectType);
673 auto& allocator = arguments->GetAllocator();
674
675 arguments->AddMember("x", ime_x, allocator);
676 arguments->AddMember("y", ime_y, allocator);
677 arguments->AddMember("width", ime_width, allocator);
678 arguments->AddMember("height", ime_height, allocator);
679
680 auto message = codec.EncodeMethodCall(
681 {"TextInput.setMarkedTextRect", std::move(arguments)});
682 messenger.SimulateEngineMessage(kChannelName, message->data(),
683 message->size(), reply_handler);
684 }
685 }
686
687 TEST_F(TextInputPluginTest, SetMarkedTextRectRequiresView) {
688 UseHeadlessEngine();
689
690 TestBinaryMessenger messenger([](const std::string& channel,
691 const uint8_t* message, size_t message_size,
692 BinaryReply reply) {});
693
694 std::string reply;
695 BinaryReply reply_handler = [&reply](const uint8_t* reply_bytes,
696 size_t reply_size) {
697 reply = std::string(reinterpret_cast<const char*>(reply_bytes), reply_size);
698 };
699
700 TextInputPlugin handler(&messenger, engine());
701
702 auto& codec = JsonMethodCodec::GetInstance();
703
704 auto arguments =
705 std::make_unique<rapidjson::Document>(rapidjson::kObjectType);
706 auto& allocator = arguments->GetAllocator();
707
708 arguments->AddMember("x", 0, allocator);
709 arguments->AddMember("y", 0, allocator);
710 arguments->AddMember("width", 0, allocator);
711 arguments->AddMember("height", 0, allocator);
712
713 auto message = codec.EncodeMethodCall(
714 {"TextInput.setMarkedTextRect", std::move(arguments)});
715 messenger.SimulateEngineMessage(kChannelName, message->data(),
716 message->size(), reply_handler);
717
718 EXPECT_EQ(
719 reply,
720 "[\"Internal Consistency Error\",\"Text input is not available because "
721 "view with view_id=0 cannot be found\",null]");
722 }
723
724 TEST_F(TextInputPluginTest, SetAndUseMultipleClients) {
725 UseEngineWithView(); // Creates the default view
726 AddViewWithId(789); // Creates the next view
727
728 bool sent_message = false;
729 TestBinaryMessenger messenger(
730 [&sent_message](const std::string& channel, const uint8_t* message,
731 size_t message_size,
732 BinaryReply reply) { sent_message = true; });
733
734 TextInputPlugin handler(&messenger, engine());
735
736 auto const set_client_and_send_message = [&](int client_id, int view_id) {
737 auto args = std::make_unique<rapidjson::Document>(rapidjson::kArrayType);
738 auto& allocator = args->GetAllocator();
739 args->PushBack(client_id, allocator); // client_id
740
741 rapidjson::Value client_config(rapidjson::kObjectType);
742 client_config.AddMember(kViewId, view_id, allocator); // view_id
743
744 args->PushBack(client_config, allocator);
745 auto encoded = JsonMethodCodec::GetInstance().EncodeMethodCall(
746 MethodCall<rapidjson::Document>(kSetClientMethod, std::move(args)));
747
748 std::string reply;
749 BinaryReply reply_handler = [&reply](const uint8_t* reply_bytes,
750 size_t reply_size) {
751 reply =
752 std::string(reinterpret_cast<const char*>(reply_bytes), reply_size);
753 };
754
755 EXPECT_TRUE(messenger.SimulateEngineMessage(
756 kChannelName, encoded->data(), encoded->size(), reply_handler));
757
758 sent_message = false;
759 handler.ComposeBeginHook();
760 EXPECT_TRUE(sent_message);
761 sent_message = false;
762 handler.ComposeChangeHook(u"4", 1);
763 EXPECT_TRUE(sent_message);
764 sent_message = false;
765 handler.ComposeCommitHook();
766 EXPECT_FALSE(sent_message);
767 sent_message = false;
768 handler.ComposeEndHook();
769 EXPECT_TRUE(sent_message);
770 };
771
772 set_client_and_send_message(123, 456); // Set and send for the first view
773 set_client_and_send_message(123, 789); // Set and send for the next view
774 }
775
776 } // namespace testing
777 } // namespace flutter
flutter::FlutterWindowsEngine
Definition: flutter_windows_engine.h:92
flutter::FlutterWindowsView::FlutterWindowsView
FlutterWindowsView(FlutterViewId view_id, FlutterWindowsEngine *engine, std::unique_ptr< WindowBindingHandler > window_binding, std::shared_ptr< WindowsProcTable > windows_proc_table=nullptr)
Definition: flutter_windows_view.cc:104
flutter::JsonMessageCodec::GetInstance
static const JsonMessageCodec & GetInstance()
Definition: json_message_codec.cc:17
flutter::JsonMethodCodec::GetInstance
static const JsonMethodCodec & GetInstance()
Definition: json_method_codec.cc:36
flutter::MessageCodec::EncodeMessage
std::unique_ptr< std::vector< uint8_t > > EncodeMessage(const T &message) const
Definition: message_codec.h:45
flutter::MethodCall
Definition: method_call.h:18
flutter::MethodCodec::DecodeMethodCall
std::unique_ptr< MethodCall< T > > DecodeMethodCall(const uint8_t *message, size_t message_size) const
Definition: method_codec.h:32
flutter::MethodCodec::EncodeMethodCall
std::unique_ptr< std::vector< uint8_t > > EncodeMethodCall(const MethodCall< T > &method_call) const
Definition: method_codec.h:48
flutter::TextInputPlugin
Definition: text_input_plugin.h:29
flutter::TextInputPlugin::ComposeCommitHook
virtual void ComposeCommitHook()
Definition: text_input_plugin.cc:141
flutter::TextInputPlugin::ComposeChangeHook
virtual void ComposeChangeHook(const std::u16string &text, int cursor_pos)
Definition: text_input_plugin.cc:193
flutter::TextInputPlugin::ComposeEndHook
virtual void ComposeEndHook()
Definition: text_input_plugin.cc:176
flutter::TextInputPlugin::KeyboardHook
virtual void KeyboardHook(int key, int scancode, int action, char32_t character, bool extended, bool was_down)
Definition: text_input_plugin.cc:86
flutter::TextInputPlugin::ComposeBeginHook
virtual void ComposeBeginHook()
Definition: text_input_plugin.cc:126
flutter::TextInputPluginModifier
Definition: text_input_plugin_unittest.cc:24
flutter::TextInputPluginModifier::SetViewId
void SetViewId(FlutterViewId view_id)
Definition: text_input_plugin_unittest.cc:29
flutter::TextInputPluginModifier::TextInputPluginModifier
TextInputPluginModifier(TextInputPlugin *text_input_plugin)
Definition: text_input_plugin_unittest.cc:26
flutter::TextRange
Definition: text_range.h:19
flutter::testing::MockFlutterWindowsView::MOCK_METHOD
MOCK_METHOD(void, NotifyWinEventWrapper,(ui::AXPlatformNodeWin *, ax::mojom::Event),(override))
flutter::testing::MockFlutterWindowsView::MockFlutterWindowsView
MockFlutterWindowsView(FlutterWindowsEngine *engine, std::unique_ptr< WindowBindingHandler > wbh)
Definition: flutter_windows_engine_unittests.cc:770
flutter::testing::TextInputPluginTest
Definition: text_input_plugin_unittest.cc:140
flutter::testing::TextInputPluginTest::UseEngineWithView
void UseEngineWithView()
Definition: text_input_plugin_unittest.cc:156
flutter::testing::TextInputPluginTest::TextInputPluginTest
TextInputPluginTest()=default
flutter::testing::TextInputPluginTest::UseHeadlessEngine
void UseHeadlessEngine()
Definition: text_input_plugin_unittest.cc:150
flutter::testing::TextInputPluginTest::window
MockWindowBindingHandler * window()
Definition: text_input_plugin_unittest.cc:148
flutter::testing::TextInputPluginTest::~TextInputPluginTest
virtual ~TextInputPluginTest()=default
flutter::testing::TextInputPluginTest::engine
FlutterWindowsEngine * engine()
Definition: text_input_plugin_unittest.cc:146
flutter::testing::TextInputPluginTest::view
MockFlutterWindowsView * view()
Definition: text_input_plugin_unittest.cc:147
flutter::testing::TextInputPluginTest::AddViewWithId
std::unique_ptr< MockFlutterWindowsView > AddViewWithId(int view_id)
Definition: text_input_plugin_unittest.cc:173
kChannelName
static constexpr char kChannelName[]
Definition: cursor_handler.cc:12
flutter_windows_view.h
json_message_codec.h
json_method_codec.h
text
std::u16string text
Definition: keyboard_unittests.cc:332
message
Win32Message message
Definition: keyboard_unittests.cc:137
flutter::testing::TEST_F
TEST_F(CompositorOpenGLTest, CreateBackingStore)
Definition: compositor_opengl_unittests.cc:139
flutter
Definition: accessibility_bridge_windows.cc:11
flutter::BinaryReply
std::function< void(const uint8_t *reply, size_t reply_size)> BinaryReply
Definition: binary_messenger.h:17
flutter::FlutterViewId
int64_t FlutterViewId
Definition: accessibility_plugin.h:15
flutter::kImplicitViewId
constexpr FlutterViewId kImplicitViewId
Definition: flutter_windows_engine.h:56
kTextPlainFormat
static constexpr char kTextPlainFormat[]
Definition: platform_handler.cc:38
kTextKey
static constexpr char kTextKey[]
Definition: platform_handler.cc:39
kEnableDeltaModel
static constexpr char kEnableDeltaModel[]
Definition: text_input_plugin.cc:39
kSelectionIsDirectionalKey
static constexpr char kSelectionIsDirectionalKey[]
Definition: text_input_plugin.cc:50
kSelectionExtentKey
static constexpr char kSelectionExtentKey[]
Definition: text_input_plugin.cc:49
kSetClientMethod
static constexpr char kSetClientMethod[]
Definition: text_input_plugin.cc:19
kUpdateEditingStateMethod
static constexpr char kUpdateEditingStateMethod[]
Definition: text_input_plugin.cc:28
kComposingExtentKey
static constexpr char kComposingExtentKey[]
Definition: text_input_plugin.cc:45
kComposingBaseKey
static constexpr char kComposingBaseKey[]
Definition: text_input_plugin.cc:44
kAffinityDownstream
static constexpr char kAffinityDownstream[]
Definition: text_input_plugin.cc:47
kSelectionAffinityKey
static constexpr char kSelectionAffinityKey[]
Definition: text_input_plugin.cc:46
kViewId
static constexpr char kViewId[]
Definition: text_input_plugin.cc:41
kSelectionBaseKey
static constexpr char kSelectionBaseKey[]
Definition: text_input_plugin.cc:48
text_input_plugin.h