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