blob: d01fa7642f8ba793ccdf1d80d448f73dac49bd35 [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 <utility>
#include "flutter/shell/platform/linux/fl_method_codec_private.h"
#include "flutter/shell/platform/linux/fl_text_input_plugin.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_method_codec.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h"
#include "flutter/shell/platform/linux/testing/fl_test.h"
#include "flutter/shell/platform/linux/testing/mock_binary_messenger.h"
#include "flutter/shell/platform/linux/testing/mock_binary_messenger_response_handle.h"
#include "flutter/shell/platform/linux/testing/mock_im_context.h"
#include "flutter/shell/platform/linux/testing/mock_text_input_view_delegate.h"
#include "flutter/testing/testing.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
void printTo(FlMethodResponse* response, ::std::ostream* os) {
*os << ::testing::PrintToString(
fl_method_response_get_result(response, nullptr));
}
MATCHER_P(SuccessResponse, result, "") {
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(FlMethodResponse) response =
fl_method_codec_decode_response(FL_METHOD_CODEC(codec), arg, nullptr);
if (fl_value_equal(fl_method_response_get_result(response, nullptr),
result)) {
return true;
}
*result_listener << ::testing::PrintToString(response);
return false;
}
MATCHER_P(FlValueEq, value, "equal to " + ::testing::PrintToString(value)) {
return fl_value_equal(arg, value);
}
class MethodCallMatcher {
public:
using is_gtest_matcher = void;
explicit MethodCallMatcher(::testing::Matcher<std::string> name,
::testing::Matcher<FlValue*> args)
: name_(std::move(name)), args_(std::move(args)) {}
bool MatchAndExplain(GBytes* method_call,
::testing::MatchResultListener* result_listener) const {
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GError) error = nullptr;
g_autofree gchar* name = nullptr;
g_autoptr(FlValue) args = nullptr;
gboolean result = fl_method_codec_decode_method_call(
FL_METHOD_CODEC(codec), method_call, &name, &args, &error);
if (!result) {
*result_listener << ::testing::PrintToString(error->message);
return false;
}
if (!name_.MatchAndExplain(name, result_listener)) {
*result_listener << " where the name doesn't match: \"" << name << "\"";
return false;
}
if (!args_.MatchAndExplain(args, result_listener)) {
*result_listener << " where the args don't match: "
<< ::testing::PrintToString(args);
return false;
}
return true;
}
void DescribeTo(std::ostream* os) const {
*os << "method name ";
name_.DescribeTo(os);
*os << " and args ";
args_.DescribeTo(os);
}
void DescribeNegationTo(std::ostream* os) const {
*os << "method name ";
name_.DescribeNegationTo(os);
*os << " or args ";
args_.DescribeNegationTo(os);
}
private:
::testing::Matcher<std::string> name_;
::testing::Matcher<FlValue*> args_;
};
::testing::Matcher<GBytes*> MethodCall(const std::string& name,
::testing::Matcher<FlValue*> args) {
return MethodCallMatcher(::testing::StrEq(name), std::move(args));
}
static FlValue* build_map(std::map<const gchar*, FlValue*> args) {
FlValue* value = fl_value_new_map();
for (auto it = args.begin(); it != args.end(); ++it) {
fl_value_set_string_take(value, it->first, it->second);
}
return value;
}
static FlValue* build_list(std::vector<FlValue*> args) {
FlValue* value = fl_value_new_list();
for (auto it = args.begin(); it != args.end(); ++it) {
fl_value_append_take(value, *it);
}
return value;
}
struct InputConfig {
int64_t client_id = -1;
const gchar* input_type = "TextInputType.text";
const gchar* input_action = "TextInputAction.none";
gboolean enable_delta_model = false;
};
static FlValue* build_input_config(InputConfig config) {
return build_list({
fl_value_new_int(config.client_id),
build_map({
{"inputAction", fl_value_new_string(config.input_action)},
{"inputType", build_map({
{"name", fl_value_new_string(config.input_type)},
})},
{"enableDeltaModel", fl_value_new_bool(config.enable_delta_model)},
}),
});
}
struct EditingState {
const gchar* text = "";
int selection_base = -1;
int selection_extent = -1;
int composing_base = -1;
int composing_extent = -1;
};
static FlValue* build_editing_state(EditingState state) {
return build_map({
{"text", fl_value_new_string(state.text)},
{"selectionBase", fl_value_new_int(state.selection_base)},
{"selectionExtent", fl_value_new_int(state.selection_extent)},
{"selectionAffinity", fl_value_new_string("TextAffinity.downstream")},
{"selectionIsDirectional", fl_value_new_bool(false)},
{"composingBase", fl_value_new_int(state.composing_base)},
{"composingExtent", fl_value_new_int(state.composing_extent)},
});
}
struct EditingDelta {
const gchar* old_text = "";
const gchar* delta_text = "";
int delta_start = -1;
int delta_end = -1;
int selection_base = -1;
int selection_extent = -1;
int composing_base = -1;
int composing_extent = -1;
};
static FlValue* build_editing_delta(EditingDelta delta) {
return build_map({
{"oldText", fl_value_new_string(delta.old_text)},
{"deltaText", fl_value_new_string(delta.delta_text)},
{"deltaStart", fl_value_new_int(delta.delta_start)},
{"deltaEnd", fl_value_new_int(delta.delta_end)},
{"selectionBase", fl_value_new_int(delta.selection_base)},
{"selectionExtent", fl_value_new_int(delta.selection_extent)},
{"selectionAffinity", fl_value_new_string("TextAffinity.downstream")},
{"selectionIsDirectional", fl_value_new_bool(false)},
{"composingBase", fl_value_new_int(delta.composing_base)},
{"composingExtent", fl_value_new_int(delta.composing_extent)},
});
}
static void send_key_event(FlTextInputPlugin* plugin,
gint keyval,
gint state = 0) {
GdkEvent* gdk_event = gdk_event_new(GDK_KEY_PRESS);
gdk_event->key.keyval = keyval;
gdk_event->key.state = state;
FlKeyEvent* key_event = fl_key_event_new_from_gdk_event(gdk_event);
fl_text_input_plugin_filter_keypress(plugin, key_event);
fl_key_event_dispose(key_event);
}
TEST(FlTextInputPluginTest, MessageHandler) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
EXPECT_TRUE(messenger.HasMessageHandler("flutter/textinput"));
}
TEST(FlTextInputPluginTest, SetClient) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
g_autoptr(FlValue) args = build_input_config({.client_id = 1});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", message);
}
TEST(FlTextInputPluginTest, Show) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
EXPECT_CALL(context,
gtk_im_context_focus_in(::testing::Eq<GtkIMContext*>(context)));
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.show", nullptr, nullptr);
messenger.ReceiveMessage("flutter/textinput", message);
}
TEST(FlTextInputPluginTest, Hide) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
EXPECT_CALL(context,
gtk_im_context_focus_out(::testing::Eq<GtkIMContext*>(context)));
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.hide", nullptr, nullptr);
messenger.ReceiveMessage("flutter/textinput", message);
}
TEST(FlTextInputPluginTest, ClearClient) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.clearClient", nullptr, nullptr);
messenger.ReceiveMessage("flutter/textinput", message);
}
TEST(FlTextInputPluginTest, PerformAction) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
// set input config
g_autoptr(FlValue) config = build_input_config({
.client_id = 1,
.input_type = "TextInputType.multiline",
.input_action = "TextInputAction.newline",
});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_client);
// set editing state
g_autoptr(FlValue) state = build_editing_state({
.text = "Flutter",
.selection_base = 7,
.selection_extent = 7,
});
g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_state);
// update editing state
g_autoptr(FlValue) new_state = build_list({
fl_value_new_int(1), // client_id
build_editing_state({
.text = "Flutter\n",
.selection_base = 8,
.selection_extent = 8,
}),
});
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingState",
FlValueEq(new_state)),
::testing::_, ::testing::_, ::testing::_));
// perform action
g_autoptr(FlValue) action = build_list({
fl_value_new_int(1), // client_id
fl_value_new_string("TextInputAction.newline"),
});
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.performAction",
FlValueEq(action)),
::testing::_, ::testing::_, ::testing::_));
send_key_event(plugin, GDK_KEY_Return);
}
// Regression test for https://github.com/flutter/flutter/issues/125879.
TEST(FlTextInputPluginTest, MultilineWithSendAction) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
// Set input config.
g_autoptr(FlValue) config = build_input_config({
.client_id = 1,
.input_type = "TextInputType.multiline",
.input_action = "TextInputAction.send",
});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_client);
// Set editing state.
g_autoptr(FlValue) state = build_editing_state({
.text = "Flutter",
.selection_base = 7,
.selection_extent = 7,
});
g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_state);
// Perform action.
g_autoptr(FlValue) action = build_list({
fl_value_new_int(1), // client_id
fl_value_new_string("TextInputAction.send"),
});
// Because the input action is not set to TextInputAction.newline, the next
// expected call is "TextInputClient.performAction". If the input action was
// set to TextInputAction.newline the next call would be
// "TextInputClient.updateEditingState" (this case is tested in the test named
// 'PerformAction').
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.performAction",
FlValueEq(action)),
::testing::_, ::testing::_, ::testing::_));
send_key_event(plugin, GDK_KEY_Return);
}
TEST(FlTextInputPluginTest, MoveCursor) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
// set input config
g_autoptr(FlValue) config = build_input_config({.client_id = 1});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_client);
// set editing state
g_autoptr(FlValue) state = build_editing_state({
.text = "Flutter",
.selection_base = 4,
.selection_extent = 4,
});
g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_state);
// move cursor to beginning
g_autoptr(FlValue) beginning = build_list({
fl_value_new_int(1), // client_id
build_editing_state({
.text = "Flutter",
.selection_base = 0,
.selection_extent = 0,
}),
});
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingState",
FlValueEq(beginning)),
::testing::_, ::testing::_, ::testing::_));
send_key_event(plugin, GDK_KEY_Home);
// move cursor to end
g_autoptr(FlValue) end = build_list({
fl_value_new_int(1), // client_id
build_editing_state({
.text = "Flutter",
.selection_base = 7,
.selection_extent = 7,
}),
});
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingState",
FlValueEq(end)),
::testing::_, ::testing::_, ::testing::_));
send_key_event(plugin, GDK_KEY_End);
}
TEST(FlTextInputPluginTest, Select) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
// set input config
g_autoptr(FlValue) config = build_input_config({.client_id = 1});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_client);
// set editing state
g_autoptr(FlValue) state = build_editing_state({
.text = "Flutter",
.selection_base = 4,
.selection_extent = 4,
});
g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_state);
// select to end
g_autoptr(FlValue) select_to_end = build_list({
fl_value_new_int(1), // client_id
build_editing_state({
.text = "Flutter",
.selection_base = 4,
.selection_extent = 7,
}),
});
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingState",
FlValueEq(select_to_end)),
::testing::_, ::testing::_, ::testing::_));
send_key_event(plugin, GDK_KEY_End, GDK_SHIFT_MASK);
// select to beginning
g_autoptr(FlValue) select_to_beginning = build_list({
fl_value_new_int(1), // client_id
build_editing_state({
.text = "Flutter",
.selection_base = 4,
.selection_extent = 0,
}),
});
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingState",
FlValueEq(select_to_beginning)),
::testing::_, ::testing::_, ::testing::_));
send_key_event(plugin, GDK_KEY_Home, GDK_SHIFT_MASK);
}
TEST(FlTextInputPluginTest, Composing) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
g_signal_emit_by_name(context, "preedit-start", nullptr);
// update
EXPECT_CALL(context,
gtk_im_context_get_preedit_string(
::testing::Eq<GtkIMContext*>(context),
::testing::A<gchar**>(), ::testing::_, ::testing::A<gint*>()))
.WillOnce(
::testing::DoAll(::testing::SetArgPointee<1>(g_strdup("Flutter")),
::testing::SetArgPointee<3>(0)));
g_autoptr(FlValue) state = build_list({
fl_value_new_int(-1), // client_id
build_editing_state({
.text = "Flutter",
.selection_base = 0,
.selection_extent = 0,
.composing_base = 0,
.composing_extent = 7,
}),
});
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingState",
FlValueEq(state)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "preedit-changed", nullptr);
// commit
g_autoptr(FlValue) commit = build_list({
fl_value_new_int(-1), // client_id
build_editing_state({
.text = "engine",
.selection_base = 6,
.selection_extent = 6,
}),
});
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingState",
FlValueEq(commit)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "commit", "engine", nullptr);
// end
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingState",
::testing::_),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "preedit-end", nullptr);
}
TEST(FlTextInputPluginTest, SurroundingText) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
// set input config
g_autoptr(FlValue) config = build_input_config({.client_id = 1});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_client);
// set editing state
g_autoptr(FlValue) state = build_editing_state({
.text = "Flutter",
.selection_base = 3,
.selection_extent = 3,
});
g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_state);
// retrieve
EXPECT_CALL(context, gtk_im_context_set_surrounding(
::testing::Eq<GtkIMContext*>(context),
::testing::StrEq("Flutter"), 7, 3));
gboolean retrieved = false;
g_signal_emit_by_name(context, "retrieve-surrounding", &retrieved, nullptr);
EXPECT_TRUE(retrieved);
// delete
g_autoptr(FlValue) update = build_list({
fl_value_new_int(1), // client_id
build_editing_state({
.text = "Flutr",
.selection_base = 3,
.selection_extent = 3,
}),
});
EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingState",
FlValueEq(update)),
::testing::_, ::testing::_, ::testing::_));
gboolean deleted = false;
g_signal_emit_by_name(context, "delete-surrounding", 1, 2, &deleted, nullptr);
EXPECT_TRUE(deleted);
}
TEST(FlTextInputPluginTest, SetMarkedTextRect) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
g_signal_emit_by_name(context, "preedit-start", nullptr);
// set editable size and transform
g_autoptr(FlValue) size_and_transform = build_map({
{
"transform",
build_list({
fl_value_new_float(1),
fl_value_new_float(2),
fl_value_new_float(3),
fl_value_new_float(4),
fl_value_new_float(5),
fl_value_new_float(6),
fl_value_new_float(7),
fl_value_new_float(8),
fl_value_new_float(9),
fl_value_new_float(10),
fl_value_new_float(11),
fl_value_new_float(12),
fl_value_new_float(13),
fl_value_new_float(14),
fl_value_new_float(15),
fl_value_new_float(16),
}),
},
});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_editable_size_and_transform =
fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setEditableSizeAndTransform",
size_and_transform, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput",
set_editable_size_and_transform);
// set marked text rect
g_autoptr(FlValue) rect = build_map({
{"x", fl_value_new_float(1)},
{"y", fl_value_new_float(2)},
{"width", fl_value_new_float(3)},
{"height", fl_value_new_float(4)},
});
g_autoptr(GBytes) set_marked_text_rect = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setMarkedTextRect", rect, nullptr);
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
EXPECT_CALL(delegate, fl_text_input_view_delegate_translate_coordinates(
::testing::Eq<FlTextInputViewDelegate*>(delegate),
::testing::Eq(27), ::testing::Eq(32), ::testing::_,
::testing::_))
.WillOnce(::testing::DoAll(::testing::SetArgPointee<3>(123),
::testing::SetArgPointee<4>(456)));
EXPECT_CALL(context, gtk_im_context_set_cursor_location(
::testing::Eq<GtkIMContext*>(context),
::testing::Pointee(::testing::AllOf(
::testing::Field(&GdkRectangle::x, 123),
::testing::Field(&GdkRectangle::y, 456),
::testing::Field(&GdkRectangle::width, 0),
::testing::Field(&GdkRectangle::height, 0)))));
messenger.ReceiveMessage("flutter/textinput", set_marked_text_rect);
}
TEST(FlTextInputPluginTest, TextInputTypeNone) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
g_autoptr(FlValue) args = build_input_config({
.client_id = 1,
.input_type = "TextInputType.none",
});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::A<FlBinaryMessengerResponseHandle*>(),
SuccessResponse(null), ::testing::A<GError**>()))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_client);
EXPECT_CALL(context,
gtk_im_context_focus_in(::testing::Eq<GtkIMContext*>(context)))
.Times(0);
EXPECT_CALL(context,
gtk_im_context_focus_out(::testing::Eq<GtkIMContext*>(context)));
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
g_autoptr(GBytes) show = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.show", nullptr, nullptr);
messenger.ReceiveMessage("flutter/textinput", show);
}
TEST(FlTextInputPluginTest, TextEditingDelta) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
// set config
g_autoptr(FlValue) args = build_input_config({
.client_id = 1,
.enable_delta_model = true,
});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::A<FlBinaryMessengerResponseHandle*>(),
SuccessResponse(null), ::testing::A<GError**>()))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_client);
// set editing state
g_autoptr(FlValue) state = build_editing_state({
.text = "Flutter",
.selection_base = 7,
.selection_extent = 7,
});
g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::_, SuccessResponse(null), ::testing::_))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_state);
// update editing state with deltas
g_autoptr(FlValue) deltas = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "Flutter",
.delta_text = "Flutter",
.delta_start = 7,
.delta_end = 7,
.selection_base = 0,
.selection_extent = 0,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(deltas)),
::testing::_, ::testing::_, ::testing::_));
send_key_event(plugin, GDK_KEY_Home);
}
TEST(FlTextInputPluginTest, ComposingDelta) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
// set config
g_autoptr(FlValue) args = build_input_config({
.client_id = 1,
.enable_delta_model = true,
});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::A<FlBinaryMessengerResponseHandle*>(),
SuccessResponse(null), ::testing::A<GError**>()))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_client);
g_signal_emit_by_name(context, "preedit-start", nullptr);
// update
EXPECT_CALL(context,
gtk_im_context_get_preedit_string(
::testing::Eq<GtkIMContext*>(context),
::testing::A<gchar**>(), ::testing::_, ::testing::A<gint*>()))
.WillOnce(
::testing::DoAll(::testing::SetArgPointee<1>(g_strdup("Flutter ")),
::testing::SetArgPointee<3>(8)));
g_autoptr(FlValue) update = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "",
.delta_text = "Flutter ",
.delta_start = 0,
.delta_end = 0,
.selection_base = 8,
.selection_extent = 8,
.composing_base = 0,
.composing_extent = 8,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(update)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "preedit-changed", nullptr);
// commit
g_autoptr(FlValue) commit = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "Flutter ",
.delta_text = "Flutter engine",
.delta_start = 0,
.delta_end = 8,
.selection_base = 14,
.selection_extent = 14,
.composing_base = -1,
.composing_extent = -1,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(commit)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "commit", "Flutter engine", nullptr);
// end
g_autoptr(FlValue) end = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "Flutter engine",
.selection_base = 14,
.selection_extent = 14,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(end)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "preedit-end", nullptr);
}
TEST(FlTextInputPluginTest, NonComposingDelta) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
::testing::NiceMock<flutter::testing::MockIMContext> context;
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
g_autoptr(FlTextInputPlugin) plugin =
fl_text_input_plugin_new(messenger, context, delegate);
EXPECT_NE(plugin, nullptr);
// set config
g_autoptr(FlValue) args = build_input_config({
.client_id = 1,
.enable_delta_model = true,
});
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);
g_autoptr(FlValue) null = fl_value_new_null();
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::A<FlBinaryMessengerResponseHandle*>(),
SuccessResponse(null), ::testing::A<GError**>()))
.WillOnce(::testing::Return(true));
messenger.ReceiveMessage("flutter/textinput", set_client);
// commit F
g_autoptr(FlValue) commit = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "",
.delta_text = "F",
.delta_start = 0,
.delta_end = 0,
.selection_base = 1,
.selection_extent = 1,
.composing_base = -1,
.composing_extent = -1,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(commit)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "commit", "F", nullptr);
// commit l
g_autoptr(FlValue) commitL = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "F",
.delta_text = "l",
.delta_start = 1,
.delta_end = 1,
.selection_base = 2,
.selection_extent = 2,
.composing_base = -1,
.composing_extent = -1,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(commitL)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "commit", "l", nullptr);
// commit u
g_autoptr(FlValue) commitU = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "Fl",
.delta_text = "u",
.delta_start = 2,
.delta_end = 2,
.selection_base = 3,
.selection_extent = 3,
.composing_base = -1,
.composing_extent = -1,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(commitU)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "commit", "u", nullptr);
// commit t
g_autoptr(FlValue) commitTa = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "Flu",
.delta_text = "t",
.delta_start = 3,
.delta_end = 3,
.selection_base = 4,
.selection_extent = 4,
.composing_base = -1,
.composing_extent = -1,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(commitTa)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "commit", "t", nullptr);
// commit t again
g_autoptr(FlValue) commitTb = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "Flut",
.delta_text = "t",
.delta_start = 4,
.delta_end = 4,
.selection_base = 5,
.selection_extent = 5,
.composing_base = -1,
.composing_extent = -1,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(commitTb)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "commit", "t", nullptr);
// commit e
g_autoptr(FlValue) commitE = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "Flutt",
.delta_text = "e",
.delta_start = 5,
.delta_end = 5,
.selection_base = 6,
.selection_extent = 6,
.composing_base = -1,
.composing_extent = -1,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(commitE)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "commit", "e", nullptr);
// commit r
g_autoptr(FlValue) commitR = build_list({
fl_value_new_int(1), // client_id
build_map({{
"deltas",
build_list({
build_editing_delta({
.old_text = "Flutte",
.delta_text = "r",
.delta_start = 6,
.delta_end = 6,
.selection_base = 7,
.selection_extent = 7,
.composing_base = -1,
.composing_extent = -1,
}),
}),
}}),
});
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/textinput"),
MethodCall("TextInputClient.updateEditingStateWithDeltas",
FlValueEq(commitR)),
::testing::_, ::testing::_, ::testing::_));
g_signal_emit_by_name(context, "commit", "r", nullptr);
}