Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
Loading...
Searching...
No Matches
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.
5
6#include <rapidjson/document.h>
7#include <windows.h>
8#include <memory>
9
10#include "flutter/fml/macros.h"
19#include "gmock/gmock.h"
20#include "gtest/gtest.h"
21
22namespace flutter {
23
25 public:
26 explicit TextInputPluginModifier(TextInputPlugin* text_input_plugin)
27 : text_input_plugin(text_input_plugin) {}
28
30 text_input_plugin->view_id_ = view_id;
31 }
32
33 private:
34 TextInputPlugin* text_input_plugin;
35
37};
38
39namespace testing {
40
41namespace {
42using ::testing::Return;
43
44static constexpr char kScanCodeKey[] = "scanCode";
45static constexpr int kHandledScanCode = 20;
46static constexpr int kUnhandledScanCode = 21;
47static constexpr char kTextPlainFormat[] = "text/plain";
48static constexpr int kDefaultClientId = 42;
49// Should be identical to constants in text_input_plugin.cc.
50static constexpr char kChannelName[] = "flutter/textinput";
51static constexpr char kEnableDeltaModel[] = "enableDeltaModel";
52static constexpr char kViewId[] = "viewId";
53static constexpr char kSetClientMethod[] = "TextInput.setClient";
54static constexpr char kAffinityDownstream[] = "TextAffinity.downstream";
55static constexpr char kTextKey[] = "text";
56static constexpr char kSelectionBaseKey[] = "selectionBase";
57static constexpr char kSelectionExtentKey[] = "selectionExtent";
58static constexpr char kSelectionAffinityKey[] = "selectionAffinity";
59static constexpr char kSelectionIsDirectionalKey[] = "selectionIsDirectional";
60static constexpr char kComposingBaseKey[] = "composingBase";
61static constexpr char kComposingExtentKey[] = "composingExtent";
62static constexpr char kUpdateEditingStateMethod[] =
63 "TextInputClient.updateEditingState";
64
65static 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
73static 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
92static 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
124class MockFlutterWindowsView : public FlutterWindowsView {
125 public:
126 MockFlutterWindowsView(FlutterWindowsEngine* engine,
127 std::unique_ptr<WindowBindingHandler> window)
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
144class 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
198TEST_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
223TEST_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.
244TEST_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.
272TEST_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
329TEST_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.
387TEST_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
434TEST_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);
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
466TEST_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);
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
499TEST_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);
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
558TEST_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) {
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);
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
618TEST_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
691TEST_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
728TEST_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);
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
static NSString *const kChannelName
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)
static const JsonMessageCodec & GetInstance()
static const JsonMethodCodec & GetInstance()
std::unique_ptr< std::vector< uint8_t > > EncodeMessage(const T &message) const
std::unique_ptr< MethodCall< T > > DecodeMethodCall(const uint8_t *message, size_t message_size) const
std::unique_ptr< std::vector< uint8_t > > EncodeMethodCall(const MethodCall< T > &method_call) const
TextInputPluginModifier(TextInputPlugin *text_input_plugin)
MOCK_METHOD(void, NotifyWinEventWrapper,(ui::AXPlatformNodeWin *, ax::mojom::Event),(override))
MockFlutterWindowsView(FlutterWindowsEngine *engine, std::unique_ptr< WindowBindingHandler > wbh)
WindowsTestContext & GetContext()
std::string type_name
GLFWwindow * window
Definition main.cc:60
FlutterEngine engine
Definition main.cc:84
FlView * view
const char * message
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
const gchar * channel
const gchar FlBinaryMessengerMessageHandler handler
G_BEGIN_DECLS FlutterViewId view_id
#define FML_DISALLOW_COPY_AND_ASSIGN(TypeName)
Definition macros.h:27
static constexpr char kUpdateEditingStateMethod[]
static constexpr char kAffinityDownstream[]
std::shared_ptr< ImpellerAllocator > allocator
static NSString *const kEnableDeltaModel
static NSString *const kSetClientMethod
static constexpr char kScanCodeKey[]
size_t length
std::u16string text
constexpr char kTextPlainFormat[]
Clipboard plain text format.
static NSString *const kViewId
static NSString *const kTextKey
static NSString *const kSelectionBaseKey
static NSString *const kSelectionAffinityKey
static NSString *const kComposingExtentKey
static NSString *const kSelectionExtentKey
static NSString *const kComposingBaseKey
static NSString *const kSelectionIsDirectionalKey
TEST_F(DisplayListTest, Defaults)
constexpr int64_t kImplicitViewId
it will be possible to load the file into Perfetto s trace viewer use test Running tests that layout and measure text will not yield consistent results across various platforms Enabling this option will make font resolution default to the Ahem test font on all disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font Indicates whether the embedding started a prefetch of the default font manager before creating the engine run In non interactive keep the shell running after the Dart script has completed enable serial On low power devices with low core running concurrent GC tasks on threads can cause them to contend with the UI thread which could potentially lead to jank This option turns off all concurrent GC activities domain network JSON encoded network policy per domain This overrides the DisallowInsecureConnections switch Embedder can specify whether to allow or disallow insecure connections at a domain level old gen heap size
std::function< void(const uint8_t *reply, size_t reply_size)> BinaryReply
int64_t FlutterViewId
Definition ref_ptr.h:261