| // Copyright 2013 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "flutter/shell/platform/windows/text_input_plugin.h" | |
| #include <rapidjson/document.h> | |
| #include <windows.h> | |
| #include <memory> | |
| #include "flutter/fml/macros.h" | |
| #include "flutter/shell/platform/common/json_message_codec.h" | |
| #include "flutter/shell/platform/common/json_method_codec.h" | |
| #include "flutter/shell/platform/windows/flutter_windows_view.h" | |
| #include "flutter/shell/platform/windows/testing/engine_modifier.h" | |
| #include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h" | |
| #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" | |
| #include "flutter/shell/platform/windows/testing/test_binary_messenger.h" | |
| #include "flutter/shell/platform/windows/testing/windows_test.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| namespace flutter { | |
| namespace testing { | |
| namespace { | |
| using ::testing::Return; | |
| static constexpr char kScanCodeKey[] = "scanCode"; | |
| static constexpr int kHandledScanCode = 20; | |
| static constexpr int kUnhandledScanCode = 21; | |
| static constexpr char kTextPlainFormat[] = "text/plain"; | |
| static constexpr int kDefaultClientId = 42; | |
| // Should be identical to constants in text_input_plugin.cc. | |
| static constexpr char kChannelName[] = "flutter/textinput"; | |
| static constexpr char kEnableDeltaModel[] = "enableDeltaModel"; | |
| static constexpr char kSetClientMethod[] = "TextInput.setClient"; | |
| static constexpr char kAffinityDownstream[] = "TextAffinity.downstream"; | |
| static constexpr char kTextKey[] = "text"; | |
| static constexpr char kSelectionBaseKey[] = "selectionBase"; | |
| static constexpr char kSelectionExtentKey[] = "selectionExtent"; | |
| static constexpr char kSelectionAffinityKey[] = "selectionAffinity"; | |
| static constexpr char kSelectionIsDirectionalKey[] = "selectionIsDirectional"; | |
| static constexpr char kComposingBaseKey[] = "composingBase"; | |
| static constexpr char kComposingExtentKey[] = "composingExtent"; | |
| static constexpr char kUpdateEditingStateMethod[] = | |
| "TextInputClient.updateEditingState"; | |
| static std::unique_ptr<std::vector<uint8_t>> CreateResponse(bool handled) { | |
| auto response_doc = | |
| std::make_unique<rapidjson::Document>(rapidjson::kObjectType); | |
| auto& allocator = response_doc->GetAllocator(); | |
| response_doc->AddMember("handled", handled, allocator); | |
| return JsonMessageCodec::GetInstance().EncodeMessage(*response_doc); | |
| } | |
| static std::unique_ptr<rapidjson::Document> EncodedClientConfig( | |
| std::string type_name, | |
| std::string input_action) { | |
| auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType); | |
| auto& allocator = arguments->GetAllocator(); | |
| arguments->PushBack(kDefaultClientId, allocator); | |
| rapidjson::Value config(rapidjson::kObjectType); | |
| config.AddMember("inputAction", input_action, allocator); | |
| config.AddMember(kEnableDeltaModel, false, allocator); | |
| rapidjson::Value type_info(rapidjson::kObjectType); | |
| type_info.AddMember("name", type_name, allocator); | |
| config.AddMember("inputType", type_info, allocator); | |
| arguments->PushBack(config, allocator); | |
| return arguments; | |
| } | |
| static std::unique_ptr<rapidjson::Document> EncodedEditingState( | |
| std::string text, | |
| TextRange selection) { | |
| auto model = std::make_unique<TextInputModel>(); | |
| model->SetText(text); | |
| model->SetSelection(selection); | |
| auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType); | |
| auto& allocator = arguments->GetAllocator(); | |
| arguments->PushBack(kDefaultClientId, allocator); | |
| rapidjson::Value editing_state(rapidjson::kObjectType); | |
| editing_state.AddMember(kSelectionAffinityKey, kAffinityDownstream, | |
| allocator); | |
| editing_state.AddMember(kSelectionBaseKey, selection.base(), allocator); | |
| editing_state.AddMember(kSelectionExtentKey, selection.extent(), allocator); | |
| editing_state.AddMember(kSelectionIsDirectionalKey, false, allocator); | |
| int composing_base = | |
| model->composing() ? model->composing_range().base() : -1; | |
| int composing_extent = | |
| model->composing() ? model->composing_range().extent() : -1; | |
| editing_state.AddMember(kComposingBaseKey, composing_base, allocator); | |
| editing_state.AddMember(kComposingExtentKey, composing_extent, allocator); | |
| editing_state.AddMember(kTextKey, | |
| rapidjson::Value(model->GetText(), allocator).Move(), | |
| allocator); | |
| arguments->PushBack(editing_state, allocator); | |
| return arguments; | |
| } | |
| class MockFlutterWindowsView : public FlutterWindowsView { | |
| public: | |
| MockFlutterWindowsView(FlutterWindowsEngine* engine, | |
| std::unique_ptr<WindowBindingHandler> window) | |
| : FlutterWindowsView(kImplicitViewId, engine, std::move(window)) {} | |
| virtual ~MockFlutterWindowsView() = default; | |
| MOCK_METHOD(void, OnCursorRectUpdated, (const Rect&), (override)); | |
| MOCK_METHOD(void, OnResetImeComposing, (), (override)); | |
| private: | |
| FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindowsView); | |
| }; | |
| } // namespace | |
| class TextInputPluginTest : public WindowsTest { | |
| public: | |
| TextInputPluginTest() = default; | |
| virtual ~TextInputPluginTest() = default; | |
| protected: | |
| FlutterWindowsEngine* engine() { return engine_.get(); } | |
| MockFlutterWindowsView* view() { return view_.get(); } | |
| MockWindowBindingHandler* window() { return window_; } | |
| void UseHeadlessEngine() { | |
| FlutterWindowsEngineBuilder builder{GetContext()}; | |
| engine_ = builder.Build(); | |
| } | |
| void UseEngineWithView() { | |
| FlutterWindowsEngineBuilder builder{GetContext()}; | |
| auto window = std::make_unique<MockWindowBindingHandler>(); | |
| window_ = window.get(); | |
| EXPECT_CALL(*window_, SetView).Times(1); | |
| EXPECT_CALL(*window, GetWindowHandle).WillRepeatedly(Return(nullptr)); | |
| engine_ = builder.Build(); | |
| view_ = std::make_unique<MockFlutterWindowsView>(engine_.get(), | |
| std::move(window)); | |
| EngineModifier modifier{engine_.get()}; | |
| modifier.SetImplicitView(view_.get()); | |
| } | |
| private: | |
| std::unique_ptr<FlutterWindowsEngine> engine_; | |
| std::unique_ptr<MockFlutterWindowsView> view_; | |
| MockWindowBindingHandler* window_; | |
| FML_DISALLOW_COPY_AND_ASSIGN(TextInputPluginTest); | |
| }; | |
| TEST_F(TextInputPluginTest, TextMethodsWorksWithEmptyModel) { | |
| UseEngineWithView(); | |
| auto handled_message = CreateResponse(true); | |
| auto unhandled_message = CreateResponse(false); | |
| int received_scancode = 0; | |
| TestBinaryMessenger messenger( | |
| [&received_scancode, &handled_message, &unhandled_message]( | |
| const std::string& channel, const uint8_t* message, | |
| size_t message_size, BinaryReply reply) {}); | |
| int redispatch_scancode = 0; | |
| TextInputPlugin handler(&messenger, engine()); | |
| handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false); | |
| handler.ComposeBeginHook(); | |
| std::u16string text; | |
| text.push_back('\n'); | |
| handler.ComposeChangeHook(text, 1); | |
| handler.ComposeEndHook(); | |
| // Passes if it did not crash | |
| } | |
| TEST_F(TextInputPluginTest, ClearClientResetsComposing) { | |
| UseEngineWithView(); | |
| TestBinaryMessenger messenger([](const std::string& channel, | |
| const uint8_t* message, size_t message_size, | |
| BinaryReply reply) {}); | |
| BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; | |
| TextInputPlugin handler(&messenger, engine()); | |
| EXPECT_CALL(*view(), OnResetImeComposing()); | |
| auto& codec = JsonMethodCodec::GetInstance(); | |
| auto message = codec.EncodeMethodCall({"TextInput.clearClient", nullptr}); | |
| messenger.SimulateEngineMessage(kChannelName, message->data(), | |
| message->size(), reply_handler); | |
| } | |
| // Verify that clear client fails if in headless mode. | |
| TEST_F(TextInputPluginTest, ClearClientRequiresView) { | |
| UseHeadlessEngine(); | |
| TestBinaryMessenger messenger([](const std::string& channel, | |
| const uint8_t* message, size_t message_size, | |
| BinaryReply reply) {}); | |
| std::string reply; | |
| BinaryReply reply_handler = [&reply](const uint8_t* reply_bytes, | |
| size_t reply_size) { | |
| reply = std::string(reinterpret_cast<const char*>(reply_bytes), reply_size); | |
| }; | |
| TextInputPlugin handler(&messenger, engine()); | |
| auto& codec = JsonMethodCodec::GetInstance(); | |
| auto message = codec.EncodeMethodCall({"TextInput.clearClient", nullptr}); | |
| messenger.SimulateEngineMessage(kChannelName, message->data(), | |
| message->size(), reply_handler); | |
| EXPECT_EQ(reply, | |
| "[\"Internal Consistency Error\",\"Text input is not available in " | |
| "Windows headless mode\",null]"); | |
| } | |
| // Verify that the embedder sends state update messages to the framework during | |
| // IME composing. | |
| TEST_F(TextInputPluginTest, VerifyComposingSendStateUpdate) { | |
| UseEngineWithView(); | |
| bool sent_message = false; | |
| TestBinaryMessenger messenger( | |
| [&sent_message](const std::string& channel, const uint8_t* message, | |
| size_t message_size, | |
| BinaryReply reply) { sent_message = true; }); | |
| BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; | |
| TextInputPlugin handler(&messenger, engine()); | |
| auto& codec = JsonMethodCodec::GetInstance(); | |
| // Call TextInput.setClient to initialize the TextInputModel. | |
| auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType); | |
| auto& allocator = arguments->GetAllocator(); | |
| arguments->PushBack(kDefaultClientId, allocator); | |
| rapidjson::Value config(rapidjson::kObjectType); | |
| config.AddMember("inputAction", "done", allocator); | |
| config.AddMember("inputType", "text", allocator); | |
| config.AddMember(kEnableDeltaModel, false, allocator); | |
| arguments->PushBack(config, allocator); | |
| auto message = | |
| codec.EncodeMethodCall({"TextInput.setClient", std::move(arguments)}); | |
| messenger.SimulateEngineMessage("flutter/textinput", message->data(), | |
| message->size(), reply_handler); | |
| // ComposeBeginHook should send state update. | |
| sent_message = false; | |
| handler.ComposeBeginHook(); | |
| EXPECT_TRUE(sent_message); | |
| // ComposeChangeHook should send state update. | |
| sent_message = false; | |
| handler.ComposeChangeHook(u"4", 1); | |
| EXPECT_TRUE(sent_message); | |
| // ComposeCommitHook should NOT send state update. | |
| // | |
| // Commit messages are always immediately followed by a change message or an | |
| // end message, both of which will send an update. Sending intermediate state | |
| // with a collapsed composing region will trigger the framework to assume | |
| // composing has ended, which is not the case until a WM_IME_ENDCOMPOSING | |
| // event is received in the main event loop, which will trigger a call to | |
| // ComposeEndHook. | |
| sent_message = false; | |
| handler.ComposeCommitHook(); | |
| EXPECT_FALSE(sent_message); | |
| // ComposeEndHook should send state update. | |
| sent_message = false; | |
| handler.ComposeEndHook(); | |
| EXPECT_TRUE(sent_message); | |
| } | |
| TEST_F(TextInputPluginTest, VerifyInputActionNewlineInsertNewLine) { | |
| UseEngineWithView(); | |
| // Store messages as std::string for convenience. | |
| std::vector<std::string> messages; | |
| TestBinaryMessenger messenger( | |
| [&messages](const std::string& channel, const uint8_t* message, | |
| size_t message_size, BinaryReply reply) { | |
| std::string last_message(reinterpret_cast<const char*>(message), | |
| message_size); | |
| messages.push_back(last_message); | |
| }); | |
| BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; | |
| TextInputPlugin handler(&messenger, engine()); | |
| auto& codec = JsonMethodCodec::GetInstance(); | |
| // Call TextInput.setClient to initialize the TextInputModel. | |
| auto set_client_arguments = | |
| EncodedClientConfig("TextInputType.multiline", "TextInputAction.newline"); | |
| auto message = codec.EncodeMethodCall( | |
| {"TextInput.setClient", std::move(set_client_arguments)}); | |
| messenger.SimulateEngineMessage("flutter/textinput", message->data(), | |
| message->size(), reply_handler); | |
| // Simulate a key down event for '\n'. | |
| handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false); | |
| // Two messages are expected, the first is TextInput.updateEditingState and | |
| // the second is TextInputClient.performAction. | |
| EXPECT_EQ(messages.size(), 2); | |
| // Editing state should have been updated. | |
| auto encoded_arguments = EncodedEditingState("\n", TextRange(1)); | |
| auto update_state_message = codec.EncodeMethodCall( | |
| {kUpdateEditingStateMethod, std::move(encoded_arguments)}); | |
| EXPECT_TRUE(std::equal(update_state_message->begin(), | |
| update_state_message->end(), | |
| messages.front().begin())); | |
| // TextInputClient.performAction should have been called. | |
| auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType); | |
| auto& allocator = arguments->GetAllocator(); | |
| arguments->PushBack(kDefaultClientId, allocator); | |
| arguments->PushBack( | |
| rapidjson::Value("TextInputAction.newline", allocator).Move(), allocator); | |
| auto invoke_action_message = codec.EncodeMethodCall( | |
| {"TextInputClient.performAction", std::move(arguments)}); | |
| EXPECT_TRUE(std::equal(invoke_action_message->begin(), | |
| invoke_action_message->end(), | |
| messages.back().begin())); | |
| } | |
| // Regression test for https://github.com/flutter/flutter/issues/125879. | |
| TEST_F(TextInputPluginTest, VerifyInputActionSendDoesNotInsertNewLine) { | |
| UseEngineWithView(); | |
| std::vector<std::vector<uint8_t>> messages; | |
| TestBinaryMessenger messenger( | |
| [&messages](const std::string& channel, const uint8_t* message, | |
| size_t message_size, BinaryReply reply) { | |
| int length = static_cast<int>(message_size); | |
| std::vector<uint8_t> last_message(length); | |
| memcpy(&last_message[0], &message[0], length * sizeof(uint8_t)); | |
| messages.push_back(last_message); | |
| }); | |
| BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; | |
| TextInputPlugin handler(&messenger, engine()); | |
| auto& codec = JsonMethodCodec::GetInstance(); | |
| // Call TextInput.setClient to initialize the TextInputModel. | |
| auto set_client_arguments = | |
| EncodedClientConfig("TextInputType.multiline", "TextInputAction.send"); | |
| auto message = codec.EncodeMethodCall( | |
| {"TextInput.setClient", std::move(set_client_arguments)}); | |
| messenger.SimulateEngineMessage("flutter/textinput", message->data(), | |
| message->size(), reply_handler); | |
| // Simulate a key down event for '\n'. | |
| handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false); | |
| // Only a call to TextInputClient.performAction is expected. | |
| EXPECT_EQ(messages.size(), 1); | |
| // TextInputClient.performAction should have been called. | |
| auto arguments = std::make_unique<rapidjson::Document>(rapidjson::kArrayType); | |
| auto& allocator = arguments->GetAllocator(); | |
| arguments->PushBack(kDefaultClientId, allocator); | |
| arguments->PushBack( | |
| rapidjson::Value("TextInputAction.send", allocator).Move(), allocator); | |
| auto invoke_action_message = codec.EncodeMethodCall( | |
| {"TextInputClient.performAction", std::move(arguments)}); | |
| EXPECT_TRUE(std::equal(invoke_action_message->begin(), | |
| invoke_action_message->end(), | |
| messages.front().begin())); | |
| } | |
| TEST_F(TextInputPluginTest, TextEditingWorksWithDeltaModel) { | |
| UseEngineWithView(); | |
| auto handled_message = CreateResponse(true); | |
| auto unhandled_message = CreateResponse(false); | |
| int received_scancode = 0; | |
| TestBinaryMessenger messenger( | |
| [&received_scancode, &handled_message, &unhandled_message]( | |
| const std::string& channel, const uint8_t* message, | |
| size_t message_size, BinaryReply reply) {}); | |
| int redispatch_scancode = 0; | |
| TextInputPlugin handler(&messenger, engine()); | |
| auto args = std::make_unique<rapidjson::Document>(rapidjson::kArrayType); | |
| auto& allocator = args->GetAllocator(); | |
| args->PushBack(123, allocator); // client_id | |
| rapidjson::Value client_config(rapidjson::kObjectType); | |
| client_config.AddMember(kEnableDeltaModel, true, allocator); | |
| args->PushBack(client_config, allocator); | |
| auto encoded = JsonMethodCodec::GetInstance().EncodeMethodCall( | |
| MethodCall<rapidjson::Document>(kSetClientMethod, std::move(args))); | |
| EXPECT_TRUE(messenger.SimulateEngineMessage( | |
| kChannelName, encoded->data(), encoded->size(), | |
| [](const uint8_t* reply, size_t reply_size) {})); | |
| handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false); | |
| handler.ComposeBeginHook(); | |
| std::u16string text; | |
| text.push_back('\n'); | |
| handler.ComposeChangeHook(text, 1); | |
| handler.ComposeEndHook(); | |
| handler.KeyboardHook(0x4E, 100, WM_KEYDOWN, 'n', false, false); | |
| handler.ComposeBeginHook(); | |
| std::u16string textN; | |
| text.push_back('n'); | |
| handler.ComposeChangeHook(textN, 1); | |
| handler.KeyboardHook(0x49, 100, WM_KEYDOWN, 'i', false, false); | |
| std::u16string textNi; | |
| text.push_back('n'); | |
| text.push_back('i'); | |
| handler.ComposeChangeHook(textNi, 2); | |
| handler.KeyboardHook(VK_RETURN, 100, WM_KEYDOWN, '\n', false, false); | |
| std::u16string textChineseCharacter; | |
| text.push_back(u'\u4F60'); | |
| handler.ComposeChangeHook(textChineseCharacter, 1); | |
| handler.ComposeCommitHook(); | |
| handler.ComposeEndHook(); | |
| // Passes if it did not crash | |
| } | |
| // Regression test for https://github.com/flutter/flutter/issues/123749 | |
| TEST_F(TextInputPluginTest, CompositionCursorPos) { | |
| UseEngineWithView(); | |
| int selection_base = -1; | |
| TestBinaryMessenger messenger([&](const std::string& channel, | |
| const uint8_t* message, size_t size, | |
| BinaryReply reply) { | |
| auto method = JsonMethodCodec::GetInstance().DecodeMethodCall( | |
| std::vector<uint8_t>(message, message + size)); | |
| if (method->method_name() == kUpdateEditingStateMethod) { | |
| const auto& args = *method->arguments(); | |
| const auto& editing_state = args[1]; | |
| auto base = editing_state.FindMember(kSelectionBaseKey); | |
| auto extent = editing_state.FindMember(kSelectionExtentKey); | |
| ASSERT_NE(base, editing_state.MemberEnd()); | |
| ASSERT_TRUE(base->value.IsInt()); | |
| ASSERT_NE(extent, editing_state.MemberEnd()); | |
| ASSERT_TRUE(extent->value.IsInt()); | |
| selection_base = base->value.GetInt(); | |
| EXPECT_EQ(extent->value.GetInt(), selection_base); | |
| } | |
| }); | |
| TextInputPlugin plugin(&messenger, engine()); | |
| auto args = std::make_unique<rapidjson::Document>(rapidjson::kArrayType); | |
| auto& allocator = args->GetAllocator(); | |
| args->PushBack(123, allocator); // client_id | |
| rapidjson::Value client_config(rapidjson::kObjectType); | |
| args->PushBack(client_config, allocator); | |
| auto encoded = JsonMethodCodec::GetInstance().EncodeMethodCall( | |
| MethodCall<rapidjson::Document>(kSetClientMethod, std::move(args))); | |
| EXPECT_TRUE(messenger.SimulateEngineMessage( | |
| kChannelName, encoded->data(), encoded->size(), | |
| [](const uint8_t* reply, size_t reply_size) {})); | |
| plugin.ComposeBeginHook(); | |
| EXPECT_EQ(selection_base, 0); | |
| plugin.ComposeChangeHook(u"abc", 3); | |
| EXPECT_EQ(selection_base, 3); | |
| plugin.ComposeCommitHook(); | |
| plugin.ComposeEndHook(); | |
| EXPECT_EQ(selection_base, 3); | |
| plugin.ComposeBeginHook(); | |
| plugin.ComposeChangeHook(u"1", 1); | |
| EXPECT_EQ(selection_base, 4); | |
| plugin.ComposeChangeHook(u"12", 2); | |
| EXPECT_EQ(selection_base, 5); | |
| plugin.ComposeChangeHook(u"12", 1); | |
| EXPECT_EQ(selection_base, 4); | |
| plugin.ComposeChangeHook(u"12", 2); | |
| EXPECT_EQ(selection_base, 5); | |
| } | |
| TEST_F(TextInputPluginTest, TransformCursorRect) { | |
| UseEngineWithView(); | |
| // A position of `EditableText`. | |
| double view_x = 100; | |
| double view_y = 200; | |
| // A position and size of marked text, in `EditableText` local coordinates. | |
| double ime_x = 3; | |
| double ime_y = 4; | |
| double ime_width = 50; | |
| double ime_height = 60; | |
| // Transformation matrix. | |
| std::array<std::array<double, 4>, 4> editabletext_transform = { | |
| 1.0, 0.0, 0.0, view_x, // | |
| 0.0, 1.0, 0.0, view_y, // | |
| 0.0, 0.0, 0.0, 0.0, // | |
| 0.0, 0.0, 0.0, 1.0}; | |
| TestBinaryMessenger messenger([](const std::string& channel, | |
| const uint8_t* message, size_t message_size, | |
| BinaryReply reply) {}); | |
| BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {}; | |
| TextInputPlugin handler(&messenger, engine()); | |
| auto& codec = JsonMethodCodec::GetInstance(); | |
| EXPECT_CALL(*view(), OnCursorRectUpdated(Rect{{view_x, view_y}, {0, 0}})); | |
| { | |
| auto arguments = | |
| std::make_unique<rapidjson::Document>(rapidjson::kObjectType); | |
| auto& allocator = arguments->GetAllocator(); | |
| rapidjson::Value transoform(rapidjson::kArrayType); | |
| for (int i = 0; i < 4 * 4; i++) { | |
| // Pack 2-dimensional array by column-major order. | |
| transoform.PushBack(editabletext_transform[i % 4][i / 4], allocator); | |
| } | |
| arguments->AddMember("transform", transoform, allocator); | |
| auto message = codec.EncodeMethodCall( | |
| {"TextInput.setEditableSizeAndTransform", std::move(arguments)}); | |
| messenger.SimulateEngineMessage(kChannelName, message->data(), | |
| message->size(), reply_handler); | |
| } | |
| EXPECT_CALL(*view(), | |
| OnCursorRectUpdated(Rect{{view_x + ime_x, view_y + ime_y}, | |
| {ime_width, ime_height}})); | |
| { | |
| auto arguments = | |
| std::make_unique<rapidjson::Document>(rapidjson::kObjectType); | |
| auto& allocator = arguments->GetAllocator(); | |
| arguments->AddMember("x", ime_x, allocator); | |
| arguments->AddMember("y", ime_y, allocator); | |
| arguments->AddMember("width", ime_width, allocator); | |
| arguments->AddMember("height", ime_height, allocator); | |
| auto message = codec.EncodeMethodCall( | |
| {"TextInput.setMarkedTextRect", std::move(arguments)}); | |
| messenger.SimulateEngineMessage(kChannelName, message->data(), | |
| message->size(), reply_handler); | |
| } | |
| } | |
| TEST_F(TextInputPluginTest, SetMarkedTextRectRequiresView) { | |
| UseHeadlessEngine(); | |
| TestBinaryMessenger messenger([](const std::string& channel, | |
| const uint8_t* message, size_t message_size, | |
| BinaryReply reply) {}); | |
| std::string reply; | |
| BinaryReply reply_handler = [&reply](const uint8_t* reply_bytes, | |
| size_t reply_size) { | |
| reply = std::string(reinterpret_cast<const char*>(reply_bytes), reply_size); | |
| }; | |
| TextInputPlugin handler(&messenger, engine()); | |
| auto& codec = JsonMethodCodec::GetInstance(); | |
| auto arguments = | |
| std::make_unique<rapidjson::Document>(rapidjson::kObjectType); | |
| auto& allocator = arguments->GetAllocator(); | |
| arguments->AddMember("x", 0, allocator); | |
| arguments->AddMember("y", 0, allocator); | |
| arguments->AddMember("width", 0, allocator); | |
| arguments->AddMember("height", 0, allocator); | |
| auto message = codec.EncodeMethodCall( | |
| {"TextInput.setMarkedTextRect", std::move(arguments)}); | |
| messenger.SimulateEngineMessage(kChannelName, message->data(), | |
| message->size(), reply_handler); | |
| EXPECT_EQ(reply, | |
| "[\"Internal Consistency Error\",\"Text input is not available in " | |
| "Windows headless mode\",null]"); | |
| } | |
| } // namespace testing | |
| } // namespace flutter |