blob: 4997c4ce5eed986213c9fa2a7fb678c48ba26bc1 [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/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/logger/cpp/fidl.h>
#include <fuchsia/memorypressure/cpp/fidl.h>
#include <fuchsia/metrics/cpp/fidl.h>
#include <fuchsia/net/interfaces/cpp/fidl.h>
#include <fuchsia/posix/socket/cpp/fidl.h>
#include <fuchsia/process/cpp/fidl.h>
#include <fuchsia/scheduler/cpp/fidl.h>
#include <fuchsia/session/scene/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/ui/test/input/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 <zircon/status.h>
#include <zircon/types.h>
#include <zircon/utc.h>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <memory>
#include <queue>
#include <string>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include "src/ui/testing/util/portable_ui_test.h"
namespace {
// Types imported for the realm_builder library.
using component_testing::ChildRef;
using component_testing::Config;
using component_testing::Directory;
using component_testing::LocalComponentImpl;
using component_testing::ParentRef;
using component_testing::Protocol;
using component_testing::Route;
using component_testing::VoidRef;
// Alias for Component child name as provided to Realm Builder.
using ChildName = 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;
// 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;
}
int ButtonsToInt(const std::vector<fuchsia::ui::test::input::MouseButton>& buttons) {
int result = 0;
for (const auto& button : buttons) {
result |= (0x1 >> button);
}
return result;
}
// Contains the current mouse input state.
//
// Used to be part of MouseInputListenerServer. The state is now externalized,
// because the new AddLocalChild API does not allow directly inspecting the
// state of the local child component itself. Instead, this state is shared
// between the test fixture and the local component below.
class MouseInputState {
public:
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:
friend class MouseInputListenerServer;
std::queue<fuchsia::ui::test::input::MouseInputListenerReportMouseInputRequest> events_;
};
// `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 LocalComponentImpl {
public:
explicit MouseInputListenerServer(async_dispatcher_t* dispatcher,
std::weak_ptr<MouseInputState> state)
: dispatcher_(dispatcher), state_(std::move(state)) {}
void ReportMouseInput(
fuchsia::ui::test::input::MouseInputListenerReportMouseInputRequest request) override {
if (auto s = state_.lock()) {
s->events_.push(std::move(request));
}
}
// When the component framework requests for this component to start, this
// method will be invoked by the realm_builder library.
void OnStart() override {
FX_CHECK(outgoing()->AddPublicService(
fidl::InterfaceRequestHandler<fuchsia::ui::test::input::MouseInputListener>(
[this](auto request) {
bindings_.AddBinding(this, std::move(request), dispatcher_);
})) == ZX_OK);
}
private:
// Not owned.
async_dispatcher_t* dispatcher_ = nullptr;
fidl::BindingSet<fuchsia::ui::test::input::MouseInputListener> bindings_;
std::weak_ptr<MouseInputState> state_;
};
constexpr auto kMouseInputListener = "mouse_input_listener";
struct Position {
double x = 0.0;
double y = 0.0;
};
class MouseInputBase : public ui_testing::PortableUITest {
protected:
MouseInputBase() : mouse_state_(std::make_shared<MouseInputState>()) {}
std::string GetTestUIStackUrl() override { return "#meta/test-ui-stack.cm"; }
void SetUp() override {
ui_testing::PortableUITest::SetUp();
// Register fake mouse device.
RegisterMouse();
}
void TearDown() override {
ui_testing::PortableUITest::TearDown();
// at the end of test, ensure event queue is empty.
ASSERT_EQ(mouse_state_->SizeOfEvents(), 0u);
}
// 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>> GetTestComponents() { 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 {}; }
// 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<fuchsia::ui::test::input::MouseButton> expected_buttons,
const fuchsia::ui::test::input::MouseEventPhase expected_phase,
const std::string& component_name) {
FX_LOGS(INFO) << "Client received mouse change at (" << pointer_data.local_x() << ", "
<< pointer_data.local_y() << ") with buttons "
<< ButtonsToInt(pointer_data.buttons()) << ".";
FX_LOGS(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<fuchsia::ui::test::input::MouseButton> expected_buttons,
const fuchsia::ui::test::input::MouseEventPhase expected_phase,
const std::string& component_name) {
FX_LOGS(INFO) << "Client received mouse change at (" << pointer_data.local_x() << ", "
<< pointer_data.local_y() << ") with buttons "
<< ButtonsToInt(pointer_data.buttons()) << ".";
FX_LOGS(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);
}
void ExtendRealm() override {
// Key part of service setup: have this test component vend the
// |MouseInputListener| service in the constructed realm.
auto* d = dispatcher();
realm_builder().AddLocalChild(kMouseInputListener, [d, s = mouse_state_]() {
return std::make_unique<MouseInputListenerServer>(d, s);
});
for (const auto& [name, component] : GetTestComponents()) {
realm_builder().AddChild(name, component);
}
// Add the necessary routing for each of the extra components added above.
for (const auto& route : GetTestRoutes()) {
realm_builder().AddRoute(route);
}
}
std::shared_ptr<MouseInputState> mouse_state_;
// Use a DPR other than 1.0, so that logical and physical coordinate spaces
// are different.
float device_pixel_ratio() override { return 2.f; }
static constexpr auto kFontsProvider = "fonts_provider";
static constexpr auto kFontsProviderUrl = "#meta/font_provider_hermetic_for_test.cm";
};
class ChromiumInputTest : public MouseInputBase {
protected:
std::vector<std::pair<ChildName, std::string>> GetTestComponents() override {
return {
std::make_pair(kMouseInputChromium, kMouseInputChromiumUrl),
std::make_pair(kBuildInfoProvider, kBuildInfoProviderUrl),
std::make_pair(kFontsProvider, kFontsProviderUrl),
std::make_pair(kMemoryPressureProvider, kMemoryPressureProviderUrl),
std::make_pair(kNetstack, kNetstackUrl),
std::make_pair(kMockCobalt, kMockCobaltUrl),
std::make_pair(kWebContextProvider, kWebContextProviderUrl),
};
}
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::accessibility::semantics::SemanticsManager::Name_},
Protocol{fuchsia::ui::composition::Allocator::Name_},
Protocol{fuchsia::ui::composition::Flatland::Name_},
Protocol{fuchsia::ui::scenic::Scenic::Name_},
},
.source = kTestUIStackRef,
.targets = {target}},
{.capabilities =
{
Protocol{fuchsia::kernel::VmexResource::Name_},
Protocol{fuchsia::process::Launcher::Name_},
Protocol{fuchsia::vulkan::loader::Loader::Name_},
},
.source = ParentRef(),
.targets = {target}},
{
.capabilities =
{
Protocol{fuchsia::logger::LogSink::Name_},
},
.source = ParentRef(),
.targets =
{
target, ChildRef{kFontsProvider}, ChildRef{kMemoryPressureProvider},
ChildRef{kBuildInfoProvider}, ChildRef{kWebContextProvider},
ChildRef{kMockCobalt},
// Not including kNetstack here, since it emits spurious
// FATAL errors.
},
},
{.capabilities = {Protocol{fuchsia::ui::test::input::MouseInputListener::Name_}},
.source = ChildRef{kMouseInputListener},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::fonts::Provider::Name_}},
.source = ChildRef{kFontsProvider},
.targets = {target}},
{.capabilities =
{
Protocol{fuchsia::tracing::provider::Registry::Name_},
},
.source = ParentRef(),
.targets = {target, ChildRef{kFontsProvider}}},
{.capabilities = {Protocol{fuchsia::memorypressure::Provider::Name_}},
.source = ChildRef{kMemoryPressureProvider},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::net::interfaces::State::Name_}},
.source = ChildRef{kNetstack},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::web::ContextProvider::Name_}},
.source = ChildRef{kWebContextProvider},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::metrics::MetricEventLoggerFactory::Name_}},
.source = ChildRef{kMockCobalt},
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Protocol{fuchsia::sysmem::Allocator::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kMemoryPressureProvider}, target}},
{.capabilities = {Protocol{fuchsia::scheduler::RoleManager::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 = {Config{kCaptureOnPressureChange}},
.source = VoidRef(),
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Config{kImminentOomCaptureDelay}},
.source = VoidRef(),
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Config{kCriticalCaptureDelay}},
.source = VoidRef(),
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Config{kWarningCaptureDelay}},
.source = VoidRef(),
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Config{kNormalCaptureDelay}},
.source = VoidRef(),
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Protocol{fuchsia::posix::socket::Provider::Name_}},
.source = ChildRef{kNetstack},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::buildinfo::Provider::Name_}},
.source = ChildRef{kBuildInfoProvider},
.targets = {target}},
{.capabilities =
{
Directory{
.name = "root-ssl-certificates",
.type = fuchsia::component::decl::DependencyType::STRONG,
},
Directory{
.name = "tzdata-icu",
.type = fuchsia::component::decl::DependencyType::STRONG,
},
},
.source = ParentRef(),
.targets = {ChildRef{kWebContextProvider}}},
};
}
// TODO(https://fxbug.dev/42136267): 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() {
for (int retry = 0; retry < kMaxRetry; retry++) {
// Mouse down and up.
SimulateMouseEvent(/* pressed_buttons = */ {fuchsia::ui::test::input::MouseButton::FIRST},
/* movement_x = */ 0, /* movement_y = */ 0);
SimulateMouseEvent(/* pressed_buttons = */ {}, /* movement_x = */ 0, /* movement_y = */ 0);
auto wait_until_last_event_phase_or_timeout =
[this](fuchsia::ui::test::input::MouseEventPhase event_phase) {
return RunLoopWithTimeoutOrUntil(
[this, event_phase] {
return mouse_state_->SizeOfEvents() > 0 &&
mouse_state_->LastEvent().phase() == event_phase;
},
kFirstEventRetryInterval);
};
bool got_mouse_up =
wait_until_last_event_phase_or_timeout(fuchsia::ui::test::input::MouseEventPhase::UP);
if (got_mouse_up) {
// There is an issue we found in retry that the mouse up we got may
// come from previous retry loop, then EnsureMouseIsReadyAndGetPosition
// exit and the down-up event caught by test body, and break the test
// expectation. Here we inject 1 more wheel event, because wheel event
// only injected once, wheel we receive the wheel event, we know all
// events from EnsureMouseIsReadyAndGetPosition are processed.
SimulateMouseScroll(/* pressed_buttons = */ {}, /* scroll_x = */ 0, /* scroll_y = */ 1);
wait_until_last_event_phase_or_timeout(fuchsia::ui::test::input::MouseEventPhase::WHEEL);
Position p;
p.x = mouse_state_->LastEvent().local_x();
p.y = mouse_state_->LastEvent().local_y();
mouse_state_->ClearEvents();
return p;
}
}
FX_LOGS(FATAL) << "Can not get mouse click in max retries " << kMaxRetry;
return Position{};
}
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.cm";
static constexpr auto kMemoryPressureProvider = "memory_pressure_provider";
static constexpr auto kMemoryPressureProviderUrl = "#meta/memory_monitor.cm";
static constexpr auto kCaptureOnPressureChange = "fuchsia.memory.CaptureOnPressureChange";
static constexpr auto kImminentOomCaptureDelay = "fuchsia.memory.ImminentOomCaptureDelay";
static constexpr auto kCriticalCaptureDelay = "fuchsia.memory.CriticalCaptureDelay";
static constexpr auto kWarningCaptureDelay = "fuchsia.memory.WarningCaptureDelay";
static constexpr auto kNormalCaptureDelay = "fuchsia.memory.NormalCaptureDelay";
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) {
LaunchClient();
auto initial_position = EnsureMouseIsReadyAndGetPosition();
double initial_x = initial_position.x;
double initial_y = initial_position.y;
SimulateMouseEvent(/* pressed_buttons = */ {},
/* movement_x = */ 5, /* movement_y = */ 0);
RunLoopUntil([this] { return mouse_state_->SizeOfEvents() == 1; });
auto event_move = mouse_state_->PopEvent();
VerifyEventLocationOnTheRightOfExpectation(
event_move,
/*expected_x_min=*/initial_x,
/*expected_y=*/initial_y,
/*expected_buttons=*/{},
/*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::MOVE,
/*component_name=*/"mouse-input-chromium");
}
TEST_F(ChromiumInputTest, ChromiumMouseDownMoveUp) {
LaunchClient();
auto initial_position = EnsureMouseIsReadyAndGetPosition();
double initial_x = initial_position.x;
double initial_y = initial_position.y;
SimulateMouseEvent(/* pressed_buttons = */ {fuchsia::ui::test::input::MouseButton::FIRST},
/* movement_x = */ 0, /* movement_y = */ 0);
SimulateMouseEvent(/* pressed_buttons = */ {fuchsia::ui::test::input::MouseButton::FIRST},
/* movement_x = */ kClickToDragThreshold, /* movement_y = */ 0);
SimulateMouseEvent(/* pressed_buttons = */ {}, /* movement_x = */ 0, /* movement_y = */ 0);
RunLoopUntil([this] { return mouse_state_->SizeOfEvents() == 3; });
auto event_down = mouse_state_->PopEvent();
auto event_move = mouse_state_->PopEvent();
auto event_up = mouse_state_->PopEvent();
VerifyEvent(event_down,
/*expected_x=*/initial_x,
/*expected_y=*/initial_y,
/*expected_buttons=*/{fuchsia::ui::test::input::MouseButton::FIRST},
/*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::DOWN,
/*component_name=*/"mouse-input-chromium");
VerifyEventLocationOnTheRightOfExpectation(
event_move,
/*expected_x_min=*/initial_x,
/*expected_y=*/initial_y,
/*expected_buttons=*/{fuchsia::ui::test::input::MouseButton::FIRST},
/*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::MOVE,
/*component_name=*/"mouse-input-chromium");
VerifyEvent(event_up,
/*expected_x=*/event_move.local_x(),
/*expected_y=*/initial_y,
/*expected_buttons=*/{},
/*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::UP,
/*component_name=*/"mouse-input-chromium");
}
TEST_F(ChromiumInputTest, ChromiumMouseWheel) {
LaunchClient();
auto initial_position = EnsureMouseIsReadyAndGetPosition();
double initial_x = initial_position.x;
double initial_y = initial_position.y;
SimulateMouseScroll(/* pressed_buttons = */ {}, /* scroll_x = */ 1, /* scroll_y = */ 0);
RunLoopUntil([this] { return mouse_state_->SizeOfEvents() == 1; });
auto event_wheel_h = mouse_state_->PopEvent();
VerifyEvent(event_wheel_h,
/*expected_x=*/initial_x,
/*expected_y=*/initial_y,
/*expected_buttons=*/{},
/*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::WHEEL,
/*component_name=*/"mouse-input-chromium");
// Chromium will scale the count of ticks to pixel.
// Positive delta in Fuchsia means scroll left, and scroll left in JS is negative delta.
EXPECT_LT(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 mouse_state_->SizeOfEvents() == 1; });
auto event_wheel_v = mouse_state_->PopEvent();
VerifyEvent(event_wheel_v,
/*expected_x=*/initial_x,
/*expected_y=*/initial_y,
/*expected_buttons=*/{},
/*expected_type=*/fuchsia::ui::test::input::MouseEventPhase::WHEEL,
/*component_name=*/"mouse-input-chromium");
// Chromium will scale the count of ticks to pixel.
// Positive delta in Fuchsia means scroll up, and scroll up in JS is negative delta.
EXPECT_LT(event_wheel_v.wheel_y_physical_pixel(), 0);
EXPECT_EQ(event_wheel_v.wheel_x_physical_pixel(), 0);
}
} // namespace