diff --git a/shell/platform/fuchsia/flutter/tests/integration/embedder/flutter-embedder-test.cc b/shell/platform/fuchsia/flutter/tests/integration/embedder/flutter-embedder-test.cc index 20200e0725c58..78ffa88c17327 100644 --- a/shell/platform/fuchsia/flutter/tests/integration/embedder/flutter-embedder-test.cc +++ b/shell/platform/fuchsia/flutter/tests/integration/embedder/flutter-embedder-test.cc @@ -86,12 +86,8 @@ constexpr auto kTestUiStackRef = ChildRef{kTestUiStack}; constexpr fuchsia_test_utils::Color kParentBackgroundColor = {0x00, 0x00, 0xFF, 0xFF}; // Blue -constexpr fuchsia_test_utils::Color kParentTappedColor = {0x00, 0x00, 0x00, - 0xFF}; // Black constexpr fuchsia_test_utils::Color kChildBackgroundColor = {0xFF, 0x00, 0xFF, 0xFF}; // Pink -constexpr fuchsia_test_utils::Color kChildTappedColor = {0xFF, 0xFF, 0x00, - 0xFF}; // Yellow // TODO(fxb/64201): Remove forced opacity colors when Flatland is enabled. constexpr fuchsia_test_utils::Color kOverlayBackgroundColor1 = { @@ -160,60 +156,18 @@ class FlutterEmbedderTest : public ::loop_fixture::RealLoop, callback = nullptr, zx::duration timeout = kTestTimeout); - // Simulates a tap at location (x, y). - void InjectTap(int32_t x, int32_t y); - - // Injects an input event, and posts a task to retry after - // `kTapRetryInterval`. - // - // We post the retry task because the first input event we send to Flutter may - // be lost. The reason the first event may be lost is that there is a race - // condition as the scene owner starts up. - // - // More specifically: in order for our app - // to receive the injected input, two things must be true before we inject - // touch input: - // * The Scenic root view must have been installed, and - // * The Input Pipeline must have received a viewport to inject touch into. - // - // The problem we have is that the `is_rendering` signal that we monitor only - // guarantees us the view is ready. If the viewport is not ready in Input - // Pipeline at that time, it will drop the touch event. - // - // TODO(fxbug.dev/96986): Improve synchronization and remove retry logic. - void TryInject(int32_t x, int32_t y); - private: fuchsia::ui::scenic::Scenic* scenic() { return scenic_.get(); } void SetUpRealmBase(); - // Registers a fake touch screen device with an injection coordinate space - // spanning [-1000, 1000] on both axes. - void RegisterTouchScreen(); - fuchsia::ui::scenic::ScenicPtr scenic_; - 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_; // Wrapped in optional since the view is not created until the middle of SetUp component_testing::RealmBuilder realm_builder_; std::unique_ptr realm_; - - // The typical latency on devices we've tested is ~60 msec. The retry interval - // is chosen to be a) Long enough that it's unlikely that we send a new tap - // while a previous tap is still being - // processed. That is, it should be far more likely that a new tap is sent - // because the first tap was lost, than because the system is just running - // slowly. - // b) Short enough that we don't slow down tryjobs. - // - // The first property is important to avoid skewing the latency metrics that - // we collect. For an explanation of why a tap might be lost, see the - // documentation for TryInject(). - static constexpr auto kTapRetryInterval = zx::sec(1); }; void FlutterEmbedderTest::SetUpRealmBase() { @@ -374,9 +328,6 @@ void FlutterEmbedderTest::LaunchParentViewInRealm( } realm_ = std::make_unique(realm_builder_.Build()); - // Register fake touch screen device. - RegisterTouchScreen(); - // Instruct Test UI Stack to present parent-view's View. std::optional view_ref_koid; scene_provider_ = realm_->Connect(); @@ -443,36 +394,6 @@ bool FlutterEmbedderTest::TakeScreenshotUntil( timeout); } -void FlutterEmbedderTest::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 FlutterEmbedderTest::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; - fake_touchscreen_->SimulateTap(std::move(tap_request), [x, y]() { - FML_LOG(INFO) << "Tap injected at (" << x << ", " << y << ")"; - }); -} - -void FlutterEmbedderTest::TryInject(int32_t x, int32_t y) { - InjectTap(x, y); - async::PostDelayedTask( - dispatcher(), [this, x, y] { TryInject(x, y); }, kTapRetryInterval); -} - TEST_F(FlutterEmbedderTest, Embedding) { LaunchParentViewInRealm(); @@ -489,53 +410,6 @@ TEST_F(FlutterEmbedderTest, Embedding) { })); } -TEST_F(FlutterEmbedderTest, HittestEmbedding) { - LaunchParentViewInRealm(); - - // Take screenshot until we see the child-view's embedded color. - ASSERT_TRUE(TakeScreenshotUntil(kChildBackgroundColor)); - - // Simulate a tap at the center of the child view. - TryInject(/* x = */ 0, /* y = */ 0); - - // Take screenshot until we see the child-view's tapped color. - ASSERT_TRUE(TakeScreenshotUntil( - kChildTappedColor, - [](std::map histogram) { - // Expect parent and child background colors, with parent color > child - // color. - EXPECT_GT(histogram[kParentBackgroundColor], 0u); - EXPECT_EQ(histogram[kChildBackgroundColor], 0u); - EXPECT_GT(histogram[kChildTappedColor], 0u); - EXPECT_GT(histogram[kParentBackgroundColor], - histogram[kChildTappedColor]); - })); -} - -TEST_F(FlutterEmbedderTest, HittestDisabledEmbedding) { - LaunchParentViewInRealm({"--no-hitTestable"}); - - // Take screenshots until we see the child-view's embedded color. - ASSERT_TRUE(TakeScreenshotUntil(kChildBackgroundColor)); - - // Simulate a tap at the center of the child view. - TryInject(/* x = */ 0, /* y = */ 0); - - // The parent-view should change color. - ASSERT_TRUE(TakeScreenshotUntil( - kParentTappedColor, - [](std::map histogram) { - // Expect parent and child background colors, with parent color > child - // color. - EXPECT_EQ(histogram[kParentBackgroundColor], 0u); - EXPECT_GT(histogram[kParentTappedColor], 0u); - EXPECT_GT(histogram[kChildBackgroundColor], 0u); - EXPECT_EQ(histogram[kChildTappedColor], 0u); - EXPECT_GT(histogram[kParentTappedColor], - histogram[kChildBackgroundColor]); - })); -} - TEST_F(FlutterEmbedderTest, EmbeddingWithOverlay) { LaunchParentViewInRealm({"--showOverlay"}); @@ -555,33 +429,4 @@ TEST_F(FlutterEmbedderTest, EmbeddingWithOverlay) { })); } -TEST_F(FlutterEmbedderTest, HittestEmbeddingWithOverlay) { - LaunchParentViewInRealm({"--showOverlay"}); - - // Take screenshot until we see the child-view's embedded color. - ASSERT_TRUE(TakeScreenshotUntil(kChildBackgroundColor)); - - // The bottom-left corner of the overlay is at the center of the screen, - // which is at (0, 0) in the injection coordinate space. Inject a pointer - // event just outside the overlay's bounds, and ensure that it goes to the - // embedded view. - TryInject(/* x = */ -1, /* y = */ 1); - - // Take screenshot until we see the child-view's tapped color. - ASSERT_TRUE(TakeScreenshotUntil( - kChildTappedColor, - [](std::map histogram) { - // Expect parent, overlay and child background colors. - // With parent color > child color and overlay color > child color. - const size_t overlay_pixel_count = OverlayPixelCount(histogram); - EXPECT_GT(histogram[kParentBackgroundColor], 0u); - EXPECT_GT(overlay_pixel_count, 0u); - EXPECT_EQ(histogram[kChildBackgroundColor], 0u); - EXPECT_GT(histogram[kChildTappedColor], 0u); - EXPECT_GT(histogram[kParentBackgroundColor], - histogram[kChildTappedColor]); - EXPECT_GT(overlay_pixel_count, histogram[kChildTappedColor]); - })); -} - } // namespace flutter_embedder_test diff --git a/shell/platform/fuchsia/flutter/tests/integration/touch-input/embedding-flutter-view/BUILD.gn b/shell/platform/fuchsia/flutter/tests/integration/touch-input/embedding-flutter-view/BUILD.gn index 665050687076d..dda2ac7cc5808 100644 --- a/shell/platform/fuchsia/flutter/tests/integration/touch-input/embedding-flutter-view/BUILD.gn +++ b/shell/platform/fuchsia/flutter/tests/integration/touch-input/embedding-flutter-view/BUILD.gn @@ -13,6 +13,8 @@ dart_library("lib") { sources = [ "embedding-flutter-view.dart" ] deps = [ + "//flutter/shell/platform/fuchsia/dart:args", + "//flutter/shell/platform/fuchsia/dart:vector_math", "//flutter/tools/fuchsia/dart:fuchsia_services", "//flutter/tools/fuchsia/dart:zircon", "//flutter/tools/fuchsia/fidl:fuchsia.ui.app", diff --git a/shell/platform/fuchsia/flutter/tests/integration/touch-input/embedding-flutter-view/lib/embedding-flutter-view.dart b/shell/platform/fuchsia/flutter/tests/integration/touch-input/embedding-flutter-view/lib/embedding-flutter-view.dart index 745556bccc20b..7473fbdeebbe9 100644 --- a/shell/platform/fuchsia/flutter/tests/integration/touch-input/embedding-flutter-view/lib/embedding-flutter-view.dart +++ b/shell/platform/fuchsia/flutter/tests/integration/touch-input/embedding-flutter-view/lib/embedding-flutter-view.dart @@ -7,15 +7,37 @@ import 'dart:typed_data'; import 'dart:io'; import 'dart:ui'; +import 'package:args/args.dart'; import 'package:fidl_fuchsia_ui_app/fidl_async.dart'; import 'package:fidl_fuchsia_ui_views/fidl_async.dart'; import 'package:fidl_fuchsia_ui_test_input/fidl_async.dart' as test_touch; import 'package:fuchsia_services/services.dart'; +import 'package:vector_math/vector_math_64.dart' as vector_math_64; import 'package:zircon/zircon.dart'; +final _argsCsvFilePath = '/config/data/args.csv'; + void main(List args) { print('Launching embedding-flutter-view'); - TestApp app = TestApp(ChildView.gfx(_launchGfxChildView())); + + args = args + _GetArgsFromConfigFile(); + final parser = ArgParser() + ..addFlag('showOverlay', defaultsTo: false) + ..addFlag('hitTestable', defaultsTo: true) + ..addFlag('focusable', defaultsTo: true); + + final arguments = parser.parse(args); + for (final option in arguments.options) { + print('embedding-flutter-view args: $option: ${arguments[option]}'); + } + + TestApp app = TestApp( + ChildView.gfx(_launchGfxChildView()), + showOverlay: arguments['showOverlay'], + hitTestable: arguments['hitTestable'], + focusable: arguments['focusable'], + ); + app.run(); } @@ -24,14 +46,22 @@ class TestApp { static const _blue = Color.fromARGB(255, 0, 0, 255); final ChildView childView; + final bool showOverlay; + final bool hitTestable; + final bool focusable; final _responseListener = test_touch.TouchInputListenerProxy(); Color _backgroundColor = _blue; - TestApp(this.childView) {} + TestApp( + this.childView, + {this.showOverlay = false, + this.hitTestable = true, + this.focusable = true}) { + } void run() { - childView.create((ByteData reply) { + childView.create(hitTestable, focusable, (ByteData reply) { // Set up window callbacks. window.onPointerDataPacket = (PointerDataPacket packet) { this.pointerDataPacket(packet); @@ -67,13 +97,52 @@ class TestApp { final sceneBuilder = SceneBuilder() ..pushClipRect(physicalBounds) ..addPicture(Offset.zero, picture); - // Child view should take up half the screen - final childPhysicalSize = window.physicalSize * 0.5; + + final childPhysicalSize = window.physicalSize * 0.25; + // Alignment.center + final windowCenter = size.center(Offset.zero); + final windowPhysicalCenter = window.physicalSize.center(Offset.zero); + final childPhysicalOffset = windowPhysicalCenter - childPhysicalSize.center(Offset.zero); + sceneBuilder + ..pushTransform( + vector_math_64.Matrix4.translationValues(childPhysicalOffset.dx, + childPhysicalOffset.dy, + 0.0).storage) ..addPlatformView(childView.viewId, width: childPhysicalSize.width, - height: size.height) + height: childPhysicalSize.height) ..pop(); + + if (showOverlay) { + final containerSize = size * 0.5; + // Alignment.center + final containerOffset = windowCenter - containerSize.center(Offset.zero); + + final overlaySize = containerSize * 0.5; + // Alignment.topRight + final overlayOffset = Offset( + containerOffset.dx + containerSize.width - overlaySize.width, + containerOffset.dy); + final overlayPhysicalSize = overlaySize * pixelRatio; + final overlayPhysicalOffset = overlayOffset * pixelRatio; + final overlayPhysicalBounds = overlayPhysicalOffset & overlayPhysicalSize; + + final recorder = PictureRecorder(); + final overlayCullRect = Offset.zero & overlayPhysicalSize; // in canvas physical coordinates + final canvas = Canvas(recorder, overlayCullRect); + canvas.scale(pixelRatio); + + final paint = Paint()..color = Color.fromARGB(255, 0, 255, 0); + canvas.drawRect(Offset.zero & overlaySize, paint); + + final overlayPicture = recorder.endRecording(); + sceneBuilder + ..pushClipRect(overlayPhysicalBounds) // in window physical coordinates + ..addPicture(overlayPhysicalOffset, overlayPicture) + ..pop(); + } + sceneBuilder.pop(); window.render(sceneBuilder.build()); } @@ -124,15 +193,18 @@ class ChildView { assert(viewId != null); } - void create(PlatformMessageResponseCallback callback) { + void create( + bool hitTestable, + bool focusable, + PlatformMessageResponseCallback callback) { // Construct the dart:ui platform message to create the view, and when the // return callback is invoked, build the scene. At that point, it is safe // to embed the child view in the scene. final viewOcclusionHint = Rect.zero; final Map args = { 'viewId': viewId, - 'hitTestable': true, - 'focusable': true, + 'hitTestable': hitTestable, + 'focusable': focusable, 'viewOcclusionHintLTRB': [ viewOcclusionHint.left, viewOcclusionHint.top, @@ -173,3 +245,14 @@ ViewHolderToken _launchGfxChildView() { return viewHolderToken; } + +List _GetArgsFromConfigFile() { + List args; + final f = File(_argsCsvFilePath); + if (!f.existsSync()) { + return List.empty(); + } + final fileContentCsv = f.readAsStringSync(); + args = fileContentCsv.split('\n'); + return args; +} diff --git a/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-test.cc b/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-test.cc index 9ebf909be4437..c5361ba877c90 100644 --- a/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-test.cc +++ b/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-test.cc @@ -112,6 +112,7 @@ namespace { // Types imported for the realm_builder library. using component_testing::ChildRef; using component_testing::ConfigValue; +using component_testing::DirectoryContents; using component_testing::LocalComponent; using component_testing::LocalComponentHandles; using component_testing::ParentRef; @@ -324,6 +325,57 @@ class FlutterTapTest : public FlutterTapTestBase { }; class FlutterEmbedTapTest : public FlutterTapTestBase { + protected: + void SetUp() override { + PortableUITest::SetUp(false); + + // 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); + } + + void LaunchClientWithEmbeddedView() { + BuildRealm(); + + // 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(); + + PortableUITest::LaunchClientWithEmbeddedView(); + } + + // Helper method to add a component argument + // This will be written into an args.csv file that can be parsed and read + // by embedding-flutter-view.dart + // + // Note: You must call this method before LaunchClientWithEmbeddedView() + // Realm Builder will not allow you to create a new directory / file in a + // realm that's already been built + void AddComponentArgument(std::string component_arg) { + auto config_directory_contents = DirectoryContents(); + config_directory_contents.AddFile("args.csv", component_arg); + realm_builder()->RouteReadOnlyDirectory( + "config-data", {kEmbeddingFlutterViewRef}, + std::move(config_directory_contents)); + } + private: void ExtendRealm() override { FML_LOG(INFO) << "Extending realm"; @@ -392,6 +444,9 @@ TEST_P(FlutterTapTest, FlutterTap) { /*expected_y=*/static_cast(display_height() / 4.0f), /*component_name=*/"touch-input-view"); }); + + // There should be 1 injected tap + ASSERT_EQ(touch_injection_request_count(), 1); } INSTANTIATE_TEST_SUITE_P(FlutterEmbedTapTestParameterized, @@ -405,19 +460,19 @@ TEST_P(FlutterEmbedTapTest, FlutterEmbedTap) { FML_LOG(INFO) << "Client launched"; { - // Embedded child view takes up the left side of the screen + // Embedded child view takes up the center of the screen // Expect a response from the child view if we inject a tap there - InjectTap(-500, -500); + InjectTap(0, 0); RunLoopUntil([this] { return LastEventReceivedMatches( - /*expected_x=*/static_cast(display_width() / 4.0f), - /*expected_y=*/static_cast(display_height() / 4.0f), + /*expected_x=*/static_cast(display_width() / 8.0f), + /*expected_y=*/static_cast(display_height() / 8.0f), /*component_name=*/"touch-input-view"); }); } { - // Parent view takes up the right side of the screen + // Parent view takes up the rest of the screen // Validate that parent can still receive taps InjectTap(500, 500); RunLoopUntil([this] { @@ -432,5 +487,63 @@ TEST_P(FlutterEmbedTapTest, FlutterEmbedTap) { ASSERT_EQ(touch_injection_request_count(), 2); } +TEST_P(FlutterEmbedTapTest, FlutterEmbedHittestDisabled) { + FML_LOG(INFO) << "Initializing scene"; + AddComponentArgument("--no-hitTestable"); + LaunchClientWithEmbeddedView(); + FML_LOG(INFO) << "Client launched"; + + // Embedded child view takes up the center of the screen + // hitTestable is turned off for the embedded child view + // Expect the parent (embedding-flutter-view) to respond if we inject a tap + // there + InjectTap(0, 0); + RunLoopUntil([this] { + return LastEventReceivedMatches( + /*expected_x=*/static_cast(display_width() / 2.0f), + /*expected_y=*/static_cast(display_height() / 2.0f), + /*component_name=*/"embedding-flutter-view"); + }); + + // There should be 1 injected tap + ASSERT_EQ(touch_injection_request_count(), 1); +} + +TEST_P(FlutterEmbedTapTest, FlutterEmbedOverlayEnabled) { + FML_LOG(INFO) << "Initializing scene"; + AddComponentArgument("--showOverlay"); + LaunchClientWithEmbeddedView(); + FML_LOG(INFO) << "Client launched"; + + { + // The bottom-left corner of the overlay is at the center of the screen + // Expect the overlay / parent view to respond if we inject a tap there + // and not the embedded child view + InjectTap(0, 0); + RunLoopUntil([this] { + return LastEventReceivedMatches( + /*expected_x=*/static_cast(display_width() / 2.0f), + /*expected_y=*/static_cast(display_height() / 2.0f), + /*component_name=*/"embedding-flutter-view"); + }); + } + + { + // The embedded child view is just outside of the bottom-left corner of the + // overlay + // Expect the embedded child view to still receive taps + InjectTap(-1, -1); + RunLoopUntil([this] { + return LastEventReceivedMatches( + /*expected_x=*/static_cast(display_width() / 8.0f), + /*expected_y=*/static_cast(display_height() / 8.0f), + /*component_name=*/"touch-input-view"); + }); + } + + // There should be 2 injected taps + ASSERT_EQ(touch_injection_request_count(), 2); +} + } // namespace } // namespace touch_input_test::testing diff --git a/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-view/lib/touch-input-view.dart b/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-view/lib/touch-input-view.dart index 58c1ac98edbad..b3b9d514061cf 100644 --- a/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-view/lib/touch-input-view.dart +++ b/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-view/lib/touch-input-view.dart @@ -69,6 +69,10 @@ class TestApp { for (PointerData data in packet.data) { print('touch-input-view received tap: ${data.toStringFull()}'); + if (data.change == PointerChange.down) { + this._backgroundColor = _yellow; + } + if (data.change == PointerChange.down || data.change == PointerChange.move) { Incoming.fromSvcPath() ..connectToService(_responseListener) diff --git a/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.cc b/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.cc index bbbf2e2c36e76..aae6eb5934195 100644 --- a/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.cc +++ b/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.cc @@ -29,9 +29,16 @@ using fuchsia_test_utils::CheckViewExistsInSnapshot; } // namespace -void PortableUITest::SetUp() { +void PortableUITest::SetUp(bool build_realm) { SetUpRealmBase(); ExtendRealm(); + + if (build_realm) { + BuildRealm(); + } +} + +void PortableUITest::BuildRealm() { realm_ = std::make_unique(realm_builder_.Build()); } diff --git a/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.h b/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.h index f6a457d3bb8ce..1e6c0fcb649fb 100644 --- a/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.h +++ b/shell/platform/fuchsia/flutter/tests/integration/utils/portable_ui_test.h @@ -44,7 +44,11 @@ class PortableUITest : public ::loop_fixture::RealLoop { "flutter_jit_runner.cm"; static constexpr auto kFlutterRunnerEnvironment = "flutter_runner_env"; - void SetUp(); + void SetUp(bool build_realm = true); + + // Calls the Build method for Realm Builder to build the realm + // Can only be called once, panics otherwise + void BuildRealm(); // Attaches a client view to the scene, and waits for it to render. void LaunchClient();