[embedder] Touch and mouse input unit tests
Change-Id: Iecfe410fbf1c0825621f3c27ae2d9f373ceadf50
Reviewed-on: https://fuchsia-review.googlesource.com/c/flutter-embedder/+/775325
Reviewed-by: Alexander Biggs <akbiggs@google.com>
Reviewed-by: Naud Ghebre <naudzghebre@google.com>
diff --git a/README.md b/README.md
index 53e851f..de895db 100644
--- a/README.md
+++ b/README.md
@@ -106,6 +106,16 @@
- To attach a debugger to the example component, see
[_`debugging.md`_](https://fuchsia.googlesource.com/flutter-embedder/+/refs/heads/main/docs/debugging.md)
+## Running unit tests
+
+To run unit tests for this repository locally, first execute steps 1 & 2 from the previos section, then run the following command:
+
+```sh
+$FUCHSIA_EMBEDDER_DIR/scripts/tests/run_unittests.sh
+```
+
+There is no CQ for this repository yet.
+
## Uploading changes for review.
See [_`git_workflow.md`_](https://fuchsia.googlesource.com/flutter-embedder/+/refs/heads/main/docs/git_workflow.md).
diff --git a/scripts/tests/run_unittests.sh b/scripts/tests/run_unittests.sh
new file mode 100755
index 0000000..caeb4f1
--- /dev/null
+++ b/scripts/tests/run_unittests.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+#
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# Runs local tests for all workflow scripts. Not runnable on CQ.
+#
+# Usage:
+# $FUCHSIA_EMBEDDER_DIR/scripts/tests/run_all.sh
+
+set -e # Fail on any error.
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/../lib/helpers.sh || exit $?
+
+tests=( "${FUCHSIA_EMBEDDER_DIR}"/src/embedder/*_unittests.cc )
+
+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}
+ echo-info "Running $test_name ..."
+ 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 5217c52..75811da 100644
--- a/src/embedder/BUILD.bazel
+++ b/src/embedder/BUILD.bazel
@@ -7,35 +7,98 @@
load(
"@rules_fuchsia//fuchsia:defs.bzl",
"fuchsia_cc_binary",
+ "fuchsia_cc_test",
+ "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"
+ ]
+)
+
+cc_library(
+ name = "embedder_state",
+ srcs = [
+ "flatland_connection.h",
+ "software_surface.h",
+ "embedder_state.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"
+ ]
+)
+
+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",
+ ]
+)
+
+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",
+ ]
+)
+
# 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 = [
- "embedder_state.h",
"flatland_connection.cc",
- "flatland_connection.h",
"flatland_ids.h",
"flatland_view_provider.h",
- "fuchsia_logger.cc",
- "fuchsia_logger.h",
"keyboard.cc",
"keyboard.h",
- "logging.h",
"main.cc",
- "mouse_delegate.cc",
- "mouse_delegate.h",
"platform_message_channels.h",
"pointer_utility.h",
"software_surface.cc",
"software_surface.h",
"text_delegate.cc",
"text_delegate.h",
- "touch_delegate.cc",
- "touch_delegate.h",
"accessibility_bridge.cc",
"accessibility_bridge.h",
"root_inspect_node.cc",
@@ -54,6 +117,40 @@
],
visibility = ["//visibility:public"],
deps = [
+ ":logging",
+ ":embedder_state",
+ ":mouse_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/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_cc_test(
+ name = "mouse_delegate_unittests",
+ size = "small",
+ visibility = ["//visibility:public"],
+ srcs = [ "mouse_delegate_unittests.cc"],
+ deps = [
+ "@com_google_googletest//:gtest_main",
+ ":logging",
+ ":embedder_state",
+ ":mouse_delegate",
+ "//src/embedder/test_util:mouse_event_builder",
"//src/embedder/engine:embedder_header",
"//src/embedder/engine:libflutter_engine_for_platform",
"@rapidjson",
@@ -72,9 +169,65 @@
"@fuchsia_sdk//pkg/trace",
"@fuchsia_sdk//pkg/trace-engine",
"@fuchsia_sdk//pkg/trace-provider-so",
+ "@fuchsia_sdk//pkg/fdio"
],
)
+fuchsia_cc_test(
+ name = "touch_delegate_unittests",
+ size = "small",
+ visibility = ["//visibility:public"],
+ srcs = ["touch_delegate_unittests.cc"],
+ deps = [
+ "@com_google_googletest//:gtest_main",
+ ":logging",
+ ":embedder_state",
+ ":touch_delegate",
+ "//src/embedder/test_util:touch_event_builder",
+ "//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/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/fdio"
+ ],
+)
+
+fuchsia_test_package(
+ name = "mouse_delegate_unittests_pkg",
+ package_name = "mouse_delegate_unittests_pkg",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//src/embedder/engine:libflutter_engine_pkg_resource"
+ ],
+ components = [
+ ":mouse_delegate_unittests"
+ ]
+)
+
+fuchsia_test_package(
+ name = "touch_delegate_unittests_pkg",
+ package_name = "touch_delegate_unittests_pkg",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//src/embedder/engine:libflutter_engine_pkg_resource"
+ ],
+ components = [
+ ":touch_delegate_unittests"
+ ]
+)
+
fuchsia_component_manifest(
name = "embedder_manifest",
src = "meta/embedder.cml",
diff --git a/src/embedder/mouse_delegate_unittests.cc b/src/embedder/mouse_delegate_unittests.cc
new file mode 100644
index 0000000..bcf2ae4
--- /dev/null
+++ b/src/embedder/mouse_delegate_unittests.cc
@@ -0,0 +1,209 @@
+// Copyright 2013 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/pointer/cpp/fidl.h>
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/async-loop/default.h>
+#include <lib/fdio/fd.h>
+#include <lib/fidl/cpp/binding_set.h>
+
+#include <array>
+#include <optional>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#include "src/embedder/mouse_delegate.h"
+#include "src/embedder/test_util/fakes/mouse_source.h"
+#include "src/embedder/test_util/mouse_event_builder.h"
+
+namespace embedder_testing {
+
+using fup_EventPhase = fuchsia::ui::pointer::EventPhase;
+using fup_ViewParameters = fuchsia::ui::pointer::ViewParameters;
+using fup_MouseEvent = fuchsia::ui::pointer::MouseEvent;
+
+constexpr std::array<std::array<float, 2>, 2> kRect = {{{0, 0}, {20, 20}}};
+constexpr std::array<float, 9> kIdentity = {1, 0, 0, 0, 1, 0, 0, 0, 1};
+
+constexpr uint32_t kMouseDeviceId = 123;
+constexpr std::array<int64_t, 2> kNoScrollInPhysicalPixelDelta = {0, 0};
+const bool kNotPrecisionScroll = false;
+const bool kPrecisionScroll = true;
+
+// Fixture to exercise Flutter runner's implementation for
+// fuchsia.ui.pointer.TouchSource.
+class MouseDelegateTest : public ::testing::Test {
+ protected:
+ MouseDelegateTest() : loop_(&kAsyncLoopConfigAttachToCurrentThread) {
+ mouse_source_ = std::make_unique<FakeMouseSource>();
+ mouse_delegate_ = std::make_unique<embedder::MouseDelegate>(
+ mouse_source_bindings_.AddBinding(mouse_source_.get()));
+ }
+
+ void RunLoopUntilIdle() { loop_.RunUntilIdle(); }
+
+ std::unique_ptr<FakeMouseSource> mouse_source_;
+ std::unique_ptr<embedder::MouseDelegate> mouse_delegate_;
+
+ async::Loop loop_;
+ fidl::BindingSet<fuchsia::ui::pointer::MouseSource> mouse_source_bindings_;
+};
+
+TEST_F(MouseDelegateTest, MouseWheel_TickBased) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ mouse_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ std::vector<fup_MouseEvent> events =
+ MouseEventBuilder()
+ .AddTime(1111789u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kMouseDeviceId, {10.f, 10.f}, {}, {0, 1}, kNoScrollInPhysicalPixelDelta,
+ kNotPrecisionScroll)
+ .AddMouseDeviceInfo(kMouseDeviceId, {0, 1, 2})
+ .BuildAsVector();
+ mouse_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 1u);
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kHover);
+ EXPECT_EQ(pointers.value()[0].signal_kind,
+ FlutterPointerSignalKind::kFlutterPointerSignalKindScroll);
+ EXPECT_EQ(pointers.value()[0].device_kind,
+ FlutterPointerDeviceKind::kFlutterPointerDeviceKindMouse);
+ EXPECT_EQ(pointers.value()[0].buttons, 0);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_x, 0);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_y, -20);
+ pointers = {};
+
+ // receive a horizontal scroll
+ events = MouseEventBuilder()
+ .AddTime(1111789u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kMouseDeviceId, {10.f, 10.f}, {}, {1, 0}, kNoScrollInPhysicalPixelDelta,
+ kNotPrecisionScroll)
+ .AddMouseDeviceInfo(kMouseDeviceId, {0, 1, 2})
+ .BuildAsVector();
+ mouse_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 1u);
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kHover);
+ EXPECT_EQ(pointers.value()[0].signal_kind,
+ FlutterPointerSignalKind::kFlutterPointerSignalKindScroll);
+ EXPECT_EQ(pointers.value()[0].device_kind,
+ FlutterPointerDeviceKind::kFlutterPointerDeviceKindMouse);
+ EXPECT_EQ(pointers.value()[0].buttons, 0);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_x, 20);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_y, 0);
+ pointers = {};
+}
+
+TEST_F(MouseDelegateTest, MouseWheel_PixelBased) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ mouse_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ std::vector<fup_MouseEvent> events =
+ MouseEventBuilder()
+ .AddTime(1111789u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kMouseDeviceId, {10.f, 10.f}, {}, {0, 1}, {0, 120}, kNotPrecisionScroll)
+ .AddMouseDeviceInfo(kMouseDeviceId, {0, 1, 2})
+ .BuildAsVector();
+ mouse_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 1u);
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kHover);
+ EXPECT_EQ(pointers.value()[0].signal_kind,
+ FlutterPointerSignalKind::kFlutterPointerSignalKindScroll);
+ EXPECT_EQ(pointers.value()[0].device_kind,
+ FlutterPointerDeviceKind::kFlutterPointerDeviceKindMouse);
+ EXPECT_EQ(pointers.value()[0].buttons, 0);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_x, 0);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_y, -120);
+ pointers = {};
+
+ // receive a horizontal scroll
+ events = MouseEventBuilder()
+ .AddTime(1111789u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kMouseDeviceId, {10.f, 10.f}, {}, {1, 0}, {120, 0}, kNotPrecisionScroll)
+ .AddMouseDeviceInfo(kMouseDeviceId, {0, 1, 2})
+ .BuildAsVector();
+ mouse_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 1u);
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kHover);
+ EXPECT_EQ(pointers.value()[0].signal_kind,
+ FlutterPointerSignalKind::kFlutterPointerSignalKindScroll);
+ EXPECT_EQ(pointers.value()[0].device_kind,
+ FlutterPointerDeviceKind::kFlutterPointerDeviceKindMouse);
+ EXPECT_EQ(pointers.value()[0].buttons, 0);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_x, 120);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_y, 0);
+ pointers = {};
+}
+
+TEST_F(MouseDelegateTest, MouseWheel_TouchpadPixelBased) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ mouse_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ std::vector<fup_MouseEvent> events =
+ MouseEventBuilder()
+ .AddTime(1111789u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kMouseDeviceId, {10.f, 10.f}, {}, {0, 1}, {0, 120}, kPrecisionScroll)
+ .AddMouseDeviceInfo(kMouseDeviceId, {0, 1, 2})
+ .BuildAsVector();
+ mouse_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 1u);
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kHover);
+ EXPECT_EQ(pointers.value()[0].signal_kind,
+ FlutterPointerSignalKind::kFlutterPointerSignalKindScroll);
+ EXPECT_EQ(pointers.value()[0].device_kind,
+ FlutterPointerDeviceKind::kFlutterPointerDeviceKindMouse);
+ EXPECT_EQ(pointers.value()[0].buttons, 0);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_x, 0);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_y, -120);
+ pointers = {};
+
+ // receive a horizontal scroll
+ events = MouseEventBuilder()
+ .AddTime(1111789u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kMouseDeviceId, {10.f, 10.f}, {}, {1, 0}, {120, 0}, kPrecisionScroll)
+ .AddMouseDeviceInfo(kMouseDeviceId, {0, 1, 2})
+ .BuildAsVector();
+ mouse_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 1u);
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kHover);
+ EXPECT_EQ(pointers.value()[0].signal_kind,
+ FlutterPointerSignalKind::kFlutterPointerSignalKindScroll);
+ EXPECT_EQ(pointers.value()[0].device_kind,
+ FlutterPointerDeviceKind::kFlutterPointerDeviceKindMouse);
+ EXPECT_EQ(pointers.value()[0].buttons, 0);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_x, 120);
+ EXPECT_EQ(pointers.value()[0].scroll_delta_y, 0);
+ pointers = {};
+}
+
+} // namespace embedder_testing
diff --git a/src/embedder/test_util/BUILD.bazel b/src/embedder/test_util/BUILD.bazel
new file mode 100644
index 0000000..0b96f2e
--- /dev/null
+++ b/src/embedder/test_util/BUILD.bazel
@@ -0,0 +1,33 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# Package for the Flutter embedder for Fuchsia.
+
+load("@rules_cc//cc:defs.bzl", "cc_library")
+
+cc_library(
+ name = "mouse_event_builder",
+ visibility = ["//visibility:public"],
+ srcs = [
+ "mouse_event_builder.h",
+ "mouse_event_builder.cc",
+ "fakes/mouse_source.h"
+ ],
+ deps = [
+ "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc"
+ ]
+)
+
+cc_library(
+ name = "touch_event_builder",
+ visibility = ["//visibility:public"],
+ srcs = [
+ "touch_event_builder.h",
+ "touch_event_builder.cc",
+ "fakes/touch_source.h"
+ ],
+ deps = [
+ "@fuchsia_sdk//fidl/fuchsia.ui.composition:fuchsia.ui.composition_cc"
+ ]
+)
\ No newline at end of file
diff --git a/src/embedder/test_util/fakes/mouse_source.h b/src/embedder/test_util/fakes/mouse_source.h
new file mode 100644
index 0000000..ed15afd
--- /dev/null
+++ b/src/embedder/test_util/fakes/mouse_source.h
@@ -0,0 +1,37 @@
+// Copyright 2013 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.
+
+#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_FAKES_MOUSE_SOURCE_H_
+#define FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_FAKES_MOUSE_SOURCE_H_
+
+#include <fuchsia/ui/pointer/cpp/fidl.h>
+
+#include <iostream>
+
+#include "src/embedder/fuchsia_logger.h"
+
+namespace embedder_testing {
+
+// A test stub to act as the protocol server. A test can control what is sent
+// back by this server implementation, via the ScheduleCallback call.
+class FakeMouseSource : public fuchsia::ui::pointer::MouseSource {
+ public:
+ // |fuchsia.ui.pointer.MouseSource|
+ void Watch(MouseSource::WatchCallback callback) override { callback_ = std::move(callback); }
+
+ // Have the server issue events to the client's hanging-get Watch call.
+ void ScheduleCallback(std::vector<fuchsia::ui::pointer::MouseEvent> events) {
+ FX_CHECK(callback_);
+ callback_(std::move(events));
+ }
+
+ private:
+ // Client-side logic to invoke on Watch() call's return. A test triggers it
+ // with ScheduleCallback().
+ MouseSource::WatchCallback callback_;
+};
+
+} // namespace embedder_testing
+
+#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_FAKES_MOUSE_SOURCE_H_
diff --git a/src/embedder/test_util/fakes/touch_source.h b/src/embedder/test_util/fakes/touch_source.h
new file mode 100644
index 0000000..58d80b2
--- /dev/null
+++ b/src/embedder/test_util/fakes/touch_source.h
@@ -0,0 +1,59 @@
+// Copyright 2013 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.
+
+#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_FAKES_TOUCH_SOURCE_H_
+#define FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_FAKES_TOUCH_SOURCE_H_
+
+#include <fuchsia/ui/pointer/cpp/fidl.h>
+
+#include <optional>
+#include <vector>
+
+#include "src/embedder/fuchsia_logger.h"
+
+namespace embedder_testing {
+
+// A test stub to act as the protocol server. A test can control what is sent
+// back by this server implementation, via the ScheduleCallback call.
+class FakeTouchSource : public fuchsia::ui::pointer::TouchSource {
+ public:
+ // |fuchsia.ui.pointer.TouchSource|
+ void Watch(std::vector<fuchsia::ui::pointer::TouchResponse> responses,
+ TouchSource::WatchCallback callback) override {
+ responses_ = std::move(responses);
+ callback_ = std::move(callback);
+ }
+
+ // Have the server issue events to the client's hanging-get Watch call.
+ void ScheduleCallback(std::vector<fuchsia::ui::pointer::TouchEvent> events) {
+ FX_CHECK(callback_);
+ callback_(std::move(events));
+ }
+
+ // Allow the test to observe what the client uploaded on the next Watch call.
+ std::optional<std::vector<fuchsia::ui::pointer::TouchResponse>> UploadedResponses() {
+ auto responses = std::move(responses_);
+ responses_.reset();
+ return responses;
+ }
+
+ private:
+ // |fuchsia.ui.pointer.TouchSource|
+ void UpdateResponse(fuchsia::ui::pointer::TouchInteractionId ixn,
+ fuchsia::ui::pointer::TouchResponse response,
+ TouchSource::UpdateResponseCallback callback) override {
+ FX_CHECK(false);
+ }
+
+ // Client uploads responses to server.
+ std::optional<std::vector<fuchsia::ui::pointer::TouchResponse>> responses_;
+
+ // Client-side logic to invoke on Watch() call's return. A test triggers it
+ // with ScheduleCallback().
+ TouchSource::WatchCallback callback_;
+};
+
+} // namespace embedder_testing
+
+#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_FAKES_TOUCH_SOURCE_H_
diff --git a/src/embedder/test_util/mouse_event_builder.cc b/src/embedder/test_util/mouse_event_builder.cc
new file mode 100644
index 0000000..aaea937
--- /dev/null
+++ b/src/embedder/test_util/mouse_event_builder.cc
@@ -0,0 +1,108 @@
+// Copyright 2013 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 "src/embedder/test_util/mouse_event_builder.h"
+
+namespace embedder_testing {
+
+using fup_EventPhase = fuchsia::ui::pointer::EventPhase;
+using fup_ViewParameters = fuchsia::ui::pointer::ViewParameters;
+using fup_MouseEvent = fuchsia::ui::pointer::MouseEvent;
+using fup_MousePointerSample = fuchsia::ui::pointer::MousePointerSample;
+using fup_MouseDeviceInfo = fuchsia::ui::pointer::MouseDeviceInfo;
+
+namespace {
+
+fup_ViewParameters CreateMouseViewParameters(std::array<std::array<float, 2>, 2> view,
+ std::array<std::array<float, 2>, 2> viewport,
+ std::array<float, 9> transform) {
+ fup_ViewParameters params;
+ fuchsia::ui::pointer::Rectangle view_rect;
+ view_rect.min = view[0];
+ view_rect.max = view[1];
+ params.view = view_rect;
+ fuchsia::ui::pointer::Rectangle viewport_rect;
+ viewport_rect.min = viewport[0];
+ viewport_rect.max = viewport[1];
+ params.viewport = viewport_rect;
+ params.viewport_to_view_transform = transform;
+ return params;
+}
+
+} // namespace
+
+MouseEventBuilder MouseEventBuilder::New() { return MouseEventBuilder(); }
+
+MouseEventBuilder& MouseEventBuilder::AddTime(zx_time_t time) {
+ time_ = time;
+ return *this;
+}
+
+MouseEventBuilder& MouseEventBuilder::AddSample(uint32_t id, std::array<float, 2> position,
+ std::vector<uint8_t> pressed_buttons,
+ std::array<int64_t, 2> scroll,
+ std::array<int64_t, 2> scroll_in_physical_pixel,
+ bool is_precision_scroll) {
+ sample_ = std::make_optional<fup_MousePointerSample>();
+ sample_->set_device_id(id);
+ if (!pressed_buttons.empty()) {
+ sample_->set_pressed_buttons(pressed_buttons);
+ }
+ sample_->set_position_in_viewport(position);
+ if (scroll[0] != 0) {
+ sample_->set_scroll_h(scroll[0]);
+ }
+ if (scroll[1] != 0) {
+ sample_->set_scroll_v(scroll[1]);
+ }
+ if (scroll_in_physical_pixel[0] != 0) {
+ sample_->set_scroll_h_physical_pixel(scroll_in_physical_pixel[0]);
+ }
+ if (scroll_in_physical_pixel[1] != 0) {
+ sample_->set_scroll_v_physical_pixel(scroll_in_physical_pixel[1]);
+ }
+ sample_->set_is_precision_scroll(is_precision_scroll);
+ return *this;
+}
+
+MouseEventBuilder& MouseEventBuilder::AddViewParameters(
+ std::array<std::array<float, 2>, 2> view, std::array<std::array<float, 2>, 2> viewport,
+ std::array<float, 9> transform) {
+ params_ = CreateMouseViewParameters(std::move(view), std::move(viewport), std::move(transform));
+ return *this;
+}
+
+MouseEventBuilder& MouseEventBuilder::AddMouseDeviceInfo(uint32_t id,
+ std::vector<uint8_t> buttons) {
+ device_info_ = std::make_optional<fup_MouseDeviceInfo>();
+ device_info_->set_id(id);
+ device_info_->set_buttons(buttons);
+ return *this;
+}
+
+fup_MouseEvent MouseEventBuilder::Build() {
+ fup_MouseEvent event;
+ if (time_) {
+ event.set_timestamp(time_.value());
+ }
+ if (params_) {
+ event.set_view_parameters(std::move(params_.value()));
+ }
+ if (sample_) {
+ event.set_pointer_sample(std::move(sample_.value()));
+ }
+ if (device_info_) {
+ event.set_device_info(std::move(device_info_.value()));
+ }
+ event.set_trace_flow_id(123);
+ return event;
+}
+
+std::vector<fup_MouseEvent> MouseEventBuilder::BuildAsVector() {
+ std::vector<fup_MouseEvent> events;
+ events.emplace_back(Build());
+ return events;
+}
+
+} // namespace embedder_testing
diff --git a/src/embedder/test_util/mouse_event_builder.h b/src/embedder/test_util/mouse_event_builder.h
new file mode 100644
index 0000000..484de6b
--- /dev/null
+++ b/src/embedder/test_util/mouse_event_builder.h
@@ -0,0 +1,43 @@
+// Copyright 2013 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.
+
+#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_MOUSE_EVENT_BUILDER_H_
+#define FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_MOUSE_EVENT_BUILDER_H_
+
+#include <fuchsia/ui/pointer/cpp/fidl.h>
+#include <zircon/types.h>
+
+#include <array>
+#include <optional>
+#include <vector>
+
+namespace embedder_testing {
+
+// A helper class for crafting a fuchsia.ui.pointer.MouseEventBuilder table.
+class MouseEventBuilder {
+ public:
+ static MouseEventBuilder New();
+
+ MouseEventBuilder& AddTime(zx_time_t time);
+ MouseEventBuilder& AddSample(uint32_t id, std::array<float, 2> position,
+ std::vector<uint8_t> pressed_buttons, std::array<int64_t, 2> scroll,
+ std::array<int64_t, 2> scroll_in_physical_pixel,
+ bool is_precision_scroll);
+ MouseEventBuilder& AddViewParameters(std::array<std::array<float, 2>, 2> view,
+ std::array<std::array<float, 2>, 2> viewport,
+ std::array<float, 9> transform);
+ MouseEventBuilder& AddMouseDeviceInfo(uint32_t id, std::vector<uint8_t> buttons);
+ fuchsia::ui::pointer::MouseEvent Build();
+ std::vector<fuchsia::ui::pointer::MouseEvent> BuildAsVector();
+
+ private:
+ std::optional<zx_time_t> time_;
+ std::optional<fuchsia::ui::pointer::MousePointerSample> sample_;
+ std::optional<fuchsia::ui::pointer::ViewParameters> params_;
+ std::optional<fuchsia::ui::pointer::MouseDeviceInfo> device_info_;
+};
+
+} // namespace embedder_testing
+
+#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_MOUSE_EVENT_BUILDER_H_
diff --git a/src/embedder/test_util/touch_event_builder.cc b/src/embedder/test_util/touch_event_builder.cc
new file mode 100644
index 0000000..c099840
--- /dev/null
+++ b/src/embedder/test_util/touch_event_builder.cc
@@ -0,0 +1,88 @@
+// Copyright 2013 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 "src/embedder/test_util/touch_event_builder.h"
+
+namespace embedder_testing {
+
+using fup_EventPhase = fuchsia::ui::pointer::EventPhase;
+using fup_TouchEvent = fuchsia::ui::pointer::TouchEvent;
+using fup_TouchIxnId = fuchsia::ui::pointer::TouchInteractionId;
+using fup_TouchIxnResult = fuchsia::ui::pointer::TouchInteractionResult;
+using fup_TouchPointerSample = fuchsia::ui::pointer::TouchPointerSample;
+using fup_ViewParameters = fuchsia::ui::pointer::ViewParameters;
+
+namespace {
+
+fup_ViewParameters CreateTouchViewParameters(std::array<std::array<float, 2>, 2> view,
+ std::array<std::array<float, 2>, 2> viewport,
+ std::array<float, 9> transform) {
+ fup_ViewParameters params;
+ fuchsia::ui::pointer::Rectangle view_rect;
+ view_rect.min = view[0];
+ view_rect.max = view[1];
+ params.view = view_rect;
+ fuchsia::ui::pointer::Rectangle viewport_rect;
+ viewport_rect.min = viewport[0];
+ viewport_rect.max = viewport[1];
+ params.viewport = viewport_rect;
+ params.viewport_to_view_transform = transform;
+ return params;
+}
+
+} // namespace
+
+TouchEventBuilder TouchEventBuilder::New() { return TouchEventBuilder(); }
+
+TouchEventBuilder& TouchEventBuilder::AddTime(zx_time_t time) {
+ time_ = time;
+ return *this;
+}
+
+TouchEventBuilder& TouchEventBuilder::AddSample(fup_TouchIxnId id, fup_EventPhase phase,
+ std::array<float, 2> position) {
+ sample_ = std::make_optional<fup_TouchPointerSample>();
+ sample_->set_interaction(id);
+ sample_->set_phase(phase);
+ sample_->set_position_in_viewport(position);
+ return *this;
+}
+
+TouchEventBuilder& TouchEventBuilder::AddViewParameters(
+ std::array<std::array<float, 2>, 2> view, std::array<std::array<float, 2>, 2> viewport,
+ std::array<float, 9> transform) {
+ params_ = CreateTouchViewParameters(std::move(view), std::move(viewport), std::move(transform));
+ return *this;
+}
+
+TouchEventBuilder& TouchEventBuilder::AddResult(fup_TouchIxnResult result) {
+ result_ = result;
+ return *this;
+}
+
+fup_TouchEvent TouchEventBuilder::Build() {
+ fup_TouchEvent event;
+ if (time_) {
+ event.set_timestamp(time_.value());
+ }
+ if (params_) {
+ event.set_view_parameters(std::move(params_.value()));
+ }
+ if (sample_) {
+ event.set_pointer_sample(std::move(sample_.value()));
+ }
+ if (result_) {
+ event.set_interaction_result(std::move(result_.value()));
+ }
+ event.set_trace_flow_id(0);
+ return event;
+}
+
+std::vector<fup_TouchEvent> TouchEventBuilder::BuildAsVector() {
+ std::vector<fup_TouchEvent> events;
+ events.emplace_back(Build());
+ return events;
+}
+
+} // namespace embedder_testing
diff --git a/src/embedder/test_util/touch_event_builder.h b/src/embedder/test_util/touch_event_builder.h
new file mode 100644
index 0000000..f7ee886
--- /dev/null
+++ b/src/embedder/test_util/touch_event_builder.h
@@ -0,0 +1,43 @@
+// Copyright 2013 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.
+
+#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_TOUCH_EVENT_BUILDER_H_
+#define FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_TOUCH_EVENT_BUILDER_H_
+
+#include <fuchsia/ui/pointer/cpp/fidl.h>
+#include <zircon/types.h>
+
+#include <array>
+#include <optional>
+#include <vector>
+
+namespace embedder_testing {
+
+// A helper class for crafting a fuchsia.ui.pointer.TouchEvent table.
+class TouchEventBuilder {
+ public:
+ static TouchEventBuilder New();
+
+ TouchEventBuilder& AddTime(zx_time_t time);
+ TouchEventBuilder& AddSample(fuchsia::ui::pointer::TouchInteractionId id,
+ fuchsia::ui::pointer::EventPhase phase,
+ std::array<float, 2> position);
+ TouchEventBuilder& AddViewParameters(std::array<std::array<float, 2>, 2> view,
+ std::array<std::array<float, 2>, 2> viewport,
+ std::array<float, 9> transform);
+ TouchEventBuilder& AddResult(fuchsia::ui::pointer::TouchInteractionResult result);
+
+ fuchsia::ui::pointer::TouchEvent Build();
+ std::vector<fuchsia::ui::pointer::TouchEvent> BuildAsVector();
+
+ private:
+ std::optional<zx_time_t> time_;
+ std::optional<fuchsia::ui::pointer::ViewParameters> params_;
+ std::optional<fuchsia::ui::pointer::TouchPointerSample> sample_;
+ std::optional<fuchsia::ui::pointer::TouchInteractionResult> result_;
+};
+
+} // namespace embedder_testing
+
+#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_TEST_UTIL_TOUCH_EVENT_BUILDER_H_
diff --git a/src/embedder/touch_delegate_unittests.cc b/src/embedder/touch_delegate_unittests.cc
new file mode 100644
index 0000000..175a0e7
--- /dev/null
+++ b/src/embedder/touch_delegate_unittests.cc
@@ -0,0 +1,621 @@
+// Copyright 2013 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/pointer/cpp/fidl.h>
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/async-loop/default.h>
+#include <lib/fdio/fd.h>
+#include <lib/fidl/cpp/binding_set.h>
+
+#include <array>
+#include <optional>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#include "src/embedder/test_util/fakes/touch_source.h"
+#include "src/embedder/test_util/touch_event_builder.h"
+#include "src/embedder/touch_delegate.h"
+
+namespace embedder_testing {
+
+using fup_EventPhase = fuchsia::ui::pointer::EventPhase;
+using fup_TouchEvent = fuchsia::ui::pointer::TouchEvent;
+using fup_TouchIxnId = fuchsia::ui::pointer::TouchInteractionId;
+using fup_TouchIxnResult = fuchsia::ui::pointer::TouchInteractionResult;
+using fup_TouchIxnStatus = fuchsia::ui::pointer::TouchInteractionStatus;
+using fup_TouchPointerSample = fuchsia::ui::pointer::TouchPointerSample;
+using fup_TouchResponse = fuchsia::ui::pointer::TouchResponse;
+using fup_TouchResponseType = fuchsia::ui::pointer::TouchResponseType;
+
+constexpr std::array<std::array<float, 2>, 2> kRect = {{{0, 0}, {20, 20}}};
+constexpr std::array<float, 9> kIdentity = {1, 0, 0, 0, 1, 0, 0, 0, 1};
+constexpr fup_TouchIxnId kIxnOne = {.device_id = 1u, .pointer_id = 1u, .interaction_id = 2u};
+
+// Fixture to exercise Flutter runner's implementation for
+// fuchsia.ui.pointer.TouchSource.
+class TouchDelegateTest : public ::testing::Test {
+ protected:
+ TouchDelegateTest() : loop_(&kAsyncLoopConfigAttachToCurrentThread) {
+ touch_source_ = std::make_unique<FakeTouchSource>();
+ touch_delegate_ = std::make_unique<embedder::TouchDelegate>(
+ touch_source_bindings_.AddBinding(touch_source_.get()));
+ }
+
+ void RunLoopUntilIdle() { loop_.RunUntilIdle(); }
+
+ std::unique_ptr<FakeTouchSource> touch_source_;
+ std::unique_ptr<embedder::TouchDelegate> touch_delegate_;
+
+ async::Loop loop_;
+ fidl::BindingSet<fuchsia::ui::pointer::TouchSource> touch_source_bindings_;
+};
+
+TEST_F(TouchDelegateTest, Data_FuchsiaTimeVersusFlutterTime) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+ // Fuchsia ADD -> Flutter ADD+DOWN
+ std::vector<fup_TouchEvent> events =
+ TouchEventBuilder::New()
+ .AddTime(/* in nanoseconds */ 1111789u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .AddResult({.interaction = kIxnOne, .status = fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 2u);
+ EXPECT_EQ((*pointers)[0].timestamp, /* in microseconds */ 1111u);
+ EXPECT_EQ((*pointers)[1].timestamp, /* in microseconds */ 1111u);
+}
+
+TEST_F(TouchDelegateTest, Phase_FlutterPhasesAreSynthesized) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ // Fuchsia ADD -> Flutter ADD+DOWN
+ std::vector<fup_TouchEvent> events =
+ TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .AddResult({.interaction = kIxnOne, .status = fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 2u);
+ EXPECT_EQ((*pointers)[0].phase, FlutterPointerPhase::kAdd);
+ EXPECT_EQ((*pointers)[1].phase, FlutterPointerPhase::kDown);
+
+ // Fuchsia CHANGE -> Flutter MOVE
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 1u);
+ EXPECT_EQ((*pointers)[0].phase, FlutterPointerPhase::kMove);
+
+ // Fuchsia REMOVE -> Flutter UP+REMOVE
+ events = TouchEventBuilder::New()
+ .AddTime(3333000u)
+ .AddSample(kIxnOne, fup_EventPhase::REMOVE, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 2u);
+ EXPECT_EQ((*pointers)[0].phase, FlutterPointerPhase::kUp);
+ EXPECT_EQ((*pointers)[1].phase, FlutterPointerPhase::kRemove);
+}
+
+TEST_F(TouchDelegateTest, Phase_FuchsiaCancelBecomesFlutterCancel) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ // Fuchsia ADD -> Flutter ADD+DOWN
+ std::vector<fup_TouchEvent> events =
+ TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .AddResult({.interaction = kIxnOne, .status = fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 2u);
+ EXPECT_EQ((*pointers)[0].phase, FlutterPointerPhase::kAdd);
+ EXPECT_EQ((*pointers)[1].phase, FlutterPointerPhase::kDown);
+
+ // Fuchsia CANCEL -> Flutter CANCEL
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddSample(kIxnOne, fup_EventPhase::CANCEL, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 1u);
+ EXPECT_EQ((*pointers)[0].phase, FlutterPointerPhase::kCancel);
+}
+
+TEST_F(TouchDelegateTest, Coordinates_CorrectMapping) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ // Fuchsia ADD event, with a view parameter that maps the viewport identically
+ // to the view. Then the center point of the viewport should map to the center
+ // of the view, (10.f, 10.f).
+ std::vector<fup_TouchEvent> events =
+ TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddViewParameters(/*view*/ {{{0, 0}, {20, 20}}},
+ /*viewport*/ {{{0, 0}, {20, 20}}},
+ /*matrix*/ {1, 0, 0, 0, 1, 0, 0, 0, 1})
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .AddResult({.interaction = kIxnOne, .status = fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 2u); // ADD - DOWN
+ EXPECT_FLOAT_EQ((*pointers)[0].x, 10.f);
+ EXPECT_FLOAT_EQ((*pointers)[0].y, 10.f);
+ pointers = {};
+
+ // Fuchsia CHANGE event, with a view parameter that translates the viewport by
+ // (10, 10) within the view. Then the minimal point in the viewport (its
+ // origin) should map to the center of the view, (10.f, 10.f).
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddViewParameters(/*view*/ {{{0, 0}, {20, 20}}},
+ /*viewport*/ {{{0, 0}, {20, 20}}},
+ /*matrix*/ {1, 0, 0, 0, 1, 0, 10, 10, 1})
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {0.f, 0.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 1u); // MOVE
+ EXPECT_FLOAT_EQ((*pointers)[0].x, 10.f);
+ EXPECT_FLOAT_EQ((*pointers)[0].y, 10.f);
+
+ // Fuchsia CHANGE event, with a view parameter that scales the viewport by
+ // (0.5, 0.5) within the view. Then the maximal point in the viewport should
+ // map to the center of the view, (10.f, 10.f).
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddViewParameters(/*view*/ {{{0, 0}, {20, 20}}},
+ /*viewport*/ {{{0, 0}, {20, 20}}},
+ /*matrix*/ {0.5f, 0, 0, 0, 0.5f, 0, 0, 0, 1})
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {20.f, 20.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 1u); // MOVE
+ EXPECT_FLOAT_EQ((*pointers)[0].x, 10.f);
+ EXPECT_FLOAT_EQ((*pointers)[0].y, 10.f);
+}
+
+TEST_F(TouchDelegateTest, Coordinates_DownEventClampedToView) {
+ const float kSmallDiscrepancy = -0.00003f;
+
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ std::vector<fup_TouchEvent> events =
+ TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, kSmallDiscrepancy})
+ .AddResult({.interaction = kIxnOne, .status = fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers->size(), 2u);
+
+ const auto& add_event = (*pointers)[0];
+ EXPECT_FLOAT_EQ(add_event.x, 10.f);
+ EXPECT_FLOAT_EQ(add_event.y, kSmallDiscrepancy);
+
+ const auto& down_event = (*pointers)[1];
+ EXPECT_FLOAT_EQ(down_event.x, 10.f);
+ EXPECT_EQ(down_event.y, 0.f);
+}
+
+TEST_F(TouchDelegateTest, Protocol_FirstResponseIsEmpty) {
+ bool called = false;
+ touch_delegate_->WatchLoop([&called](std::vector<FlutterPointerEvent> events) { called = true; });
+ RunLoopUntilIdle(); // Server gets Watch call.
+
+ EXPECT_FALSE(called); // No events yet received to forward to client.
+ // Server sees an initial "response" from client, which is empty, by contract.
+ const auto responses = touch_source_->UploadedResponses();
+ ASSERT_TRUE(responses.has_value());
+ ASSERT_EQ(responses->size(), 0u);
+}
+
+TEST_F(TouchDelegateTest, Protocol_ResponseMatchesEarlierEvents) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ // Fuchsia view parameter only. Empty response.
+ std::vector<fup_TouchEvent> events = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .BuildAsVector();
+
+ // Fuchsia ptr 1 ADD sample. Yes response.
+ fup_TouchEvent e1 = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample({.device_id = 0u, .pointer_id = 1u, .interaction_id = 3u},
+ fup_EventPhase::ADD, {10.f, 10.f})
+ .Build();
+ events.emplace_back(std::move(e1));
+
+ // Fuchsia ptr 2 ADD sample. Yes response.
+ fup_TouchEvent e2 = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddSample({.device_id = 0u, .pointer_id = 2u, .interaction_id = 3u},
+ fup_EventPhase::ADD, {5.f, 5.f})
+ .Build();
+ events.emplace_back(std::move(e2));
+
+ // Fuchsia ptr 3 ADD sample. Yes response.
+ fup_TouchEvent e3 = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddSample({.device_id = 0u, .pointer_id = 3u, .interaction_id = 3u},
+ fup_EventPhase::ADD, {1.f, 1.f})
+ .Build();
+ events.emplace_back(std::move(e3));
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ const auto responses = touch_source_->UploadedResponses();
+ ASSERT_TRUE(responses.has_value());
+ ASSERT_EQ(responses.value().size(), 4u);
+ // Event 0 did not carry a sample, so no response.
+ EXPECT_FALSE(responses.value()[0].has_response_type());
+ // Events 1-3 had a sample, must have a response.
+ EXPECT_TRUE(responses.value()[1].has_response_type());
+ EXPECT_EQ(responses.value()[1].response_type(), fup_TouchResponseType::YES);
+ EXPECT_TRUE(responses.value()[2].has_response_type());
+ EXPECT_EQ(responses.value()[2].response_type(), fup_TouchResponseType::YES);
+ EXPECT_TRUE(responses.value()[3].has_response_type());
+ EXPECT_EQ(responses.value()[3].response_type(), fup_TouchResponseType::YES);
+}
+
+TEST_F(TouchDelegateTest, Protocol_LateGrant) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ // Fuchsia ADD, no grant result - buffer it.
+ std::vector<fup_TouchEvent> events = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ EXPECT_EQ(pointers.value().size(), 0u);
+ pointers = {};
+
+ // Fuchsia CHANGE, no grant result - buffer it.
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ EXPECT_EQ(pointers.value().size(), 0u);
+ pointers = {};
+
+ // Fuchsia result: ownership granted. Buffered pointers released.
+ events = TouchEventBuilder::New()
+ .AddTime(3333000u)
+ .AddResult({kIxnOne, fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 3u); // ADD - DOWN - MOVE
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kAdd);
+ EXPECT_EQ(pointers.value()[1].phase, FlutterPointerPhase::kDown);
+ EXPECT_EQ(pointers.value()[2].phase, FlutterPointerPhase::kMove);
+ pointers = {};
+
+ // Fuchsia CHANGE, grant result - release immediately.
+ events = TouchEventBuilder::New()
+ .AddTime(/* in nanoseconds */ 4444000u)
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 1u);
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kMove);
+ EXPECT_EQ(pointers.value()[0].timestamp, /* in microseconds */ 4444u);
+ pointers = {};
+}
+
+TEST_F(TouchDelegateTest, Protocol_LateGrantCombo) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ // Fuchsia ADD, no grant result - buffer it.
+ std::vector<fup_TouchEvent> events = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ EXPECT_EQ(pointers.value().size(), 0u);
+ pointers = {};
+
+ // Fuchsia CHANGE, no grant result - buffer it.
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ EXPECT_EQ(pointers.value().size(), 0u);
+ pointers = {};
+
+ // Fuchsia CHANGE, with grant result - release buffered events.
+ events = TouchEventBuilder::New()
+ .AddTime(3333000u)
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {10.f, 10.f})
+ .AddResult({kIxnOne, fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 4u); // ADD - DOWN - MOVE - MOVE
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kAdd);
+ EXPECT_EQ(pointers.value()[0].timestamp, 1111u);
+ EXPECT_EQ(pointers.value()[1].phase, FlutterPointerPhase::kDown);
+ EXPECT_EQ(pointers.value()[1].timestamp, 1111u);
+ EXPECT_EQ(pointers.value()[2].phase, FlutterPointerPhase::kMove);
+ EXPECT_EQ(pointers.value()[2].timestamp, 2222u);
+ EXPECT_EQ(pointers.value()[3].phase, FlutterPointerPhase::kMove);
+ EXPECT_EQ(pointers.value()[3].timestamp, 3333u);
+ pointers = {};
+}
+
+TEST_F(TouchDelegateTest, Protocol_EarlyGrant) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ // Fuchsia ADD, with grant result - release immediately.
+ std::vector<fup_TouchEvent> events = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .AddResult({kIxnOne, fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 2u);
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kAdd);
+ EXPECT_EQ(pointers.value()[1].phase, FlutterPointerPhase::kDown);
+ pointers = {};
+
+ // Fuchsia CHANGE, after grant result - release immediately.
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 1u);
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kMove);
+ pointers = {};
+}
+
+TEST_F(TouchDelegateTest, Protocol_LateDeny) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ // Fuchsia ADD, no grant result - buffer it.
+ std::vector<fup_TouchEvent> events = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ EXPECT_EQ(pointers.value().size(), 0u);
+ pointers = {};
+
+ // Fuchsia CHANGE, no grant result - buffer it.
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ EXPECT_EQ(pointers.value().size(), 0u);
+ pointers = {};
+
+ // Fuchsia result: ownership denied. Buffered pointers deleted.
+ events = TouchEventBuilder::New()
+ .AddTime(3333000u)
+ .AddResult({kIxnOne, fup_TouchIxnStatus::DENIED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 0u); // Do not release to client!
+ pointers = {};
+}
+
+TEST_F(TouchDelegateTest, Protocol_LateDenyCombo) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ // Fuchsia ADD, no grant result - buffer it.
+ std::vector<fup_TouchEvent> events = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ EXPECT_EQ(pointers.value().size(), 0u);
+ pointers = {};
+
+ // Fuchsia CHANGE, no grant result - buffer it.
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddSample(kIxnOne, fup_EventPhase::CHANGE, {10.f, 10.f})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ EXPECT_EQ(pointers.value().size(), 0u);
+ pointers = {};
+
+ // Fuchsia result: ownership denied. Buffered pointers deleted.
+ events = TouchEventBuilder::New()
+ .AddTime(3333000u)
+ .AddSample(kIxnOne, fup_EventPhase::CANCEL, {10.f, 10.f})
+ .AddResult({kIxnOne, fup_TouchIxnStatus::DENIED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 0u); // Do not release to client!
+ pointers = {};
+}
+
+TEST_F(TouchDelegateTest, Protocol_PointersAreIndependent) {
+ std::optional<std::vector<FlutterPointerEvent>> pointers;
+ touch_delegate_->WatchLoop(
+ [&pointers](std::vector<FlutterPointerEvent> events) { pointers = std::move(events); });
+ RunLoopUntilIdle(); // Server gets watch call.
+
+ constexpr fup_TouchIxnId kIxnTwo = {.device_id = 1u, .pointer_id = 2u, .interaction_id = 2u};
+
+ // Fuchsia ptr1 ADD and ptr2 ADD, no grant result for either - buffer them.
+ std::vector<fup_TouchEvent> events = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddViewParameters(kRect, kRect, kIdentity)
+ .AddSample(kIxnOne, fup_EventPhase::ADD, {10.f, 10.f})
+ .BuildAsVector();
+ fup_TouchEvent ptr2 = TouchEventBuilder::New()
+ .AddTime(1111000u)
+ .AddSample(kIxnTwo, fup_EventPhase::ADD, {15.f, 15.f})
+ .Build();
+ events.emplace_back(std::move(ptr2));
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ EXPECT_EQ(pointers.value().size(), 0u);
+ pointers = {};
+
+ // Server grants win to pointer 2.
+ events = TouchEventBuilder::New()
+ .AddTime(2222000u)
+ .AddResult({kIxnTwo, fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ // Note: Fuchsia's device and pointer IDs (both 32 bit) are coerced together
+ // to fit in Flutter's 64-bit device ID. However, Flutter's pointer_identifier
+ // is not set by platform runner code - PointerDataCaptureConverter (PDPC)
+ // sets it.
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 2u);
+ EXPECT_EQ(pointers.value()[0].device, (int64_t)((1ul << 16) | 2u));
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kAdd);
+ EXPECT_EQ(pointers.value()[1].device, (int64_t)((1ul << 16) | 2u));
+ EXPECT_EQ(pointers.value()[1].phase, FlutterPointerPhase::kDown);
+ pointers = {};
+
+ // Server grants win to pointer 1.
+ events = TouchEventBuilder::New()
+ .AddTime(3333000u)
+ .AddResult({kIxnOne, fup_TouchIxnStatus::GRANTED})
+ .BuildAsVector();
+ touch_source_->ScheduleCallback(std::move(events));
+ RunLoopUntilIdle();
+
+ ASSERT_TRUE(pointers.has_value());
+ ASSERT_EQ(pointers.value().size(), 2u);
+ EXPECT_EQ(pointers.value()[0].device, (int64_t)((1ul << 16) | 1u));
+ EXPECT_EQ(pointers.value()[0].phase, FlutterPointerPhase::kAdd);
+ EXPECT_EQ(pointers.value()[1].device, (int64_t)((1ul << 16) | 1u));
+ EXPECT_EQ(pointers.value()[1].phase, FlutterPointerPhase::kDown);
+ pointers = {};
+}
+
+} // namespace embedder_testing
diff --git a/third_party/googletest b/third_party/googletest
index 42ca3da..0b56cbe 160000
--- a/third_party/googletest
+++ b/third_party/googletest
@@ -1 +1 @@
-Subproject commit 42ca3da5798750c2998dd09b751838227e1f58d3
+Subproject commit 0b56cbec076adbb75d22e3d9f75e99ad063d1ac3
diff --git a/third_party/sdk-integration b/third_party/sdk-integration
index 43888d9..5956f5f 160000
--- a/third_party/sdk-integration
+++ b/third_party/sdk-integration
@@ -1 +1 @@
-Subproject commit 43888d9923fee6cd774cf6932c94bbea41307f8e
+Subproject commit 5956f5f770fa73f62949bc1429b1575403db9064