[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