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