mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[Linux] Test text input (flutter/engine#34186)
This PR finishes what #33661 and #33111 started - most of `FlTextInputPlugin` is now covered by tests.
This commit is contained in:
parent
9b4d1ccd49
commit
b7bfa342dd
@ -226,6 +226,7 @@ executable("flutter_linux_unittests") {
|
||||
"testing/mock_binary_messenger_response_handle.cc",
|
||||
"testing/mock_engine.cc",
|
||||
"testing/mock_epoxy.cc",
|
||||
"testing/mock_im_context.cc",
|
||||
"testing/mock_plugin_registrar.cc",
|
||||
"testing/mock_renderer.cc",
|
||||
"testing/mock_settings.cc",
|
||||
|
||||
@ -80,14 +80,8 @@ struct FlTextInputPluginPrivate {
|
||||
// Input method.
|
||||
GtkIMContext* im_context;
|
||||
|
||||
// IM filter.
|
||||
FlTextInputPluginImFilter im_filter;
|
||||
|
||||
flutter::TextInputModel* text_model;
|
||||
|
||||
// The owning native window.
|
||||
GdkWindow* window;
|
||||
|
||||
// A 4x4 matrix that maps from `EditableText` local coordinates to the
|
||||
// coordinate system of `PipelineOwner.rootNode`.
|
||||
double editabletext_transform[4][4];
|
||||
@ -216,8 +210,8 @@ static void update_editing_state_with_delta(FlTextInputPlugin* self,
|
||||
|
||||
g_autoptr(FlValue) deltas = fl_value_new_list();
|
||||
fl_value_append(deltas, deltaValue);
|
||||
FlValue* value = fl_value_new_map();
|
||||
fl_value_set_string_take(value, "deltas", deltas);
|
||||
g_autoptr(FlValue) value = fl_value_new_map();
|
||||
fl_value_set_string(value, "deltas", deltas);
|
||||
|
||||
fl_value_append(args, value);
|
||||
|
||||
@ -258,9 +252,6 @@ static void im_preedit_start_cb(FlTextInputPlugin* self) {
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
priv->text_model->BeginComposing();
|
||||
|
||||
// Set the native window used for system input method windows.
|
||||
gtk_im_context_set_client_window(priv->im_context, priv->window);
|
||||
}
|
||||
|
||||
// Signal handler for GtkIMContext::preedit-changed
|
||||
@ -418,9 +409,6 @@ static FlMethodResponse* show(FlTextInputPlugin* self) {
|
||||
return hide(self);
|
||||
}
|
||||
|
||||
// Set the native window used for system input method windows.
|
||||
gtk_im_context_set_client_window(priv->im_context, priv->window);
|
||||
|
||||
gtk_im_context_focus_in(priv->im_context);
|
||||
|
||||
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
@ -599,7 +587,6 @@ static void fl_text_input_plugin_dispose(GObject* object) {
|
||||
delete priv->text_model;
|
||||
priv->text_model = nullptr;
|
||||
}
|
||||
priv->window = nullptr;
|
||||
|
||||
G_OBJECT_CLASS(fl_text_input_plugin_parent_class)->dispose(object);
|
||||
}
|
||||
@ -617,7 +604,8 @@ static gboolean fl_text_input_plugin_filter_keypress_default(
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (priv->im_filter(priv->im_context, event->origin)) {
|
||||
GdkEventKey* key_event = reinterpret_cast<GdkEventKey*>(event->origin);
|
||||
if (gtk_im_context_filter_keypress(priv->im_context, key_event)) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@ -698,12 +686,20 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
|
||||
priv->client_id = kClientIdUnset;
|
||||
priv->im_context = gtk_im_multicontext_new();
|
||||
priv->input_type = FL_TEXT_INPUT_TYPE_TEXT;
|
||||
priv->text_model = new flutter::TextInputModel();
|
||||
}
|
||||
|
||||
static void init_im_context(FlTextInputPlugin* self, GtkIMContext* im_context) {
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
priv->im_context = GTK_IM_CONTEXT(g_object_ref(im_context));
|
||||
|
||||
// On Wayland, this call sets up the input method so it can be enabled
|
||||
// immediately when required. Without it, on-screen keyboard's don't come up
|
||||
// the first time a text field is focused.
|
||||
gtk_im_context_focus_out(priv->im_context);
|
||||
priv->input_type = FL_TEXT_INPUT_TYPE_TEXT;
|
||||
|
||||
g_signal_connect_object(priv->im_context, "preedit-start",
|
||||
G_CALLBACK(im_preedit_start_cb), self,
|
||||
G_CONNECT_SWAPPED);
|
||||
@ -721,15 +717,12 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
|
||||
g_signal_connect_object(priv->im_context, "delete-surrounding",
|
||||
G_CALLBACK(im_delete_surrounding_cb), self,
|
||||
G_CONNECT_SWAPPED);
|
||||
priv->text_model = new flutter::TextInputModel();
|
||||
}
|
||||
|
||||
FlTextInputPlugin* fl_text_input_plugin_new(
|
||||
FlBinaryMessenger* messenger,
|
||||
GdkWindow* window,
|
||||
FlTextInputPluginImFilter im_filter) {
|
||||
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger,
|
||||
GtkIMContext* im_context) {
|
||||
g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
|
||||
g_return_val_if_fail(im_filter != nullptr, nullptr);
|
||||
g_return_val_if_fail(GTK_IS_IM_CONTEXT(im_context), nullptr);
|
||||
|
||||
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(
|
||||
g_object_new(fl_text_input_plugin_get_type(), nullptr));
|
||||
@ -741,8 +734,8 @@ FlTextInputPlugin* fl_text_input_plugin_new(
|
||||
fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec));
|
||||
fl_method_channel_set_method_call_handler(priv->channel, method_call_cb, self,
|
||||
nullptr);
|
||||
priv->window = window;
|
||||
priv->im_filter = im_filter;
|
||||
|
||||
init_im_context(self, im_context);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@ -10,19 +10,6 @@
|
||||
#include "flutter/shell/platform/linux/fl_key_event.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h"
|
||||
|
||||
/**
|
||||
* FlTextInputPluginImFilter:
|
||||
* @event: the pointer to the GdkEventKey.
|
||||
*
|
||||
* The signature for a callback with which a #FlTextInputPlugin allow an input
|
||||
* method to internally handle key press and release events.
|
||||
*
|
||||
* The #gdk_event is an opaque pointer. It will be GdkEvent* in actual
|
||||
* applications, or a dummy pointer in unit tests.
|
||||
**/
|
||||
typedef gboolean (*FlTextInputPluginImFilter)(GtkIMContext* im_context,
|
||||
gpointer gdk_event);
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
G_DECLARE_DERIVABLE_TYPE(FlTextInputPlugin,
|
||||
@ -50,20 +37,15 @@ struct _FlTextInputPluginClass {
|
||||
/**
|
||||
* fl_text_input_plugin_new:
|
||||
* @messenger: an #FlBinaryMessenger.
|
||||
* @window: the #GdkWindow with which the text input plugin is associated.
|
||||
* @im_filter: a function used to allow an input method to internally handle
|
||||
* key press and release events. Typically a wrap of
|
||||
* #gtk_im_context_filter_keypress. Must not be nullptr.
|
||||
* @im_context: (allow-none): a #GtkIMContext.
|
||||
*
|
||||
* Creates a new plugin that implements SystemChannels.textInput from the
|
||||
* Flutter services library.
|
||||
*
|
||||
* Returns: a new #FlTextInputPlugin.
|
||||
*/
|
||||
FlTextInputPlugin* fl_text_input_plugin_new(
|
||||
FlBinaryMessenger* messenger,
|
||||
GdkWindow* window,
|
||||
FlTextInputPluginImFilter im_filter);
|
||||
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger,
|
||||
GtkIMContext* im_context);
|
||||
|
||||
/**
|
||||
* fl_text_input_plugin_filter_keypress
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
#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/testing/testing.h"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
@ -32,6 +33,66 @@ MATCHER_P(SuccessResponse, result, "") {
|
||||
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_(name), args_(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(std::string name,
|
||||
::testing::Matcher<FlValue*> args) {
|
||||
return MethodCallMatcher(::testing::StrEq(name), 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) {
|
||||
@ -48,29 +109,641 @@ static FlValue* build_list(std::vector<FlValue*> args) {
|
||||
return value;
|
||||
}
|
||||
|
||||
TEST(FlTextInputPluginTest, SetClient) {
|
||||
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
|
||||
auto filter =
|
||||
+[](GtkIMContext* im_context, gpointer gdk_event) { return false; };
|
||||
struct InputConfig {
|
||||
int64_t client_id = -1;
|
||||
const gchar* input_type = "TextInputType.text";
|
||||
const gchar* input_action = "TextInputAction.none";
|
||||
gboolean enable_delta_model = false;
|
||||
};
|
||||
|
||||
fl_text_input_plugin_new(messenger, nullptr,
|
||||
FlTextInputPluginImFilter(filter));
|
||||
|
||||
EXPECT_TRUE(messenger.HasMessageHandler("flutter/textinput"));
|
||||
|
||||
g_autoptr(FlValue) args = build_list({
|
||||
fl_value_new_int(1), // client id
|
||||
static FlValue* build_input_config(InputConfig config) {
|
||||
return build_list({
|
||||
fl_value_new_int(config.client_id),
|
||||
build_map({
|
||||
{"inputAction", fl_value_new_string("")},
|
||||
{"inputAction", fl_value_new_string(config.input_action)},
|
||||
{"inputType", build_map({
|
||||
{"name", fl_value_new_string("TextInputType.text")},
|
||||
{"name", fl_value_new_string(config.input_type)},
|
||||
})},
|
||||
{"enableDeltaModel", fl_value_new_bool(false)},
|
||||
{"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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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);
|
||||
}
|
||||
|
||||
TEST(FlTextInputPluginTest, MoveCursor) {
|
||||
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
|
||||
::testing::NiceMock<flutter::testing::MockIMContext> context;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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(context, gtk_im_context_set_cursor_location(
|
||||
::testing::Eq<GtkIMContext*>(context),
|
||||
::testing::Pointee(::testing::AllOf(
|
||||
::testing::Field(&GdkRectangle::x, 27),
|
||||
::testing::Field(&GdkRectangle::y, 32),
|
||||
::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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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();
|
||||
@ -80,5 +753,212 @@ TEST(FlTextInputPluginTest, SetClient) {
|
||||
SuccessResponse(null), ::testing::A<GError**>()))
|
||||
.WillOnce(::testing::Return(true));
|
||||
|
||||
messenger.ReceiveMessage("flutter/textinput", message);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context);
|
||||
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);
|
||||
|
||||
// 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 = 8,
|
||||
.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 = "engine",
|
||||
.delta_start = 8,
|
||||
.delta_end = 8,
|
||||
.selection_base = 14,
|
||||
.selection_extent = 14,
|
||||
.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(commit)),
|
||||
::testing::_, ::testing::_, ::testing::_));
|
||||
|
||||
g_signal_emit_by_name(context, "commit", "engine", nullptr);
|
||||
|
||||
// end
|
||||
g_autoptr(FlValue) end = build_list({
|
||||
fl_value_new_int(1), // client_id
|
||||
build_map({{
|
||||
"deltas",
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.delta_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);
|
||||
}
|
||||
|
||||
@ -92,20 +92,16 @@ G_DEFINE_TYPE_WITH_CODE(
|
||||
G_IMPLEMENT_INTERFACE(fl_scrolling_view_delegate_get_type(),
|
||||
fl_view_scrolling_delegate_iface_init))
|
||||
|
||||
static gboolean text_input_im_filter_by_gtk(GtkIMContext* im_context,
|
||||
gpointer gdk_event) {
|
||||
GdkEventKey* event = reinterpret_cast<GdkEventKey*>(gdk_event);
|
||||
GdkEventType type = event->type;
|
||||
g_return_val_if_fail(type == GDK_KEY_PRESS || type == GDK_KEY_RELEASE, false);
|
||||
return gtk_im_context_filter_keypress(im_context, event);
|
||||
}
|
||||
|
||||
// Initialize keyboard manager.
|
||||
static void init_keyboard(FlView* self) {
|
||||
FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine);
|
||||
self->text_input_plugin = fl_text_input_plugin_new(
|
||||
messenger, gtk_widget_get_window(GTK_WIDGET(self)),
|
||||
text_input_im_filter_by_gtk);
|
||||
|
||||
GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(self));
|
||||
g_return_if_fail(GDK_IS_WINDOW(window));
|
||||
g_autoptr(GtkIMContext) im_context = gtk_im_multicontext_new();
|
||||
gtk_im_context_set_client_window(im_context, window);
|
||||
|
||||
self->text_input_plugin = fl_text_input_plugin_new(messenger, im_context);
|
||||
self->keyboard_manager =
|
||||
fl_keyboard_manager_new(FL_KEYBOARD_VIEW_DELEGATE(self));
|
||||
}
|
||||
@ -242,6 +238,7 @@ static void on_pre_engine_restart_cb(FlEngine* engine, gpointer user_data) {
|
||||
FlView* self = FL_VIEW(user_data);
|
||||
|
||||
g_clear_object(&self->keyboard_manager);
|
||||
g_clear_object(&self->text_input_plugin);
|
||||
g_clear_object(&self->scrolling_manager);
|
||||
init_keyboard(self);
|
||||
init_scrolling(self);
|
||||
@ -488,7 +485,6 @@ static void fl_view_constructed(GObject* object) {
|
||||
// Create system channel handlers.
|
||||
FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine);
|
||||
self->accessibility_plugin = fl_accessibility_plugin_new(self);
|
||||
init_keyboard(self);
|
||||
init_scrolling(self);
|
||||
self->mouse_cursor_plugin = fl_mouse_cursor_plugin_new(messenger, self);
|
||||
self->platform_plugin = fl_platform_plugin_new(messenger);
|
||||
@ -622,6 +618,8 @@ static void fl_view_realize(GtkWidget* widget) {
|
||||
gtk_widget_set_window(widget, window);
|
||||
gtk_widget_register_window(widget, window);
|
||||
|
||||
init_keyboard(self);
|
||||
|
||||
if (!fl_renderer_start(self->renderer, self, &error)) {
|
||||
g_warning("Failed to start Flutter renderer: %s", error->message);
|
||||
return;
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
// 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/linux/testing/mock_im_context.h"
|
||||
|
||||
using namespace flutter::testing;
|
||||
|
||||
G_DECLARE_FINAL_TYPE(FlMockIMContext,
|
||||
fl_mock_im_context,
|
||||
FL,
|
||||
MOCK_IM_CONTEXT,
|
||||
GtkIMContext)
|
||||
|
||||
struct _FlMockIMContext {
|
||||
GtkIMContext parent_instance;
|
||||
MockIMContext* mock;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(FlMockIMContext, fl_mock_im_context, GTK_TYPE_IM_CONTEXT)
|
||||
|
||||
static void fl_mock_im_context_set_client_window(GtkIMContext* context,
|
||||
GdkWindow* window) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
self->mock->gtk_im_context_set_client_window(context, window);
|
||||
}
|
||||
|
||||
static void fl_mock_im_context_get_preedit_string(GtkIMContext* context,
|
||||
gchar** str,
|
||||
PangoAttrList** attrs,
|
||||
gint* cursor_pos) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
self->mock->gtk_im_context_get_preedit_string(context, str, attrs,
|
||||
cursor_pos);
|
||||
}
|
||||
|
||||
static gboolean fl_mock_im_context_filter_keypress(GtkIMContext* context,
|
||||
GdkEventKey* event) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
return self->mock->gtk_im_context_filter_keypress(context, event);
|
||||
}
|
||||
|
||||
static void fl_mock_im_context_focus_in(GtkIMContext* context) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
self->mock->gtk_im_context_focus_in(context);
|
||||
}
|
||||
|
||||
static void fl_mock_im_context_focus_out(GtkIMContext* context) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
self->mock->gtk_im_context_focus_out(context);
|
||||
}
|
||||
|
||||
static void fl_mock_im_context_reset(GtkIMContext* context) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
self->mock->gtk_im_context_reset(context);
|
||||
}
|
||||
|
||||
static void fl_mock_im_context_set_cursor_location(GtkIMContext* context,
|
||||
GdkRectangle* area) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
self->mock->gtk_im_context_set_cursor_location(context, area);
|
||||
}
|
||||
|
||||
static void fl_mock_im_context_set_use_preedit(GtkIMContext* context,
|
||||
gboolean use_preedit) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
self->mock->gtk_im_context_set_use_preedit(context, use_preedit);
|
||||
}
|
||||
|
||||
static void fl_mock_im_context_set_surrounding(GtkIMContext* context,
|
||||
const gchar* text,
|
||||
gint len,
|
||||
gint cursor_index) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
self->mock->gtk_im_context_set_surrounding(context, text, len, cursor_index);
|
||||
}
|
||||
|
||||
static gboolean fl_mock_im_context_get_surrounding(GtkIMContext* context,
|
||||
gchar** text,
|
||||
gint* cursor_index) {
|
||||
FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context);
|
||||
return self->mock->gtk_im_context_get_surrounding(context, text,
|
||||
cursor_index);
|
||||
}
|
||||
|
||||
static void fl_mock_im_context_class_init(FlMockIMContextClass* klass) {
|
||||
GtkIMContextClass* im_context_class = GTK_IM_CONTEXT_CLASS(klass);
|
||||
im_context_class->set_client_window = fl_mock_im_context_set_client_window;
|
||||
im_context_class->get_preedit_string = fl_mock_im_context_get_preedit_string;
|
||||
im_context_class->filter_keypress = fl_mock_im_context_filter_keypress;
|
||||
im_context_class->focus_in = fl_mock_im_context_focus_in;
|
||||
im_context_class->focus_out = fl_mock_im_context_focus_out;
|
||||
im_context_class->reset = fl_mock_im_context_reset;
|
||||
im_context_class->set_cursor_location =
|
||||
fl_mock_im_context_set_cursor_location;
|
||||
im_context_class->set_use_preedit = fl_mock_im_context_set_use_preedit;
|
||||
im_context_class->set_surrounding = fl_mock_im_context_set_surrounding;
|
||||
im_context_class->get_surrounding = fl_mock_im_context_get_surrounding;
|
||||
}
|
||||
|
||||
static void fl_mock_im_context_init(FlMockIMContext* self) {}
|
||||
|
||||
static GtkIMContext* fl_mock_im_context_new(MockIMContext* mock) {
|
||||
FlMockIMContext* self =
|
||||
FL_MOCK_IM_CONTEXT(g_object_new(fl_mock_im_context_get_type(), nullptr));
|
||||
self->mock = mock;
|
||||
return GTK_IM_CONTEXT(self);
|
||||
}
|
||||
|
||||
MockIMContext::~MockIMContext() {
|
||||
if (FL_IS_MOCK_IM_CONTEXT(instance_)) {
|
||||
g_clear_object(&instance_);
|
||||
}
|
||||
}
|
||||
|
||||
MockIMContext::operator GtkIMContext*() {
|
||||
if (instance_ == nullptr) {
|
||||
instance_ = fl_mock_im_context_new(this);
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
// 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.
|
||||
|
||||
#ifndef FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_IM_CONTEXT_H_
|
||||
#define FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_IM_CONTEXT_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
|
||||
namespace flutter {
|
||||
namespace testing {
|
||||
|
||||
class MockIMContext {
|
||||
public:
|
||||
~MockIMContext();
|
||||
|
||||
operator GtkIMContext*();
|
||||
|
||||
MOCK_METHOD2(gtk_im_context_set_client_window,
|
||||
void(GtkIMContext* context, GdkWindow* window));
|
||||
MOCK_METHOD4(gtk_im_context_get_preedit_string,
|
||||
void(GtkIMContext* context,
|
||||
gchar** str,
|
||||
PangoAttrList** attrs,
|
||||
gint* cursor_pos));
|
||||
MOCK_METHOD2(gtk_im_context_filter_keypress,
|
||||
gboolean(GtkIMContext* context, GdkEventKey* event));
|
||||
MOCK_METHOD1(gtk_im_context_focus_in, gboolean(GtkIMContext* context));
|
||||
MOCK_METHOD1(gtk_im_context_focus_out, void(GtkIMContext* context));
|
||||
MOCK_METHOD1(gtk_im_context_reset, void(GtkIMContext* context));
|
||||
MOCK_METHOD2(gtk_im_context_set_cursor_location,
|
||||
void(GtkIMContext* context, GdkRectangle* area));
|
||||
MOCK_METHOD2(gtk_im_context_set_use_preedit,
|
||||
void(GtkIMContext* context, gboolean use_preedit));
|
||||
MOCK_METHOD4(gtk_im_context_set_surrounding,
|
||||
void(GtkIMContext* context,
|
||||
const gchar* text,
|
||||
gint len,
|
||||
gint cursor_index));
|
||||
MOCK_METHOD3(gtk_im_context_get_surrounding,
|
||||
gboolean(GtkIMContext* context,
|
||||
gchar** text,
|
||||
gint* cursor_index));
|
||||
|
||||
private:
|
||||
GtkIMContext* instance_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace testing
|
||||
} // namespace flutter
|
||||
|
||||
#endif // FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_IM_CONTEXT_H_
|
||||
Loading…
x
Reference in New Issue
Block a user