From b797d693859fdba351a43206177febb375cd11d0 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 3 Oct 2022 11:31:03 -0700 Subject: [PATCH] Port over C++ components of the one-flutter test (flutter/engine#36546) --- .../flutter/tests/integration/BUILD.gn | 1 + .../tests/integration/touch-input/BUILD.gn | 71 ++++ .../touch-input/meta/gtest_runner.shard.cml | 17 + .../touch-input/meta/touch-input-test.cml | 38 ++ .../touch-input/one-flutter/BUILD.gn | 43 ++ .../one-flutter/lib/one-flutter.dart | 18 + .../one-flutter/meta/one-flutter-realm.cml | 56 +++ .../one-flutter/meta/one-flutter-view.cml | 38 ++ .../touch-input/touch-input-test.cc | 400 ++++++++++++++++++ .../flutter/tests/integration/utils/BUILD.gn | 16 + .../integration/utils/portable_ui_test.cc | 164 +++++++ .../integration/utils/portable_ui_test.h | 99 +++++ 12 files changed, 961 insertions(+) create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/BUILD.gn create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/meta/gtest_runner.shard.cml create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/meta/touch-input-test.cml create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/BUILD.gn create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/lib/one-flutter.dart create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/meta/one-flutter-realm.cml create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/meta/one-flutter-view.cml create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-test.cc create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.cc create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.h diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/BUILD.gn index 7746be62d91..0f98a94885a 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/BUILD.gn +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/BUILD.gn @@ -11,5 +11,6 @@ group("integration") { deps = [ "embedder:tests", "text-input:tests", + "touch-input:tests", ] } diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/BUILD.gn new file mode 100644 index 00000000000..27f422029fc --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/BUILD.gn @@ -0,0 +1,71 @@ +# 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. + +assert(is_fuchsia) + +import("//build/fuchsia/sdk.gni") +import("//flutter/tools/fuchsia/fuchsia_archive.gni") +import("//flutter/tools/fuchsia/gn-sdk/package.gni") + +group("tests") { + testonly = true + deps = [ ":touch-input-test" ] +} + +executable("touch-input-test-bin") { + testonly = true + output_name = "touch-input-test" + sources = [ "touch-input-test.cc" ] + + # This is needed for //third_party/googletest for linking zircon symbols. + libs = [ "$fuchsia_sdk_path/arch/$target_cpu/sysroot/lib/libzircon.so" ] + + deps = [ + "$fuchsia_sdk_root/fidl:fuchsia.accessibility.semantics", + "$fuchsia_sdk_root/fidl:fuchsia.buildinfo", + "$fuchsia_sdk_root/fidl:fuchsia.component", + "$fuchsia_sdk_root/fidl:fuchsia.fonts", + "$fuchsia_sdk_root/fidl:fuchsia.intl", + "$fuchsia_sdk_root/fidl:fuchsia.kernel", + "$fuchsia_sdk_root/fidl:fuchsia.memorypressure", + "$fuchsia_sdk_root/fidl:fuchsia.metrics", + "$fuchsia_sdk_root/fidl:fuchsia.net.interfaces", + "$fuchsia_sdk_root/fidl:fuchsia.tracing.provider", + "$fuchsia_sdk_root/fidl:fuchsia.ui.app", + "$fuchsia_sdk_root/fidl:fuchsia.ui.input", + "$fuchsia_sdk_root/fidl:fuchsia.ui.policy", + "$fuchsia_sdk_root/fidl:fuchsia.ui.scenic", + "$fuchsia_sdk_root/fidl:fuchsia.ui.test.input", + "$fuchsia_sdk_root/fidl:fuchsia.ui.test.scene", + "$fuchsia_sdk_root/fidl:fuchsia.ui.test.scene", + "$fuchsia_sdk_root/fidl:fuchsia.web", + "$fuchsia_sdk_root/pkg:async", + "$fuchsia_sdk_root/pkg:async-loop-testing", + "$fuchsia_sdk_root/pkg:fidl_cpp", + "$fuchsia_sdk_root/pkg:scenic_cpp", + "$fuchsia_sdk_root/pkg:sys_component_cpp_testing", + "$fuchsia_sdk_root/pkg:zx", + "//build/fuchsia/fidl:fuchsia.ui.gfx", + "//flutter/fml", + "//flutter/shell/platform/fuchsia/flutter/tests/integration/utils:portable_ui_test", + "//third_party/googletest:gtest", + "//third_party/googletest:gtest_main", + ] +} + +fuchsia_test_archive("touch-input-test") { + testonly = true + deps = [ + ":touch-input-test-bin", + "one-flutter:package", + + # "OOT" copies of the runners used by tests, to avoid conflicting with the + # runners in the base fuchsia image. + # TODO(fxbug.dev/106575): Fix this with subpackages. + "//flutter/shell/platform/fuchsia/flutter:oot_flutter_jit_runner", + ] + + binary = "$target_name" + cml_file = rebase_path("meta/$target_name.cml") +} diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/meta/gtest_runner.shard.cml b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/meta/gtest_runner.shard.cml new file mode 100644 index 00000000000..d9871b70f30 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/meta/gtest_runner.shard.cml @@ -0,0 +1,17 @@ +// 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. +{ + program: { + runner: "gtest_runner", + }, + capabilities: [ + { protocol: "fuchsia.test.Suite" }, + ], + expose: [ + { + protocol: "fuchsia.test.Suite", + from: "self", + }, + ], +} diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/meta/touch-input-test.cml b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/meta/touch-input-test.cml new file mode 100644 index 00000000000..937e9c3604c --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/meta/touch-input-test.cml @@ -0,0 +1,38 @@ +// 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: [ + "gtest_runner.shard.cml", + "sys/component/realm_builder_absolute.shard.cml", + + "syslog/client.shard.cml", + "vulkan/client.shard.cml", + + // This test needs both the vulkan facet and the hermetic-tier-2 facet, + // so we are forced to make it a system test. + "sys/testing/system-test.shard.cml", + ], + program: { + binary: "bin/app", + }, + offer: [ + { + // Offer capabilities needed by components in this test realm. + // Keep it minimal, describe only what's actually needed. + // TODO(fxbug.dev/81446): Remove this list. + protocol: [ + "fuchsia.kernel.RootJobForInspect", + "fuchsia.kernel.Stats", + "fuchsia.logger.LogSink", + "fuchsia.scheduler.ProfileProvider", + "fuchsia.sysmem.Allocator", + "fuchsia.tracing.provider.Registry", + "fuchsia.ui.input.ImeService", + "fuchsia.vulkan.loader.Loader", + ], + from: "parent", + to: "#realm_builder", + }, + ], +} diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/BUILD.gn new file mode 100644 index 00000000000..06983c0d684 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/BUILD.gn @@ -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. + +import("//build/fuchsia/sdk.gni") +import("//flutter/tools/fuchsia/dart/dart_library.gni") +import("//flutter/tools/fuchsia/flutter/flutter_component.gni") +import("//flutter/tools/fuchsia/gn-sdk/component.gni") +import("//flutter/tools/fuchsia/gn-sdk/package.gni") + +dart_library("lib") { + package_name = "one-flutter" + sources = [ "one-flutter.dart" ] + deps = [ + "//flutter/tools/fuchsia/dart:fuchsia_services", + "//flutter/tools/fuchsia/dart:zircon", + "//flutter/tools/fuchsia/fidl:fuchsia.ui.test.input", + ] +} + +flutter_component("component") { + testonly = true + component_name = "one-flutter-view" + manifest = rebase_path("meta/one-flutter-view.cml") + main_package = "one-flutter" + main_dart = "one-flutter.dart" + deps = [ ":lib" ] +} + +fuchsia_component("realm") { + testonly = true + manifest = "meta/one-flutter-realm.cml" + manifest_output_name = "one-flutter-realm.cml" + deps = [ ":component" ] +} + +fuchsia_package("package") { + testonly = true + deps = [ + ":component", + ":realm", + ] +} diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/lib/one-flutter.dart b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/lib/one-flutter.dart new file mode 100644 index 00000000000..34aca414f09 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/lib/one-flutter.dart @@ -0,0 +1,18 @@ +// 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. + +// TODO(https://fxbug.dev/84961): Fix null safety and remove this language version. +// @dart=2.9 + +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:fidl_fuchsia_ui_test_input/fidl_async.dart'; +import 'package:fuchsia_services/services.dart'; + +int main() { + print('touch-input-view: starting'); +} diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/meta/one-flutter-realm.cml b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/meta/one-flutter-realm.cml new file mode 100644 index 00000000000..ccc7f991a47 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/meta/one-flutter-realm.cml @@ -0,0 +1,56 @@ +// 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. +{ + children: [ + { + name: "flutter_jit_runner", + url: "fuchsia-pkg://fuchsia.com/flutter_jit_runner#meta/flutter_jit_runner.cm", + }, + { + name: "one_flutter_view", + url: "#meta/one-flutter-view.cm", + environment: "#one_flutter_view_env", + }, + ], + offer: [ + { + protocol: [ + "fuchsia.logger.LogSink", + "fuchsia.sysmem.Allocator", + "fuchsia.tracing.provider.Registry", + "fuchsia.ui.scenic.Scenic", + "fuchsia.vulkan.loader.Loader", + ], + from: "parent", + to: [ + "#flutter_jit_runner", + "#one_flutter_view", + ], + }, + { + protocol: [ "fuchsia.ui.test.input.TouchInputListener" ], + from: "parent", + to: "#one_flutter_view", + }, + ], + expose: [ + { + protocol: [ "fuchsia.ui.app.ViewProvider" ], + from: "#one_flutter_view", + to: "parent", + }, + ], + environments: [ + { + name: "one_flutter_view_env", + extends: "realm", + runners: [ + { + runner: "flutter_jit_runner", + from: "#flutter_jit_runner", + }, + ], + }, + ], +} diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/meta/one-flutter-view.cml b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/meta/one-flutter-view.cml new file mode 100644 index 00000000000..8fb7dd9e259 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/one-flutter/meta/one-flutter-view.cml @@ -0,0 +1,38 @@ +// 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: [ + "sys/component/realm_builder_absolute.shard.cml", + "syslog/client.shard.cml", + ], + program: { + data: "data/one-flutter", + + // Always use the jit runner for now. + // TODO(fxbug.dev/106577): Implement manifest merging build rules for V2 components. + runner: "flutter_jit_runner", + }, + capabilities: [ + { + protocol: [ "fuchsia.ui.app.ViewProvider" ], + }, + ], + use: [ + { + protocol: [ + "fuchsia.sysmem.Allocator", + "fuchsia.tracing.provider.Registry", + "fuchsia.ui.scenic.Scenic", + "fuchsia.ui.test.input.TouchInputListener", + "fuchsia.vulkan.loader.Loader", + ], + }, + ], + expose: [ + { + protocol: [ "fuchsia.ui.app.ViewProvider" ], + from: "self", + }, + ], +} diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-test.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-test.cc new file mode 100644 index 00000000000..e8eede23d15 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-test.cc @@ -0,0 +1,400 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "flutter/fml/logging.h" +#include "flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.h" + +// This test exercises the touch input dispatch path from Input Pipeline to a +// Scenic client. It is a multi-component test, and carefully avoids sleeping or +// polling for component coordination. +// - It runs real Root Presenter, Input Pipeline, and Scenic components. +// - It uses a fake display controller; the physical device is unused. +// +// Components involved +// - This test program +// - Input Pipeline +// - Root Presenter +// - Scenic +// - Child view, a Scenic client +// +// Touch dispatch path +// - Test program's injection -> Input Pipeline -> Scenic -> Child view +// +// Setup sequence +// - The test sets up this view hierarchy: +// - Top level scene, owned by Root Presenter. +// - Child view, owned by the ui client. +// - The test waits for a Scenic event that verifies the child has UI content in +// the scene graph. +// - The test injects input into Input Pipeline, emulating a display's touch +// report. +// - Input Pipeline dispatches the touch event to Scenic, which in turn +// dispatches it to the child. +// - The child receives the touch event and reports back to the test over a +// custom test-only FIDL. +// - Test waits for the child to report a touch; when the test receives the +// report, the test quits +// successfully. +// +// This test uses the realm_builder library to construct the topology of +// components and routes services between them. For v2 components, every test +// driver component sits as a child of test_manager in the topology. Thus, the +// topology of a test driver component such as this one looks like this: +// +// test_manager +// | +// touch-input-test.cml (this component) +// +// With the usage of the realm_builder library, we construct a realm during +// runtime and then extend the topology to look like: +// +// test_manager +// | +// touch-input-test.cml (this component) +// | +// +// / \ +// scenic input-pipeline +// +// For more information about testing v2 components and realm_builder, +// visit the following links: +// +// Testing: https://fuchsia.dev/fuchsia-src/concepts/testing/v2 +// Realm Builder: +// https://fuchsia.dev/fuchsia-src/development/components/v2/realm_builder + +namespace touch_input_test::testing { +namespace { +// Types imported for the realm_builder library. +using component_testing::ChildRef; +using component_testing::ConfigValue; +using component_testing::LocalComponent; +using component_testing::LocalComponentHandles; +using component_testing::ParentRef; +using component_testing::Protocol; +using component_testing::Realm; +using component_testing::RealmRoot; +using component_testing::Route; + +using fuchsia_test_utils::PortableUITest; + +using RealmBuilder = component_testing::RealmBuilder; +// Alias for Component child name as provided to Realm Builder. +using ChildName = std::string; +// Alias for Component Legacy URL as provided to Realm Builder. +using LegacyUrl = std::string; + +// Max timeout in failure cases. +// Set this as low as you can that still works across all test platforms. +constexpr zx::duration kTimeout = zx::min(5); + +constexpr auto kMockResponseListener = "response_listener"; + +enum class TapLocation { kTopLeft, kTopRight }; + +// Combines all vectors in `vecs` into one. +template +std::vector merge(std::initializer_list> vecs) { + std::vector result; + for (auto v : vecs) { + result.insert(result.end(), v.begin(), v.end()); + } + return result; +} + +bool CompareDouble(double f0, double f1, double epsilon) { + return std::abs(f0 - f1) <= epsilon; +} + +// // This component implements the test.touch.ResponseListener protocol +// // and the interface for a RealmBuilder LocalComponent. A LocalComponent +// // is a component that is implemented here in the test, as opposed to +// elsewhere +// // in the system. When it's inserted to the realm, it will act like a proper +// // component. This is accomplished, in part, because the realm_builder +// // library creates the necessary plumbing. It creates a manifest for the +// // component and routes all capabilities to and from it. +class ResponseListenerServer + : public fuchsia::ui::test::input::TouchInputListener, + public LocalComponent { + public: + explicit ResponseListenerServer(async_dispatcher_t* dispatcher) + : dispatcher_(dispatcher) {} + + // |fuchsia::ui::test::input::TouchInputListener| + void ReportTouchInput( + fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest + request) override { + events_received_.push_back(std::move(request)); + } + + // |LocalComponent::Start| + // When the component framework requests for this component to start, this + // method will be invoked by the realm_builder library. + void Start(std::unique_ptr local_handles) override { + // When this component starts, add a binding to the + // test.touch.ResponseListener protocol to this component's outgoing + // directory. + ASSERT_EQ(ZX_OK, local_handles->outgoing()->AddPublicService( + fidl::InterfaceRequestHandler< + fuchsia::ui::test::input::TouchInputListener>( + [this](auto request) { + bindings_.AddBinding(this, std::move(request), + dispatcher_); + }))); + local_handles_.emplace_back(std::move(local_handles)); + } + + const std::vector< + fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest>& + events_received() { + return events_received_; + } + + private: + async_dispatcher_t* dispatcher_ = nullptr; + std::vector> local_handles_; + fidl::BindingSet bindings_; + std::vector< + fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest> + events_received_; +}; + +class FlutterTapTest : public PortableUITest, + public ::testing::Test, + public ::testing::WithParamInterface { + protected: + ~FlutterTapTest() override { + FML_CHECK(touch_injection_request_count() > 0) + << "Injection expected but didn't happen."; + } + + void SetUp() override { + PortableUITest::SetUp(); + + // Post a "just in case" quit task, if the test hangs. + async::PostDelayedTask( + dispatcher(), + [] { + FML_LOG(FATAL) + << "\n\n>> Test did not complete in time, terminating. <<\n\n"; + }, + kTimeout); + + // Get the display dimensions. + FML_LOG(INFO) << "Waiting for scenic display info"; + scenic_ = realm_root()->template Connect(); + scenic_->GetDisplayInfo([this](fuchsia::ui::gfx::DisplayInfo display_info) { + display_width_ = display_info.width_in_px; + display_height_ = display_info.height_in_px; + FML_LOG(INFO) << "Got display_width = " << display_width_ + << " and display_height = " << display_height_; + }); + RunLoopUntil( + [this] { return display_width_ != 0 && display_height_ != 0; }); + + // Register input injection device. + FML_LOG(INFO) << "Registering input injection device"; + RegisterTouchScreen(); + } + + // Routes needed to setup Flutter client. + static std::vector GetFlutterRoutes(ChildRef target) { + return { + {.capabilities = {Protocol{ + fuchsia::ui::test::input::TouchInputListener::Name_}}, + .source = ChildRef{kMockResponseListener}, + .targets = {target}}, + {.capabilities = {Protocol{fuchsia::logger::LogSink::Name_}, + Protocol{fuchsia::sysmem::Allocator::Name_}, + Protocol{ + fuchsia::tracing::provider::Registry::Name_}}, + .source = ParentRef(), + .targets = {target}}, + {.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_}}, + .source = kTestUIStackRef, + .targets = {target}}, + }; + } + + std::vector GetTestRoutes() { + return merge( + {GetFlutterRoutes(ChildRef{kFlutterRealm}), + { + {.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}}, + .source = ChildRef{kFlutterRealm}, + .targets = {ParentRef()}}, + }}); + } + + std::vector> GetTestV2Components() { + return { + std::make_pair(kFlutterRealm, kFlutterRealmUrl), + }; + }; + + bool LastEventReceivedMatches(float expected_x, + float expected_y, + std::string component_name) { + const auto& events_received = response_listener_server_->events_received(); + if (events_received.empty()) { + return false; + } + + const auto& last_event = events_received.back(); + + auto pixel_scale = last_event.has_device_pixel_ratio() + ? last_event.device_pixel_ratio() + : 1; + + auto actual_x = pixel_scale * last_event.local_x(); + auto actual_y = pixel_scale * last_event.local_y(); + + FML_LOG(INFO) << "Expecting event for component " << component_name + << " at (" << expected_x << ", " << expected_y << ")"; + FML_LOG(INFO) << "Received event for component " << component_name + << " at (" << actual_x << ", " << actual_y + << "), accounting for pixel scale of " << pixel_scale; + + return CompareDouble(actual_x, expected_x, pixel_scale) && + CompareDouble(actual_y, expected_y, pixel_scale) && + last_event.component_name() == component_name; + } + + void InjectInput(TapLocation tap_location) { + // The /config/data/display_rotation (90) specifies how many degrees to + // rotate the presentation child view, counter-clockwise, in a + // right-handed coordinate system. Thus, the user observes the child + // view to rotate *clockwise* by that amount (90). + // + // Hence, a tap in the center of the display's top-right quadrant is + // observed by the child view as a tap in the center of its top-left + // quadrant. + auto touch = std::make_unique(); + switch (tap_location) { + case TapLocation::kTopLeft: + // center of top right quadrant -> ends up as center of top left + // quadrant + InjectTap(/* x = */ 500, /* y = */ -500); + break; + case TapLocation::kTopRight: + // center of bottom right quadrant -> ends up as center of top right + // quadrant + InjectTap(/* x = */ 500, /* y = */ 500); + break; + default: + FML_CHECK(false) << "Received invalid TapLocation"; + } + } + + // Guaranteed to be initialized after SetUp(). + uint32_t display_width() const { return display_width_; } + uint32_t display_height() const { return display_height_; } + + static constexpr auto kFlutterRealm = "flutter-realm"; + static constexpr auto kFlutterRealmUrl = + "fuchsia-pkg://fuchsia.com/one-flutter#meta/one-flutter-realm.cm"; + + private: + void ExtendRealm() override { + // Key part of service setup: have this test component vend the + // |ResponseListener| service in the constructed realm. + response_listener_server_ = + std::make_unique(dispatcher()); + realm_builder()->AddLocalChild(kMockResponseListener, + response_listener_server_.get()); + + realm_builder()->AddRoute( + {.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_}}, + .source = kTestUIStackRef, + .targets = {ParentRef()}}); + + // Add components specific for this test case to the realm. + for (const auto& [name, component] : GetTestV2Components()) { + realm_builder()->AddChild(name, component); + } + + // Add the necessary routing for each of the extra components added + // above. + for (const auto& route : GetTestRoutes()) { + realm_builder()->AddRoute(route); + } + } + + ParamType GetTestUIStackUrl() override { return GetParam(); }; + + std::unique_ptr response_listener_server_; + + fuchsia::ui::scenic::ScenicPtr scenic_; + uint32_t display_width_ = 0; + uint32_t display_height_ = 0; +}; + +INSTANTIATE_TEST_SUITE_P( + FlutterTapTestParameterized, + FlutterTapTest, + ::testing::Values( + "fuchsia-pkg://fuchsia.com/gfx-root-presenter-test-ui-stack#meta/" + "test-ui-stack.cm")); + +TEST_P(FlutterTapTest, FlutterTap) { + // Launch client view, and wait until it's rendering to proceed with the test. + FML_LOG(INFO) << "Initializing scene"; + LaunchClient(); + FML_LOG(INFO) << "Client launched"; + + InjectInput(TapLocation::kTopLeft); + RunLoopUntil([this] { + return LastEventReceivedMatches( + /*expected_x=*/static_cast(display_height()) / 4.f, + /*expected_y=*/static_cast(display_width()) / 4.f, + /*component_name=*/"one-flutter"); + }); +} + +} // namespace +} // namespace touch_input_test::testing diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/BUILD.gn index 78b693d2edc..b41ffae3a39 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/BUILD.gn +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/BUILD.gn @@ -40,3 +40,19 @@ source_set("screenshot") { "//flutter/fml", ] } + +source_set("portable_ui_test") { + testonly = true + sources = [ + "portable_ui_test.cc", + "portable_ui_test.h", + ] + + deps = [ + ":check_view", + "$fuchsia_sdk_root/fidl:fuchsia.ui.observation.geometry", + "$fuchsia_sdk_root/pkg:async-loop-testing", + "$fuchsia_sdk_root/pkg:sys_component_cpp_testing", + "//flutter/fml", + ] +} diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.cc new file mode 100644 index 00000000000..9d5cc3d094e --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.cc @@ -0,0 +1,164 @@ +// 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 "portable_ui_test.h" + +#include +#include +#include +#include +#include +#include + +#include "check_view.h" +#include "flutter/fml/logging.h" + +namespace fuchsia_test_utils { +namespace { + +// Types imported for the realm_builder library. +using component_testing::ChildRef; +using component_testing::ParentRef; +using component_testing::Protocol; +using component_testing::RealmRoot; +using component_testing::Route; + +using fuchsia_test_utils::CheckViewExistsInSnapshot; + +} // namespace + +void PortableUITest::SetUp() { + SetUpRealmBase(); + + ExtendRealm(); + + realm_ = std::make_unique(realm_builder_.Build()); +} + +void PortableUITest::SetUpRealmBase() { + FML_LOG(INFO) << "Setting up realm base"; + + // Add test UI stack component. + realm_builder_.AddChild(kTestUIStack, GetTestUIStackUrl()); + + // Route base system services to flutter and the test UI stack. + realm_builder_.AddRoute( + Route{.capabilities = + { + Protocol{fuchsia::logger::LogSink::Name_}, + Protocol{fuchsia::sys::Environment::Name_}, + Protocol{fuchsia::sysmem::Allocator::Name_}, + Protocol{fuchsia::tracing::provider::Registry::Name_}, + Protocol{kVulkanLoaderServiceName}, + Protocol{kProfileProviderServiceName}, + }, + .source = ParentRef{}, + .targets = {kTestUIStackRef}}); + + // Capabilities routed to test driver. + realm_builder_.AddRoute(Route{ + .capabilities = {Protocol{fuchsia::ui::test::input::Registry::Name_}, + Protocol{fuchsia::ui::test::scene::Controller::Name_}}, + .source = kTestUIStackRef, + .targets = {ParentRef{}}}); +} + +void PortableUITest::ProcessViewGeometryResponse( + fuchsia::ui::observation::geometry::WatchResponse response) { + // Process update if no error + if (!response.has_error()) { + std::vector* updates = + response.mutable_updates(); + if (updates && !updates->empty()) { + last_view_tree_snapshot_ = std::move(updates->back()); + } + } else { + // Otherwise process error + const auto& error = response.error(); + if (error | fuchsia::ui::observation::geometry::Error::CHANNEL_OVERFLOW) { + FML_LOG(INFO) << "View Tree watcher channel overflowed"; + } else if (error | + fuchsia::ui::observation::geometry::Error::BUFFER_OVERFLOW) { + FML_LOG(INFO) << "View Tree watcher buffer overflowed"; + } else if (error | + fuchsia::ui::observation::geometry::Error::VIEWS_OVERFLOW) { + // This one indicates some possible data loss, so we log with a high + // severity + FML_LOG(WARNING) + << "View Tree watcher attempted to report too many views"; + } + } +} + +void PortableUITest::WatchViewGeometry() { + FML_CHECK(view_tree_watcher_) + << "View Tree watcher must be registered before calling Watch()"; + + view_tree_watcher_->Watch([this](auto response) { + ProcessViewGeometryResponse(std::move(response)); + WatchViewGeometry(); + }); +} + +bool PortableUITest::HasViewConnected(zx_koid_t view_ref_koid) { + return last_view_tree_snapshot_.has_value() && + CheckViewExistsInSnapshot(*last_view_tree_snapshot_, view_ref_koid); +} + +void PortableUITest::LaunchClient() { + scene_provider_ = realm_->Connect(); + scene_provider_.set_error_handler( + [](auto) { FML_LOG(ERROR) << "Error from test scene provider"; }); + fuchsia::ui::test::scene::ControllerAttachClientViewRequest request; + request.set_view_provider(realm_->Connect()); + scene_provider_->RegisterViewTreeWatcher(view_tree_watcher_.NewRequest(), + []() {}); + scene_provider_->AttachClientView( + std::move(request), [this](auto client_view_ref_koid) { + client_root_view_ref_koid_ = client_view_ref_koid; + }); + + FML_LOG(INFO) << "Waiting for client view ref koid"; + RunLoopUntil([this] { return client_root_view_ref_koid_.has_value(); }); + + WatchViewGeometry(); + + FML_LOG(INFO) << "Waiting for client view to connect"; + RunLoopUntil( + [this] { return HasViewConnected(*client_root_view_ref_koid_); }); + FML_LOG(INFO) << "Client view has rendered"; +} + +void PortableUITest::RegisterTouchScreen() { + FML_LOG(INFO) << "Registering fake touch screen"; + input_registry_ = realm_->Connect(); + input_registry_.set_error_handler( + [](auto) { FML_LOG(ERROR) << "Error from input helper"; }); + + bool touchscreen_registered = false; + fuchsia::ui::test::input::RegistryRegisterTouchScreenRequest request; + request.set_device(fake_touchscreen_.NewRequest()); + input_registry_->RegisterTouchScreen( + std::move(request), + [&touchscreen_registered]() { touchscreen_registered = true; }); + + RunLoopUntil([&touchscreen_registered] { return touchscreen_registered; }); + FML_LOG(INFO) << "Touchscreen registered"; +} + +void PortableUITest::InjectTap(int32_t x, int32_t y) { + fuchsia::ui::test::input::TouchScreenSimulateTapRequest tap_request; + tap_request.mutable_tap_location()->x = x; + tap_request.mutable_tap_location()->y = y; + + FML_LOG(INFO) << "Injecting tap at (" << tap_request.tap_location().x << ", " + << tap_request.tap_location().y << ")"; + fake_touchscreen_->SimulateTap(std::move(tap_request), [this]() { + ++touch_injection_request_count_; + FML_LOG(INFO) << "*** Tap injected, count: " + << touch_injection_request_count_; + }); +} + +} // namespace fuchsia_test_utils diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.h b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.h new file mode 100644 index 00000000000..f246846d44c --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.h @@ -0,0 +1,99 @@ +// 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_FLUTTER_TESTS_INTEGRATION_UTILS_PORTABLE_UI_TEST_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_TESTS_INTEGRATION_UTILS_PORTABLE_UI_TEST_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace fuchsia_test_utils { +class PortableUITest : public ::loop_fixture::RealLoop { + public: + // The FIDL bindings for these services are not exposed in the Fuchsia SDK so + // we must encode the names manually here. + static constexpr auto kVulkanLoaderServiceName = + "fuchsia.vulkan.loader.Loader"; + static constexpr auto kProfileProviderServiceName = + "fuchsia.sheduler.ProfileProvider"; + static constexpr auto kTestUIStack = "ui"; + static constexpr auto kTestUIStackRef = + component_testing::ChildRef{kTestUIStack}; + + void SetUp(); + + // Attaches a client view to the scene, and waits for it to render. + void LaunchClient(); + + // Returns true when the specified view is fully connected to the scene AND + // has presented at least one frame of content. + bool HasViewConnected(zx_koid_t view_ref_koid); + + // Registers a fake touch screen device with an injection coordinate space + // spanning [-1000, 1000] on both axes. + void RegisterTouchScreen(); + + // Simulates a tap at location (x, y). + void InjectTap(int32_t x, int32_t y); + + protected: + component_testing::RealmBuilder* realm_builder() { return &realm_builder_; } + component_testing::RealmRoot* realm_root() { return realm_.get(); } + + int touch_injection_request_count() const { + return touch_injection_request_count_; + } + + private: + void SetUpRealmBase(); + + // Configures the test-specific component topology. + virtual void ExtendRealm() = 0; + + // Returns the test-ui-stack component url to use in this test. + virtual std::string GetTestUIStackUrl() = 0; + + // Helper method to watch watch for view geometry updates. + void WatchViewGeometry(); + + // Helper method to process a view geometry update. + void ProcessViewGeometryResponse( + fuchsia::ui::observation::geometry::WatchResponse response); + + fuchsia::ui::test::input::RegistryPtr input_registry_; + fuchsia::ui::test::input::TouchScreenPtr fake_touchscreen_; + fuchsia::ui::test::scene::ControllerPtr scene_provider_; + fuchsia::ui::observation::geometry::ViewTreeWatcherPtr view_tree_watcher_; + + component_testing::RealmBuilder realm_builder_ = + component_testing::RealmBuilder::Create(); + std::unique_ptr realm_; + + // Counts the number of completed requests to inject touch reports into input + // pipeline. + int touch_injection_request_count_ = 0; + + // The KOID of the client root view's `ViewRef`. + std::optional client_root_view_ref_koid_; + + // Holds the most recent view tree snapshot received from the view tree + // watcher. + // + // From this snapshot, we can retrieve relevant view tree state on demand, + // e.g. if the client view is rendering content. + std::optional + last_view_tree_snapshot_; +}; + +} // namespace fuchsia_test_utils + +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_TESTS_INTEGRATION_UTILS_PORTABLE_UI_TEST_H_