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