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