diff --git a/shell/platform/fuchsia/flutter/tests/integration/BUILD.gn b/shell/platform/fuchsia/flutter/tests/integration/BUILD.gn index 0f98a94885a66..30ba3b7690f42 100644 --- a/shell/platform/fuchsia/flutter/tests/integration/BUILD.gn +++ b/shell/platform/fuchsia/flutter/tests/integration/BUILD.gn @@ -10,6 +10,7 @@ group("integration") { testonly = true deps = [ "embedder:tests", + "mouse-input:tests", "text-input:tests", "touch-input:tests", ] diff --git a/shell/platform/fuchsia/flutter/tests/integration/mouse-input/BUILD.gn b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/BUILD.gn new file mode 100644 index 0000000000000..c26b44b7da874 --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/integration/mouse-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 = [ ":mouse-input-test" ] +} + +executable("mouse-input-test-bin") { + testonly = true + output_name = "mouse-input-test" + sources = [ "mouse-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.pointerinjector", + "$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.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", + "mouse-input-view:package", + "//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("mouse-input-test") { + testonly = true + deps = [ + ":mouse-input-test-bin", + + # "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/shell/platform/fuchsia/flutter/tests/integration/mouse-input/README.md b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/README.md new file mode 100644 index 0000000000000..c36111b38633a --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/README.md @@ -0,0 +1,60 @@ +# mouse-input + +`mouse-input-test` exercises mouse input through a child view (in this case, the `mouse-input-view` Dart component) and +asserting the location as well as what button was used (mouse down, mouse up, wheel, etc) during the event. We do this by +attaching the child view, injecting mouse input, and validating that the view reports the event back with the expected +payload. + +```shell +Injecting the mouse input +[mouse-input-test.cm] INFO: [portable_ui_test.cc(227)] Injecting mouse input + +View receives the event +[flutter_jit_runner] INFO: mouse-input-view.cm(flutter): mouse-input-view received input: PointerData(embedderId: 0, timeStamp: 23:18:05.031003, change: PointerChange.add, kind: PointerDeviceKind.mouse, signalKind: PointerSignalKind.none, device: 4294967295, pointerIdentifier: 0, physicalX: 641.4656372070312, physicalY: 402.9313049316406, physicalDeltaX: 0.0, physicalDeltaY: 0.0, buttons: 0, synthesized: true, pressure: 0.0, pressureMin: 0.0, pressureMax: 0.0, distance: 0.0, distanceMax: 0.0, size: 0.0, radiusMajor: 0.0, radiusMinor: 0.0, radiusMin: 0.0, radiusMax: 0.0, orientation: 0.0, tilt: 0.0, platformData: 0, scrollDeltaX: 0.0, scrollDeltaY: 0.0, panX: 0.0, panY: 0.0, panDeltaX: 0.0, panDeltaY: 0.0, scale: 0.0, rotation: 0.0) + +Successfully received response from view +[mouse-input-test.cm] INFO: [mouse-input-test.cc(120)] Received MouseInput event +[mouse-input-test.cm] INFO: [mouse-input-test.cc(207)] Client received mouse change at (641.466, 402.931) with buttons 0. +[mouse-input-test.cm] INFO: [mouse-input-test.cc(211)] Expected mouse change is at approximately (641, 402) with buttons 0. +``` + +## Running the Test + +Reference the Flutter integration test [documentation](https://github.com/flutter/engine/blob/main/shell/platform/fuchsia/flutter/tests/integration/README.md) at `//flutter/shell/platform/fuchsia/flutter/tests/integration/README.md` + +## Playing around with `mouse-input-view` + +Build Fuchsia with `workstation_eng.qemu-x64` +```shell +fx set workstation_eng.qemu-x64 && fx build +``` + +Build flutter/engine +```shell +$ENGINE_DIR/flutter/tools/gn --fuchsia --no-lto && ninja -C $ENGINE_DIR/out/fuchsia_debug_x64 flutter/shell/platform/fuchsia/flutter/tests/ +integration/mouse_input:tests +``` + +Start a Fuchsia package server +```shell +cd "$FUCHSIA_DIR" +fx serve +``` + +Publish `touch-input-view` +```shell +$FUCHSIA_DIR/.jiri_root/bin/fx pm publish -a -repo $FUCHSIA_DIR/$(cat $FUCHSIA_DIR/.fx-build-dir)/amber-files -f $ENGINE_DIR/out/ +fuchsia_debug_x64/gen/flutter/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/mouse-input-view/mouse-input-view.far +``` + +Launch Fuchsia emulator in a graphical environment +```shell +ffx emu start +``` + +**Before proceeding, make sure you have successfully completed the "Set a Password" screen** + +Add `mouse-input-view` +```shell +ffx session add fuchsia-pkg://fuchsia.com/mouse-input-view#meta/mouse-input-view.cm +``` diff --git a/shell/platform/fuchsia/flutter/tests/integration/mouse-input/meta/gtest_runner.shard.cml b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/meta/gtest_runner.shard.cml new file mode 100644 index 0000000000000..d9871b70f3022 --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/integration/mouse-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/shell/platform/fuchsia/flutter/tests/integration/mouse-input/meta/mouse-input-test.cml b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/meta/mouse-input-test.cml new file mode 100644 index 0000000000000..a2994117bd742 --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/meta/mouse-input-test.cml @@ -0,0 +1,55 @@ +// 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", + + // 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", + }, + use: [ + { + protocol: [ + "fuchsia.ui.test.input.MouseInputListener", + ] + } + ], + offer: [ + { + // Offer capabilities needed by components in this test realm. + // Keep it minimal, describe only what's actually needed. + 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", + "fuchsia.ui.scenic.Scenic", + "fuchsia.ui.composition.Allocator", + "fuchsia.ui.composition.Flatland", + "fuchsia.ui.test.input.MouseInputListener", + "fuchsia.intl.PropertyProvider", + "fuchsia.posix.socket.Provider", + "fuchsia.ui.pointerinjector.Registry", + ], + from: "parent", + to: "#realm_builder", + }, + { + directory: "pkg", + subdir: "config", + as: "config-data", + from: "framework", + to: "#realm_builder", + }, + ], +} diff --git a/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-test.cc b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-test.cc new file mode 100644 index 0000000000000..2839b6f39e5a6 --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-test.cc @@ -0,0 +1,612 @@ +// 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. + +#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" +#include "lib/fidl/cpp/interface_ptr.h" + +namespace mouse_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::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; + +// Maximum pointer movement during a clickpad press for the gesture to +// be guaranteed to be interpreted as a click. For movement greater than +// this value, upper layers may, e.g., interpret the gesture as a drag. +// +// This value corresponds to the one used to instantiate the ClickDragHandler +// registered by Input Pipeline in Scene Manager. +constexpr int64_t kClickToDragThreshold = 16.0; + +constexpr auto kMouseInputListener = "mouse_input_listener"; +constexpr auto kMouseInputListenerRef = ChildRef{kMouseInputListener}; +constexpr auto kMouseInputView = "mouse-input-view"; +constexpr auto kMouseInputViewRef = ChildRef{kMouseInputView}; +constexpr auto kMouseInputViewUrl = + "fuchsia-pkg://fuchsia.com/mouse-input-view#meta/mouse-input-view.cm"; + +struct Position { + double x = 0.0; + double y = 0.0; +}; + +// 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; +} + +int ButtonsToInt( + const std::vector& buttons) { + int result = 0; + for (const auto& button : buttons) { + result |= (0x1 >> button); + } + + return result; +} + +// `MouseInputListener` is a local test protocol that our test apps use to let +// us know what position and button press state the mouse cursor has. +class MouseInputListenerServer + : public fuchsia::ui::test::input::MouseInputListener, + public LocalComponent { + public: + explicit MouseInputListenerServer(async_dispatcher_t* dispatcher) + : dispatcher_(dispatcher) {} + + void ReportMouseInput( + fuchsia::ui::test::input::MouseInputListenerReportMouseInputRequest + request) override { + FML_LOG(INFO) << "Received MouseInput event"; + events_.push(std::move(request)); + } + + // |MockComponent::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 mock_handles) override { + FML_LOG(INFO) << "Starting MouseInputServer"; + ASSERT_EQ(ZX_OK, mock_handles->outgoing()->AddPublicService( + fidl::InterfaceRequestHandler< + fuchsia::ui::test::input::MouseInputListener>( + [this](auto request) { + bindings_.AddBinding(this, std::move(request), + dispatcher_); + }))); + mock_handles_.emplace_back(std::move(mock_handles)); + } + + size_t SizeOfEvents() const { return events_.size(); } + + fuchsia::ui::test::input::MouseInputListenerReportMouseInputRequest + PopEvent() { + auto e = std::move(events_.front()); + events_.pop(); + return e; + } + + const fuchsia::ui::test::input::MouseInputListenerReportMouseInputRequest& + LastEvent() const { + return events_.back(); + } + + void ClearEvents() { events_ = {}; } + + private: + // Not owned. + async_dispatcher_t* dispatcher_ = nullptr; + fidl::BindingSet bindings_; + std::vector> mock_handles_; + std::queue< + fuchsia::ui::test::input::MouseInputListenerReportMouseInputRequest> + events_; +}; + +class MouseInputTest : public PortableUITest, + public ::testing::Test, + public ::testing::WithParamInterface { + protected: + void SetUp() override { + PortableUITest::SetUp(); + + // Register fake mouse device. + RegisterMouse(); + + // Get the display dimensions. + FML_LOG(INFO) << "Waiting for scenic display info"; + scenic_ = realm_root()->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; }); + } + + void TearDown() override { + // at the end of test, ensure event queue is empty. + ASSERT_EQ(mouse_input_listener_->SizeOfEvents(), 0u); + } + + MouseInputListenerServer* mouse_input_listener() { + return mouse_input_listener_.get(); + } + + // Helper method for checking the test.mouse.MouseInputListener response from + // the client app. + void VerifyEvent( + fuchsia::ui::test::input::MouseInputListenerReportMouseInputRequest& + pointer_data, + double expected_x, + double expected_y, + std::vector expected_buttons, + const fuchsia::ui::test::input::MouseEventPhase expected_phase, + const std::string& component_name) { + FML_LOG(INFO) << "Client received mouse change at (" + << pointer_data.local_x() << ", " << pointer_data.local_y() + << ") with buttons " << ButtonsToInt(pointer_data.buttons()) + << "."; + FML_LOG(INFO) << "Expected mouse change is at approximately (" << expected_x + << ", " << expected_y << ") with buttons " + << ButtonsToInt(expected_buttons) << "."; + + // Allow for minor rounding differences in coordinates. + // Note: These approximations don't account for + // `PointerMotionDisplayScaleHandler` or `PointerMotionSensorScaleHandler`. + // We will need to do so in order to validate larger motion or different + // sized displays. + EXPECT_NEAR(pointer_data.local_x(), expected_x, 1); + EXPECT_NEAR(pointer_data.local_y(), expected_y, 1); + EXPECT_EQ(pointer_data.buttons(), expected_buttons); + EXPECT_EQ(pointer_data.phase(), expected_phase); + EXPECT_EQ(pointer_data.component_name(), component_name); + } + + void VerifyEventLocationOnTheRightOfExpectation( + fuchsia::ui::test::input::MouseInputListenerReportMouseInputRequest& + pointer_data, + double expected_x_min, + double expected_y, + std::vector expected_buttons, + const fuchsia::ui::test::input::MouseEventPhase expected_phase, + const std::string& component_name) { + FML_LOG(INFO) << "Client received mouse change at (" + << pointer_data.local_x() << ", " << pointer_data.local_y() + << ") with buttons " << ButtonsToInt(pointer_data.buttons()) + << "."; + FML_LOG(INFO) << "Expected mouse change is at approximately (>" + << expected_x_min << ", " << expected_y << ") with buttons " + << ButtonsToInt(expected_buttons) << "."; + + EXPECT_GT(pointer_data.local_x(), expected_x_min); + EXPECT_NEAR(pointer_data.local_y(), expected_y, 1); + EXPECT_EQ(pointer_data.buttons(), expected_buttons); + EXPECT_EQ(pointer_data.phase(), expected_phase); + EXPECT_EQ(pointer_data.component_name(), component_name); + } + + // Guaranteed to be initialized after SetUp(). + uint32_t display_width() const { return display_width_; } + uint32_t display_height() const { return display_height_; } + + private: + void ExtendRealm() override { + FML_LOG(INFO) << "Extending realm"; + mouse_input_listener_ = + std::make_unique(dispatcher()); + + // Key part of service setup: have this test component vend the + // |MouseInputListener| service in the constructed realm. + realm_builder()->AddLocalChild(kMouseInputListener, + mouse_input_listener_.get()); + + realm_builder()->AddChild(kMouseInputView, kMouseInputViewUrl, + component_testing::ChildOptions{ + .environment = kFlutterRunnerEnvironment, + }); + + realm_builder()->AddRoute( + Route{.capabilities = {Protocol{ + fuchsia::ui::test::input::MouseInputListener::Name_}}, + .source = kMouseInputListenerRef, + .targets = {kFlutterJitRunnerRef, kMouseInputViewRef}}); + + realm_builder()->AddRoute( + Route{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}}, + .source = kMouseInputViewRef, + .targets = {ParentRef()}}); + } + + ParamType GetTestUIStackUrl() override { return GetParam(); }; + + std::unique_ptr mouse_input_listener_; + + fuchsia::ui::scenic::ScenicPtr scenic_; + uint32_t display_width_ = 0; + uint32_t display_height_ = 0; +}; + +// Makes use of gtest's parameterized testing, allowing us +// to test different combinations of test-ui-stack + runners. Currently, there +// is just one combination. Documentation: +// http://go/gunitadvanced#value-parameterized-tests +INSTANTIATE_TEST_SUITE_P( + MouseInputTestParameterized, + MouseInputTest, + ::testing::Values( + "fuchsia-pkg://fuchsia.com/flatland-scene-manager-test-ui-stack#meta/" + "test-ui-stack.cm")); + +TEST_P(MouseInputTest, FlutterMouseMove) { + LaunchClient(); + + SimulateMouseEvent(/* pressed_buttons = */ {}, /* movement_x = */ 1, + /* movement_y = */ 2); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 1; }); + + ASSERT_EQ(mouse_input_listener()->SizeOfEvents(), 1u); + + auto e = mouse_input_listener()->PopEvent(); + + // If the first mouse event is cursor movement, Flutter first sends an ADD + // event with updated location. + VerifyEvent(e, + /*expected_x=*/static_cast(display_width()) / 2.f + 1, + /*expected_y=*/static_cast(display_height()) / 2.f + 2, + /*expected_buttons=*/{}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::ADD, + /*component_name=*/"mouse-input-view"); +} + +TEST_P(MouseInputTest, FlutterMouseDown) { + LaunchClient(); + + SimulateMouseEvent( + /* pressed_buttons = */ {fuchsia::ui::test::input::MouseButton::FIRST}, + /* movement_x = */ 0, /* movement_y = */ 0); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 3; }); + + ASSERT_EQ(mouse_input_listener()->SizeOfEvents(), 3u); + + auto event_add = mouse_input_listener()->PopEvent(); + auto event_down = mouse_input_listener()->PopEvent(); + auto event_noop_move = mouse_input_listener()->PopEvent(); + + // If the first mouse event is a button press, Flutter first sends an ADD + // event with no buttons. + VerifyEvent(event_add, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::ADD, + /*component_name=*/"mouse-input-view"); + + // Then Flutter sends a DOWN pointer event with the buttons we care about. + VerifyEvent( + event_down, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{fuchsia::ui::test::input::MouseButton::FIRST}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::DOWN, + /*component_name=*/"mouse-input-view"); + + // Then Flutter sends a MOVE pointer event with no new information. + VerifyEvent( + event_noop_move, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{fuchsia::ui::test::input::MouseButton::FIRST}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::MOVE, + /*component_name=*/"mouse-input-view"); +} + +TEST_P(MouseInputTest, FlutterMouseDownUp) { + LaunchClient(); + + SimulateMouseEvent( + /* pressed_buttons = */ {fuchsia::ui::test::input::MouseButton::FIRST}, + /* movement_x = */ 0, /* movement_y = */ 0); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 3; }); + + ASSERT_EQ(mouse_input_listener()->SizeOfEvents(), 3u); + + auto event_add = mouse_input_listener()->PopEvent(); + auto event_down = mouse_input_listener()->PopEvent(); + auto event_noop_move = mouse_input_listener()->PopEvent(); + + // If the first mouse event is a button press, Flutter first sends an ADD + // event with no buttons. + VerifyEvent(event_add, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::ADD, + /*component_name=*/"mouse-input-view"); + + // Then Flutter sends a DOWN pointer event with the buttons we care about. + VerifyEvent( + event_down, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{fuchsia::ui::test::input::MouseButton::FIRST}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::DOWN, + /*component_name=*/"mouse-input-view"); + + // Then Flutter sends a MOVE pointer event with no new information. + VerifyEvent( + event_noop_move, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{fuchsia::ui::test::input::MouseButton::FIRST}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::MOVE, + /*component_name=*/"mouse-input-view"); + + SimulateMouseEvent(/* pressed_buttons = */ {}, /* movement_x = */ 0, + /* movement_y = */ 0); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 1; }); + + ASSERT_EQ(mouse_input_listener()->SizeOfEvents(), 1u); + + auto event_up = mouse_input_listener()->PopEvent(); + VerifyEvent(event_up, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::UP, + /*component_name=*/"mouse-input-view"); +} + +TEST_P(MouseInputTest, FlutterMouseDownMoveUp) { + LaunchClient(); + + SimulateMouseEvent( + /* pressed_buttons = */ {fuchsia::ui::test::input::MouseButton::FIRST}, + /* movement_x = */ 0, /* movement_y = */ 0); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 3; }); + + ASSERT_EQ(mouse_input_listener()->SizeOfEvents(), 3u); + + auto event_add = mouse_input_listener()->PopEvent(); + auto event_down = mouse_input_listener()->PopEvent(); + auto event_noop_move = mouse_input_listener()->PopEvent(); + + // If the first mouse event is a button press, Flutter first sends an ADD + // event with no buttons. + VerifyEvent(event_add, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::ADD, + /*component_name=*/"mouse-input-view"); + + // Then Flutter sends a DOWN pointer event with the buttons we care about. + VerifyEvent( + event_down, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{fuchsia::ui::test::input::MouseButton::FIRST}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::DOWN, + /*component_name=*/"mouse-input-view"); + + // Then Flutter sends a MOVE pointer event with no new information. + VerifyEvent( + event_noop_move, + /*expected_x=*/static_cast(display_width()) / 2.f, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{fuchsia::ui::test::input::MouseButton::FIRST}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::MOVE, + /*component_name=*/"mouse-input-view"); + + SimulateMouseEvent( + /* pressed_buttons = */ {fuchsia::ui::test::input::MouseButton::FIRST}, + /* movement_x = */ kClickToDragThreshold, /* movement_y = */ 0); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 1; }); + + ASSERT_EQ(mouse_input_listener()->SizeOfEvents(), 1u); + + auto event_move = mouse_input_listener()->PopEvent(); + + VerifyEventLocationOnTheRightOfExpectation( + event_move, + /*expected_x_min=*/static_cast(display_width()) / 2.f + 1, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{fuchsia::ui::test::input::MouseButton::FIRST}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::MOVE, + /*component_name=*/"mouse-input-view"); + + SimulateMouseEvent(/* pressed_buttons = */ {}, /* movement_x = */ 0, + /* movement_y = */ 0); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 1; }); + + ASSERT_EQ(mouse_input_listener()->SizeOfEvents(), 1u); + + auto event_up = mouse_input_listener()->PopEvent(); + + VerifyEventLocationOnTheRightOfExpectation( + event_up, + /*expected_x_min=*/static_cast(display_width()) / 2.f + 1, + /*expected_y=*/static_cast(display_height()) / 2.f, + /*expected_buttons=*/{}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::UP, + /*component_name=*/"mouse-input-view"); +} + +// TODO(fxbug.dev/103098): This test shows the issue when sending mouse wheel as +// the first event to Flutter. +// 1. expect Flutter app receive 2 events: ADD - Scroll, but got 3 events: Move +// - Scroll - Scroll. +// 2. the first event flutter app received has random value in buttons field +// Disabled until flutter rolls, since it changes the behavior of this issue. +TEST_P(MouseInputTest, DISABLED_FlutterMouseWheelIssue103098) { + LaunchClient(); + + SimulateMouseScroll(/* pressed_buttons = */ {}, /* scroll_x = */ 1, + /* scroll_y = */ 0); + // Here we expected 2 events, ADD - Scroll, but got 3, Move - Scroll - Scroll. + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 3; }); + + double initial_x = static_cast(display_width()) / 2.f; + double initial_y = static_cast(display_height()) / 2.f; + + auto event_1 = mouse_input_listener()->PopEvent(); + EXPECT_NEAR(event_1.local_x(), initial_x, 1); + EXPECT_NEAR(event_1.local_y(), initial_y, 1); + // Flutter will scale the count of ticks to pixel. + EXPECT_GT(event_1.wheel_x_physical_pixel(), 0); + EXPECT_EQ(event_1.wheel_y_physical_pixel(), 0); + EXPECT_EQ(event_1.phase(), fuchsia::ui::test::input::MouseEventPhase::MOVE); + + auto event_2 = mouse_input_listener()->PopEvent(); + VerifyEvent( + event_2, + /*expected_x=*/initial_x, + /*expected_y=*/initial_y, + /*expected_buttons=*/{}, + /*expected_phase=*/fuchsia::ui::test::input::MouseEventPhase::HOVER, + /*component_name=*/"mouse-input-view"); + // Flutter will scale the count of ticks to pixel. + EXPECT_GT(event_2.wheel_x_physical_pixel(), 0); + EXPECT_EQ(event_2.wheel_y_physical_pixel(), 0); + + auto event_3 = mouse_input_listener()->PopEvent(); + VerifyEvent( + event_3, + /*expected_x=*/initial_x, + /*expected_y=*/initial_y, + /*expected_buttons=*/{}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::HOVER, + /*component_name=*/"mouse-input-view"); + // Flutter will scale the count of ticks to pixel. + EXPECT_GT(event_3.wheel_x_physical_pixel(), 0); + EXPECT_EQ(event_3.wheel_y_physical_pixel(), 0); +} + +TEST_P(MouseInputTest, FlutterMouseWheel) { + LaunchClient(); + + double initial_x = static_cast(display_width()) / 2.f + 1; + double initial_y = static_cast(display_height()) / 2.f + 2; + + // TODO(fxbug.dev/103098): Send a mouse move as the first event to workaround. + SimulateMouseEvent(/* pressed_buttons = */ {}, + /* movement_x = */ 1, /* movement_y = */ 2); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 1; }); + + auto event_add = mouse_input_listener()->PopEvent(); + VerifyEvent(event_add, + /*expected_x=*/initial_x, + /*expected_y=*/initial_y, + /*expected_buttons=*/{}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::ADD, + /*component_name=*/"mouse-input-view"); + + SimulateMouseScroll(/* pressed_buttons = */ {}, /* scroll_x = */ 1, + /* scroll_y = */ 0); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 1; }); + + auto event_wheel_h = mouse_input_listener()->PopEvent(); + + VerifyEvent( + event_wheel_h, + /*expected_x=*/initial_x, + /*expected_y=*/initial_y, + /*expected_buttons=*/{}, + /*expected_phase=*/fuchsia::ui::test::input::MouseEventPhase::HOVER, + /*component_name=*/"mouse-input-view"); + // Flutter will scale the count of ticks to pixel. + EXPECT_GT(event_wheel_h.wheel_x_physical_pixel(), 0); + EXPECT_EQ(event_wheel_h.wheel_y_physical_pixel(), 0); + + SimulateMouseScroll(/* pressed_buttons = */ {}, /* scroll_x = */ 0, + /* scroll_y = */ 1); + RunLoopUntil( + [this] { return this->mouse_input_listener()->SizeOfEvents() == 1; }); + + auto event_wheel_v = mouse_input_listener()->PopEvent(); + + VerifyEvent( + event_wheel_v, + /*expected_x=*/initial_x, + /*expected_y=*/initial_y, + /*expected_buttons=*/{}, + /*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::HOVER, + /*component_name=*/"mouse-input-view"); + // Flutter will scale the count of ticks to pixel. + EXPECT_LT(event_wheel_v.wheel_y_physical_pixel(), 0); + EXPECT_EQ(event_wheel_v.wheel_x_physical_pixel(), 0); +} + +} // namespace +} // namespace mouse_input_test::testing diff --git a/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/BUILD.gn b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/BUILD.gn new file mode 100644 index 0000000000000..edb58ee0fd16f --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/BUILD.gn @@ -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. + +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 = "mouse-input-view" + sources = [ "mouse-input-view.dart" ] + + deps = [ + "//flutter/shell/platform/fuchsia/dart:args", + "//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 = "mouse-input-view" + manifest = rebase_path("meta/mouse-input-view.cml") + main_package = "mouse-input-view" + main_dart = "mouse-input-view.dart" + + deps = [ ":lib" ] +} + +fuchsia_package("package") { + testonly = true + package_name = "mouse-input-view" + + deps = [ ":component" ] +} diff --git a/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/lib/mouse-input-view.dart b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/lib/mouse-input-view.dart new file mode 100644 index 0000000000000..9c657ff658f4e --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/lib/mouse-input-view.dart @@ -0,0 +1,149 @@ +// Copyright 2020 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. + +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:fidl_fuchsia_ui_test_input/fidl_async.dart' as test_mouse; +import 'package:fuchsia_services/services.dart'; +import 'package:zircon/zircon.dart'; + +void main() { + print('Launching mouse-input-view'); + MyApp app = MyApp(); + app.run(); +} + +List getPressedButtons(int buttons) { + var pressed_buttons = []; + if (buttons & 0x1 != 0) { + pressed_buttons.add(test_mouse.MouseButton.first); + } + if (buttons & (0x1 >> 1) != 0) { + pressed_buttons.add(test_mouse.MouseButton.second); + } + if (buttons & (0x1 >> 2) != 0) { + pressed_buttons.add(test_mouse.MouseButton.third); + } + + return pressed_buttons; +} + +test_mouse.MouseEventPhase getPhase(String event_type) { + switch (event_type) { + case 'add': + return test_mouse.MouseEventPhase.add; + case 'hover': + return test_mouse.MouseEventPhase.hover; + case 'down': + return test_mouse.MouseEventPhase.down; + case 'move': + return test_mouse.MouseEventPhase.move; + case 'up': + return test_mouse.MouseEventPhase.up; + default: + print('Invalid event type: ${event_type}'); + } +} + +class MyApp { + static const _red = Color.fromARGB(255, 244, 67, 54); + static const _orange = Color.fromARGB(255, 255, 152, 0); + static const _yellow = Color.fromARGB(255, 255, 235, 59); + static const _green = Color.fromARGB(255, 76, 175, 80); + static const _blue = Color.fromARGB(255, 33, 150, 143); + static const _purple = Color.fromARGB(255, 156, 39, 176); + + final List _colors = [ + _red, + _orange, + _yellow, + _green, + _blue, + _purple, + ]; + + // Each tap will increment the counter, we then determine what color to choose + int _touchCounter = 0; + final _responseListener = test_mouse.MouseInputListenerProxy(); + + void run() { + Incoming.fromSvcPath() + ..connectToService(_responseListener); + // Set up window callbacks. + window.onPointerDataPacket = (PointerDataPacket packet) { + this.pointerDataPacket(packet); + }; + window.onMetricsChanged = () { + window.scheduleFrame(); + }; + window.onBeginFrame = (Duration duration) { + this.beginFrame(duration); + }; + + // The child view should be attached to Scenic now. + // Ready to build the scene. + window.scheduleFrame(); + } + + void beginFrame(Duration duration) { + // Convert physical screen size of device to values + final pixelRatio = window.devicePixelRatio; + final size = window.physicalSize / pixelRatio; + final physicalBounds = Offset.zero & size * pixelRatio; + // Set up Canvas that uses the screen size + final recorder = PictureRecorder(); + final canvas = Canvas(recorder, physicalBounds); + canvas.scale(pixelRatio, pixelRatio); + // Draw something + // Color of the screen is set initially to the first value in _colors + // Incrementing _touchCounter will change screen color + final paint = Paint()..color = _colors[_touchCounter % _colors.length]; + canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint); + // Build the scene + final picture = recorder.endRecording(); + final sceneBuilder = SceneBuilder() + ..pushClipRect(physicalBounds) + ..addPicture(Offset.zero, picture) + ..pop(); + window.render(sceneBuilder.build()); + } + + void pointerDataPacket(PointerDataPacket packet) async { + int nowNanos = System.clockGetMonotonic(); + + for (PointerData data in packet.data) { + print('mouse-input-view received input: ${data.toStringFull()}'); + + if (data.kind == PointerDeviceKind.mouse) { + if (data.change == PointerChange.down) { + _touchCounter++; + } + + // Incoming.fromSvcPath() + // ..connectToService(_responseListener) + // ..close(); + + _respond(test_mouse.MouseInputListenerReportMouseInputRequest( + localX: data.physicalX, + localY: data.physicalY, + buttons: getPressedButtons(data.buttons), + phase: getPhase(data.change.name), + timeReceived: nowNanos, + wheelXPhysicalPixel: data.scrollDeltaX, + wheelYPhysicalPixel: data.scrollDeltaY, + componentName: 'mouse-input-view', + )); + } + } + + window.scheduleFrame(); + } + + void _respond(test_mouse.MouseInputListenerReportMouseInputRequest request) async { + print('mouse-input-view reporting mouse input to MouseInputListener'); + await _responseListener.reportMouseInput(request); + } +} diff --git a/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/meta/mouse-input-view.cml b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/meta/mouse-input-view.cml new file mode 100644 index 0000000000000..38d23643e4664 --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/meta/mouse-input-view.cml @@ -0,0 +1,36 @@ +// 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: [ "syslog/client.shard.cml" ], + program: { + data: "data/mouse-input-view", + + // 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" ], + }, + ], + expose: [ + { + protocol: [ "fuchsia.ui.app.ViewProvider" ], + from: "self", + }, + ], + use: [ + { + protocol: [ + "fuchsia.sysmem.Allocator", + "fuchsia.tracing.provider.Registry", + "fuchsia.ui.scenic.Scenic", + "fuchsia.ui.composition.Flatland", + "fuchsia.ui.test.input.MouseInputListener", + "fuchsia.vulkan.loader.Loader", + ] + } + ] +} diff --git a/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/pubspec.yaml b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/pubspec.yaml new file mode 100644 index 0000000000000..a557dbe433abd --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/pubspec.yaml @@ -0,0 +1,8 @@ +# 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. + +name: mouse-input-view + +environment: + sdk: '>=2.18.0 <3.0.0' diff --git a/shell/platform/fuchsia/flutter/tests/integration/touch-input/README.md b/shell/platform/fuchsia/flutter/tests/integration/touch-input/README.md index 1d37bde6dc7b2..828db8566346d 100644 --- a/shell/platform/fuchsia/flutter/tests/integration/touch-input/README.md +++ b/shell/platform/fuchsia/flutter/tests/integration/touch-input/README.md @@ -42,7 +42,7 @@ Reference the Flutter integration test [documentation](https://github.com/flutte Build Fuchsia with `workstation_eng.qemu-x64` ```shell -fx set workstation_eng.qemu-x64 --with-base=//src/session/bin/session_manager && fx build +fx set workstation_eng.qemu-x64 && fx build ``` Build flutter/engine 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 86a3147f9f63c..80aa700945294 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 @@ -70,36 +70,28 @@ void PortableUITest::SetUpRealmBase() { 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{fuchsia::ui::input::ImeService::Name_}, - Protocol{kPointerInjectorRegistryName}, - Protocol{kPosixSocketProviderName}, - Protocol{kVulkanLoaderServiceName}, - component_testing::Directory{"config-data"}, - }, - .source = ParentRef(), - .targets = {kFlutterJitRunnerRef, 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_}, - Protocol{fuchsia::ui::scenic::Scenic::Name_}}, - .source = kTestUIStackRef, - .targets = {ParentRef()}}); - - // Route UI capabilities from test UI stack to flutter runners. + .capabilities = {Protocol{fuchsia::logger::LogSink::Name_}, + Protocol{fuchsia::sys::Environment::Name_}, + Protocol{fuchsia::sysmem::Allocator::Name_}, + Protocol{fuchsia::tracing::provider::Registry::Name_}, + Protocol{fuchsia::ui::input::ImeService::Name_}, + Protocol{kPointerInjectorRegistryName}, + Protocol{kPosixSocketProviderName}, + Protocol{kVulkanLoaderServiceName}, + component_testing::Directory{"config-data"}}, + .source = ParentRef(), + .targets = {kFlutterJitRunnerRef, kTestUIStackRef}}); + + // Route UI capabilities to test driver and Flutter runner realm_builder_.AddRoute(Route{ - .capabilities = {Protocol{fuchsia::ui::composition::Flatland::Name_}, - Protocol{fuchsia::ui::scenic::Scenic::Name_}}, + .capabilities = {Protocol{fuchsia::ui::composition::Allocator::Name_}, + Protocol{fuchsia::ui::composition::Flatland::Name_}, + Protocol{fuchsia::ui::scenic::Scenic::Name_}, + Protocol{fuchsia::ui::test::input::Registry::Name_}, + Protocol{fuchsia::ui::test::scene::Controller::Name_}}, .source = kTestUIStackRef, - .targets = {kFlutterJitRunnerRef}}); + .targets = {ParentRef(), kFlutterJitRunnerRef}}); } void PortableUITest::ProcessViewGeometryResponse( @@ -146,8 +138,10 @@ bool PortableUITest::HasViewConnected(zx_koid_t view_ref_koid) { void PortableUITest::LaunchClient() { scene_provider_ = realm_->Connect(); - scene_provider_.set_error_handler( - [](auto) { FML_LOG(ERROR) << "Error from test scene provider"; }); + scene_provider_.set_error_handler([](auto) { + FML_LOG(ERROR) << "Error from test scene provider: " + << &zx_status_get_string; + }); fuchsia::ui::test::scene::ControllerAttachClientViewRequest request; request.set_view_provider(realm_->Connect()); scene_provider_->RegisterViewTreeWatcher(view_tree_watcher_.NewRequest(), @@ -171,8 +165,9 @@ void PortableUITest::LaunchClient() { 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"; }); + input_registry_.set_error_handler([](auto) { + FML_LOG(ERROR) << "Error from input helper: " << &zx_status_get_string; + }); bool touchscreen_registered = false; fuchsia::ui::test::input::RegistryRegisterTouchScreenRequest request; @@ -185,6 +180,23 @@ void PortableUITest::RegisterTouchScreen() { FML_LOG(INFO) << "Touchscreen registered"; } +void PortableUITest::RegisterMouse() { + FML_LOG(INFO) << "Registering fake mouse"; + input_registry_ = realm_->Connect(); + input_registry_.set_error_handler([](auto) { + FML_LOG(ERROR) << "Error from input helper: " << &zx_status_get_string; + }); + + bool mouse_registered = false; + fuchsia::ui::test::input::RegistryRegisterMouseRequest request; + request.set_device(fake_mouse_.NewRequest()); + input_registry_->RegisterMouse( + std::move(request), [&mouse_registered]() { mouse_registered = true; }); + + RunLoopUntil([&mouse_registered] { return mouse_registered; }); + FML_LOG(INFO) << "Mouse registered"; +} + void PortableUITest::InjectTap(int32_t x, int32_t y) { fuchsia::ui::test::input::TouchScreenSimulateTapRequest tap_request; tap_request.mutable_tap_location()->x = x; @@ -199,4 +211,40 @@ void PortableUITest::InjectTap(int32_t x, int32_t y) { }); } +void PortableUITest::SimulateMouseEvent( + std::vector pressed_buttons, + int movement_x, + int movement_y) { + fuchsia::ui::test::input::MouseSimulateMouseEventRequest request; + request.set_pressed_buttons(std::move(pressed_buttons)); + request.set_movement_x(movement_x); + request.set_movement_y(movement_y); + + FML_LOG(INFO) << "Injecting mouse input"; + + fake_mouse_->SimulateMouseEvent( + std::move(request), [] { FML_LOG(INFO) << "Mouse event injected"; }); +} + +void PortableUITest::SimulateMouseScroll( + std::vector pressed_buttons, + int scroll_x, + int scroll_y, + bool use_physical_units) { + FML_LOG(INFO) << "Requesting mouse scroll"; + fuchsia::ui::test::input::MouseSimulateMouseEventRequest request; + request.set_pressed_buttons(std::move(pressed_buttons)); + if (use_physical_units) { + request.set_scroll_h_physical_pixel(scroll_x); + request.set_scroll_v_physical_pixel(scroll_y); + } else { + request.set_scroll_h_detent(scroll_x); + request.set_scroll_v_detent(scroll_y); + } + + fake_mouse_->SimulateMouseEvent(std::move(request), [] { + FML_LOG(INFO) << "Mouse scroll event injected"; + }); +} + } // namespace fuchsia_test_utils 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 55afc990dd0bf..ad39c125f120a 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 @@ -57,9 +57,31 @@ class PortableUITest : public ::loop_fixture::RealLoop { // spanning [-1000, 1000] on both axes. void RegisterTouchScreen(); + // Registers a fake mouse device, for which mouse movement is measured on a + // scale of [-1000, 1000] on both axes and scroll is measured from [-100, 100] + // on both axes. + void RegisterMouse(); + // Simulates a tap at location (x, y). void InjectTap(int32_t x, int32_t y); + // Helper method to simulate combinations of button presses/releases and/or + // mouse movements. + void SimulateMouseEvent( + std::vector pressed_buttons, + int movement_x, + int movement_y); + + // Helper method to simulate a mouse scroll event. + // + // Set `use_physical_units` to true to specify scroll in physical pixels and + // false to specify scroll in detents. + void SimulateMouseScroll( + std::vector pressed_buttons, + int scroll_x, + int scroll_y, + bool use_physical_units = false); + protected: component_testing::RealmBuilder* realm_builder() { return &realm_builder_; } component_testing::RealmRoot* realm_root() { return realm_.get(); } @@ -87,6 +109,7 @@ class PortableUITest : public ::loop_fixture::RealLoop { fuchsia::ui::test::input::RegistryPtr input_registry_; fuchsia::ui::test::input::TouchScreenPtr fake_touchscreen_; + fuchsia::ui::test::input::MousePtr fake_mouse_; fuchsia::ui::test::scene::ControllerPtr scene_provider_; fuchsia::ui::observation::geometry::ViewTreeWatcherPtr view_tree_watcher_; diff --git a/testing/fuchsia/test_suites.yaml b/testing/fuchsia/test_suites.yaml index 25e91cedefb93..fa33a1503bbc3 100644 --- a/testing/fuchsia/test_suites.yaml +++ b/testing/fuchsia/test_suites.yaml @@ -53,3 +53,8 @@ - touch-input-test-0.far - oot_flutter_jit_runner-0.far - gen/flutter/shell/platform/fuchsia/flutter/tests/integration/touch-input/touch-input-view/touch-input-view/touch-input-view.far +- test_command: run-test-suite fuchsia-pkg://fuchsia.com/mouse-input-test#meta/mouse-input-test.cm + packages: + - mouse-input-test-0.far + - oot_flutter_jit_runner-0.far + - gen/flutter/shell/platform/fuchsia/flutter/tests/integration/mouse-input/mouse-input-view/mouse-input-view/mouse-input-view.far