Flutter Linux Embedder
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 
10 #include "flutter/shell/platform/linux/testing/fl_mock_binary_messenger.h"
11 #include "flutter/shell/platform/linux/testing/fl_test.h"
12 #include "flutter/shell/platform/linux/testing/mock_gtk.h"
13 #include "flutter/testing/testing.h"
14 
15 #include "flutter/shell/platform/linux/testing/linux_test.h"
16 #include "gmock/gmock.h"
17 #include "gtest/gtest.h"
18 
19 static 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 
27 static FlValue* build_list(std::vector<FlValue*> args) {
29  for (auto it = args.begin(); it != args.end(); ++it) {
31  }
32  return value;
33 }
34 
35 struct InputConfig {
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 
55 struct EditingState {
56  const gchar* text = "";
57  int selection_base = -1;
58  int selection_extent = -1;
59  int composing_base = -1;
60  int composing_extent = -1;
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 
75 struct EditingDelta {
76  const gchar* old_text = "";
77  const gchar* delta_text = "";
78  int delta_start = -1;
79  int delta_end = -1;
80  int selection_base = -1;
81  int selection_extent = -1;
82  int composing_base = -1;
83  int composing_extent = -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 
101 static void set_client(FlMockBinaryMessenger* messenger, InputConfig config) {
102  gboolean called = FALSE;
104  fl_mock_binary_messenger_invoke_json_method(
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 
122 static void set_editing_state(FlMockBinaryMessenger* messenger,
123  EditingState state) {
124  gboolean called = FALSE;
126  fl_mock_binary_messenger_invoke_json_method(
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 
144 static 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);
151  fl_text_input_handler_filter_keypress(handler, key_event);
152 }
153 
154 class FlTextInputHandlerTest : public flutter::testing::LinuxTest {
155  protected:
156  void SetUp() override {
157  messenger = fl_mock_binary_messenger_new();
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;
185  fl_mock_binary_messenger_invoke_json_method(
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;
207  fl_mock_binary_messenger_invoke_json_method(
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;
227  fl_mock_binary_messenger_invoke_json_method(
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;
259  fl_mock_binary_messenger_set_json_method_channel(
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.
296 TEST_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;
314  fl_mock_binary_messenger_set_json_method_channel(
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;
354  fl_mock_binary_messenger_set_json_method_channel(
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;
410  fl_mock_binary_messenger_set_json_method_channel(
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;
467  fl_mock_binary_messenger_set_json_method_channel(
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 
532 TEST_F(FlTextInputHandlerTest, SurroundingText) {
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;
550  fl_mock_binary_messenger_set_json_method_channel(
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 
588 TEST_F(FlTextInputHandlerTest, SetMarkedTextRect) {
589  GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
590  g_object_ref_sink(window);
591  fl_text_input_handler_set_widget(handler, 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({
610  fl_value_new_float(10),
611  fl_value_new_float(11),
612  fl_value_new_float(12),
613  fl_value_new_float(13),
614  fl_value_new_float(14),
615  fl_value_new_float(15),
616  fl_value_new_float(16),
617  }),
618  },
619  });
620  gboolean called = FALSE;
621  fl_mock_binary_messenger_invoke_json_method(
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;
662  fl_mock_binary_messenger_invoke_json_method(
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 
679  fl_text_input_handler_set_widget(handler, nullptr);
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
685 TEST_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;
698  fl_mock_binary_messenger_invoke_json_method(
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 
710 TEST_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;
720  fl_mock_binary_messenger_invoke_json_method(
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 
738 TEST_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;
751  fl_mock_binary_messenger_set_json_method_channel(
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 
793 TEST_F(FlTextInputHandlerTest, ComposingDelta) {
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;
812  fl_mock_binary_messenger_set_json_method_channel(
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 
898 TEST_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;
906  fl_mock_binary_messenger_set_json_method_channel(
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 
1090 TEST_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 
1104 TEST_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 
1118 TEST_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 
1132 TEST_F(FlTextInputHandlerTest, InputPurposeUrl) {
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 
1146 TEST_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 
1160 TEST_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 
1174 TEST_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 
1188 TEST_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 
1204 TEST_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 
1218 TEST_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 
1234 TEST_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  });
1263  fl_mock_binary_messenger_invoke_json_method(
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;
1288  fl_mock_binary_messenger_set_json_method_channel(
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
g_autoptr(FlEngine) engine
const char FlTextDirection FlAssertiveness gpointer user_data
void fl_binary_messenger_shutdown(FlBinaryMessenger *self)
FlFramebuffer double double int int height
FlFramebuffer double x
FlFramebuffer double double int width
FlFramebuffer double double y
return TRUE
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
FlKeyEvent * fl_key_event_new_from_gdk_event(GdkEvent *event)
Definition: fl_key_event.cc:53
G_MODULE_EXPORT FlMethodSuccessResponse * fl_method_success_response_new(FlValue *result)
G_MODULE_EXPORT FlValue * fl_method_success_response_get_result(FlMethodSuccessResponse *self)
uint8_t value
FlTextInputHandler * fl_text_input_handler_new(FlBinaryMessenger *messenger)
gboolean fl_text_input_handler_filter_keypress(FlTextInputHandler *self, FlKeyEvent *event)
GtkIMContext * fl_text_input_handler_get_im_context(FlTextInputHandler *self)
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_list(std::vector< FlValue * > args)
static void set_client(FlMockBinaryMessenger *messenger, InputConfig config)
static FlValue * build_editing_delta(EditingDelta delta)
static FlValue * build_editing_state(EditingState state)
static FlValue * build_map(std::map< const gchar *, FlValue * > args)
static FlValue * build_input_config(InputConfig config)
static void set_editing_state(FlMockBinaryMessenger *messenger, EditingState state)
TEST_F(FlTextInputHandlerTest, MessageHandler)
G_MODULE_EXPORT FlValue * fl_value_new_float(double value)
Definition: fl_value.cc:269
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_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_list()
Definition: fl_value.cc:349
G_MODULE_EXPORT void fl_value_append_take(FlValue *self, FlValue *value)
Definition: fl_value.cc:600
G_MODULE_EXPORT FlValue * fl_value_new_null()
Definition: fl_value.cc:251
G_MODULE_EXPORT bool fl_value_equal(FlValue *a, FlValue *b)
Definition: fl_value.cc:471
G_MODULE_EXPORT FlValue * fl_value_new_int(int64_t value)
Definition: fl_value.cc:262
G_MODULE_EXPORT FlValue * fl_value_new_map()
Definition: fl_value.cc:366
typedefG_BEGIN_DECLS struct _FlValue FlValue
Definition: fl_value.h:42
std::function< void(const T &message, const MessageReply< T > &reply)> MessageHandler
const gchar * input_action