[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