blob: d1fb6ee671b1dc51fc3069823ce70680b22b0260 [file] [log] [blame]
// 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