Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
Loading...
Searching...
No Matches
fl_text_input_handler_test.cc
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <utility>
6
14
16#include "gmock/gmock.h"
17#include "gtest/gtest.h"
18
19static FlValue* build_map(std::map<const gchar*, FlValue*> args) {
21 for (auto it = args.begin(); it != args.end(); ++it) {
22 fl_value_set_string_take(value, it->first, it->second);
23 }
24 return value;
25}
26
27static FlValue* build_list(std::vector<FlValue*> args) {
29 for (auto it = args.begin(); it != args.end(); ++it) {
31 }
32 return value;
33}
34
36 int64_t client_id = -1;
37 const gchar* input_type = "TextInputType.text";
38 const gchar* input_action = "TextInputAction.none";
39 gboolean enable_delta_model = false;
40};
41
43 return build_list({
45 build_map({
46 {"inputAction", fl_value_new_string(config.input_action)},
47 {"inputType", build_map({
48 {"name", fl_value_new_string(config.input_type)},
49 })},
50 {"enableDeltaModel", fl_value_new_bool(config.enable_delta_model)},
51 }),
52 });
53}
54
56 const gchar* text = "";
61};
62
64 return build_map({
65 {"text", fl_value_new_string(state.text)},
66 {"selectionBase", fl_value_new_int(state.selection_base)},
67 {"selectionExtent", fl_value_new_int(state.selection_extent)},
68 {"selectionAffinity", fl_value_new_string("TextAffinity.downstream")},
69 {"selectionIsDirectional", fl_value_new_bool(false)},
70 {"composingBase", fl_value_new_int(state.composing_base)},
71 {"composingExtent", fl_value_new_int(state.composing_extent)},
72 });
73}
74
76 const gchar* old_text = "";
77 const gchar* delta_text = "";
78 int delta_start = -1;
79 int delta_end = -1;
84};
85
87 return build_map({
88 {"oldText", fl_value_new_string(delta.old_text)},
89 {"deltaText", fl_value_new_string(delta.delta_text)},
90 {"deltaStart", fl_value_new_int(delta.delta_start)},
91 {"deltaEnd", fl_value_new_int(delta.delta_end)},
92 {"selectionBase", fl_value_new_int(delta.selection_base)},
93 {"selectionExtent", fl_value_new_int(delta.selection_extent)},
94 {"selectionAffinity", fl_value_new_string("TextAffinity.downstream")},
95 {"selectionIsDirectional", fl_value_new_bool(false)},
96 {"composingBase", fl_value_new_int(delta.composing_base)},
97 {"composingExtent", fl_value_new_int(delta.composing_extent)},
98 });
99}
100
101static void set_client(FlMockBinaryMessenger* messenger, InputConfig config) {
102 gboolean called = FALSE;
105 messenger, "flutter/textinput", "TextInput.setClient", args,
106 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
107 gpointer user_data) {
108 gboolean* called = static_cast<gboolean*>(user_data);
109 *called = TRUE;
110
111 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
112
113 g_autoptr(FlValue) expected_result = fl_value_new_null();
115 FL_METHOD_SUCCESS_RESPONSE(response)),
116 expected_result));
117 },
118 &called);
119 EXPECT_TRUE(called);
120}
121
122static void set_editing_state(FlMockBinaryMessenger* messenger,
123 EditingState state) {
124 gboolean called = FALSE;
127 messenger, "flutter/textinput", "TextInput.setEditingState", args,
128 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
129 gpointer user_data) {
130 gboolean* called = static_cast<gboolean*>(user_data);
131 *called = TRUE;
132
133 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
134
135 g_autoptr(FlValue) expected_result = fl_value_new_null();
137 FL_METHOD_SUCCESS_RESPONSE(response)),
138 expected_result));
139 },
140 &called);
141 EXPECT_TRUE(called);
142}
143
144static void send_key_event(FlTextInputHandler* handler,
145 gint keyval,
146 gint state = 0) {
147 GdkEvent* gdk_event = gdk_event_new(GDK_KEY_PRESS);
148 gdk_event->key.keyval = keyval;
149 gdk_event->key.state = state;
150 g_autoptr(FlKeyEvent) key_event = fl_key_event_new_from_gdk_event(gdk_event);
152}
153
155 protected:
156 void SetUp() override {
158 handler = fl_text_input_handler_new(FL_BINARY_MESSENGER(messenger));
159 }
160
162 fl_binary_messenger_shutdown(FL_BINARY_MESSENGER(messenger));
163 g_clear_object(&handler);
164 g_clear_object(&messenger);
165 }
166
167 FlMockBinaryMessenger* messenger = nullptr;
168 FlTextInputHandler* handler = nullptr;
169 ::testing::NiceMock<flutter::testing::MockGtk> mock_gtk;
170};
171
173 EXPECT_TRUE(
174 fl_mock_binary_messenger_has_handler(messenger, "flutter/textinput"));
175}
176
178 set_client(messenger, {.client_id = 1});
179}
180
182 EXPECT_CALL(mock_gtk, gtk_im_context_focus_in);
183
184 gboolean called = FALSE;
186 messenger, "flutter/textinput", "TextInput.show", nullptr,
187 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
188 gpointer user_data) {
189 gboolean* called = static_cast<gboolean*>(user_data);
190 *called = TRUE;
191
192 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
193
194 g_autoptr(FlValue) expected_result = fl_value_new_null();
196 FL_METHOD_SUCCESS_RESPONSE(response)),
197 expected_result));
198 },
199 &called);
200 EXPECT_TRUE(called);
201}
202
204 EXPECT_CALL(mock_gtk, gtk_im_context_focus_out);
205
206 gboolean called = FALSE;
208 messenger, "flutter/textinput", "TextInput.hide", nullptr,
209 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
210 gpointer user_data) {
211 gboolean* called = static_cast<gboolean*>(user_data);
212 *called = TRUE;
213
214 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
215
216 g_autoptr(FlValue) expected_result = fl_value_new_null();
218 FL_METHOD_SUCCESS_RESPONSE(response)),
219 expected_result));
220 },
221 &called);
222 EXPECT_TRUE(called);
223}
224
226 gboolean called = FALSE;
228 messenger, "flutter/textinput", "TextInput.clearClient", nullptr,
229 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
230 gpointer user_data) {
231 gboolean* called = static_cast<gboolean*>(user_data);
232 *called = TRUE;
233
234 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
235
236 g_autoptr(FlValue) expected_result = fl_value_new_null();
238 FL_METHOD_SUCCESS_RESPONSE(response)),
239 expected_result));
240 },
241 &called);
242 EXPECT_TRUE(called);
243}
244
246 set_client(messenger, {
247 .client_id = 1,
248 .input_type = "TextInputType.multiline",
249 .input_action = "TextInputAction.newline",
250 });
251 set_editing_state(messenger, {
252 .text = "Flutter",
253 .selection_base = 7,
254 .selection_extent = 7,
255 });
256
257 // Client will update editing state and perform action
258 int call_count = 0;
260 messenger, "flutter/textinput",
261 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
262 FlValue* args, gpointer user_data) {
263 int* call_count = static_cast<int*>(user_data);
264
265 if (strcmp(name, "TextInputClient.updateEditingState") == 0) {
266 g_autoptr(FlValue) expected_args = build_list({
267 fl_value_new_int(1), // client_id
269 .text = "Flutter\n",
270 .selection_base = 8,
271 .selection_extent = 8,
272 }),
273 });
274 EXPECT_TRUE(fl_value_equal(args, expected_args));
275 EXPECT_EQ(*call_count, 0);
276 (*call_count)++;
277 } else if (strcmp(name, "TextInputClient.performAction") == 0) {
278 g_autoptr(FlValue) expected_args = build_list({
279 fl_value_new_int(1), // client_id
280 fl_value_new_string("TextInputAction.newline"),
281 });
282 EXPECT_TRUE(fl_value_equal(args, expected_args));
283 EXPECT_EQ(*call_count, 1);
284 (*call_count)++;
285 }
286
287 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
288 },
289 &call_count);
290
291 send_key_event(handler, GDK_KEY_Return);
292 EXPECT_EQ(call_count, 2);
293}
294
295// Regression test for https://github.com/flutter/flutter/issues/125879.
296TEST_F(FlTextInputHandlerTest, MultilineWithSendAction) {
297 set_client(messenger, {
298 .client_id = 1,
299 .input_type = "TextInputType.multiline",
300 .input_action = "TextInputAction.send",
301 });
302 set_editing_state(messenger, {
303 .text = "Flutter",
304 .selection_base = 7,
305 .selection_extent = 7,
306 });
307
308 // Because the input action is not set to TextInputAction.newline, the next
309 // expected call is "TextInputClient.performAction". If the input action was
310 // set to TextInputAction.newline the next call would be
311 // "TextInputClient.updateEditingState" (this case is tested in the test named
312 // 'PerformAction').
313 int call_count = 0;
315 messenger, "flutter/textinput",
316 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
317 FlValue* args, gpointer user_data) {
318 int* call_count = static_cast<int*>(user_data);
319
320 EXPECT_STREQ(name, "TextInputClient.performAction");
321 g_autoptr(FlValue) expected_args = nullptr;
322 switch (*call_count) {
323 case 0:
324 // Perform action.
325 expected_args = build_list({
326 fl_value_new_int(1), // client_id
327 fl_value_new_string("TextInputAction.send"),
328 });
329 break;
330 default:
331 g_assert_not_reached();
332 break;
333 }
334 EXPECT_TRUE(fl_value_equal(args, expected_args));
335 (*call_count)++;
336
337 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
338 },
339 &call_count);
340
341 send_key_event(handler, GDK_KEY_Return);
342 EXPECT_EQ(call_count, 1);
343}
344
346 set_client(messenger, {.client_id = 1});
347 set_editing_state(messenger, {
348 .text = "Flutter",
349 .selection_base = 4,
350 .selection_extent = 4,
351 });
352
353 int call_count = 0;
355 messenger, "flutter/textinput",
356 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
357 FlValue* args, gpointer user_data) {
358 int* call_count = static_cast<int*>(user_data);
359
360 EXPECT_STREQ(name, "TextInputClient.updateEditingState");
361 g_autoptr(FlValue) expected_args = nullptr;
362 switch (*call_count) {
363 case 0:
364 // move cursor to beginning
365 expected_args = build_list({
366 fl_value_new_int(1), // client_id
368 .text = "Flutter",
369 .selection_base = 0,
370 .selection_extent = 0,
371 }),
372 });
373 break;
374 case 1:
375 // move cursor to end
376 expected_args = build_list({
377 fl_value_new_int(1), // client_id
379 .text = "Flutter",
380 .selection_base = 7,
381 .selection_extent = 7,
382 }),
383 });
384 break;
385 default:
386 g_assert_not_reached();
387 break;
388 }
389 EXPECT_TRUE(fl_value_equal(args, expected_args));
390 (*call_count)++;
391
392 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
393 },
394 &call_count);
395
396 send_key_event(handler, GDK_KEY_Home);
397 send_key_event(handler, GDK_KEY_End);
398 EXPECT_EQ(call_count, 2);
399}
400
402 set_client(messenger, {.client_id = 1});
403 set_editing_state(messenger, {
404 .text = "Flutter",
405 .selection_base = 4,
406 .selection_extent = 4,
407 });
408
409 int call_count = 0;
411 messenger, "flutter/textinput",
412 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
413 FlValue* args, gpointer user_data) {
414 int* call_count = static_cast<int*>(user_data);
415
416 EXPECT_STREQ(name, "TextInputClient.updateEditingState");
417 g_autoptr(FlValue) expected_args = nullptr;
418 switch (*call_count) {
419 case 0:
420 // select to end
421 expected_args = build_list({
422 fl_value_new_int(1), // client_id
424 .text = "Flutter",
425 .selection_base = 4,
426 .selection_extent = 7,
427 }),
428 });
429 break;
430 case 1:
431 // select to beginning
432 expected_args = build_list({
433 fl_value_new_int(1), // client_id
435 .text = "Flutter",
436 .selection_base = 4,
437 .selection_extent = 0,
438 }),
439 });
440 break;
441 default:
442 g_assert_not_reached();
443 break;
444 }
445 EXPECT_TRUE(fl_value_equal(args, expected_args));
446 (*call_count)++;
447
448 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
449 },
450 &call_count);
451
452 send_key_event(handler, GDK_KEY_End, GDK_SHIFT_MASK);
453 send_key_event(handler, GDK_KEY_Home, GDK_SHIFT_MASK);
454 EXPECT_EQ(call_count, 2);
455}
456
458 // update
459 EXPECT_CALL(mock_gtk, gtk_im_context_get_preedit_string(
460 ::testing::_, ::testing::A<gchar**>(), ::testing::_,
461 ::testing::A<gint*>()))
462 .WillOnce(
463 ::testing::DoAll(::testing::SetArgPointee<1>(g_strdup("Flutter")),
464 ::testing::SetArgPointee<3>(0)));
465
466 int call_count = 0;
468 messenger, "flutter/textinput",
469 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
470 FlValue* args, gpointer user_data) {
471 int* call_count = static_cast<int*>(user_data);
472
473 EXPECT_STREQ(name, "TextInputClient.updateEditingState");
474 g_autoptr(FlValue) expected_args = nullptr;
475 switch (*call_count) {
476 case 0:
477 expected_args = build_list({
478 fl_value_new_int(-1), // client_id
480 .text = "Flutter",
481 .selection_base = 0,
482 .selection_extent = 0,
483 .composing_base = 0,
484 .composing_extent = 7,
485 }),
486 });
487 break;
488 case 1:
489 // commit
490 expected_args = build_list({
491 fl_value_new_int(-1), // client_id
493 .text = "engine",
494 .selection_base = 6,
495 .selection_extent = 6,
496 }),
497 });
498 break;
499 case 2:
500 // end
501 expected_args = build_list({
502 fl_value_new_int(-1), // client_id
504 .text = "engine",
505 .selection_base = 6,
506 .selection_extent = 6,
507 }),
508 });
509 break;
510 default:
511 g_assert_not_reached();
512 break;
513 }
514 EXPECT_TRUE(fl_value_equal(args, expected_args));
515 (*call_count)++;
516
517 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
518 },
519 &call_count);
520
521 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
522 "preedit-start", nullptr);
523 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
524 "preedit-changed", nullptr);
525 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler), "commit",
526 "engine", nullptr);
527 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
528 "preedit-end", nullptr);
529 EXPECT_EQ(call_count, 3);
530}
531
533 set_client(messenger, {.client_id = 1});
534 set_editing_state(messenger, {
535 .text = "Flutter",
536 .selection_base = 3,
537 .selection_extent = 3,
538 });
539
540 // retrieve
541 EXPECT_CALL(mock_gtk, gtk_im_context_set_surrounding(
542 ::testing::_, ::testing::StrEq("Flutter"), -1, 3));
543
544 gboolean retrieved = false;
545 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
546 "retrieve-surrounding", &retrieved, nullptr);
547 EXPECT_TRUE(retrieved);
548
549 int call_count = 0;
551 messenger, "flutter/textinput",
552 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
553 FlValue* args, gpointer user_data) {
554 int* call_count = static_cast<int*>(user_data);
555
556 EXPECT_STREQ(name, "TextInputClient.updateEditingState");
557 g_autoptr(FlValue) expected_args = nullptr;
558 switch (*call_count) {
559 case 0:
560 // delete
561 expected_args = build_list({
562 fl_value_new_int(1), // client_id
564 .text = "Flutr",
565 .selection_base = 3,
566 .selection_extent = 3,
567 }),
568 });
569 break;
570 default:
571 g_assert_not_reached();
572 break;
573 }
574 EXPECT_TRUE(fl_value_equal(args, expected_args));
575 (*call_count)++;
576
577 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
578 },
579 &call_count);
580
581 gboolean deleted = false;
582 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
583 "delete-surrounding", 1, 2, &deleted, nullptr);
584 EXPECT_TRUE(deleted);
585 EXPECT_EQ(call_count, 1);
586}
587
588TEST_F(FlTextInputHandlerTest, SetMarkedTextRect) {
589 GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
590 g_object_ref_sink(window);
592
593 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
594 "preedit-start", nullptr);
595
596 // set editable size and transform
597 g_autoptr(FlValue) size_and_transform = build_map({
598 {
599 "transform",
600 build_list({
617 }),
618 },
619 });
620 gboolean called = FALSE;
622 messenger, "flutter/textinput", "TextInput.setEditableSizeAndTransform",
623 size_and_transform,
624 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
625 gpointer user_data) {
626 gboolean* called = static_cast<gboolean*>(user_data);
627 *called = TRUE;
628
629 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
630
631 g_autoptr(FlValue) expected_result = fl_value_new_null();
633 FL_METHOD_SUCCESS_RESPONSE(response)),
634 expected_result));
635 },
636 &called);
637 EXPECT_TRUE(called);
638
639 EXPECT_CALL(mock_gtk, gtk_widget_translate_coordinates(
640 ::testing::_, ::testing::_, ::testing::Eq(27),
641 ::testing::Eq(32), ::testing::_, ::testing::_))
642 .WillOnce(::testing::DoAll(::testing::SetArgPointee<4>(123),
643 ::testing::SetArgPointee<5>(456),
644 ::testing::Return(true)));
645
646 EXPECT_CALL(mock_gtk, gtk_im_context_set_cursor_location(
647 ::testing::_,
648 ::testing::Pointee(::testing::AllOf(
649 ::testing::Field(&GdkRectangle::x, 123),
650 ::testing::Field(&GdkRectangle::y, 456),
651 ::testing::Field(&GdkRectangle::width, 0),
652 ::testing::Field(&GdkRectangle::height, 0)))));
653
654 // set marked text rect
655 g_autoptr(FlValue) rect = build_map({
656 {"x", fl_value_new_float(1)},
657 {"y", fl_value_new_float(2)},
658 {"width", fl_value_new_float(3)},
659 {"height", fl_value_new_float(4)},
660 });
661 called = FALSE;
663 messenger, "flutter/textinput", "TextInput.setMarkedTextRect", rect,
664 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
665 gpointer user_data) {
666 gboolean* called = static_cast<gboolean*>(user_data);
667 *called = TRUE;
668
669 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
670
671 g_autoptr(FlValue) expected_result = fl_value_new_null();
673 FL_METHOD_SUCCESS_RESPONSE(response)),
674 expected_result));
675 },
676 &called);
677 EXPECT_TRUE(called);
678
680 g_object_unref(window);
681}
682
683// Updating the marked text rect with no widget set (e.g. after the view is
684// disposed) must not crash. https://github.com/flutter/flutter/issues/188657
685TEST_F(FlTextInputHandlerTest, SetMarkedTextRectWithoutWidget) {
686 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
687 "preedit-start", nullptr);
688
689 EXPECT_CALL(mock_gtk, gtk_widget_translate_coordinates).Times(0);
690
691 g_autoptr(FlValue) rect = build_map({
692 {"x", fl_value_new_float(1)},
693 {"y", fl_value_new_float(2)},
694 {"width", fl_value_new_float(3)},
695 {"height", fl_value_new_float(4)},
696 });
697 gboolean called = FALSE;
699 messenger, "flutter/textinput", "TextInput.setMarkedTextRect", rect,
700 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
701 gpointer user_data) {
702 gboolean* called = static_cast<gboolean*>(user_data);
703 *called = TRUE;
704 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
705 },
706 &called);
707 EXPECT_TRUE(called);
708}
709
710TEST_F(FlTextInputHandlerTest, TextInputTypeNone) {
711 set_client(messenger, {
712 .client_id = 1,
713 .input_type = "TextInputType.none",
714 });
715
716 EXPECT_CALL(mock_gtk, gtk_im_context_focus_in).Times(0);
717 EXPECT_CALL(mock_gtk, gtk_im_context_focus_out);
718
719 gboolean called = FALSE;
721 messenger, "flutter/textinput", "TextInput.show", nullptr,
722 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
723 gpointer user_data) {
724 gboolean* called = static_cast<gboolean*>(user_data);
725 *called = TRUE;
726
727 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
728
729 g_autoptr(FlValue) expected_result = fl_value_new_null();
731 FL_METHOD_SUCCESS_RESPONSE(response)),
732 expected_result));
733 },
734 &called);
735 EXPECT_TRUE(called);
736}
737
738TEST_F(FlTextInputHandlerTest, TextEditingDelta) {
739 set_client(messenger, {
740 .client_id = 1,
741 .enable_delta_model = true,
742 });
743 set_editing_state(messenger, {
744 .text = "Flutter",
745 .selection_base = 7,
746 .selection_extent = 7,
747 });
748
749 // update editing state with deltas
750 int call_count = 0;
752 messenger, "flutter/textinput",
753 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
754 FlValue* args, gpointer user_data) {
755 int* call_count = static_cast<int*>(user_data);
756
757 EXPECT_STREQ(name, "TextInputClient.updateEditingStateWithDeltas");
758 g_autoptr(FlValue) expected_args = nullptr;
759 switch (*call_count) {
760 case 0:
761 expected_args = build_list({
762 fl_value_new_int(1), // client_id
763 build_map({{
764 "deltas",
765 build_list({
767 .old_text = "Flutter",
768 .delta_text = "Flutter",
769 .delta_start = 7,
770 .delta_end = 7,
771 .selection_base = 0,
772 .selection_extent = 0,
773 }),
774 }),
775 }}),
776 });
777 break;
778 default:
779 g_assert_not_reached();
780 break;
781 }
782 EXPECT_TRUE(fl_value_equal(args, expected_args));
783 (*call_count)++;
784
785 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
786 },
787 &call_count);
788
789 send_key_event(handler, GDK_KEY_Home);
790 EXPECT_EQ(call_count, 1);
791}
792
794 // set config
795 set_client(messenger, {
796 .client_id = 1,
797 .enable_delta_model = true,
798 });
799
800 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
801 "preedit-start", nullptr);
802
803 // update
804 EXPECT_CALL(mock_gtk, gtk_im_context_get_preedit_string(
805 ::testing::_, ::testing::A<gchar**>(), ::testing::_,
806 ::testing::A<gint*>()))
807 .WillOnce(
808 ::testing::DoAll(::testing::SetArgPointee<1>(g_strdup("Flutter ")),
809 ::testing::SetArgPointee<3>(8)));
810
811 int call_count = 0;
813 messenger, "flutter/textinput",
814 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
815 FlValue* args, gpointer user_data) {
816 int* call_count = static_cast<int*>(user_data);
817
818 EXPECT_STREQ(name, "TextInputClient.updateEditingStateWithDeltas");
819 g_autoptr(FlValue) expected_args = nullptr;
820 switch (*call_count) {
821 case 0:
822 expected_args = build_list({
823 fl_value_new_int(1), // client_id
824 build_map({{
825 "deltas",
826 build_list({
828 .old_text = "",
829 .delta_text = "Flutter ",
830 .delta_start = 0,
831 .delta_end = 0,
832 .selection_base = 8,
833 .selection_extent = 8,
834 .composing_base = 0,
835 .composing_extent = 8,
836 }),
837 }),
838 }}),
839 });
840 break;
841 case 1:
842 // commit
843 expected_args = build_list({
844 fl_value_new_int(1), // client_id
845 build_map({{
846 "deltas",
847 build_list({
849 .old_text = "Flutter ",
850 .delta_text = "Flutter engine",
851 .delta_start = 0,
852 .delta_end = 8,
853 .selection_base = 14,
854 .selection_extent = 14,
855 .composing_base = -1,
856 .composing_extent = -1,
857 }),
858 }),
859 }}),
860 });
861 break;
862 case 2:
863 // end
864 expected_args = build_list({
865 fl_value_new_int(1), // client_id
866 build_map({{
867 "deltas",
868 build_list({
870 .old_text = "Flutter engine",
871 .selection_base = 14,
872 .selection_extent = 14,
873 }),
874 }),
875 }}),
876 });
877 break;
878 default:
879 g_assert_not_reached();
880 break;
881 }
882 EXPECT_TRUE(fl_value_equal(args, expected_args));
883 (*call_count)++;
884
885 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
886 },
887 &call_count);
888
889 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
890 "preedit-changed", nullptr);
891 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler), "commit",
892 "Flutter engine", nullptr);
893 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler),
894 "preedit-end", nullptr);
895 EXPECT_EQ(call_count, 3);
896}
897
898TEST_F(FlTextInputHandlerTest, NonComposingDelta) {
899 // set config
900 set_client(messenger, {
901 .client_id = 1,
902 .enable_delta_model = true,
903 });
904
905 int call_count = 0;
907 messenger, "flutter/textinput",
908 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
909 FlValue* args, gpointer user_data) {
910 int* call_count = static_cast<int*>(user_data);
911
912 EXPECT_STREQ(name, "TextInputClient.updateEditingStateWithDeltas");
913 g_autoptr(FlValue) expected_args = nullptr;
914 switch (*call_count) {
915 case 0:
916 // commit F
917 expected_args = build_list({
918 fl_value_new_int(1), // client_id
919 build_map({{
920 "deltas",
921 build_list({
923 .old_text = "",
924 .delta_text = "F",
925 .delta_start = 0,
926 .delta_end = 0,
927 .selection_base = 1,
928 .selection_extent = 1,
929 .composing_base = -1,
930 .composing_extent = -1,
931 }),
932 }),
933 }}),
934 });
935 break;
936 case 1:
937 // commit l
938 expected_args = build_list({
939 fl_value_new_int(1), // client_id
940 build_map({{
941 "deltas",
942 build_list({
944 .old_text = "F",
945 .delta_text = "l",
946 .delta_start = 1,
947 .delta_end = 1,
948 .selection_base = 2,
949 .selection_extent = 2,
950 .composing_base = -1,
951 .composing_extent = -1,
952 }),
953 }),
954 }}),
955 });
956 break;
957 case 2:
958 // commit u
959 expected_args = build_list({
960 fl_value_new_int(1), // client_id
961 build_map({{
962 "deltas",
963 build_list({
965 .old_text = "Fl",
966 .delta_text = "u",
967 .delta_start = 2,
968 .delta_end = 2,
969 .selection_base = 3,
970 .selection_extent = 3,
971 .composing_base = -1,
972 .composing_extent = -1,
973 }),
974 }),
975 }}),
976 });
977 break;
978 case 3:
979 // commit t
980 expected_args = build_list({
981 fl_value_new_int(1), // client_id
982 build_map({{
983 "deltas",
984 build_list({
986 .old_text = "Flu",
987 .delta_text = "t",
988 .delta_start = 3,
989 .delta_end = 3,
990 .selection_base = 4,
991 .selection_extent = 4,
992 .composing_base = -1,
993 .composing_extent = -1,
994 }),
995 }),
996 }}),
997 });
998 break;
999 case 4:
1000 // commit t again
1001 expected_args = build_list({
1002 fl_value_new_int(1), // client_id
1003 build_map({{
1004 "deltas",
1005 build_list({
1007 .old_text = "Flut",
1008 .delta_text = "t",
1009 .delta_start = 4,
1010 .delta_end = 4,
1011 .selection_base = 5,
1012 .selection_extent = 5,
1013 .composing_base = -1,
1014 .composing_extent = -1,
1015 }),
1016 }),
1017 }}),
1018 });
1019 break;
1020 case 5:
1021 // commit e
1022 expected_args = build_list({
1023 fl_value_new_int(1), // client_id
1024 build_map({{
1025 "deltas",
1026 build_list({
1028 .old_text = "Flutt",
1029 .delta_text = "e",
1030 .delta_start = 5,
1031 .delta_end = 5,
1032 .selection_base = 6,
1033 .selection_extent = 6,
1034 .composing_base = -1,
1035 .composing_extent = -1,
1036 }),
1037 }),
1038 }}),
1039 });
1040 break;
1041 case 6:
1042 // commit r
1043 expected_args = build_list({
1044 fl_value_new_int(1), // client_id
1045 build_map({{
1046 "deltas",
1047 build_list({
1049 .old_text = "Flutte",
1050 .delta_text = "r",
1051 .delta_start = 6,
1052 .delta_end = 6,
1053 .selection_base = 7,
1054 .selection_extent = 7,
1055 .composing_base = -1,
1056 .composing_extent = -1,
1057 }),
1058 }),
1059 }}),
1060 });
1061 break;
1062 default:
1063 g_assert_not_reached();
1064 break;
1065 }
1066 EXPECT_TRUE(fl_value_equal(args, expected_args));
1067 (*call_count)++;
1068
1069 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
1070 },
1071 &call_count);
1072
1073 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler), "commit",
1074 "F", nullptr);
1075 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler), "commit",
1076 "l", nullptr);
1077 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler), "commit",
1078 "u", nullptr);
1079 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler), "commit",
1080 "t", nullptr);
1081 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler), "commit",
1082 "t", nullptr);
1083 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler), "commit",
1084 "e", nullptr);
1085 g_signal_emit_by_name(fl_text_input_handler_get_im_context(handler), "commit",
1086 "r", nullptr);
1087 EXPECT_EQ(call_count, 7);
1088}
1089
1090TEST_F(FlTextInputHandlerTest, InputPurposeNumber) {
1091 EXPECT_CALL(mock_gtk,
1092 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1093 GTK_INPUT_PURPOSE_NUMBER));
1094 EXPECT_CALL(mock_gtk,
1095 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1096 GTK_INPUT_HINT_NONE));
1097
1098 set_client(messenger, {
1099 .client_id = 1,
1100 .input_type = "TextInputType.number",
1101 });
1102}
1103
1104TEST_F(FlTextInputHandlerTest, InputPurposePhone) {
1105 EXPECT_CALL(mock_gtk,
1106 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1107 GTK_INPUT_PURPOSE_PHONE));
1108 EXPECT_CALL(mock_gtk,
1109 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1110 GTK_INPUT_HINT_NONE));
1111
1112 set_client(messenger, {
1113 .client_id = 1,
1114 .input_type = "TextInputType.phone",
1115 });
1116}
1117
1118TEST_F(FlTextInputHandlerTest, InputPurposeEmail) {
1119 EXPECT_CALL(mock_gtk,
1120 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1121 GTK_INPUT_PURPOSE_EMAIL));
1122 EXPECT_CALL(mock_gtk,
1123 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1124 GTK_INPUT_HINT_NONE));
1125
1126 set_client(messenger, {
1127 .client_id = 1,
1128 .input_type = "TextInputType.emailAddress",
1129 });
1130}
1131
1133 EXPECT_CALL(mock_gtk,
1134 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1135 GTK_INPUT_PURPOSE_URL));
1136 EXPECT_CALL(mock_gtk,
1137 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1138 GTK_INPUT_HINT_NONE));
1139
1140 set_client(messenger, {
1141 .client_id = 1,
1142 .input_type = "TextInputType.url",
1143 });
1144}
1145
1146TEST_F(FlTextInputHandlerTest, InputPurposePassword) {
1147 EXPECT_CALL(mock_gtk,
1148 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1149 GTK_INPUT_PURPOSE_PASSWORD));
1150 EXPECT_CALL(mock_gtk,
1151 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1152 GTK_INPUT_HINT_NONE));
1153
1154 set_client(messenger, {
1155 .client_id = 1,
1156 .input_type = "TextInputType.visiblePassword",
1157 });
1158}
1159
1160TEST_F(FlTextInputHandlerTest, InputPurposeName) {
1161 EXPECT_CALL(mock_gtk,
1162 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1163 GTK_INPUT_PURPOSE_NAME));
1164 EXPECT_CALL(mock_gtk,
1165 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1166 GTK_INPUT_HINT_UPPERCASE_WORDS));
1167
1168 set_client(messenger, {
1169 .client_id = 1,
1170 .input_type = "TextInputType.name",
1171 });
1172}
1173
1174TEST_F(FlTextInputHandlerTest, InputHintsAddress) {
1175 EXPECT_CALL(mock_gtk,
1176 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1177 GTK_INPUT_PURPOSE_FREE_FORM));
1178 EXPECT_CALL(mock_gtk,
1179 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1180 GTK_INPUT_HINT_UPPERCASE_WORDS));
1181
1182 set_client(messenger, {
1183 .client_id = 1,
1184 .input_type = "TextInputType.address",
1185 });
1186}
1187
1188TEST_F(FlTextInputHandlerTest, InputHintsMultiline) {
1189 EXPECT_CALL(mock_gtk,
1190 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1191 GTK_INPUT_PURPOSE_FREE_FORM));
1192 EXPECT_CALL(
1193 mock_gtk,
1194 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1195 static_cast<gint>(GTK_INPUT_HINT_SPELLCHECK |
1196 GTK_INPUT_HINT_UPPERCASE_SENTENCES)));
1197
1198 set_client(messenger, {
1199 .client_id = 1,
1200 .input_type = "TextInputType.multiline",
1201 });
1202}
1203
1204TEST_F(FlTextInputHandlerTest, InputHintsWebSearch) {
1205 EXPECT_CALL(mock_gtk,
1206 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1207 GTK_INPUT_PURPOSE_FREE_FORM));
1208 EXPECT_CALL(mock_gtk,
1209 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1210 GTK_INPUT_HINT_LOWERCASE));
1211
1212 set_client(messenger, {
1213 .client_id = 1,
1214 .input_type = "TextInputType.webSearch",
1215 });
1216}
1217
1218TEST_F(FlTextInputHandlerTest, InputHintsTwitter) {
1219 EXPECT_CALL(mock_gtk,
1220 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1221 GTK_INPUT_PURPOSE_FREE_FORM));
1222 EXPECT_CALL(
1223 mock_gtk,
1224 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1225 static_cast<gint>(GTK_INPUT_HINT_SPELLCHECK |
1226 GTK_INPUT_HINT_UPPERCASE_SENTENCES)));
1227
1228 set_client(messenger, {
1229 .client_id = 1,
1230 .input_type = "TextInputType.twitter",
1231 });
1232}
1233
1234TEST_F(FlTextInputHandlerTest, DefaultInputPurposeAndHints) {
1235 EXPECT_CALL(mock_gtk,
1236 g_object_set(::testing::_, ::testing::StrEq("input-purpose"),
1237 GTK_INPUT_PURPOSE_FREE_FORM));
1238 EXPECT_CALL(mock_gtk,
1239 g_object_set(::testing::_, ::testing::StrEq("input-hints"),
1240 GTK_INPUT_HINT_NONE));
1241
1242 set_client(messenger, {
1243 .client_id = 1,
1244 .input_type = "TextInputType.text",
1245 });
1246}
1247
1249 set_client(messenger, {
1250 .client_id = 1,
1251 .input_action = "TextInputAction.none",
1252 });
1253
1254 // Update config with a different input action
1255 gboolean called = FALSE;
1256 g_autoptr(FlValue) config = build_map({
1257 {"inputAction", fl_value_new_string("TextInputAction.send")},
1258 {"inputType", build_map({
1259 {"name", fl_value_new_string("TextInputType.text")},
1260 })},
1261 {"enableDeltaModel", fl_value_new_bool(false)},
1262 });
1264 messenger, "flutter/textinput", "TextInput.updateConfig", config,
1265 [](FlMockBinaryMessenger* messenger, FlMethodResponse* response,
1266 gpointer user_data) {
1267 gboolean* called = static_cast<gboolean*>(user_data);
1268 *called = TRUE;
1269
1270 EXPECT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response));
1271
1272 g_autoptr(FlValue) expected_result = fl_value_new_null();
1274 FL_METHOD_SUCCESS_RESPONSE(response)),
1275 expected_result));
1276 },
1277 &called);
1278 EXPECT_TRUE(called);
1279
1280 set_editing_state(messenger, {
1281 .text = "Flutter",
1282 .selection_base = 7,
1283 .selection_extent = 7,
1284 });
1285
1286 // Verify the action was updated by checking perform action call
1287 int call_count = 0;
1289 messenger, "flutter/textinput",
1290 [](FlMockBinaryMessenger* messenger, GTask* task, const gchar* name,
1291 FlValue* args, gpointer user_data) {
1292 int* call_count = static_cast<int*>(user_data);
1293
1294 EXPECT_STREQ(name, "TextInputClient.performAction");
1295 g_autoptr(FlValue) expected_args = build_list({
1296 fl_value_new_int(1), // client_id
1297 fl_value_new_string("TextInputAction.send"),
1298 });
1299 EXPECT_TRUE(fl_value_equal(args, expected_args));
1300 (*call_count)++;
1301
1302 return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
1303 },
1304 &call_count);
1305
1306 send_key_event(handler, GDK_KEY_Return);
1307 EXPECT_EQ(call_count, 1);
1308}
FlMockBinaryMessenger * messenger
::testing::NiceMock< flutter::testing::MockGtk > mock_gtk
int32_t value
GLFWwindow * window
Definition main.cc:60
g_autoptr(FlEngine) engine
void fl_binary_messenger_shutdown(FlBinaryMessenger *self)
return TRUE
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
FlKeyEvent * fl_key_event_new_from_gdk_event(GdkEvent *event)
G_MODULE_EXPORT FlValue * fl_method_success_response_get_result(FlMethodSuccessResponse *self)
G_MODULE_EXPORT FlMethodSuccessResponse * fl_method_success_response_new(FlValue *result)
gboolean fl_mock_binary_messenger_has_handler(FlMockBinaryMessenger *self, const gchar *channel)
void fl_mock_binary_messenger_invoke_json_method(FlMockBinaryMessenger *self, const gchar *channel, const char *name, FlValue *args, FlMockBinaryMessengerMethodCallback callback, gpointer user_data)
const gchar FlBinaryMessengerMessageHandler handler
FlMockBinaryMessenger * fl_mock_binary_messenger_new()
void fl_mock_binary_messenger_set_json_method_channel(FlMockBinaryMessenger *self, const gchar *channel, FlMockBinaryMessengerMethodChannelHandler handler, gpointer user_data)
GtkIMContext * fl_text_input_handler_get_im_context(FlTextInputHandler *self)
FlTextInputHandler * fl_text_input_handler_new(FlBinaryMessenger *messenger)
gboolean fl_text_input_handler_filter_keypress(FlTextInputHandler *self, FlKeyEvent *event)
void fl_text_input_handler_set_widget(FlTextInputHandler *self, GtkWidget *widget)
static void send_key_event(FlTextInputHandler *handler, gint keyval, gint state=0)
static FlValue * build_input_config(InputConfig config)
static void set_client(FlMockBinaryMessenger *messenger, InputConfig config)
static FlValue * build_editing_state(EditingState state)
static FlValue * build_map(std::map< const gchar *, FlValue * > args)
static FlValue * build_list(std::vector< FlValue * > args)
static void set_editing_state(FlMockBinaryMessenger *messenger, EditingState state)
TEST_F(FlTextInputHandlerTest, MessageHandler)
static FlValue * build_editing_delta(EditingDelta delta)
G_MODULE_EXPORT FlValue * fl_value_new_map()
Definition fl_value.cc:366
G_MODULE_EXPORT void fl_value_set_string_take(FlValue *self, const gchar *key, FlValue *value)
Definition fl_value.cc:650
G_MODULE_EXPORT FlValue * fl_value_new_null()
Definition fl_value.cc:251
G_MODULE_EXPORT FlValue * fl_value_new_string(const gchar *value)
Definition fl_value.cc:276
G_MODULE_EXPORT FlValue * fl_value_new_bool(bool value)
Definition fl_value.cc:255
G_MODULE_EXPORT FlValue * fl_value_new_int(int64_t value)
Definition fl_value.cc:262
G_MODULE_EXPORT FlValue * fl_value_new_float(double value)
Definition fl_value.cc:269
G_MODULE_EXPORT void fl_value_append_take(FlValue *self, FlValue *value)
Definition fl_value.cc:600
G_MODULE_EXPORT FlValue * fl_value_new_list()
Definition fl_value.cc:349
G_MODULE_EXPORT bool fl_value_equal(FlValue *a, FlValue *b)
Definition fl_value.cc:471
typedefG_BEGIN_DECLS struct _FlValue FlValue
Definition fl_value.h:42
const char * name
Definition fuchsia.cc:50
FlutterKeyEvent key_event
void gtk_im_context_focus_in(GtkIMContext *context)
Definition mock_gtk.cc:342
void gtk_im_context_set_cursor_location(GtkIMContext *context, const GdkRectangle *area)
Definition mock_gtk.cc:356
void gtk_im_context_focus_out(GtkIMContext *context)
Definition mock_gtk.cc:349
void gtk_im_context_get_preedit_string(GtkIMContext *context, gchar **str, PangoAttrList **attrs, gint *cursor_pos)
Definition mock_gtk.cc:322
GtkWidget * gtk_window_new(GtkWindowType type)
Definition mock_gtk.cc:182
void g_object_set(gpointer object, const gchar *first_property_name,...)
Definition mock_gtk.cc:400
void gtk_im_context_set_surrounding(GtkIMContext *context, const gchar *text, gint len, gint cursor_index)
Definition mock_gtk.cc:364
gboolean gtk_widget_translate_coordinates(GtkWidget *src_widget, GtkWidget *dest_widget, gint src_x, gint src_y, gint *dest_x, gint *dest_y)
Definition mock_gtk.cc:287