[keyboard/text][testing] Add keyboard and text-delegate unittests. These are mostly a port over of the tests that exist in flutter/engine. Change-Id: I6ecd3b62fe493826f593e17d49b02d66bc1b639b Reviewed-on: https://fuchsia-review.googlesource.com/c/flutter-embedder/+/790604 Reviewed-by: Filip Filmar <fmil@google.com>
diff --git a/scripts/tests/run_unittests.sh b/scripts/tests/run_unittests.sh index caeb4f1..a91225f 100755 --- a/scripts/tests/run_unittests.sh +++ b/scripts/tests/run_unittests.sh
@@ -7,7 +7,7 @@ # Runs local tests for all workflow scripts. Not runnable on CQ. # # Usage: -# $FUCHSIA_EMBEDDER_DIR/scripts/tests/run_all.sh +# $FUCHSIA_EMBEDDER_DIR/scripts/tests/run_unittest.sh set -e # Fail on any error. source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/../lib/helpers.sh || exit $? @@ -16,9 +16,8 @@ for test in "${tests[@]}" do - # Convert filename to test name by removing prefix path (73 characters) - # and file extension (3 chacarcters) - test_name=${test:73:-3} + # Convert filename to test name by removing prefix path and extension + test_name=$(basename $test .cc) echo-info "Running $test_name ..." - bazel test //src/embedder:${test_name}_pkg --config=fuchsia_x64 + "${FUCHSIA_EMBEDDER_DIR}"/tools/bazel test //src/embedder:${test_name}_pkg --config=fuchsia_x64 done \ No newline at end of file
diff --git a/src/embedder/BUILD.bazel b/src/embedder/BUILD.bazel index 75811da..3778f70 100644 --- a/src/embedder/BUILD.bazel +++ b/src/embedder/BUILD.bazel
@@ -8,101 +8,126 @@ "@rules_fuchsia//fuchsia:defs.bzl", "fuchsia_cc_binary", "fuchsia_cc_test", + "fuchsia_component_manifest", "fuchsia_test_group", "fuchsia_test_package", "fuchsia_tests", - "fuchsia_component_manifest", ) load("@rules_cc//cc:defs.bzl", "cc_library") - cc_library( - name = "logging", - srcs = [ - "logging.h", - "fuchsia_logger.h", - "fuchsia_logger.cc" - ], - deps = [ - "@fuchsia_sdk//pkg/syslog" - ] + name = "logging", + srcs = [ + "fuchsia_logger.cc", + "fuchsia_logger.h", + "logging.h", + ], + deps = [ + "@fuchsia_sdk//pkg/syslog", + ], ) cc_library( - name = "embedder_state", - srcs = [ - "flatland_connection.h", - "software_surface.h", - "embedder_state.h" - ], - deps = [ - "//src/embedder/engine:embedder_header" - ] + name = "embedder_state", + srcs = [ + "embedder_state.h", + "flatland_connection.h", + "software_surface.h", + ], + deps = [ + "//src/embedder/engine:embedder_header", + ], ) cc_library( - name = "pointer_utility", - srcs = [ - ":logging", - "pointer_utility.h" - ], - deps = [ - "@fuchsia_sdk//pkg/trace", - "@fuchsia_sdk//pkg/trace-engine", - "@fuchsia_sdk//pkg/trace-provider-so" - ] + name = "pointer_utility", + srcs = [ + "pointer_utility.h", + ":logging", + ], + deps = [ + "@fuchsia_sdk//pkg/trace", + "@fuchsia_sdk//pkg/trace-engine", + "@fuchsia_sdk//pkg/trace-provider-so", + ], ) cc_library( - name = "mouse_delegate", - srcs = [ - "mouse_delegate.h", - "mouse_delegate.cc" - ], - deps = [ - ":logging", - ":pointer_utility", - "//src/embedder/engine:embedder_header", - "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc", - ] + name = "mouse_delegate", + srcs = [ + "mouse_delegate.cc", + "mouse_delegate.h", + ], + deps = [ + ":logging", + ":pointer_utility", + "//src/embedder/engine:embedder_header", + "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc", + ], ) cc_library( - name = "touch_delegate", - srcs = [ - "touch_delegate.h", - "touch_delegate.cc" - ], - deps = [ - ":logging", - ":pointer_utility", - "//src/embedder/engine:embedder_header", - "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc", - ] + name = "keyboard", + srcs = [ + "keyboard.cc", + "keyboard.h", + ], + deps = [ + ":logging", + "//src/embedder/engine:embedder_header", + "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc", + "@fuchsia_sdk//fidl/fuchsia.ui.input:fuchsia.ui.input_cc", + ], ) +cc_library( + name = "text_delegate", + srcs = [ + "platform_message_channels.h", + "text_delegate.cc", + "text_delegate.h", + ], + deps = [ + ":keyboard", + ":logging", + "//src/embedder/engine:embedder_header", + "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc", + "@fuchsia_sdk//fidl/fuchsia.ui.input:fuchsia.ui.input_cc", + "@rapidjson", + ], +) + +cc_library( + name = "touch_delegate", + srcs = [ + "touch_delegate.cc", + "touch_delegate.h", + ], + deps = [ + ":logging", + ":pointer_utility", + "//src/embedder/engine:embedder_header", + "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc", + ], +) # ELF binary that takes the path to Flutter app assets as an argument # and runs the Flutter app defined by those assets. fuchsia_cc_binary( name = "embedder", srcs = [ + "accessibility_bridge.cc", + "accessibility_bridge.h", "flatland_connection.cc", "flatland_ids.h", "flatland_view_provider.h", - "keyboard.cc", - "keyboard.h", "main.cc", "platform_message_channels.h", "pointer_utility.h", - "software_surface.cc", - "software_surface.h", - "text_delegate.cc", - "text_delegate.h", - "accessibility_bridge.cc", - "accessibility_bridge.h", "root_inspect_node.cc", "root_inspect_node.h", + "software_surface.cc", + "software_surface.h", "standard_message_codec/byte_buffer_streams.h", "standard_message_codec/byte_streams.h", "standard_message_codec/encodable_value.h", @@ -113,86 +138,125 @@ "standard_message_codec/standard_codec_serializer.h", "standard_message_codec/standard_message_codec.cc", "standard_message_codec/standard_message_codec.h", - "standard_message_codec/standard_method_codec.h" + "standard_message_codec/standard_method_codec.h", ], visibility = ["//visibility:public"], deps = [ - ":logging", ":embedder_state", + ":keyboard", + ":logging", ":mouse_delegate", + ":text_delegate", ":touch_delegate", "//src/embedder/engine:embedder_header", "//src/embedder/engine:libflutter_engine_for_platform", - "@rapidjson", "@fuchsia_sdk//fidl/fuchsia.fonts:fuchsia.fonts_cc", "@fuchsia_sdk//fidl/fuchsia.sysmem:fuchsia.sysmem_cc", "@fuchsia_sdk//fidl/fuchsia.ui.app:fuchsia.ui.app_cc", "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc", - "@fuchsia_sdk//pkg/async-loop-default", "@fuchsia_sdk//pkg/async-loop-cpp", + "@fuchsia_sdk//pkg/async-loop-default", "@fuchsia_sdk//pkg/fit", "@fuchsia_sdk//pkg/scenic_cpp", "@fuchsia_sdk//pkg/sys_cpp", "@fuchsia_sdk//pkg/syslog", "@fuchsia_sdk//pkg/trace", "@fuchsia_sdk//pkg/trace-engine", - "@fuchsia_sdk//pkg/trace-provider-so" + "@fuchsia_sdk//pkg/trace-provider-so", + "@rapidjson", + ], +) + +fuchsia_cc_test( + name = "keyboard_unittests", + size = "small", + srcs = ["keyboard_unittests.cc"], + visibility = ["//visibility:public"], + deps = [ + ":embedder_state", + ":keyboard", + ":logging", + "//src/embedder/engine:embedder_header", + "//src/embedder/engine:libflutter_engine_for_platform", + "@com_google_googletest//:gtest_main", + "@fuchsia_sdk//pkg/async-loop-cpp", + "@fuchsia_sdk//pkg/async-loop-default", + "@fuchsia_sdk//pkg/scenic_cpp", + "@rapidjson", ], ) fuchsia_cc_test( name = "mouse_delegate_unittests", size = "small", + srcs = ["mouse_delegate_unittests.cc"], visibility = ["//visibility:public"], - srcs = [ "mouse_delegate_unittests.cc"], deps = [ - "@com_google_googletest//:gtest_main", - ":logging", ":embedder_state", + ":logging", ":mouse_delegate", - "//src/embedder/test_util:mouse_event_builder", "//src/embedder/engine:embedder_header", "//src/embedder/engine:libflutter_engine_for_platform", - "@rapidjson", + "//src/embedder/test_util:mouse_event_builder", + "@com_google_googletest//:gtest_main", + "@fuchsia_sdk//fidl/fuchsia.accessibility.semantics:fuchsia.accessibility.semantics_cc", "@fuchsia_sdk//fidl/fuchsia.fonts:fuchsia.fonts_cc", "@fuchsia_sdk//fidl/fuchsia.sysmem:fuchsia.sysmem_cc", "@fuchsia_sdk//fidl/fuchsia.ui.app:fuchsia.ui.app_cc", "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc", - "@fuchsia_sdk//fidl/fuchsia.accessibility.semantics:fuchsia.accessibility.semantics_cc", - "@fuchsia_sdk//pkg/sys_inspect_cpp", - "@fuchsia_sdk//pkg/async-loop-default", "@fuchsia_sdk//pkg/async-loop-cpp", + "@fuchsia_sdk//pkg/async-loop-default", + "@fuchsia_sdk//pkg/fdio", "@fuchsia_sdk//pkg/fit", "@fuchsia_sdk//pkg/scenic_cpp", "@fuchsia_sdk//pkg/sys_cpp", + "@fuchsia_sdk//pkg/sys_inspect_cpp", "@fuchsia_sdk//pkg/syslog", "@fuchsia_sdk//pkg/trace", "@fuchsia_sdk//pkg/trace-engine", "@fuchsia_sdk//pkg/trace-provider-so", - "@fuchsia_sdk//pkg/fdio" + ], +) + +fuchsia_cc_test( + name = "text_delegate_unittests", + size = "small", + srcs = ["text_delegate_unittests.cc"], + visibility = ["//visibility:public"], + deps = [ + ":embedder_state", + ":logging", + ":text_delegate", + "//src/embedder/engine:embedder_header", + "//src/embedder/engine:libflutter_engine_for_platform", + "@com_google_googletest//:gtest_main", + "@fuchsia_sdk//pkg/async-loop-cpp", + "@fuchsia_sdk//pkg/async-loop-default", + "@fuchsia_sdk//pkg/scenic_cpp", + "@rapidjson", ], ) fuchsia_cc_test( name = "touch_delegate_unittests", size = "small", - visibility = ["//visibility:public"], srcs = ["touch_delegate_unittests.cc"], + visibility = ["//visibility:public"], deps = [ - "@com_google_googletest//:gtest_main", - ":logging", ":embedder_state", + ":logging", ":touch_delegate", - "//src/embedder/test_util:touch_event_builder", "//src/embedder/engine:embedder_header", "//src/embedder/engine:libflutter_engine_for_platform", - "@rapidjson", + "//src/embedder/test_util:touch_event_builder", + "@com_google_googletest//:gtest_main", "@fuchsia_sdk//fidl/fuchsia.fonts:fuchsia.fonts_cc", "@fuchsia_sdk//fidl/fuchsia.sysmem:fuchsia.sysmem_cc", "@fuchsia_sdk//fidl/fuchsia.ui.app:fuchsia.ui.app_cc", "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc", - "@fuchsia_sdk//pkg/async-loop-default", "@fuchsia_sdk//pkg/async-loop-cpp", + "@fuchsia_sdk//pkg/async-loop-default", + "@fuchsia_sdk//pkg/fdio", "@fuchsia_sdk//pkg/fit", "@fuchsia_sdk//pkg/scenic_cpp", "@fuchsia_sdk//pkg/sys_cpp", @@ -200,32 +264,55 @@ "@fuchsia_sdk//pkg/trace", "@fuchsia_sdk//pkg/trace-engine", "@fuchsia_sdk//pkg/trace-provider-so", - "@fuchsia_sdk//pkg/fdio" + ], +) + +fuchsia_test_package( + name = "keyboard_unittests_pkg", + package_name = "keyboard_unittests_pkg", + components = [ + ":keyboard_unittests", + ], + visibility = ["//visibility:public"], + deps = [ + "//src/embedder/engine:libflutter_engine_pkg_resource", ], ) fuchsia_test_package( name = "mouse_delegate_unittests_pkg", package_name = "mouse_delegate_unittests_pkg", + components = [ + ":mouse_delegate_unittests", + ], visibility = ["//visibility:public"], deps = [ - "//src/embedder/engine:libflutter_engine_pkg_resource" + "//src/embedder/engine:libflutter_engine_pkg_resource", ], +) + +fuchsia_test_package( + name = "text_delegate_unittests_pkg", + package_name = "text_delegate_unittests_pkg", components = [ - ":mouse_delegate_unittests" - ] + ":text_delegate_unittests", + ], + visibility = ["//visibility:public"], + deps = [ + "//src/embedder/engine:libflutter_engine_pkg_resource", + ], ) fuchsia_test_package( name = "touch_delegate_unittests_pkg", package_name = "touch_delegate_unittests_pkg", + components = [ + ":touch_delegate_unittests", + ], visibility = ["//visibility:public"], deps = [ - "//src/embedder/engine:libflutter_engine_pkg_resource" + "//src/embedder/engine:libflutter_engine_pkg_resource", ], - components = [ - ":touch_delegate_unittests" - ] ) fuchsia_component_manifest(
diff --git a/src/embedder/keyboard_unittests.cc b/src/embedder/keyboard_unittests.cc new file mode 100644 index 0000000..cb38e4e --- /dev/null +++ b/src/embedder/keyboard_unittests.cc
@@ -0,0 +1,234 @@ +// 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. + +// Testing the stateful Fuchsia Input3 keyboard interactions. This test case +// is not intended to be exhaustive: it is intended to capture the tests that +// demonstrate how we think Input3 interaction should work, and possibly +// regression tests if we catch some behavior that needs to be guarded long +// term. Pragmatically, this should be enough to ensure no specific bug +// happens twice. + +#include <fuchsia/input/cpp/fidl.h> +#include <fuchsia/ui/input/cpp/fidl.h> +#include <fuchsia/ui/input3/cpp/fidl.h> +#include <zircon/time.h> + +#include <vector> + +#include <gtest/gtest.h> + +#include "src/embedder/keyboard.h" + +namespace embedder_testing { +namespace { + +using fuchsia::input::Key; +using fuchsia::ui::input::kModifierCapsLock; +using fuchsia::ui::input::kModifierLeftAlt; +using fuchsia::ui::input::kModifierLeftControl; +using fuchsia::ui::input::kModifierLeftShift; +using fuchsia::ui::input::kModifierNone; +using fuchsia::ui::input::kModifierRightAlt; +using fuchsia::ui::input::kModifierRightControl; +using fuchsia::ui::input::kModifierRightShift; +using fuchsia::ui::input3::KeyEvent; +using fuchsia::ui::input3::KeyEventType; +using fuchsia::ui::input3::KeyMeaning; + +class KeyboardTest : public testing::Test { + protected: + static void SetUpTestCase() { testing::Test::SetUpTestCase(); } + + // Creates a new key event for testing. + KeyEvent NewKeyEvent(KeyEventType event_type, Key key) { + KeyEvent event; + // Assume events are delivered with correct timing. + event.set_timestamp(++timestamp_); + event.set_type(event_type); + event.set_key(key); + return event; + } + + KeyEvent NewKeyEventWithMeaning(KeyEventType event_type, KeyMeaning key_meaning) { + KeyEvent event; + // Assume events are delivered with correct timing. + event.set_timestamp(++timestamp_); + event.set_type(event_type); + event.set_key_meaning(std::move(key_meaning)); + return event; + } + + // Makes the keyboard consume all the provided `events`. The end state of + // the keyboard is as if all of the specified events happened between the + // start state of the keyboard and its end state. Returns false if any of + // the event was not consumed. + bool ConsumeEvents(embedder::Keyboard* keyboard, const std::vector<KeyEvent>& events) { + for (const auto& event : events) { + KeyEvent e; + event.Clone(&e); + if (keyboard->ConsumeEvent(std::move(e)) == false) { + return false; + } + } + return true; + } + + // Converts a pressed key to usage value. + uint32_t ToUsage(Key key) { return static_cast<uint64_t>(key) & 0xFFFFFFFF; } + + private: + zx_time_t timestamp_ = 0; +}; + +// Checks whether the HID usage, page and ID values are reported correctly. +TEST_F(KeyboardTest, UsageValues) { + std::vector<KeyEvent> keys; + keys.emplace_back(NewKeyEvent(KeyEventType::SYNC, Key::CAPS_LOCK)); + embedder::Keyboard keyboard; + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + + // Values for Caps Lock. + // See spec at: + // https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/fidl/fuchsia.input/keys.fidl;l=177;drc=e3b39f2b57e720770773b857feca4f770ee0619e + EXPECT_EQ(0x07u, keyboard.LastHIDUsagePage()); + EXPECT_EQ(0x39u, keyboard.LastHIDUsageID()); + EXPECT_EQ(0x70039u, keyboard.LastHIDUsage()); + + // Try also an usage that is not on page 7. This one is on page 0x0C. + // See spec at: + // https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/fidl/fuchsia.input/keys.fidl;l=339;drc=e3b39f2b57e720770773b857feca4f770ee0619e + // Note that Fuchsia does not define constants for every key you may think of, + // rather only those that we had the need for. However it is not an issue + // to add more keys if needed. + keys.clear(); + keys.emplace_back(NewKeyEvent(KeyEventType::SYNC, Key::MEDIA_MUTE)); + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + EXPECT_EQ(0x0Cu, keyboard.LastHIDUsagePage()); + EXPECT_EQ(0xE2u, keyboard.LastHIDUsageID()); + EXPECT_EQ(0xC00E2u, keyboard.LastHIDUsage()); + + // Don't crash when a key with only a meaning comes in. + keys.clear(); + keys.emplace_back(NewKeyEventWithMeaning(KeyEventType::SYNC, KeyMeaning::WithCodepoint(32))); + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + EXPECT_EQ(0x0u, keyboard.LastHIDUsagePage()); + EXPECT_EQ(0x0u, keyboard.LastHIDUsageID()); + EXPECT_EQ(0x0u, keyboard.LastHIDUsage()); + EXPECT_EQ(0x20u, keyboard.LastCodePoint()); + + keys.clear(); + auto key = NewKeyEventWithMeaning(KeyEventType::SYNC, KeyMeaning::WithCodepoint(65)); + key.set_key(Key::A); + keys.emplace_back(std::move(key)); + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + EXPECT_EQ(0x07u, keyboard.LastHIDUsagePage()); + EXPECT_EQ(0x04u, keyboard.LastHIDUsageID()); + EXPECT_EQ(0x70004u, keyboard.LastHIDUsage()); + EXPECT_EQ(65u, keyboard.LastCodePoint()); +} + +// This test checks that if a caps lock has been pressed when we didn't have +// focus, the effect of caps lock remains. Only this first test case is +// commented to explain how the test case works. +TEST_F(KeyboardTest, CapsLockSync) { + // Place the key events since the beginning of time into `keys`. + std::vector<KeyEvent> keys; + keys.emplace_back(NewKeyEvent(KeyEventType::SYNC, Key::CAPS_LOCK)); + + // Replay them on the keyboard. + embedder::Keyboard keyboard; + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + + // Verify the state of the keyboard's public API: + // - check that the key sync had no code point (it was a caps lock press). + // - check that the registered usage was that of caps lock. + // - check that the net effect is that the caps lock modifier is locked + // active. + EXPECT_EQ(0u, keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::CAPS_LOCK), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierCapsLock, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, CapsLockPress) { + std::vector<KeyEvent> keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::CAPS_LOCK)); + + embedder::Keyboard keyboard; + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + + EXPECT_EQ(0u, keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::CAPS_LOCK), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierCapsLock, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, CapsLockPressRelease) { + std::vector<KeyEvent> keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::CAPS_LOCK)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::CAPS_LOCK)); + + embedder::Keyboard keyboard; + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + + EXPECT_EQ(0u, keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::CAPS_LOCK), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierCapsLock, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, ShiftA) { + std::vector<KeyEvent> keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::LEFT_SHIFT)); + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::A)); + + embedder::Keyboard keyboard; + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + + EXPECT_EQ(static_cast<uint32_t>('A'), keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::A), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierLeftShift, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, ShiftAWithRelease) { + std::vector<KeyEvent> keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::LEFT_SHIFT)); + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::A)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::A)); + + embedder::Keyboard keyboard; + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + + EXPECT_EQ(static_cast<uint32_t>('A'), keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::A), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierLeftShift, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, ShiftAWithReleaseShift) { + std::vector<KeyEvent> keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::LEFT_SHIFT)); + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::A)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::LEFT_SHIFT)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::A)); + + embedder::Keyboard keyboard; + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + + EXPECT_EQ(static_cast<uint32_t>('a'), keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::A), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierNone, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, LowcaseA) { + std::vector<KeyEvent> keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::A)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::A)); + + embedder::Keyboard keyboard; + ASSERT_TRUE(ConsumeEvents(&keyboard, keys)); + + EXPECT_EQ(static_cast<uint32_t>('a'), keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::A), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierNone, keyboard.Modifiers()); +} + +} // namespace +} // namespace embedder_testing
diff --git a/src/embedder/logging.h b/src/embedder/logging.h index 84eee0a..faf9946 100644 --- a/src/embedder/logging.h +++ b/src/embedder/logging.h
@@ -12,4 +12,10 @@ } // namespace embedder +namespace embedder_testing { + +constexpr char kLogUnittestTag[] = "flutter_embedder_unittest"; + +} // namespace embedder_testing + #endif // SRC_EMBEDDER_LOGGING_H_
diff --git a/src/embedder/text_delegate_unittests.cc b/src/embedder/text_delegate_unittests.cc new file mode 100644 index 0000000..228850d --- /dev/null +++ b/src/embedder/text_delegate_unittests.cc
@@ -0,0 +1,297 @@ +// 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
diff --git a/third_party/googletest b/third_party/googletest index 0b56cbe..7b0ac59 160000 --- a/third_party/googletest +++ b/third_party/googletest
@@ -1 +1 @@ -Subproject commit 0b56cbec076adbb75d22e3d9f75e99ad063d1ac3 +Subproject commit 7b0ac59d94c49dcb40c787de0c19d929f56465d6