blob: be31f8ccf86612aa9f8706ad6a824799a4497068 [file] [log] [blame]
// 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/injection/cpp/fidl.h>
#include <fuchsia/ui/app/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <fuchsia/ui/test/input/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/ui/scenic/cpp/resources.h>
#include <lib/ui/scenic/cpp/session.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 <type_traits>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include <test/accessibility/cpp/fidl.h>
#include "src/lib/testing/loop_fixture/real_loop_fixture.h"
#include "src/ui/testing/ui_test_manager/ui_test_manager.h"
// This test exercises the pointer injector code in the context of Input Pipeline and a real Scenic
// client. It is a multi-component test, and carefully avoids sleeping or polling for component
// coordination.
// - It runs real (Root Presenter + Input Pipeline | Scene Manager) components, and a real Scenic
// component.
// - It uses a fake display controller; the physical device is unused.
//
// Components involved
// - This test program
// - Root Presenter (with separate Input Pipeline) or Scene Manager
// - Scenic
// - Child view, a Scenic client
//
// Touch dispatch path
// - Test program's injection -> Input Pipeline -> Scenic -> Child view
//
// Setup sequence
// - The test sets up this view hierarchy:
// - Top level scene, owned by Root Presenter.
// - Child view, owned by the ui client.
// - The test waits for a Scenic event that verifies the child has UI content in the scene graph.
// - The test injects input into Input Pipeline, emulating a display's touch report.
// - Input Pipeline dispatches the touch event to Scenic, which in turn dispatches it to the child.
// - The child receives the touch event and reports back to the test over a custom test-only FIDL.
// - Test waits for the child to report a touch; when the test receives the report, the test quits
// successfully.
//
// This test uses the realm_builder library to construct the topology of components
// and routes services between them. For v2 components, every test driver component
// sits as a child of test_manager in the topology. Thus, the topology of a test
// driver component such as this one looks like this:
//
// test_manager
// |
// pointerinjector-config-test.cml (this component)
//
// With the usage of the realm_builder library, we construct a realm during runtime
// and then extend the topology to look like:
//
// test_manager
// |
// pointerinjector-config-test.cml (this component)
// |
// <created realm root>
// / \
// scenic input-pipeline
//
// For more information about testing v2 components and realm_builder,
// visit the following links:
//
// Testing: https://fuchsia.dev/fuchsia-src/concepts/testing/v2
// Realm Builder: https://fuchsia.dev/fuchsia-src/development/components/v2/realm_builder
namespace {
using ScenicEvent = fuchsia::ui::scenic::Event;
using GfxEvent = fuchsia::ui::gfx::Event;
// 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;
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;
// 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);
// Maximum distance between two view coordinates so that they are considered equal.
constexpr auto kViewCoordinateEpsilon = 0.01;
constexpr auto kMockResponseListener = "response_listener";
constexpr auto kTapRetryInterval = zx::sec(1);
enum class TapLocation { kTopLeft };
// This component implements fuchsia.ui.test.input.TouchInputListener
// and the interface for a RealmBuilder LocalComponent. A LocalComponent
// is a component that is implemented here in the test, as opposed to elsewhere
// in the system. When it's inserted to the realm, it will act like a proper
// component. This is accomplished, in part, because the realm_builder
// library creates the necessary plumbing. It creates a manifest for the component
// and routes all capabilities to and from it.
class ResponseListenerServer : public fuchsia::ui::test::input::TouchInputListener,
public LocalComponent {
public:
explicit ResponseListenerServer(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {}
// |fuchsia::ui::test::input::TouchInputListener|
void ReportTouchInput(
fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest request) override {
FX_CHECK(respond_callback_) << "Expected callback to be set.";
respond_callback_(std::move(request));
}
// |LocalComponent::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> local_handles) override {
// When this component starts, add a binding to the test.touch.ResponseListener
// protocol to this component's outgoing directory.
FX_CHECK(local_handles->outgoing()->AddPublicService(
fidl::InterfaceRequestHandler<fuchsia::ui::test::input::TouchInputListener>(
[this](auto request) {
bindings_.AddBinding(this, std::move(request), dispatcher_);
})) == ZX_OK);
local_handles_.emplace_back(std::move(local_handles));
}
void SetRespondCallback(
fit::function<void(fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest)>
callback) {
respond_callback_ = std::move(callback);
}
private:
async_dispatcher_t* dispatcher_ = nullptr;
std::vector<std::unique_ptr<LocalComponentHandles>> local_handles_;
fidl::BindingSet<fuchsia::ui::test::input::TouchInputListener> bindings_;
fit::function<void(fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest)>
respond_callback_;
};
struct PointerInjectorConfigTestData {
int display_rotation;
float clip_scale = 1.f;
float clip_translation_x = 0.f;
float clip_translation_y = 0.f;
// expected location of the pointer event, in client view space, where the
// range of the X and Y axes is [0, 1]
float expected_x;
float expected_y;
};
using PointerInjectorConfigTestParams =
std::tuple<ui_testing::UITestRealm::SceneOwnerType, PointerInjectorConfigTestData>;
class PointerInjectorConfigTest
: public gtest::RealLoopFixture,
public testing::WithParamInterface<PointerInjectorConfigTestParams> {
protected:
PointerInjectorConfigTest() = default;
~PointerInjectorConfigTest() override {
FX_CHECK(injection_count_ > 0) << "injection expected but didn't happen.";
}
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);
auto [scene_owner, test_data] = GetParam();
ui_testing::UITestRealm::Config config;
config.display_rotation = test_data.display_rotation;
config.scene_owner = scene_owner;
config.use_input = true;
config.accessibility_owner = ui_testing::UITestRealm::AccessibilityOwnerType::FAKE;
config.ui_to_client_services = {fuchsia::ui::scenic::Scenic::Name_};
ui_test_manager_ = std::make_unique<ui_testing::UITestManager>(std::move(config));
// Assemble realm.
BuildRealm();
// Get the display dimensions.
FX_LOGS(INFO) << "Waiting for scenic display info";
auto [width, height] = ui_test_manager_->GetDisplayDimensions();
display_width_ = static_cast<uint32_t>(width);
display_height_ = static_cast<uint32_t>(height);
FX_LOGS(INFO) << "Got display_width = " << display_width_
<< " and display_height = " << display_height_;
// Register input injection device.
FX_LOGS(INFO) << "Registering input injection device";
RegisterInjectionDevice();
// Launch client view, and wait until it's rendering to proceed with the test.
ui_test_manager_->InitializeScene();
RunLoopUntil([this]() { return ui_test_manager_->ClientViewIsRendering(); });
realm_exposed_services_->Connect<test::accessibility::Magnifier>(
this->fake_magnifier_.NewRequest());
}
// Waits for one or more pointer events; calls QuitLoop once one meets expectations.
void WaitForAResponseMeetingExpectations(float expected_x, float expected_y,
const std::string& component_name) {
response_listener()->SetRespondCallback(
[this, expected_x, expected_y, component_name](
fuchsia::ui::test::input::TouchInputListenerReportTouchInputRequest request) {
FX_LOGS(INFO) << "Client received tap at (" << request.local_x() << ", "
<< request.local_y() << ").";
FX_LOGS(INFO) << "Expected tap is at approximately (" << expected_x << ", " << expected_y
<< ").";
// Allow for minor rounding differences in coordinates.
EXPECT_EQ(request.component_name(), component_name);
if (abs(request.local_x() - expected_x) <= kViewCoordinateEpsilon &&
abs(request.local_y() - expected_y) <= kViewCoordinateEpsilon) {
response_listener()->SetRespondCallback([](auto) {});
QuitLoop();
}
});
}
void RegisterInjectionDevice() {
FX_LOGS(INFO) << "Registering fake touch screen";
input_registry_ =
realm_exposed_services()->template Connect<fuchsia::ui::test::input::Registry>();
input_registry_.set_error_handler([](auto) { FX_LOGS(ERROR) << "Error from input helper"; });
bool touchscreen_registered = false;
fuchsia::ui::test::input::RegistryRegisterTouchScreenRequest request;
request.set_device(fake_touchscreen_.NewRequest());
input_registry_->RegisterTouchScreen(
std::move(request), [&touchscreen_registered]() { touchscreen_registered = true; });
RunLoopUntil([&touchscreen_registered] { return touchscreen_registered; });
FX_LOGS(INFO) << "Touchscreen registered";
}
void TapTopLeft() {
fuchsia::ui::test::input::TouchScreenSimulateTapRequest tap_request;
auto [scene_owner, test_data] = GetParam();
// Inject one input report, then a conclusion (empty) report.
switch (test_data.display_rotation) {
case 0:
tap_request.mutable_tap_location()->x = -500;
tap_request.mutable_tap_location()->y = -500;
break;
case 90:
// The /config/data/display_rotation (90) specifies how many degrees to rotate the
// presentation child view, counter-clockwise, in a right-handed coordinate system. Thus,
// the user observes the child view to rotate *clockwise* by that amount (90).
tap_request.mutable_tap_location()->x = 500;
tap_request.mutable_tap_location()->y = -500;
break;
default:
FX_NOTREACHED();
}
FX_LOGS(INFO) << "Injecting tap at (" << tap_request.tap_location().x << ", "
<< tap_request.tap_location().y << ")";
fake_touchscreen_->SimulateTap(std::move(tap_request), [this]() {
++injection_count_;
FX_LOGS(INFO) << "*** Tap injected, count: " << injection_count_;
});
}
// Try injecting a tap every `kTapRetryInterval` until the test completes.
void TryInjectRepeatedly(TapLocation tap_location) {
TapTopLeft();
async::PostDelayedTask(
dispatcher(), [this, tap_location] { TryInjectRepeatedly(tap_location); },
kTapRetryInterval);
}
void SetClipSpaceTransform(float scale, float x, float y) {
fake_magnifier_->SetMagnification(scale, x, y);
}
// Guaranteed to be initialized after SetUp().
float display_width() const { return static_cast<float>(display_width_); }
float display_height() const { return static_cast<float>(display_height_); }
sys::ServiceDirectory* realm_exposed_services() { return realm_exposed_services_.get(); }
Realm* realm() { return realm_.get(); }
ResponseListenerServer* response_listener() { return response_listener_.get(); }
private:
void BuildRealm() {
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.
response_listener_ = std::make_unique<ResponseListenerServer>(dispatcher());
realm()->AddLocalChild(kMockResponseListener, response_listener_.get());
realm()->AddChild(kCppGfxClient, kCppGfxClientUrl);
realm()->AddRoute({.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
.source = ChildRef{kCppGfxClient},
.targets = {ParentRef()}});
realm()->AddRoute(
{.capabilities = {Protocol{fuchsia::ui::test::input::TouchInputListener::Name_}},
.source = ChildRef{kMockResponseListener},
.targets = {ChildRef{kCppGfxClient}}});
realm()->AddRoute({.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kCppGfxClient}}});
ui_test_manager_->BuildRealm();
realm_exposed_services_ = ui_test_manager_->CloneExposedServicesDirectory();
}
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_;
fuchsia::ui::test::input::RegistryPtr input_registry_;
fuchsia::ui::test::input::TouchScreenPtr fake_touchscreen_;
int injection_count_ = 0;
uint32_t display_width_ = 0;
uint32_t display_height_ = 0;
test::accessibility::MagnifierSyncPtr fake_magnifier_;
static constexpr auto kCppGfxClient = "gfx_client";
static constexpr auto kCppGfxClientUrl = "#meta/touch-gfx-client.cm";
};
// Declare test data.
// In all these tests, we tap the center of the top left quadrant of the
// physical display (after rotation), and verify that the client view gets a
// pointer event with the expected coordinates.
// No changes to display rotation or clip space
constexpr PointerInjectorConfigTestData kTestDataBaseCase = {
.display_rotation = 0, .expected_x = 1.f / 4.f, .expected_y = 1.f / 4.f};
// Test scale by a factor of 2.
// Intuitive argument for these expected coordinates:
// Here we've zoomed into the center of the client view, scaling it up by 2x. So, the touch point
// will have 'migrated' halfway towards the center of the client view: 3/8 instead of 1/4.
constexpr PointerInjectorConfigTestData kTestDataScale = {
.display_rotation = 0, .clip_scale = 2.f, .expected_x = 3.f / 8.f, .expected_y = 3.f / 8.f};
// Test display rotation by 90 degrees.
// In this case, rotation shouldn't affect what the client view sees.
constexpr PointerInjectorConfigTestData kTestDataRotateAndScale = {
.display_rotation = 90, .clip_scale = 2.f, .expected_x = 3.f / 8.f, .expected_y = 3.f / 8.f};
// Test scaling and translation.
constexpr float kScale = 3.f;
constexpr float kTranslationX = -0.2f;
constexpr float kTranslationY = 0.1f;
constexpr PointerInjectorConfigTestData kTestDataScaleAndTranslate = {
.display_rotation = 0,
.clip_scale = kScale,
.clip_translation_x = kTranslationX,
.clip_translation_y = kTranslationY,
// Terms: 'Original position' + 'movement due to scale' + 'movement due to translation'
.expected_x = 0.25f + 0.25f * (1.f - 1.f / kScale) - kTranslationX / 2.f / kScale,
.expected_y = 0.25f + 0.25f * (1.f - 1.f / kScale) - kTranslationY / 2.f / kScale};
// Test scaling, translation, and rotation at once.
//
// Here, the translation does affect what the client view sees, so we have to account for it.
// This is what the translation looks like in client view coordinates, where it's rotated 90
// degrees.
constexpr float kClientViewTranslationX = kTranslationY;
constexpr float kClientViewTranslationY = -kTranslationX;
constexpr PointerInjectorConfigTestData kTestDataScaleTranslateRotate = {
.display_rotation = 90,
.clip_scale = kScale,
.clip_translation_x = kTranslationX,
.clip_translation_y = kTranslationY,
// Same formula as before, but with different transform values.
.expected_x = 0.25f + 0.25f * (1.f - 1.f / kScale) - kClientViewTranslationX / 2.f / kScale,
.expected_y = 0.25f + 0.25f * (1.f - 1.f / kScale) - kClientViewTranslationY / 2.f / kScale};
INSTANTIATE_TEST_SUITE_P(
PointerInjectorConfigTestWithParams, PointerInjectorConfigTest,
::testing::Combine(::testing::Values(ui_testing::UITestRealm::SceneOwnerType::ROOT_PRESENTER,
ui_testing::UITestRealm::SceneOwnerType::SCENE_MANAGER),
::testing::Values(kTestDataBaseCase, kTestDataScale, kTestDataRotateAndScale,
kTestDataScaleAndTranslate,
kTestDataScaleTranslateRotate)));
TEST_P(PointerInjectorConfigTest, CppGfxClientTapTest) {
auto [scene_owner, test_data] = GetParam();
FX_LOGS(INFO) << "Starting test with params: display_rotation=" << test_data.display_rotation
<< ", clip_scale=" << test_data.clip_scale
<< ", clip_translation_x=" << test_data.clip_translation_x
<< ", clip_translation_y=" << test_data.clip_translation_y
<< ", expected_x=" << test_data.expected_x
<< ", expected_y=" << test_data.expected_y;
SetClipSpaceTransform(test_data.clip_scale, test_data.clip_translation_x,
test_data.clip_translation_y);
TryInjectRepeatedly(TapLocation::kTopLeft);
switch (test_data.display_rotation) {
case 0:
WaitForAResponseMeetingExpectations(
/*expected_x=*/display_width() * test_data.expected_x,
/*expected_y=*/display_height() * test_data.expected_y,
/*component_name=*/"touch-gfx-client");
break;
case 90:
WaitForAResponseMeetingExpectations(
/*expected_x=*/display_height() * test_data.expected_x,
/*expected_y=*/display_width() * test_data.expected_y,
/*component_name=*/"touch-gfx-client");
break;
default:
FX_NOTREACHED();
}
RunLoop();
}
} // namespace