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