blob: cd80a1de718a6fa2f383c3f0e05e0cfda2bcd7fe [file]
// Copyright 2021 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/input/report/cpp/fidl.h>
#include <fuchsia/ui/composition/cpp/fidl.h>
#include <fuchsia/ui/pointer/cpp/fidl.h>
#include <fuchsia/ui/pointerinjector/cpp/fidl.h>
#include <lib/async-loop/testing/cpp/real_loop.h>
#include <lib/sys/component/cpp/testing/realm_builder.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/ui/scenic/cpp/view_creation_tokens.h>
#include <lib/ui/scenic/cpp/view_identity.h>
#include <zircon/status.h>
#include <zxtest/zxtest.h>
#include "src/ui/scenic/tests/utils/scenic_realm_builder.h"
// These tests exercise the integration between Flatland and the InputSystem, including the
// View-to-View transform logic between the injection point and the receiver.
// Setup:
// - The test fixture sets up the display + the root session and view.
// - Injection done in context View Space, with fuchsia.ui.pointerinjector
// - Target(s) specified by View (using view ref koids)
// - Dispatch done to fuchsia.ui.pointer.MouseSource in receiver View Space.
namespace integration_tests {
using fuc_ParentViewportWatcher = fuchsia::ui::composition::ParentViewportWatcher;
using fuc_ChildViewWatcher = fuchsia::ui::composition::ChildViewWatcher;
using fuc_Flatland = fuchsia::ui::composition::Flatland;
using fuc_FlatlandPtr = fuchsia::ui::composition::FlatlandPtr;
using fuc_ViewBoundProtocols = fuchsia::ui::composition::ViewBoundProtocols;
using fuc_FlatlandDisplay = fuchsia::ui::composition::FlatlandDisplay;
using fuc_FlatlandDisplayPtr = fuchsia::ui::composition::FlatlandDisplayPtr;
using fuc_ViewportProperties = fuchsia::ui::composition::ViewportProperties;
using fuc_TransformId = fuchsia::ui::composition::TransformId;
using fuc_ContentId = fuchsia::ui::composition::ContentId;
using fuv_ViewRefFocused = fuchsia::ui::views::ViewRefFocused;
using fuv_ViewRefFocusedPtr = fuchsia::ui::views::ViewRefFocusedPtr;
using fuv_ViewRef = fuchsia::ui::views::ViewRef;
using fuv_FocusState = fuchsia::ui::views::FocusState;
using fup_MouseEvent = fuchsia::ui::pointer::MouseEvent;
using fup_MouseSource = fuchsia::ui::pointer::MouseSource;
using fup_MouseSourcePtr = fuchsia::ui::pointer::MouseSourcePtr;
using fupi_Config = fuchsia::ui::pointerinjector::Config;
using fupi_DispatchPolicy = fuchsia::ui::pointerinjector::DispatchPolicy;
using fupi_Event = fuchsia::ui::pointerinjector::Event;
using fupi_EventPhase = fuchsia::ui::pointerinjector::EventPhase;
using fupi_PointerSample = fuchsia::ui::pointerinjector::PointerSample;
using fupi_Context = fuchsia::ui::pointerinjector::Context;
using fupi_Data = fuchsia::ui::pointerinjector::Data;
using fupi_Registry = fuchsia::ui::pointerinjector::Registry;
using fupi_RegistryPtr = fuchsia::ui::pointerinjector::RegistryPtr;
using fupi_DevicePtr = fuchsia::ui::pointerinjector::DevicePtr;
using fupi_DeviceType = fuchsia::ui::pointerinjector::DeviceType;
using fupi_Target = fuchsia::ui::pointerinjector::Target;
using fupi_Viewport = fuchsia::ui::pointerinjector::Viewport;
using RealmRoot = component_testing::RealmRoot;
using fir_Axis = fuchsia::input::report::Axis;
class FlatlandMouseIntegrationTest : public zxtest::Test, public loop_fixture::RealLoop {
protected:
static constexpr uint32_t kDeviceId = 1111;
static constexpr uint32_t kPointerId = 2222;
static constexpr uint32_t kDefaultSize = 1;
// clang-format off
static constexpr std::array<float, 9> kIdentityMatrix = {
1, 0, 0, // column one
0, 1, 0, // column two
0, 0, 1, // column three
};
// clang-format on
void SetUp() override {
// Build the realm topology and route the protocols required by this test fixture from the
// scenic subrealm.
realm_ = std::make_unique<RealmRoot>(
ScenicRealmBuilder()
.AddRealmProtocol(fuchsia::ui::composition::Flatland::Name_)
.AddRealmProtocol(fuchsia::ui::composition::FlatlandDisplay::Name_)
.AddRealmProtocol(fuchsia::ui::composition::Allocator::Name_)
.AddRealmProtocol(fuchsia::ui::pointerinjector::Registry::Name_)
.Build());
flatland_display_ = realm_->Connect<fuc_FlatlandDisplay>();
flatland_display_.set_error_handler([](zx_status_t status) {
FAIL("Lost connection to Scenic: %s", zx_status_get_string(status));
});
pointerinjector_registry_ = realm_->Connect<fupi_Registry>();
pointerinjector_registry_.set_error_handler([](zx_status_t status) {
FAIL("Lost connection to pointerinjector Registry: %s", zx_status_get_string(status));
});
// Set up root view.
root_session_ = realm_->Connect<fuc_Flatland>();
root_session_.set_error_handler([](zx_status_t status) {
FAIL("Lost connection to Scenic: %s", zx_status_get_string(status));
});
fidl::InterfacePtr<fuc_ChildViewWatcher> child_view_watcher;
fuc_ViewBoundProtocols protocols;
fuv_ViewRefFocusedPtr root_focused_ptr;
auto [child_token, parent_token] = scenic::ViewCreationTokenPair::New();
fidl::InterfacePtr<fuc_ParentViewportWatcher> parent_viewport_watcher;
auto identity = scenic::NewViewIdentityOnCreation();
root_view_ref_ = fidl::Clone(identity.view_ref);
protocols.set_view_ref_focused(root_focused_ptr.NewRequest());
root_session_->CreateView2(std::move(child_token), std::move(identity), std::move(protocols),
parent_viewport_watcher.NewRequest());
parent_viewport_watcher->GetLayout([this](auto layout_info) {
ASSERT_TRUE(layout_info.has_logical_size());
const auto [width, height] = layout_info.logical_size();
display_width_ = static_cast<float>(width);
display_height_ = static_cast<float>(height);
});
flatland_display_->SetContent(std::move(parent_token), child_view_watcher.NewRequest());
BlockingPresent(root_session_);
// Wait until we get the display size.
RunLoopUntil([this] { return display_width_ != 0 && display_height_ != 0; });
}
void BlockingPresent(fuc_FlatlandPtr& flatland) {
bool presented = false;
flatland.events().OnFramePresented = [&presented](auto) { presented = true; };
flatland->Present({});
RunLoopUntil([&presented] { return presented; });
flatland.events().OnFramePresented = nullptr;
}
void Inject(float x, float y, fupi_EventPhase phase, std::vector<uint8_t> pressed_buttons = {},
std::optional<int64_t> scroll_v = std::nullopt,
std::optional<int64_t> scroll_h = std::nullopt) {
FX_DCHECK(injector_);
fupi_Event event;
event.set_timestamp(0);
{
fupi_PointerSample pointer_sample;
pointer_sample.set_pointer_id(kPointerId);
pointer_sample.set_phase(phase);
pointer_sample.set_position_in_viewport({x, y});
if (scroll_v.has_value()) {
pointer_sample.set_scroll_v(scroll_v.value());
}
if (scroll_h.has_value()) {
pointer_sample.set_scroll_h(scroll_h.value());
}
if (!pressed_buttons.empty()) {
pointer_sample.set_pressed_buttons(pressed_buttons);
}
fupi_Data data;
data.set_pointer_sample(std::move(pointer_sample));
event.set_data(std::move(data));
}
std::vector<fupi_Event> events;
events.emplace_back(std::move(event));
injector_->Inject(std::move(events), [] {});
}
void RegisterInjector(fuv_ViewRef context_view_ref, fuv_ViewRef target_view_ref,
fupi_DispatchPolicy dispatch_policy, std::vector<uint8_t> buttons,
std::array<float, 9> viewport_to_context_transform) {
fupi_Config config;
config.set_device_id(kDeviceId);
config.set_device_type(fupi_DeviceType::MOUSE);
config.set_dispatch_policy(dispatch_policy);
{
fir_Axis axis;
axis.range.min = -1;
axis.range.max = 1;
config.set_scroll_v_range(axis);
}
{
fir_Axis axis;
axis.range.min = -1;
axis.range.max = 1;
config.set_scroll_h_range(axis);
}
if (!buttons.empty()) {
config.set_buttons(buttons);
}
{
{
fupi_Context context;
context.set_view(std::move(context_view_ref));
config.set_context(std::move(context));
}
{
fupi_Target target;
target.set_view(std::move(target_view_ref));
config.set_target(std::move(target));
}
{
fupi_Viewport viewport;
viewport.set_extents(FullScreenExtents());
viewport.set_viewport_to_context_transform(viewport_to_context_transform);
config.set_viewport(std::move(viewport));
}
}
injector_.set_error_handler([this](zx_status_t) { injector_channel_closed_ = true; });
bool register_callback_fired = false;
pointerinjector_registry_->Register(
std::move(config), injector_.NewRequest(),
[&register_callback_fired] { register_callback_fired = true; });
RunLoopUntil([&register_callback_fired] { return register_callback_fired; });
EXPECT_FALSE(injector_channel_closed_);
}
// Starts a recursive MouseSource::Watch() loop that collects all received events into
// |out_events|.
void StartWatchLoop(fup_MouseSourcePtr& mouse_source, std::vector<fup_MouseEvent>& out_events) {
const size_t index = watch_loops_.size();
watch_loops_.emplace_back();
watch_loops_.at(index) = [this, &mouse_source, &out_events,
index](std::vector<fup_MouseEvent> events) {
std::move(events.begin(), events.end(), std::back_inserter(out_events));
mouse_source->Watch([this, index](std::vector<fup_MouseEvent> events) {
watch_loops_.at(index)(std::move(events));
});
};
mouse_source->Watch(watch_loops_.at(index));
}
std::array<std::array<float, 2>, 2> FullScreenExtents() const {
return {{{0, 0}, {display_width_, display_height_}}};
}
fuv_ViewRef CreateChildView(fuc_FlatlandPtr& child_session,
fidl::InterfaceRequest<fup_MouseSource> child_mouse_source,
fidl::InterfaceRequest<fuv_ViewRefFocused> child_focused_ptr) {
child_session = realm_->Connect<fuc_Flatland>();
// Set up the child view watcher.
fidl::InterfacePtr<fuc_ChildViewWatcher> child_view_watcher;
auto [child_token, parent_token] = scenic::ViewCreationTokenPair::New();
fuc_ViewportProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
const fuc_TransformId kRootTransform{.value = 1};
root_session_->CreateTransform(kRootTransform);
root_session_->SetRootTransform(kRootTransform);
const fuc_ContentId kRootContent{.value = 1};
root_session_->CreateViewport(kRootContent, std::move(parent_token), std::move(properties),
child_view_watcher.NewRequest());
root_session_->SetContent(kRootTransform, kRootContent);
BlockingPresent(root_session_);
// Set up the child view along with its MouseSource and ViewRefFocused channel.
fidl::InterfacePtr<fuc_ParentViewportWatcher> parent_viewport_watcher;
auto identity = scenic::NewViewIdentityOnCreation();
auto child_view_ref = fidl::Clone(identity.view_ref);
fuc_ViewBoundProtocols protocols;
protocols.set_mouse_source(std::move(child_mouse_source));
protocols.set_view_ref_focused(std::move(child_focused_ptr));
child_session->CreateView2(std::move(child_token), std::move(identity), std::move(protocols),
parent_viewport_watcher.NewRequest());
const fuchsia::ui::composition::TransformId kTransform{.value = 42};
child_session->CreateTransform(kTransform);
child_session->SetRootTransform(kTransform);
BlockingPresent(child_session);
return child_view_ref;
}
fuc_FlatlandPtr root_session_;
fuv_ViewRef root_view_ref_;
bool injector_channel_closed_ = false;
float display_width_ = 0;
float display_height_ = 0;
std::unique_ptr<RealmRoot> realm_;
private:
fuc_FlatlandDisplayPtr flatland_display_;
fupi_RegistryPtr pointerinjector_registry_;
fupi_DevicePtr injector_;
// Holds watch loops so they stay alive through the duration of the test.
std::vector<std::function<void(std::vector<fup_MouseEvent>)>> watch_loops_;
};
// The child view should receive focus and input events when the mouse button is pressed over its
// view.
TEST_F(FlatlandMouseIntegrationTest, ChildReceivesFocus_OnMouseLatch) {
fuc_FlatlandPtr child_session;
fup_MouseSourcePtr child_mouse_source;
fuv_ViewRefFocusedPtr child_focused_ptr;
child_session.set_error_handler([](zx_status_t status) {
FAIL("Lost connection to Scenic: %s", zx_status_get_string(status));
});
child_mouse_source.set_error_handler([](zx_status_t status) {
FAIL("Mouse source closed with status: %s", zx_status_get_string(status));
});
child_focused_ptr.set_error_handler([](zx_status_t status) {
FAIL("ViewRefFocused closed with status: %s", zx_status_get_string(status));
});
auto child_view_ref = CreateChildView(child_session, child_mouse_source.NewRequest(),
child_focused_ptr.NewRequest());
// Listen for input events.
std::vector<fup_MouseEvent> child_events;
StartWatchLoop(child_mouse_source, child_events);
// Inject an input event at (0,0) which is the point of overlap between the parent and the
// child.
const std::vector<uint8_t> button_vec = {1};
RegisterInjector(fidl::Clone(root_view_ref_), fidl::Clone(child_view_ref),
fupi_DispatchPolicy::MOUSE_HOVER_AND_LATCH_IN_TARGET, button_vec,
kIdentityMatrix);
Inject(0, 0, fupi_EventPhase::ADD, button_vec);
// Child should receive mouse input events.
RunLoopUntil([&child_events] { return child_events.size() == 1u; });
// Child view should receive focus.
std::optional<fuv_FocusState> child_focused;
child_focused_ptr->Watch([&child_focused](auto update) { child_focused = std::move(update); });
RunLoopUntil([&child_focused] { return child_focused.has_value(); });
EXPECT_TRUE(child_focused->focused());
}
// Send wheel events to scenic ensure client receives wheel events.
TEST_F(FlatlandMouseIntegrationTest, Wheel) {
fuc_FlatlandPtr child_session;
fup_MouseSourcePtr child_mouse_source;
fuv_ViewRefFocusedPtr child_focused_ptr;
child_session.set_error_handler([](zx_status_t status) {
FAIL("Lost connection to Scenic: %s", zx_status_get_string(status));
});
child_mouse_source.set_error_handler([](zx_status_t status) {
FAIL("Mouse source closed with status: %s", zx_status_get_string(status));
});
child_focused_ptr.set_error_handler([](zx_status_t status) {
FAIL("ViewRefFocused closed with status: %s", zx_status_get_string(status));
});
auto child_view_ref = CreateChildView(child_session, child_mouse_source.NewRequest(),
child_focused_ptr.NewRequest());
// Listen for input events.
std::vector<fup_MouseEvent> child_events;
StartWatchLoop(child_mouse_source, child_events);
// Inject an input event at (0,0) which is the point of overlap between the parent and the
// child.
const std::vector<uint8_t> button_vec = {1};
RegisterInjector(fidl::Clone(root_view_ref_), fidl::Clone(child_view_ref),
fupi_DispatchPolicy::MOUSE_HOVER_AND_LATCH_IN_TARGET, button_vec,
kIdentityMatrix);
Inject(0, 0, fupi_EventPhase::ADD, button_vec);
Inject(0, 0, fupi_EventPhase::CHANGE, /* pressed_buttons= */ {},
/* scroll_v= */ std::optional<int64_t>(1), /* scroll_h= */ std::optional<int64_t>(-1));
RunLoopUntil([&child_events] { return child_events.size() == 2u; });
ASSERT_TRUE(child_events[0].has_pointer_sample());
EXPECT_FALSE(child_events[0].pointer_sample().has_scroll_v());
EXPECT_FALSE(child_events[0].pointer_sample().has_scroll_h());
ASSERT_TRUE(child_events[1].has_pointer_sample());
ASSERT_TRUE(child_events[1].pointer_sample().has_scroll_v());
EXPECT_EQ(child_events[1].pointer_sample().scroll_v(), 1);
ASSERT_TRUE(child_events[1].pointer_sample().has_scroll_h());
EXPECT_EQ(child_events[1].pointer_sample().scroll_h(), -1);
}
// Send wheel events in button pressing sequence to scenic ensure client receives correct wheel
// events.
TEST_F(FlatlandMouseIntegrationTest, DownWheelUpWheel) {
fuc_FlatlandPtr child_session;
fup_MouseSourcePtr child_mouse_source;
fuv_ViewRefFocusedPtr child_focused_ptr;
child_session.set_error_handler([](zx_status_t status) {
FAIL("Lost connection to Scenic: %s", zx_status_get_string(status));
});
child_mouse_source.set_error_handler([](zx_status_t status) {
FAIL("Mouse source closed with status: %s", zx_status_get_string(status));
});
child_focused_ptr.set_error_handler([](zx_status_t status) {
FAIL("ViewRefFocused closed with status: %s", zx_status_get_string(status));
});
auto child_view_ref = CreateChildView(child_session, child_mouse_source.NewRequest(),
child_focused_ptr.NewRequest());
// Listen for input events.
std::vector<fup_MouseEvent> child_events;
StartWatchLoop(child_mouse_source, child_events);
// Inject an input event at (0,0) which is the point of overlap between the parent and the
// child.
const std::vector<uint8_t> button_vec = {1};
RegisterInjector(fidl::Clone(root_view_ref_), fidl::Clone(child_view_ref),
fupi_DispatchPolicy::MOUSE_HOVER_AND_LATCH_IN_TARGET, button_vec,
kIdentityMatrix);
Inject(0, 0, fupi_EventPhase::ADD, button_vec);
Inject(0, 0, fupi_EventPhase::CHANGE, button_vec);
Inject(0, 0, fupi_EventPhase::CHANGE, button_vec,
/* scroll_v= */ std::optional<int64_t>(1));
Inject(0, 0, fupi_EventPhase::CHANGE, /* pressed_buttons= */ {});
Inject(0, 0, fupi_EventPhase::CHANGE, /* pressed_buttons= */ {},
/* scroll_v= */ std::optional<int64_t>(1));
RunLoopUntil([&child_events] { return child_events.size() == 5u; });
ASSERT_TRUE(child_events[0].has_pointer_sample());
EXPECT_FALSE(child_events[0].pointer_sample().has_scroll_v());
EXPECT_FALSE(child_events[0].pointer_sample().has_scroll_h());
ASSERT_TRUE(child_events[1].has_pointer_sample());
EXPECT_EQ(child_events[1].pointer_sample().pressed_buttons(), button_vec);
ASSERT_TRUE(child_events[2].has_pointer_sample());
ASSERT_TRUE(child_events[2].pointer_sample().has_scroll_v());
EXPECT_EQ(child_events[2].pointer_sample().scroll_v(), 1);
EXPECT_FALSE(child_events[2].pointer_sample().has_scroll_h());
EXPECT_EQ(child_events[2].pointer_sample().pressed_buttons(), button_vec);
ASSERT_TRUE(child_events[3].has_pointer_sample());
EXPECT_FALSE(child_events[3].pointer_sample().has_pressed_buttons());
EXPECT_FALSE(child_events[3].pointer_sample().has_scroll_v());
ASSERT_TRUE(child_events[4].has_pointer_sample());
ASSERT_TRUE(child_events[4].pointer_sample().has_scroll_v());
EXPECT_EQ(child_events[4].pointer_sample().scroll_v(), 1);
EXPECT_FALSE(child_events[4].pointer_sample().has_scroll_h());
EXPECT_FALSE(child_events[4].pointer_sample().has_pressed_buttons());
}
// Send wheel events bundled with button changess to scenic ensure client receives correct wheel
// events.
TEST_F(FlatlandMouseIntegrationTest, DownWheelUpWheelBundled) {
fuc_FlatlandPtr child_session;
fup_MouseSourcePtr child_mouse_source;
fuv_ViewRefFocusedPtr child_focused_ptr;
child_session.set_error_handler([](zx_status_t status) {
FAIL("Lost connection to Scenic: %s", zx_status_get_string(status));
});
child_mouse_source.set_error_handler([](zx_status_t status) {
FAIL("Mouse source closed with status: %s", zx_status_get_string(status));
});
child_focused_ptr.set_error_handler([](zx_status_t status) {
FAIL("ViewRefFocused closed with status: %s", zx_status_get_string(status));
});
auto child_view_ref = CreateChildView(child_session, child_mouse_source.NewRequest(),
child_focused_ptr.NewRequest());
// Listen for input events.
std::vector<fup_MouseEvent> child_events;
StartWatchLoop(child_mouse_source, child_events);
// Inject an input event at (0,0) which is the point of overlap between the parent and the
// child.
const std::vector<uint8_t> button_vec = {1};
RegisterInjector(fidl::Clone(root_view_ref_), fidl::Clone(child_view_ref),
fupi_DispatchPolicy::MOUSE_HOVER_AND_LATCH_IN_TARGET, button_vec,
kIdentityMatrix);
Inject(0, 0, fupi_EventPhase::ADD, button_vec);
// This event bundled button down and wheel.
Inject(0, 0, fupi_EventPhase::CHANGE, button_vec, /* scroll_v= */ std::optional<int64_t>(1));
Inject(0, 0, fupi_EventPhase::CHANGE, button_vec, /* scroll_v= */ std::optional<int64_t>(1));
// This event bundled button up and wheel.
Inject(0, 0, fupi_EventPhase::CHANGE, /* pressed_buttons= */ {},
/* scroll_v= */ std::optional<int64_t>(1));
Inject(0, 0, fupi_EventPhase::CHANGE, /* pressed_buttons= */ {},
/* scroll_v= */ std::optional<int64_t>(1));
RunLoopUntil([&child_events] { return child_events.size() == 5u; });
ASSERT_TRUE(child_events[0].has_pointer_sample());
EXPECT_FALSE(child_events[0].pointer_sample().has_scroll_v());
EXPECT_FALSE(child_events[0].pointer_sample().has_scroll_h());
ASSERT_TRUE(child_events[1].has_pointer_sample());
ASSERT_TRUE(child_events[1].pointer_sample().has_scroll_v());
EXPECT_EQ(child_events[1].pointer_sample().scroll_v(), 1);
EXPECT_EQ(child_events[1].pointer_sample().pressed_buttons(), button_vec);
ASSERT_TRUE(child_events[2].has_pointer_sample());
ASSERT_TRUE(child_events[2].pointer_sample().has_scroll_v());
EXPECT_EQ(child_events[2].pointer_sample().scroll_v(), 1);
EXPECT_EQ(child_events[2].pointer_sample().pressed_buttons(), button_vec);
ASSERT_TRUE(child_events[3].has_pointer_sample());
ASSERT_TRUE(child_events[3].pointer_sample().has_scroll_v());
EXPECT_EQ(child_events[3].pointer_sample().scroll_v(), 1);
EXPECT_FALSE(child_events[3].pointer_sample().has_pressed_buttons());
ASSERT_TRUE(child_events[4].has_pointer_sample());
ASSERT_TRUE(child_events[4].pointer_sample().has_scroll_v());
EXPECT_EQ(child_events[4].pointer_sample().scroll_v(), 1);
EXPECT_FALSE(child_events[4].pointer_sample().has_pressed_buttons());
}
} // namespace integration_tests