blob: c72e812b19cc0578bf9392f6fdef42ad212d8b2c [file] [log] [blame]
// 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 <fuchsia/accessibility/semantics/cpp/fidl.h>
#include <fuchsia/buildinfo/cpp/fidl.h>
#include <fuchsia/cobalt/cpp/fidl.h>
#include <fuchsia/component/cpp/fidl.h>
#include <fuchsia/fonts/cpp/fidl.h>
#include <fuchsia/input/report/cpp/fidl.h>
#include <fuchsia/kernel/cpp/fidl.h>
#include <fuchsia/memorypressure/cpp/fidl.h>
#include <fuchsia/net/interfaces/cpp/fidl.h>
#include <fuchsia/netstack/cpp/fidl.h>
#include <fuchsia/posix/socket/cpp/fidl.h>
#include <fuchsia/scheduler/cpp/fidl.h>
#include <fuchsia/session/scene/cpp/fidl.h>
#include <fuchsia/sys/cpp/fidl.h>
#include <fuchsia/tracing/provider/cpp/fidl.h>
#include <fuchsia/ui/app/cpp/fidl.h>
#include <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <fuchsia/vulkan/loader/cpp/fidl.h>
#include <fuchsia/web/cpp/fidl.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/sys/component/cpp/testing/realm_builder.h>
#include <lib/sys/component/cpp/testing/realm_builder_types.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/clock.h>
#include <lib/zx/time.h>
#include <zircon/status.h>
#include <zircon/types.h>
#include <zircon/utc.h>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <memory>
#include <optional>
#include <queue>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include <test/inputsynthesis/cpp/fidl.h>
#include <test/mouse/cpp/fidl.h>
#include "lib/fidl/cpp/interface_ptr.h"
#include "src/lib/testing/loop_fixture/real_loop_fixture.h"
#include "src/ui/testing/ui_test_manager/ui_test_manager.h"
namespace {
// Types imported for the realm_builder library.
using component_testing::ChildRef;
using component_testing::LocalComponent;
using component_testing::LocalComponentHandles;
using component_testing::ParentRef;
using component_testing::Protocol;
using component_testing::Realm;
using component_testing::Route;
// 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;
// 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);
// Combines all vectors in `vecs` into one.
template <typename T>
std::vector<T> merge(std::initializer_list<std::vector<T>> vecs) {
std::vector<T> result;
for (auto v : vecs) {
result.insert(result.end(), v.begin(), v.end());
}
return result;
}
// `ResponseListener` is a local test protocol that our test Flutter app uses to let us know
// what position and button press state the mouse cursor has.
// No need to use mutex in ResponseListenerServer because `MouseInputBase` inherits from
// `gtest::RealLoopFixture`, `RealLoopFixture` inherits from `RealLoop`, `RealLoop` has-an
// `async::Loop`, `Loop::Run()` Runs the message loop on the same thread with test.
class ResponseListenerServer : public test::mouse::ResponseListener, public LocalComponent {
public:
explicit ResponseListenerServer(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {}
// |test::mouse::ResponseListener|
void Respond(test::mouse::PointerData pointer_data) override {
events_.push(std::move(pointer_data));
}
// |test::mouse::ResponseListener|
void NotifyWebEngineReady() override { web_engine_ready_ = true; }
bool IsWebEngineReady() const { return web_engine_ready_; }
// |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<LocalComponentHandles> mock_handles) override {
// When this component starts, add a binding to the test.mouse.ResponseListener
// protocol to this component's outgoing directory.
FX_CHECK(mock_handles->outgoing()->AddPublicService(
fidl::InterfaceRequestHandler<test::mouse::ResponseListener>([this](auto request) {
bindings_.AddBinding(this, std::move(request), dispatcher_);
})) == ZX_OK);
mock_handles_.emplace_back(std::move(mock_handles));
}
size_t SizeOfEvents() const { return events_.size(); }
test::mouse::PointerData PopEvent() {
test::mouse::PointerData e = std::move(events_.front());
events_.pop();
return e;
}
const test::mouse::PointerData& LastEvent() const { return events_.back(); }
void ClearEvents() { events_ = {}; }
private:
// Not owned.
async_dispatcher_t* dispatcher_ = nullptr;
fidl::BindingSet<test::mouse::ResponseListener> bindings_;
std::vector<std::unique_ptr<LocalComponentHandles>> mock_handles_;
std::queue<test::mouse::PointerData> events_;
bool web_engine_ready_ = false;
};
constexpr auto kResponseListener = "response_listener";
struct Position {
double x = 0.0;
double y = 0.0;
};
class MouseInputBase : public gtest::RealLoopFixture {
protected:
MouseInputBase() : response_listener_(std::make_unique<ResponseListenerServer>(dispatcher())) {}
sys::ServiceDirectory* realm_exposed_services() { return realm_exposed_services_.get(); }
ResponseListenerServer* response_listener() { return response_listener_.get(); }
void SetUp() override {
// Post a "just in case" quit task, if the test hangs.
async::PostDelayedTask(
dispatcher(),
[] { FX_LOGS(FATAL) << "\n\n>> Test did not complete in time, terminating. <<\n\n"; },
kTimeout);
ui_testing::UITestManager::Config config;
config.use_flatland = true;
config.scene_owner = ui_testing::UITestManager::SceneOwnerType::SCENE_MANAGER;
config.use_input = true;
config.accessibility_owner = ui_testing::UITestManager::AccessibilityOwnerType::FAKE;
config.ui_to_client_services = {fuchsia::ui::scenic::Scenic::Name_,
fuchsia::ui::composition::Flatland::Name_,
fuchsia::ui::composition::Allocator::Name_,
fuchsia::ui::input::ImeService::Name_,
fuchsia::ui::input3::Keyboard::Name_,
fuchsia::accessibility::semantics::SemanticsManager::Name_};
ui_test_manager_ = std::make_unique<ui_testing::UITestManager>(std::move(config));
AssembleRealm(this->GetTestComponents(), this->GetTestV2Components(), this->GetTestRoutes());
// Get the display dimensions.
FX_LOGS(INFO) << "Waiting for scenic display info";
auto scenic = realm_exposed_services()->Connect<fuchsia::ui::scenic::Scenic>();
scenic->GetDisplayInfo([this](fuchsia::ui::gfx::DisplayInfo display_info) {
display_width_ = display_info.width_in_px;
display_height_ = display_info.height_in_px;
FX_LOGS(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(response_listener_->SizeOfEvents(), 0u);
}
// Subclass should implement this method to add components to the test realm
// next to the base ones added.
virtual std::vector<std::pair<ChildName, LegacyUrl>> GetTestComponents() { return {}; }
// Subclass should implement this method to add v2 components to the test realm
// next to the base ones added.
virtual std::vector<std::pair<ChildName, std::string>> GetTestV2Components() { return {}; }
// Subclass should implement this method to add capability routes to the test
// realm next to the base ones added.
virtual std::vector<Route> GetTestRoutes() { return {}; }
// Send a synthesis mouse event.
void SendMouseEvent(fidl::InterfacePtr<test::inputsynthesis::Mouse>& input_synthesis,
uint32_t device_id, fuchsia::input::report::MouseInputReport report,
uint64_t ts) {
bool injection_initiated = false;
input_synthesis->SendInputReport(
device_id, std::move(report), ts, [&injection_initiated](auto result) {
ASSERT_FALSE(result.is_err()) << "SendInputReport failed " << result.err();
injection_initiated = true;
});
RunLoopUntil([&injection_initiated] { return injection_initiated; });
}
// Helper method for checking the test.mouse.ResponseListener response from the client app.
void VerifyEvent(test::mouse::PointerData& pointer_data, double expected_x, double expected_y,
int64_t expected_buttons, const std::string& expected_type,
zx::basic_time<ZX_CLOCK_MONOTONIC>& input_injection_time,
const std::string& component_name) {
FX_LOGS(INFO) << "Client received mouse change at (" << pointer_data.local_x() << ", "
<< pointer_data.local_y() << ") with buttons " << pointer_data.buttons() << ".";
FX_LOGS(INFO) << "Expected mouse change is at approximately (" << expected_x << ", "
<< expected_y << ") with buttons " << expected_buttons << ".";
zx::duration elapsed_time =
zx::basic_time<ZX_CLOCK_MONOTONIC>(pointer_data.time_received()) - input_injection_time;
EXPECT_TRUE(elapsed_time.get() > 0 && elapsed_time.get() != ZX_TIME_INFINITE);
FX_LOGS(INFO) << "Input Injection Time (ns): " << input_injection_time.get();
FX_LOGS(INFO) << "Client Received Time (ns): " << pointer_data.time_received();
FX_LOGS(INFO) << "Elapsed Time (ns): " << elapsed_time.to_nsecs();
// 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.type(), expected_type);
EXPECT_EQ(pointer_data.component_name(), component_name);
}
void VerifyEventLocationOnTheRightOfExpectation(
test::mouse::PointerData& pointer_data, double expected_x_min, double expected_y,
int64_t expected_buttons, const std::string& expected_type,
zx::basic_time<ZX_CLOCK_MONOTONIC>& input_injection_time, const std::string& component_name) {
FX_LOGS(INFO) << "Client received mouse change at (" << pointer_data.local_x() << ", "
<< pointer_data.local_y() << ") with buttons " << pointer_data.buttons() << ".";
FX_LOGS(INFO) << "Expected mouse change is at approximately (>" << expected_x_min << ", "
<< expected_y << ") with buttons " << expected_buttons << ".";
zx::duration elapsed_time =
zx::basic_time<ZX_CLOCK_MONOTONIC>(pointer_data.time_received()) - input_injection_time;
EXPECT_TRUE(elapsed_time.get() > 0 && elapsed_time.get() != ZX_TIME_INFINITE);
FX_LOGS(INFO) << "Input Injection Time (ns): " << input_injection_time.get();
FX_LOGS(INFO) << "Client Received Time (ns): " << pointer_data.time_received();
FX_LOGS(INFO) << "Elapsed Time (ns): " << elapsed_time.to_nsecs();
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.type(), expected_type);
EXPECT_EQ(pointer_data.component_name(), component_name);
}
void AssembleRealm(const std::vector<std::pair<ChildName, LegacyUrl>>& components,
const std::vector<std::pair<ChildName, std::string>>& components_v2,
const std::vector<Route>& routes) {
FX_LOGS(INFO) << "Building realm";
realm_ = std::make_unique<Realm>(ui_test_manager_->AddSubrealm());
// Key part of service setup: have this test component vend the
// |ResponseListener| service in the constructed realm.
realm_->AddLocalChild(kResponseListener, response_listener());
// Add components specific for this test case to the realm.
for (const auto& [name, component] : components) {
realm_->AddLegacyChild(name, component);
}
for (const auto& [name, component] : components_v2) {
realm_->AddChild(name, component);
}
// Add the necessary routing for each of the extra components added above.
for (const auto& route : routes) {
realm_->AddRoute(route);
}
// Finally, build the realm using the provided components and routes.
ui_test_manager_->BuildRealm();
realm_exposed_services_ = ui_test_manager_->TakeExposedServicesDirectory();
}
void LaunchClient() {
// Initialize scene, and attach client view.
ui_test_manager_->InitializeScene();
FX_LOGS(INFO) << "Wait for client view to render";
RunLoopUntil([this]() { return ui_test_manager_->ClientViewIsRendering(); });
}
uint32_t AddMouseDevice(fidl::InterfacePtr<test::inputsynthesis::Mouse>& input_synthesis) {
uint32_t device_id;
bool new_device_completed = false;
input_synthesis->AddDevice([&device_id, &new_device_completed](uint32_t id) {
device_id = id;
new_device_completed = true;
});
// wait for new device creation.
RunLoopUntil([&new_device_completed] { return new_device_completed; });
return device_id;
}
// Guaranteed to be initialized after SetUp().
uint32_t display_width() const { return display_width_; }
uint32_t display_height() const { return display_height_; }
std::unique_ptr<ui_testing::UITestManager> ui_test_manager_;
std::unique_ptr<sys::ServiceDirectory> realm_exposed_services_;
std::unique_ptr<Realm> realm_;
std::unique_ptr<ResponseListenerServer> response_listener_;
private:
uint32_t display_width_ = 0;
uint32_t display_height_ = 0;
};
class FlutterInputTest : public MouseInputBase {
protected:
std::vector<std::pair<ChildName, std::string>> GetTestV2Components() override {
return {
std::make_pair(kMouseInputFlutter, kMouseInputFlutterUrl),
std::make_pair(kMemoryPressureProvider, kMemoryPressureProviderUrl),
std::make_pair(kNetstack, kNetstackUrl),
};
}
std::vector<Route> GetTestRoutes() override {
return merge({GetFlutterRoutes(ChildRef{kMouseInputFlutter}),
{
{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
.source = ChildRef{kMouseInputFlutter},
.targets = {ParentRef()}},
}});
}
// Routes needed to setup Flutter client.
static std::vector<Route> GetFlutterRoutes(ChildRef target) {
return {{.capabilities =
{
Protocol{test::mouse::ResponseListener::Name_},
},
.source = ChildRef{kResponseListener},
.targets = {target}},
{.capabilities =
{
Protocol{fuchsia::ui::composition::Allocator::Name_},
Protocol{fuchsia::ui::composition::Flatland::Name_},
Protocol{fuchsia::ui::scenic::Scenic::Name_},
// Redirect logging output for the test realm to
// the host console output.
Protocol{fuchsia::logger::LogSink::Name_},
Protocol{fuchsia::scheduler::ProfileProvider::Name_},
Protocol{fuchsia::sysmem::Allocator::Name_},
Protocol{fuchsia::tracing::provider::Registry::Name_},
Protocol{fuchsia::vulkan::loader::Loader::Name_},
},
.source = ParentRef(),
.targets = {target}},
{.capabilities = {Protocol{fuchsia::memorypressure::Provider::Name_}},
.source = ChildRef{kMemoryPressureProvider},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::posix::socket::Provider::Name_}},
.source = ChildRef{kNetstack},
.targets = {target}}};
}
static constexpr auto kMouseInputFlutter = "mouse-input-flutter";
static constexpr auto kMouseInputFlutterUrl = "#meta/mouse-input-flutter-realm.cm";
private:
static constexpr auto kMemoryPressureProvider = "memory_pressure_provider";
static constexpr auto kMemoryPressureProviderUrl = "#meta/memory_monitor.cm";
static constexpr auto kNetstack = "netstack";
static constexpr auto kNetstackUrl = "#meta/netstack.cm";
};
TEST_F(FlutterInputTest, FlutterMouseMove) {
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
LaunchClient();
auto input_synthesis = realm_exposed_services()->Connect<test::inputsynthesis::Mouse>();
uint32_t device_id = AddMouseDevice(input_synthesis);
bool injection_initiated = false;
fuchsia::input::report::MouseInputReport report;
report.set_movement_x(1);
report.set_movement_y(2);
auto ts = static_cast<uint64_t>(zx::clock::get_monotonic().get());
input_synthesis->SendInputReport(
device_id, std::move(report), ts, [&injection_initiated](auto result) {
ASSERT_FALSE(result.is_err()) << "SendInputReport failed " << result.err();
injection_initiated = true;
});
RunLoopUntil([&injection_initiated] { return injection_initiated; });
RunLoopUntil([this] { return this->response_listener_->SizeOfEvents() == 1; });
ASSERT_EQ(response_listener_->SizeOfEvents(), 1u);
auto e = response_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<double>(display_width()) / 2.f + 1,
/*expected_y=*/static_cast<double>(display_height()) / 2.f + 2,
/*expected_buttons=*/0,
/*expected_type=*/"add", input_injection_time,
/*component_name=*/"mouse-input-flutter");
}
TEST_F(FlutterInputTest, FlutterMouseDown) {
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
LaunchClient();
auto input_synthesis = realm_exposed_services()->Connect<test::inputsynthesis::Mouse>();
uint32_t device_id = AddMouseDevice(input_synthesis);
bool injection_initiated = false;
fuchsia::input::report::MouseInputReport report;
report.set_movement_x(0);
report.set_movement_y(0);
report.set_pressed_buttons({0});
auto ts = static_cast<uint64_t>(zx::clock::get_monotonic().get());
input_synthesis->SendInputReport(
device_id, std::move(report), ts, [&injection_initiated](auto result) {
ASSERT_FALSE(result.is_err()) << "SendInputReport failed " << result.err();
injection_initiated = true;
});
RunLoopUntil([&injection_initiated] { return injection_initiated; });
RunLoopUntil([this] { return this->response_listener_->SizeOfEvents() == 3; });
ASSERT_EQ(response_listener_->SizeOfEvents(), 3u);
auto event_add = response_listener_->PopEvent();
auto event_down = response_listener_->PopEvent();
auto event_noop_move = response_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<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/0,
/*expected_type=*/"add", input_injection_time,
/*component_name=*/"mouse-input-flutter");
// Then Flutter sends a DOWN pointer event with the buttons we care about.
VerifyEvent(event_down,
/*expected_x=*/static_cast<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/fuchsia::ui::input::kMousePrimaryButton,
/*expected_type=*/"down", input_injection_time,
/*component_name=*/"mouse-input-flutter");
// Then Flutter sends a MOVE pointer event with no new information.
VerifyEvent(event_noop_move,
/*expected_x=*/static_cast<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/fuchsia::ui::input::kMousePrimaryButton,
/*expected_type=*/"move", input_injection_time,
/*component_name=*/"mouse-input-flutter");
}
TEST_F(FlutterInputTest, FlutterMouseDownUp) {
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
LaunchClient();
auto input_synthesis = realm_exposed_services()->Connect<test::inputsynthesis::Mouse>();
uint32_t device_id = AddMouseDevice(input_synthesis);
bool down_injection_initiated = false;
fuchsia::input::report::MouseInputReport down_report;
down_report.set_movement_x(0);
down_report.set_movement_y(0);
down_report.set_pressed_buttons({0});
auto ts = static_cast<uint64_t>(zx::clock::get_monotonic().get());
input_synthesis->SendInputReport(
device_id, std::move(down_report), ts, [&down_injection_initiated](auto result) {
ASSERT_FALSE(result.is_err()) << "SendInputReport failed " << result.err();
down_injection_initiated = true;
});
RunLoopUntil([&down_injection_initiated] { return down_injection_initiated; });
RunLoopUntil([this] { return this->response_listener_->SizeOfEvents() == 3; });
ASSERT_EQ(response_listener_->SizeOfEvents(), 3u);
auto event_add = response_listener_->PopEvent();
auto event_down = response_listener_->PopEvent();
auto event_noop_move = response_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<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/0,
/*expected_type=*/"add", input_injection_time,
/*component_name=*/"mouse-input-flutter");
// Then Flutter sends a DOWN pointer event with the buttons we care about.
VerifyEvent(event_down,
/*expected_x=*/static_cast<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/fuchsia::ui::input::kMousePrimaryButton,
/*expected_type=*/"down", input_injection_time,
/*component_name=*/"mouse-input-flutter");
// Then Flutter sends a MOVE pointer event with no new information.
VerifyEvent(event_noop_move,
/*expected_x=*/static_cast<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/fuchsia::ui::input::kMousePrimaryButton,
/*expected_type=*/"move", input_injection_time,
/*component_name=*/"mouse-input-flutter");
bool up_injection_initiated = false;
fuchsia::input::report::MouseInputReport up_report;
up_report.set_movement_x(0);
up_report.set_movement_y(0);
up_report.set_pressed_buttons({});
input_synthesis->SendInputReport(
device_id, std::move(up_report), ts, [&up_injection_initiated](auto result) {
ASSERT_FALSE(result.is_err()) << "SendInputReport failed " << result.err();
up_injection_initiated = true;
});
RunLoopUntil([&up_injection_initiated] { return up_injection_initiated; });
RunLoopUntil([this] { return this->response_listener_->SizeOfEvents() == 1; });
ASSERT_EQ(response_listener_->SizeOfEvents(), 1u);
auto event_up = response_listener_->PopEvent();
VerifyEvent(event_up,
/*expected_x=*/static_cast<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/0,
/*expected_type=*/"up", input_injection_time,
/*component_name=*/"mouse-input-flutter");
}
TEST_F(FlutterInputTest, FlutterMouseDownMoveUp) {
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
LaunchClient();
auto input_synthesis = realm_exposed_services()->Connect<test::inputsynthesis::Mouse>();
uint32_t device_id = AddMouseDevice(input_synthesis);
bool down_injection_initiated = false;
fuchsia::input::report::MouseInputReport down_report;
down_report.set_movement_x(0);
down_report.set_movement_y(0);
down_report.set_pressed_buttons({0});
auto ts = static_cast<uint64_t>(zx::clock::get_monotonic().get());
input_synthesis->SendInputReport(
device_id, std::move(down_report), ts, [&down_injection_initiated](auto result) {
ASSERT_FALSE(result.is_err()) << "SendInputReport failed " << result.err();
down_injection_initiated = true;
});
RunLoopUntil([&down_injection_initiated] { return down_injection_initiated; });
RunLoopUntil([&down_injection_initiated] { return down_injection_initiated; });
RunLoopUntil([this] { return this->response_listener_->SizeOfEvents() == 3; });
ASSERT_EQ(response_listener_->SizeOfEvents(), 3u);
auto event_add = response_listener_->PopEvent();
auto event_down = response_listener_->PopEvent();
auto event_noop_move = response_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<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/0,
/*expected_type=*/"add", input_injection_time,
/*component_name=*/"mouse-input-flutter");
// Then Flutter sends a DOWN pointer event with the buttons we care about.
VerifyEvent(event_down,
/*expected_x=*/static_cast<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/fuchsia::ui::input::kMousePrimaryButton,
/*expected_type=*/"down", input_injection_time,
/*component_name=*/"mouse-input-flutter");
// Then Flutter sends a MOVE pointer event with no new information.
VerifyEvent(event_noop_move,
/*expected_x=*/static_cast<double>(display_width()) / 2.f,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/fuchsia::ui::input::kMousePrimaryButton,
/*expected_type=*/"move", input_injection_time,
/*component_name=*/"mouse-input-flutter");
bool move_injection_initiated = false;
fuchsia::input::report::MouseInputReport move_report;
// We use `kClickToDragThreshold` to make sure the mouse handler registers movement.
move_report.set_movement_x(kClickToDragThreshold);
move_report.set_pressed_buttons({0});
input_synthesis->SendInputReport(
device_id, std::move(move_report), ts, [&move_injection_initiated](auto result) {
ASSERT_FALSE(result.is_err()) << "SendInputReport failed " << result.err();
move_injection_initiated = true;
});
RunLoopUntil([&move_injection_initiated] { return move_injection_initiated; });
RunLoopUntil([this] { return this->response_listener_->SizeOfEvents() == 1; });
ASSERT_EQ(response_listener_->SizeOfEvents(), 1u);
auto event_move = response_listener_->PopEvent();
VerifyEventLocationOnTheRightOfExpectation(
event_move,
/*expected_x_min=*/static_cast<double>(display_width()) / 2.f + 1,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/fuchsia::ui::input::kMousePrimaryButton,
/*expected_type=*/"move", input_injection_time,
/*component_name=*/"mouse-input-flutter");
bool up_injection_initiated = false;
fuchsia::input::report::MouseInputReport up_report;
up_report.set_movement_x(0);
up_report.set_movement_y(0);
up_report.set_pressed_buttons({});
input_synthesis->SendInputReport(
device_id, std::move(up_report), ts, [&up_injection_initiated](auto result) {
ASSERT_FALSE(result.is_err()) << "SendInputReport failed " << result.err();
up_injection_initiated = true;
});
RunLoopUntil([&up_injection_initiated] { return up_injection_initiated; });
RunLoopUntil([this] { return this->response_listener_->SizeOfEvents() == 1; });
ASSERT_EQ(response_listener_->SizeOfEvents(), 1u);
auto event_up = response_listener_->PopEvent();
VerifyEventLocationOnTheRightOfExpectation(
event_up,
/*expected_x_min=*/static_cast<double>(display_width()) / 2.f + 1,
/*expected_y=*/static_cast<double>(display_height()) / 2.f,
/*expected_buttons=*/0,
/*expected_type=*/"up", input_injection_time,
/*component_name=*/"mouse-input-flutter");
}
class ChromiumInputTest : public MouseInputBase {
protected:
std::vector<std::pair<ChildName, LegacyUrl>> GetTestComponents() override {
return {
std::make_pair(kWebContextProvider, kWebContextProviderUrl),
};
}
std::vector<std::pair<ChildName, std::string>> GetTestV2Components() override {
return {
std::make_pair(kMouseInputChromium, kMouseInputChromiumUrl),
std::make_pair(kBuildInfoProvider, kBuildInfoProviderUrl),
std::make_pair(kMemoryPressureProvider, kMemoryPressureProviderUrl),
std::make_pair(kNetstack, kNetstackUrl),
std::make_pair(kMockCobalt, kMockCobaltUrl),
};
}
std::vector<Route> GetTestRoutes() override {
return merge({GetChromiumRoutes(ChildRef{kMouseInputChromium}),
{
{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
.source = ChildRef{kMouseInputChromium},
.targets = {ParentRef()}},
}});
}
// Routes needed to setup Chromium client.
static std::vector<Route> GetChromiumRoutes(ChildRef target) {
return {
{.capabilities =
{
Protocol{fuchsia::ui::composition::Allocator::Name_},
Protocol{fuchsia::ui::composition::Flatland::Name_},
Protocol{fuchsia::vulkan::loader::Loader::Name_},
},
.source = ParentRef(),
.targets = {target}},
{.capabilities = {Protocol{test::mouse::ResponseListener::Name_}},
.source = ChildRef{kResponseListener},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::memorypressure::Provider::Name_}},
.source = ChildRef{kMemoryPressureProvider},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::netstack::Netstack::Name_}},
.source = ChildRef{kNetstack},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::net::interfaces::State::Name_}},
.source = ChildRef{kNetstack},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::accessibility::semantics::SemanticsManager::Name_}},
.source = ParentRef(),
.targets = {target}},
{.capabilities = {Protocol{fuchsia::web::ContextProvider::Name_}},
.source = ChildRef{kWebContextProvider},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::sys::Environment::Name_},
Protocol{fuchsia::logger::LogSink::Name_}},
.source = ParentRef(),
.targets = {target}},
{.capabilities = {Protocol{fuchsia::cobalt::LoggerFactory::Name_}},
.source = ChildRef{kMockCobalt},
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Protocol{fuchsia::sysmem::Allocator::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kMemoryPressureProvider}, ChildRef{kMouseInputChromium}}},
{.capabilities = {Protocol{fuchsia::scheduler::ProfileProvider::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Protocol{fuchsia::kernel::RootJobForInspect::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Protocol{fuchsia::kernel::Stats::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Protocol{fuchsia::tracing::provider::Registry::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_}},
.source = ParentRef(),
.targets = {target}},
{.capabilities = {Protocol{fuchsia::posix::socket::Provider::Name_}},
.source = ChildRef{kNetstack},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::buildinfo::Provider::Name_}},
.source = ChildRef{kBuildInfoProvider},
.targets = {target, ChildRef{kWebContextProvider}}},
};
}
// TODO(fxbug.dev/58322): EnsureMouseIsReadyAndGetPosition will send a mouse click
// (down and up) and wait for response to ensure the mouse is ready to use. We will retry a mouse
// click if we can not get the mouseup response in small timeout. This function returns
// the cursor position in WebEngine coordinate system.
Position EnsureMouseIsReadyAndGetPosition(
fidl::InterfacePtr<test::inputsynthesis::Mouse>& input_synthesis, uint32_t device_id) {
for (int retry = 0; retry < kMaxRetry; retry++) {
// Mouse down and up.
{
auto ts = static_cast<uint64_t>(zx::clock::get_monotonic().get());
fuchsia::input::report::MouseInputReport report;
report.set_pressed_buttons({0});
SendMouseEvent(input_synthesis, device_id, std::move(report), ts);
}
{
auto ts = static_cast<uint64_t>(zx::clock::get_monotonic().get());
fuchsia::input::report::MouseInputReport report;
report.set_pressed_buttons({});
SendMouseEvent(input_synthesis, device_id, std::move(report), ts);
}
RunLoopWithTimeoutOrUntil(
[this] {
return this->response_listener_->SizeOfEvents() > 0 &&
this->response_listener_->LastEvent().type() == "mouseup";
},
kFirstEventRetryInterval);
if (response_listener_->SizeOfEvents() > 0 &&
response_listener_->LastEvent().type() == "mouseup") {
Position p;
p.x = response_listener_->LastEvent().local_x();
p.y = response_listener_->LastEvent().local_y();
response_listener_->ClearEvents();
return p;
}
}
FX_LOGS(FATAL) << "Can not get mouse click in max retries " << kMaxRetry;
return Position{};
}
void LaunchWebEngineClient() {
LaunchClient();
// In WebEngine |is_rendering| only indicated WebEngine is rendering but input tests require JS
// loaded (JS event callback registered).
RunLoopUntil([this]() { return this->response_listener()->IsWebEngineReady(); });
RunLoopUntil([this] { return ui_test_manager_->ClientViewIsFocused(); });
}
static constexpr auto kMouseInputChromium = "mouse-input-chromium";
static constexpr auto kMouseInputChromiumUrl = "#meta/mouse-input-chromium.cm";
static constexpr auto kWebContextProvider = "web_context_provider";
static constexpr auto kWebContextProviderUrl =
"fuchsia-pkg://fuchsia.com/web_engine#meta/context_provider.cmx";
static constexpr auto kMemoryPressureProvider = "memory_pressure_provider";
static constexpr auto kMemoryPressureProviderUrl = "#meta/memory_monitor.cm";
static constexpr auto kNetstack = "netstack";
static constexpr auto kNetstackUrl = "#meta/netstack.cm";
static constexpr auto kBuildInfoProvider = "build_info_provider";
static constexpr auto kBuildInfoProviderUrl = "#meta/fake_build_info.cm";
static constexpr auto kMockCobalt = "cobalt";
static constexpr auto kMockCobaltUrl = "#meta/mock_cobalt.cm";
// The first event to WebEngine may lost, see EnsureMouseIsReadyAndGetPosition. Retry to ensure
// WebEngine is ready to process events.
static constexpr auto kFirstEventRetryInterval = zx::sec(1);
// To avoid retry to timeout, limit 10 retries, if still not ready, fail it with meaningful error.
static const int kMaxRetry = 10;
};
TEST_F(ChromiumInputTest, ChromiumMouseMove) {
LaunchWebEngineClient();
auto input_synthesis = realm_exposed_services()->Connect<test::inputsynthesis::Mouse>();
uint32_t device_id = AddMouseDevice(input_synthesis);
auto initial_position = EnsureMouseIsReadyAndGetPosition(input_synthesis, device_id);
double initial_x = initial_position.x;
double initial_y = initial_position.y;
auto input_injection_time = zx::clock::get_monotonic();
auto ts = static_cast<uint64_t>(input_injection_time.get());
fuchsia::input::report::MouseInputReport report;
report.set_movement_x(5);
report.set_movement_y(0);
SendMouseEvent(input_synthesis, device_id, std::move(report), ts);
RunLoopUntil([this] { return this->response_listener_->SizeOfEvents() == 1; });
auto event_move = response_listener_->PopEvent();
VerifyEventLocationOnTheRightOfExpectation(event_move,
/*expected_x_min=*/initial_x,
/*expected_y=*/initial_y,
/*expected_buttons=*/0,
/*expected_type=*/"mousemove", input_injection_time,
/*component_name=*/"mouse-input-chromium");
}
TEST_F(ChromiumInputTest, ChromiumMouseDownMoveUp) {
LaunchWebEngineClient();
auto input_synthesis = realm_exposed_services()->Connect<test::inputsynthesis::Mouse>();
uint32_t device_id = AddMouseDevice(input_synthesis);
auto initial_position = EnsureMouseIsReadyAndGetPosition(input_synthesis, device_id);
double initial_x = initial_position.x;
double initial_y = initial_position.y;
auto down_injection_time = zx::clock::get_monotonic();
{
auto ts = static_cast<uint64_t>(down_injection_time.get());
fuchsia::input::report::MouseInputReport report;
report.set_pressed_buttons({0});
SendMouseEvent(input_synthesis, device_id, std::move(report), ts);
}
auto move_injection_time = zx::clock::get_monotonic();
{
auto ts = static_cast<uint64_t>(move_injection_time.get());
fuchsia::input::report::MouseInputReport report;
report.set_pressed_buttons({0});
report.set_movement_x(kClickToDragThreshold);
report.set_movement_y(0);
SendMouseEvent(input_synthesis, device_id, std::move(report), ts);
}
auto up_injection_time = zx::clock::get_monotonic();
{
auto ts = static_cast<uint64_t>(up_injection_time.get());
fuchsia::input::report::MouseInputReport report;
report.set_pressed_buttons({});
SendMouseEvent(input_synthesis, device_id, std::move(report), ts);
}
RunLoopUntil([this] { return this->response_listener_->SizeOfEvents() == 3; });
auto event_down = response_listener_->PopEvent();
auto event_move = response_listener_->PopEvent();
auto event_up = response_listener_->PopEvent();
VerifyEvent(event_down,
/*expected_x=*/initial_x,
/*expected_y=*/initial_y,
/*expected_buttons=*/1,
/*expected_type=*/"mousedown", down_injection_time,
/*component_name=*/"mouse-input-chromium");
VerifyEventLocationOnTheRightOfExpectation(event_move,
/*expected_x_min=*/initial_x,
/*expected_y=*/initial_y,
/*expected_buttons=*/1,
/*expected_type=*/"mousemove", move_injection_time,
/*component_name=*/"mouse-input-chromium");
VerifyEvent(event_up,
/*expected_x=*/event_move.local_x(),
/*expected_y=*/initial_y,
/*expected_buttons=*/0,
/*expected_type=*/"mouseup", up_injection_time,
/*component_name=*/"mouse-input-chromium");
}
} // namespace