blob: 228850dc6e5aa58891c9d4a027c42939092566c1 [file] [log] [blame]
// Copyright 2022 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 <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/input3/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include <memory>
#include <gtest/gtest.h>
#include "src/embedder/engine/embedder.h"
#include "src/embedder/fuchsia_logger.h"
#include "src/embedder/platform_message_channels.h"
#include "src/embedder/text_delegate.h"
namespace embedder_testing {
// Convert a |FlutterPlatformMessage| to string for ease of testing.
static std::string MessageToString(FlutterPlatformMessage* message) {
const char* data = reinterpret_cast<const char*>(message->message);
return std::string(data, message->message_size);
}
// Fake |KeyboardService| implementation. Only responsibility is to remember
// what it was called with.
class FakeKeyboardService : public fuchsia::ui::input3::Keyboard {
public:
// |fuchsia.ui.input3/Keyboard.AddListener|
virtual void AddListener(fuchsia::ui::views::ViewRef,
fidl::InterfaceHandle<fuchsia::ui::input3::KeyboardListener> listener,
fuchsia::ui::input3::Keyboard::AddListenerCallback callback) {
listener_ = listener.Bind();
callback();
}
fidl::InterfacePtr<fuchsia::ui::input3::KeyboardListener> listener_;
};
// Fake ImeService implementation. Only responsibility is to remember what
// it was called with.
class FakeImeService : public fuchsia::ui::input::ImeService {
public:
virtual void GetInputMethodEditor(
fuchsia::ui::input::KeyboardType keyboard_type, fuchsia::ui::input::InputMethodAction action,
fuchsia::ui::input::TextInputState input_state,
fidl::InterfaceHandle<fuchsia::ui::input::InputMethodEditorClient> client,
fidl::InterfaceRequest<fuchsia::ui::input::InputMethodEditor> ime) {
keyboard_type_ = std::move(keyboard_type);
action_ = std::move(action);
input_state_ = std::move(input_state);
client_ = client.Bind();
ime_ = std::move(ime);
}
virtual void ShowKeyboard() { keyboard_shown_ = true; }
virtual void HideKeyboard() { keyboard_shown_ = false; }
bool IsKeyboardShown() { return keyboard_shown_; }
bool keyboard_shown_ = false;
fuchsia::ui::input::KeyboardType keyboard_type_;
fuchsia::ui::input::InputMethodAction action_;
fuchsia::ui::input::TextInputState input_state_;
fidl::InterfacePtr<fuchsia::ui::input::InputMethodEditorClient> client_;
fidl::InterfaceRequest<fuchsia::ui::input::InputMethodEditor> ime_;
};
class TextDelegateTest : public ::testing::Test {
protected:
TextDelegateTest()
: loop_(&kAsyncLoopConfigAttachToCurrentThread),
keyboard_service_binding_(&keyboard_service_),
ime_service_binding_(&ime_service_) {
auto fake_view_ref_pair = scenic::ViewRefPair::New();
fidl::InterfaceHandle<fuchsia::ui::input::ImeService> ime_service;
ime_service_binding_.Bind(ime_service.NewRequest().TakeChannel());
fidl::InterfaceHandle<fuchsia::ui::input3::Keyboard> keyboard;
auto keyboard_request = keyboard.NewRequest();
keyboard_service_binding_.Bind(keyboard_request.TakeChannel());
text_delegate_ = std::make_unique<embedder::TextDelegate>(
std::move(fake_view_ref_pair.view_ref), std::move(ime_service), std::move(keyboard),
/* key_event_dispatch_callback: [flutter/keydata] */
[](const FlutterKeyEvent* event) {
FX_LOG(INFO, embedder_testing::kLogUnittestTag,
"TextDelegateTest - key_event_dispatch_callback");
},
/*platform_dispatch_callback: [flutter/keyevent, flutter/textinput] */
[this](const FlutterPlatformMessage* message) {
FX_LOG(INFO, embedder_testing::kLogUnittestTag,
"TextDelegateTest - platform_dispatch_callback");
memcpy(&last_message_, &message, sizeof(FlutterPlatformMessage));
},
nullptr);
// TextDelegate has some async initialization that needs to happen.
RunLoopUntilIdle();
}
// Runs the event loop until all scheduled events are spent.
void RunLoopUntilIdle() { loop_.RunUntilIdle(); }
void TearDown() override {
loop_.Quit();
ASSERT_EQ(loop_.ResetQuit(), 0);
}
async::Loop loop_;
FakeKeyboardService keyboard_service_;
fidl::Binding<fuchsia::ui::input3::Keyboard> keyboard_service_binding_;
FakeImeService ime_service_;
fidl::Binding<fuchsia::ui::input::ImeService> ime_service_binding_;
// Unit under test.
std::unique_ptr<embedder::TextDelegate> text_delegate_;
FlutterPlatformMessage* last_message_;
};
// Goes through several steps of a text edit protocol. These are hard to test
// in isolation because the text edit protocol depends on the correct method
// invocation sequence. The text editor is initialized with the editing
// parameters, and we verify that the correct input action is parsed out. We
// then exercise showing and hiding the keyboard, as well as a text state
// update.
TEST_F(TextDelegateTest, ActivateIme) {
// auto fake_platform_message_response = FakePlatformMessageResponse::Create();
{
// Initialize the editor. Without this initialization, the protocol code
// will crash.
const auto set_client_msg = R"(
{
"method": "TextInput.setClient",
"args": [
7,
{
"inputType": {
"name": "TextInputType.multiline",
"signed":null,
"decimal":null
},
"readOnly": false,
"obscureText": false,
"autocorrect":true,
"smartDashesType":"1",
"smartQuotesType":"1",
"enableSuggestions":true,
"enableInteractiveSelection":true,
"actionLabel":null,
"inputAction":"TextInputAction.newline",
"textCapitalization":"TextCapitalization.none",
"keyboardAppearance":"Brightness.dark",
"enableIMEPersonalizedLearning":true,
"enableDeltaModel":false
}
]
}
)";
FlutterPlatformMessage flutter_platform_message = {
.struct_size = sizeof(FlutterPlatformMessage),
.channel = embedder::kTextInputChannel,
.message = reinterpret_cast<const uint8_t*>(set_client_msg),
.message_size = strlen(set_client_msg),
.response_handle = nullptr};
text_delegate_->HandleFlutterTextInputChannelPlatformMessage(&flutter_platform_message);
RunLoopUntilIdle();
EXPECT_EQ(ime_service_.action_, fuchsia::ui::input::InputMethodAction::NEWLINE);
EXPECT_FALSE(ime_service_.IsKeyboardShown());
}
{
// Verify that showing keyboard results in the correct platform effect.
const auto set_client_msg = R"(
{
"method": "TextInput.show"
}
)";
FlutterPlatformMessage flutter_platform_message = {
.struct_size = sizeof(FlutterPlatformMessage),
.channel = embedder::kTextInputChannel,
.message = reinterpret_cast<const uint8_t*>(set_client_msg),
.message_size = strlen(set_client_msg),
.response_handle = nullptr};
text_delegate_->HandleFlutterTextInputChannelPlatformMessage(&flutter_platform_message);
RunLoopUntilIdle();
EXPECT_TRUE(ime_service_.IsKeyboardShown());
}
{
// Verify that hiding keyboard results in the correct platform effect.
const auto set_client_msg = R"(
{
"method": "TextInput.hide"
}
)";
FlutterPlatformMessage flutter_platform_message = {
.struct_size = sizeof(FlutterPlatformMessage),
.channel = embedder::kTextInputChannel,
.message = reinterpret_cast<const uint8_t*>(set_client_msg),
.message_size = strlen(set_client_msg),
.response_handle = nullptr};
text_delegate_->HandleFlutterTextInputChannelPlatformMessage(&flutter_platform_message);
RunLoopUntilIdle();
EXPECT_FALSE(ime_service_.IsKeyboardShown());
}
{
// Update the editing state from the Fuchsia platform side.
fuchsia::ui::input::TextInputState state = {
.revision = 42,
.text = "Foo",
.selection = fuchsia::ui::input::TextSelection{},
.composing = fuchsia::ui::input::TextRange{},
};
auto input_event = std::make_unique<fuchsia::ui::input::InputEvent>();
ime_service_.client_->DidUpdateState(std::move(state), std::move(input_event));
RunLoopUntilIdle();
EXPECT_EQ(
R"({"method":"TextInputClient.updateEditingState","args":[7,{"text":"Foo","selectionBase":0,"selectionExtent":0,"selectionAffinity":"TextAffinity.upstream","selectionIsDirectional":true,"composingBase":-1,"composingExtent":-1}]})",
MessageToString(last_message_));
}
{
// Notify Flutter that the action key has been pressed.
ime_service_.client_->OnAction(fuchsia::ui::input::InputMethodAction::DONE);
RunLoopUntilIdle();
EXPECT_EQ(R"({"method":"TextInputClient.performAction","args":[7,"TextInputAction.done"]})",
MessageToString(last_message_));
}
}
// Hands a few typical |KeyEvent|s to the text delegate. Regular key events are
// handled, "odd" key events are rejected (not handled). "Handling" a key event
// means converting it to an appropriate |FlutterPlatformMessage| and forwarding it.
TEST_F(TextDelegateTest, OnAction) {
{
// A sensible key event is converted into a platform message.
fuchsia::ui::input3::KeyEvent key_event;
*key_event.mutable_type() = fuchsia::ui::input3::KeyEventType::PRESSED;
*key_event.mutable_key() = fuchsia::input::Key::A;
key_event.mutable_key_meaning()->set_codepoint('a');
fuchsia::ui::input3::KeyEventStatus status;
keyboard_service_.listener_->OnKeyEvent(
std::move(key_event),
[&status](fuchsia::ui::input3::KeyEventStatus s) { status = std::move(s); });
RunLoopUntilIdle();
EXPECT_EQ(fuchsia::ui::input3::KeyEventStatus::HANDLED, status);
EXPECT_EQ(
R"({"type":"keydown","keymap":"fuchsia","hidUsage":458756,"codePoint":97,"modifiers":0})",
MessageToString(last_message_));
}
{
// SYNC event is not handled.
// This is currently expected, though we may need to change that behavior.
fuchsia::ui::input3::KeyEvent key_event;
*key_event.mutable_type() = fuchsia::ui::input3::KeyEventType::SYNC;
fuchsia::ui::input3::KeyEventStatus status;
keyboard_service_.listener_->OnKeyEvent(
std::move(key_event),
[&status](fuchsia::ui::input3::KeyEventStatus s) { status = std::move(s); });
RunLoopUntilIdle();
EXPECT_EQ(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED, status);
}
{
// CANCEL event is not handled.
// This is currently expected, though we may need to change that behavior.
fuchsia::ui::input3::KeyEvent key_event;
*key_event.mutable_type() = fuchsia::ui::input3::KeyEventType::CANCEL;
fuchsia::ui::input3::KeyEventStatus status;
keyboard_service_.listener_->OnKeyEvent(
std::move(key_event),
[&status](fuchsia::ui::input3::KeyEventStatus s) { status = std::move(s); });
RunLoopUntilIdle();
EXPECT_EQ(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED, status);
}
}
} // namespace embedder_testing