Flutter Engine
 
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 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
140class 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
194TEST_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
219TEST_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.
240TEST_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.
268TEST_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
325TEST_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.
383TEST_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
430TEST_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);
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
462TEST_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);
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
495TEST_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);
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
554TEST_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) {
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);
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
614TEST_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
687TEST_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
724TEST_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);
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
static NSString *const kChannelName
FlutterWindowsView(FlutterViewId view_id, FlutterWindowsEngine *engine, std::unique_ptr< WindowBindingHandler > window_binding, 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()
GLFWwindow * window
Definition main.cc:60
FlutterEngine engine
Definition main.cc:84
FlView * view
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
const gchar * channel
const gchar FlBinaryMessengerMessageHandler handler
G_BEGIN_DECLS GBytes * message
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[]
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