Skip to content

Commit

Permalink
[Linux] take first steps towards testable text input (flutter#33661)
Browse files Browse the repository at this point in the history
This PR is a continuation of flutter#33111 and includes necessary changes to test the `TextInputType.setClient()` platform channel call. Testing the actual text input flow requires a mock IM context that will follow in a separate PR.

* [Linux] pass GdkWindow instead of FlView to FlTextInputPlugin
* [Linux] add MockBinaryMessenger::ReceiveMessage()
* [Linux] add test for TextInputType.setClient
* Drop the offscreen window

GdkOffscreenWindow requires display & screen which is a problem in
headless mode. Luckily, we don't really need a window instance in tests
because its only passed as a client window to the IM context which will
be mocked out when we get to tests that depend on it.
  • Loading branch information
jpnurmi authored Jun 10, 2022
1 parent 746481e commit bae667e
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 15 deletions.
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -2216,6 +2216,7 @@ FILE: ../../../flutter/shell/platform/linux/fl_task_runner.cc
FILE: ../../../flutter/shell/platform/linux/fl_task_runner.h
FILE: ../../../flutter/shell/platform/linux/fl_text_input_plugin.cc
FILE: ../../../flutter/shell/platform/linux/fl_text_input_plugin.h
FILE: ../../../flutter/shell/platform/linux/fl_text_input_plugin_test.cc
FILE: ../../../flutter/shell/platform/linux/fl_texture.cc
FILE: ../../../flutter/shell/platform/linux/fl_texture_gl.cc
FILE: ../../../flutter/shell/platform/linux/fl_texture_gl_private.h
Expand Down
1 change: 1 addition & 0 deletions shell/platform/linux/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ executable("flutter_linux_unittests") {
"fl_standard_message_codec_test.cc",
"fl_standard_method_codec_test.cc",
"fl_string_codec_test.cc",
"fl_text_input_plugin_test.cc",
"fl_texture_gl_test.cc",
"fl_texture_registrar_test.cc",
"fl_value_test.cc",
Expand Down
16 changes: 7 additions & 9 deletions shell/platform/linux/fl_text_input_plugin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ struct FlTextInputPluginPrivate {

flutter::TextInputModel* text_model;

// The owning Flutter view.
FlView* view;
// The owning native window.
GdkWindow* window;

// A 4x4 matrix that maps from `EditableText` local coordinates to the
// coordinate system of `PipelineOwner.rootNode`.
Expand Down Expand Up @@ -260,8 +260,7 @@ static void im_preedit_start_cb(FlTextInputPlugin* self) {
priv->text_model->BeginComposing();

// Set the native window used for system input method windows.
GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(priv->view));
gtk_im_context_set_client_window(priv->im_context, window);
gtk_im_context_set_client_window(priv->im_context, priv->window);
}

// Signal handler for GtkIMContext::preedit-changed
Expand Down Expand Up @@ -420,8 +419,7 @@ static FlMethodResponse* show(FlTextInputPlugin* self) {
}

// Set the native window used for system input method windows.
GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(priv->view));
gtk_im_context_set_client_window(priv->im_context, window);
gtk_im_context_set_client_window(priv->im_context, priv->window);

gtk_im_context_focus_in(priv->im_context);

Expand Down Expand Up @@ -601,7 +599,7 @@ static void fl_text_input_plugin_dispose(GObject* object) {
delete priv->text_model;
priv->text_model = nullptr;
}
priv->view = nullptr;
priv->window = nullptr;

G_OBJECT_CLASS(fl_text_input_plugin_parent_class)->dispose(object);
}
Expand Down Expand Up @@ -728,7 +726,7 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) {

FlTextInputPlugin* fl_text_input_plugin_new(
FlBinaryMessenger* messenger,
FlView* view,
GdkWindow* window,
FlTextInputPluginImFilter im_filter) {
g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
g_return_val_if_fail(im_filter != nullptr, nullptr);
Expand All @@ -743,7 +741,7 @@ 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->view = view;
priv->window = window;
priv->im_filter = im_filter;

return self;
Expand Down
7 changes: 3 additions & 4 deletions shell/platform/linux/fl_text_input_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
#ifndef FLUTTER_SHELL_TEXT_INPUT_LINUX_FL_TEXT_INPUT_PLUGIN_H_
#define FLUTTER_SHELL_TEXT_INPUT_LINUX_FL_TEXT_INPUT_PLUGIN_H_

#include <gdk/gdk.h>
#include <gtk/gtk.h>

#include "flutter/shell/platform/linux/fl_key_event.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h"

/**
* FlTextInputPluginImFilter:
Expand Down Expand Up @@ -51,7 +50,7 @@ struct _FlTextInputPluginClass {
/**
* fl_text_input_plugin_new:
* @messenger: an #FlBinaryMessenger.
* @view: the #FlView with which the text input plugin is associated.
* @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.
Expand All @@ -63,7 +62,7 @@ struct _FlTextInputPluginClass {
*/
FlTextInputPlugin* fl_text_input_plugin_new(
FlBinaryMessenger* messenger,
FlView* view,
GdkWindow* window,
FlTextInputPluginImFilter im_filter);

/**
Expand Down
85 changes: 85 additions & 0 deletions shell/platform/linux/fl_text_input_plugin_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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/fl_text_input_plugin.h"
#include "flutter/shell/platform/linux/fl_method_codec_private.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/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;
}

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;
}

TEST(FlTextInputPluginTest, SetClient) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> mock;
g_autoptr(FlBinaryMessenger) messenger = fl_binary_messenger_new_mock(&mock);
auto filter =
+[](GtkIMContext* im_context, gpointer gdk_event) { return false; };

fl_text_input_plugin_new(messenger, nullptr,
FlTextInputPluginImFilter(filter));

EXPECT_TRUE(mock.HasMessageHandler("flutter/textinput"));

g_autoptr(FlValue) args = build_list({
fl_value_new_int(1), // client id
build_map({
{"inputAction", fl_value_new_string("")},
{"inputType", build_map({
{"name", fl_value_new_string("TextInputType.text")},
})},
{"enableDeltaModel", fl_value_new_bool(false)},
}),
});

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(mock, fl_binary_messenger_send_response(
::testing::Eq(messenger),
::testing::A<FlBinaryMessengerResponseHandle*>(),
SuccessResponse(null), ::testing::A<GError**>()))
.WillOnce(::testing::Return(true));

mock.ReceiveMessage(messenger, "flutter/textinput", message);
}
5 changes: 3 additions & 2 deletions shell/platform/linux/fl_view.cc
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ static gboolean text_input_im_filter_by_gtk(GtkIMContext* im_context,
// 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, self, text_input_im_filter_by_gtk);
self->text_input_plugin = fl_text_input_plugin_new(
messenger, gtk_widget_get_window(GTK_WIDGET(self)),
text_input_im_filter_by_gtk);
self->keyboard_manager =
fl_keyboard_manager_new(FL_KEYBOARD_VIEW_DELEGATE(self));
}
Expand Down
26 changes: 26 additions & 0 deletions shell/platform/linux/testing/mock_binary_messenger.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

#include "flutter/shell/platform/linux/testing/mock_binary_messenger.h"
#include "flutter/shell/platform/linux/testing/mock_binary_messenger_response_handle.h"

using namespace flutter::testing;

Expand All @@ -17,6 +18,30 @@ struct _FlMockBinaryMessenger {
MockBinaryMessenger* mock;
};

bool MockBinaryMessenger::HasMessageHandler(const gchar* channel) const {
return message_handlers.at(channel) != nullptr;
}

void MockBinaryMessenger::SetMessageHandler(
const gchar* channel,
FlBinaryMessengerMessageHandler handler,
gpointer user_data) {
message_handlers[channel] = handler;
user_datas[channel] = user_data;
}

void MockBinaryMessenger::ReceiveMessage(FlBinaryMessenger* messenger,
const gchar* channel,
GBytes* message) {
FlBinaryMessengerMessageHandler handler = message_handlers[channel];
if (response_handles[channel] == nullptr) {
response_handles[channel] = FL_BINARY_MESSENGER_RESPONSE_HANDLE(
fl_mock_binary_messenger_response_handle_new());
}
handler(messenger, channel, message, response_handles[channel],
user_datas[channel]);
}

static void fl_mock_binary_messenger_iface_init(
FlBinaryMessengerInterface* iface);

Expand All @@ -38,6 +63,7 @@ static void fl_mock_binary_messenger_set_message_handler_on_channel(
GDestroyNotify destroy_notify) {
g_return_if_fail(FL_IS_MOCK_BINARY_MESSENGER(messenger));
FlMockBinaryMessenger* self = FL_MOCK_BINARY_MESSENGER(messenger);
self->mock->SetMessageHandler(channel, handler, user_data);
self->mock->fl_binary_messenger_set_message_handler_on_channel(
messenger, channel, handler, user_data, destroy_notify);
}
Expand Down
19 changes: 19 additions & 0 deletions shell/platform/linux/testing/mock_binary_messenger.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#ifndef FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_BINARY_MESSENGER_H_
#define FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_BINARY_MESSENGER_H_

#include <unordered_map>

#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h"

#include "gmock/gmock.h"
Expand Down Expand Up @@ -40,6 +42,23 @@ class MockBinaryMessenger {
GBytes*(FlBinaryMessenger* messenger,
GAsyncResult* result,
GError** error));

bool HasMessageHandler(const gchar* channel) const;

void SetMessageHandler(const gchar* channel,
FlBinaryMessengerMessageHandler handler,
gpointer user_data);

void ReceiveMessage(FlBinaryMessenger* messenger,
const gchar* channel,
GBytes* message);

private:
std::unordered_map<std::string, FlBinaryMessengerMessageHandler>
message_handlers;
std::unordered_map<std::string, FlBinaryMessengerResponseHandle*>
response_handles;
std::unordered_map<std::string, gpointer> user_datas;
};

} // namespace testing
Expand Down

0 comments on commit bae667e

Please sign in to comment.