blob: 1645e282dd6847ce28903a24acd4ad54035492e6 [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 <fidl/fuchsia.element/cpp/fidl.h>
#include <fidl/fuchsia.input.injection/cpp/fidl.h>
#include <fidl/fuchsia.ui.composition/cpp/fidl.h>
#include <fidl/fuchsia.ui.test.input/cpp/fidl.h>
#include <fidl/test.accessibility/cpp/fidl.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/cpp/channel.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 <utility>
#include <gtest/gtest.h>
#include <src/ui/testing/util/fidl_cpp_helpers.h>
#include <src/ui/testing/util/portable_ui_test.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 Scene Manager and Scenic components.
// - It uses a fake display controller; the physical device is unused.
//
// Components involved
// - This test program
// - 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 Scene Manager.
// - 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 scene_manager
//
// 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 {
// Types imported for the realm_builder library.
using component_testing::ChildRef;
using component_testing::LocalComponentImpl;
using component_testing::ParentRef;
using component_testing::Protocol;
// 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";
class ResponseState {
public:
const std::vector<fuchsia_ui_test_input::TouchInputListenerReportTouchInputRequest>&
events_received() {
return events_received_;
}
bool ready_to_inject() const { return ready_to_inject_; }
private:
friend class ResponseListenerServer;
std::vector<fuchsia_ui_test_input::TouchInputListenerReportTouchInputRequest> events_received_;
bool ready_to_inject_ = false;
};
// 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 fidl::Server<fuchsia_ui_test_input::TouchInputListener>,
public fidl::Server<fuchsia_ui_test_input::TestAppStatusListener>,
public LocalComponentImpl {
public:
explicit ResponseListenerServer(async_dispatcher_t* dispatcher,
std::weak_ptr<ResponseState> state)
: dispatcher_(dispatcher), state_(std::move(state)) {}
// |fuchsia_ui_test_input::TouchInputListener|
void ReportTouchInput(ReportTouchInputRequest& request,
ReportTouchInputCompleter::Sync& completer) override {
FX_LOGS(INFO) << "ReportTouchInput";
if (auto s = state_.lock()) {
s->events_received_.push_back(std::move(request));
}
}
// |fuchsia_ui_test_input::TestAppStatusListener|
void ReportStatus(ReportStatusRequest& req, ReportStatusCompleter::Sync& completer) override {
if (req.status() == fuchsia_ui_test_input::TestAppStatus::kHandlersRegistered) {
if (auto s = state_.lock()) {
s->ready_to_inject_ = true;
}
}
completer.Reply();
}
// |fuchsia_ui_test_input::TestAppStatusListener|
void handle_unknown_method(
fidl::UnknownMethodMetadata<fuchsia_ui_test_input::TestAppStatusListener> metadata,
fidl::UnknownMethodCompleter::Sync& completer) override {
FX_LOGS(WARNING) << "TestAppStatusListener Received an unknown method with ordinal "
<< metadata.method_ordinal;
}
// |LocalComponentImpl::Start|
// When the component framework requests for this component to start, this
// method will be invoked by the realm_builder library.
void OnStart() override {
// When this component starts, add a binding to the test.touch.ResponseListener
// protocol to this component's outgoing directory.
outgoing()->AddProtocol<fuchsia_ui_test_input::TouchInputListener>(
touch_input_listener_bindings_.CreateHandler(this, dispatcher_,
fidl::kIgnoreBindingClosure));
outgoing()->AddProtocol<fuchsia_ui_test_input::TestAppStatusListener>(
app_status_listener_bindings_.CreateHandler(this, dispatcher_,
fidl::kIgnoreBindingClosure));
}
private:
async_dispatcher_t* dispatcher_ = nullptr;
fidl::ServerBindingGroup<fuchsia_ui_test_input::TouchInputListener>
touch_input_listener_bindings_;
fidl::ServerBindingGroup<fuchsia_ui_test_input::TestAppStatusListener>
app_status_listener_bindings_;
std::weak_ptr<ResponseState> state_;
};
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;
};
void ExpectLocationAndPhase(
const fuchsia_ui_test_input::TouchInputListenerReportTouchInputRequest& e, double expected_x,
double expected_y, fuchsia_ui_pointer::EventPhase expected_phase) {
auto pixel_scale = e.device_pixel_ratio().has_value() ? e.device_pixel_ratio().value() : 1;
auto actual_x = pixel_scale * e.local_x().value();
auto actual_y = pixel_scale * e.local_y().value();
EXPECT_NEAR(expected_x, actual_x, kViewCoordinateEpsilon);
EXPECT_NEAR(expected_y, actual_y, kViewCoordinateEpsilon);
EXPECT_EQ(expected_phase, e.phase());
}
class PointerInjectorConfigTest
: public ui_testing::PortableUITest,
public testing::WithParamInterface<PointerInjectorConfigTestData> {
protected:
PointerInjectorConfigTest() = default;
~PointerInjectorConfigTest() override {
FX_CHECK(touch_injection_request_count() > 0) << "injection expected but didn't happen.";
}
std::string GetTestUIStackUrl() override { return "#meta/test-ui-stack.cm"; }
uint32_t display_rotation() override { return GetParam().display_rotation; }
void SetUp() override {
ui_testing::PortableUITest::SetUp();
// 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);
// Get the display dimensions.
FX_LOGS(INFO) << "Waiting for scenic display info";
FX_LOGS(INFO) << "Got display_width = " << display_width()
<< " and display_height = " << display_height();
// Register input injection device.
FX_LOGS(INFO) << "Registering input injection device";
RegisterTouchScreen();
// Wait until eager client view is rendering to proceed with the test.
WaitForViewPresentation();
auto magnifier_connect = realm_root()->component().Connect<test_accessibility::Magnifier>();
ZX_ASSERT_OK(magnifier_connect);
fake_magnifier_ = fidl::SyncClient(std::move(magnifier_connect.value()));
FX_LOGS(INFO) << "Wait for test app status: kHandlersRegistered";
RunLoopUntil([&]() { return response_state()->ready_to_inject(); });
FX_LOGS(INFO) << "test app status: kHandlersRegistered";
}
bool LastEventReceivedMatchesPhase(fuchsia_ui_pointer::EventPhase phase,
const std::string& component_name) {
const auto& events_received = response_state_->events_received();
if (events_received.empty()) {
return false;
}
const auto& last_event = events_received.back();
const auto actual_phase = last_event.phase().value();
auto actual_component_name = last_event.component_name().value();
FX_LOGS(INFO) << "Expecting event for component " << component_name << " at phase ("
<< static_cast<uint32_t>(phase) << ")";
FX_LOGS(INFO) << "Received event for component " << actual_component_name << " at phase ("
<< static_cast<uint32_t>(actual_phase) << ")";
return phase == actual_phase && actual_component_name == component_name;
}
void InjectTapOnTopLeft() {
int32_t x = 0;
int32_t y = 0;
auto test_data = GetParam();
switch (test_data.display_rotation) {
case 0:
x = display_width() / 4;
y = display_height() / 4;
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).
x = 3 * display_width() / 4;
y = display_height() / 4;
break;
default:
FX_NOTREACHED();
}
InjectTap(x, y);
}
void SetClipSpaceTransform(float scale, float x, float y, int display_rotation) {
// HACK HACK HACK
// TODO(https://fxbug.dev/42081619): Remove this when we move to the new gesture
// disambiguation protocols.
//
// Because the FlatlandAcessibilityView::SetMagnificationTransform
// hardcodes translation values at a display rotation of 270, this test
// must normalize the values given the non-270 display rotations
// config values.
switch (display_rotation) {
case 0:
// Since a display rotation of 270 uses (x, y) as (y, -x), pass
// in (y, -x), which will yield (x, y) to get an effective display
// rotation of 0.
ZX_ASSERT_OK(fake_magnifier_->SetMagnification({scale, y, -x}));
break;
case 90:
// Since a display rotation of 270 uses (x, y) as (y, -x), pass
// in (-x, -y), which will yield (-y, x) to get an effective display
// rotation of 90.
ZX_ASSERT_OK(fake_magnifier_->SetMagnification({scale, -x, -y}));
break;
default:
FX_NOTREACHED();
}
}
float display_width_float() { return static_cast<float>(display_width()); }
float display_height_float() { return static_cast<float>(display_height()); }
std::shared_ptr<ResponseState> response_state() { return response_state_; }
static constexpr auto kCppFlatlandClient = "touch-flatland-client";
private:
std::vector<std::pair<ChildName, std::string>> GetEagerTestComponents() override {
return {std::make_pair(kCppFlatlandClient, kCppFlatlandClientUrl)};
}
void ExtendRealm() override {
// Key part of service setup: have this test component vend the
// |ResponseListener| service in the constructed realm.
realm_builder().AddLocalChild(kMockResponseListener,
[d = dispatcher(), s = response_state()]() {
return std::make_unique<ResponseListenerServer>(d, s);
});
realm_builder().AddRoute(
{.capabilities =
{
Protocol{
fidl::DiscoverableProtocolName<fuchsia_ui_test_input::TouchInputListener>},
Protocol{
fidl::DiscoverableProtocolName<fuchsia_ui_test_input::TestAppStatusListener>},
},
.source = ChildRef{kMockResponseListener},
.targets = {ChildRef{kCppFlatlandClient}}});
realm_builder().AddRoute(
{.capabilities =
{Protocol{fidl::DiscoverableProtocolName<fuchsia_element::GraphicalPresenter>},
Protocol{fidl::DiscoverableProtocolName<fuchsia_ui_composition::Allocator>},
Protocol{fidl::DiscoverableProtocolName<fuchsia_ui_composition::Flatland>}},
.source = ui_testing::PortableUITest::kTestUIStackRef,
.targets = {ChildRef{kCppFlatlandClient}}});
}
std::shared_ptr<ResponseState> response_state_ = std::make_shared<ResponseState>();
fidl::SyncClient<test_accessibility::Magnifier> fake_magnifier_;
static constexpr auto kCppFlatlandClientUrl = "#meta/touch-flatland-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::Values(kTestDataBaseCase, kTestDataScale,
kTestDataRotateAndScale, kTestDataScaleAndTranslate,
kTestDataScaleTranslateRotate));
TEST_P(PointerInjectorConfigTest, CppClientTapTest) {
auto 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, test_data.display_rotation);
InjectTapOnTopLeft();
RunLoopUntil([this] {
return LastEventReceivedMatchesPhase(fuchsia_ui_pointer::EventPhase::kRemove,
kCppFlatlandClient);
});
const auto& events_received = this->response_state()->events_received();
ASSERT_EQ(events_received.size(), 2u);
float expect_x = display_width_float() * test_data.expected_x;
float expect_y = display_height_float() * test_data.expected_y;
if (test_data.display_rotation == 90) {
expect_x = display_height_float() * test_data.expected_x;
expect_y = display_width_float() * test_data.expected_y;
}
ExpectLocationAndPhase(events_received[0], expect_x, expect_y,
fuchsia_ui_pointer::EventPhase::kAdd);
ExpectLocationAndPhase(events_received[1], expect_x, expect_y,
fuchsia_ui_pointer::EventPhase::kRemove);
}
} // namespace