| // 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 |