blob: 7f46027cb519ab1d677b1933ccb3ca512ca1fcc2 [file] [log] [blame]
// Copyright 2020 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/intl/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/tracing/provider/cpp/fidl.h>
#include <fuchsia/ui/app/cpp/fidl.h>
#include <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/observation/test/cpp/fidl.h>
#include <fuchsia/ui/pointerinjector/cpp/fidl.h>
#include <fuchsia/ui/policy/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/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/ui/scenic/cpp/session.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include <lib/ui/scenic/cpp/view_token_pair.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 <src/lib/fostr/fidl/fuchsia/ui/gfx/formatting.h>
#include <test/touch/cpp/fidl.h>
#include "fuchsia/sysmem/cpp/fidl.h"
#include "src/lib/testing/loop_fixture/real_loop_fixture.h"
// This test exercises the touch input dispatch path from Root Presenter to a Scenic client. It is a
// multi-component test, and carefully avoids sleeping or polling for component coordination.
// - It runs real Root Presenter and Scenic components.
// - It uses a fake display controller; the physical device is unused.
//
// Components involved
// - This test program
// - Root Presenter
// - Scenic
// - Child view, a Scenic client
//
// Touch dispatch path
// - Test program's injection -> Root Presenter -> Scenic -> Child view
//
// Setup sequence
// - The test sets up this view hierarchy:
// - Top level scene, owned by Root Presenter.
// - Bottom view, owned by the child view.
// - The test waits for geometry observer to return a view tree with one more view in it.
// - The test injects input into Root Presenter, emulating a display's touch report.
// - Root Presenter 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
// |
// touch-input-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
// |
// touch-input-test.cml (this component)
// |
// <created realm root>
// / \
// scenic root_presenter
//
// For more information about testing v2 components and realm_builder,
// visit the following links:
//
// Testing: https://fuchsia.dev/fuchsia-src/development/testing/components
// Realm Builder: https://fuchsia.dev/fuchsia-src/development/testing/components/realm_builder
namespace {
using test::touch::ResponseListener;
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::RealmRoot;
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);
constexpr auto kRootPresenter = "root_presenter";
constexpr auto kScenicTestRealm = "scenic-test-realm";
constexpr auto kMockResponseListener = "response_listener";
enum class TapLocation { kTopLeft, kTopRight };
// The type used to measure UTC time. The integer value here does not matter so
// long as it differs from the ZX_CLOCK_MONOTONIC=0 defined by Zircon.
using time_utc = zx::basic_time<1>;
// Components used by all tests. These will be installed as direct children of
// the root component of the realm. In v2, every protocol must be *explicitly*
// routed from one source to a target. In this case, these base components
// provide capabilities to be used either by the client components, e.g. OneFlutter,
// created below, or by this component. Note, that when I refer to "this component",
// I'm referring to the test suite, which is itself a component.
void AddBaseComponents(RealmBuilder* realm_builder) {
realm_builder->AddChild(kRootPresenter, "#meta/root_presenter.cm");
realm_builder->AddChild(kScenicTestRealm, "#meta/scenic_only.cm");
}
void AddBaseRoutes(RealmBuilder* realm_builder) {
// Capabilities routed from test_manager to components in realm.
realm_builder->AddRoute(
Route{.capabilities = {Protocol{fuchsia::logger::LogSink::Name_},
Protocol{fuchsia::vulkan::loader::Loader::Name_},
Protocol{fuchsia::scheduler::ProfileProvider::Name_},
Protocol{fuchsia::sysmem::Allocator::Name_},
Protocol{fuchsia::tracing::provider::Registry::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kScenicTestRealm}}});
realm_builder->AddRoute(
Route{.capabilities = {Protocol{fuchsia::logger::LogSink::Name_},
Protocol{fuchsia::tracing::provider::Registry::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kRootPresenter}}});
// Capabilities routed between siblings in realm.
realm_builder->AddRoute(
Route{.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_},
Protocol{fuchsia::ui::pointerinjector::Registry::Name_}},
.source = ChildRef{kScenicTestRealm},
.targets = {ChildRef{kRootPresenter}}});
// Capabilities routed up to test driver (this component).
realm_builder->AddRoute(
Route{.capabilities = {Protocol{fuchsia::ui::input::InputDeviceRegistry::Name_},
Protocol{fuchsia::ui::policy::Presenter::Name_}},
.source = ChildRef{kRootPresenter},
.targets = {ParentRef()}});
realm_builder->AddRoute(Route{.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_}},
.source = ChildRef{kScenicTestRealm},
.targets = {ParentRef()}});
realm_builder->AddRoute(
Route{.capabilities = {Protocol{fuchsia::ui::observation::test::Registry::Name_}},
.source = ChildRef{kScenicTestRealm},
.targets = {ParentRef()}});
// Set display rotation.
auto config_directory_contents = component_testing::DirectoryContents();
config_directory_contents.AddFile("display_rotation", "90");
realm_builder->RouteReadOnlyDirectory("config-data", {ChildRef{kRootPresenter}},
std::move(config_directory_contents));
}
// 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;
}
bool CheckViewExistsInSnapshot(const fuchsia::ui::observation::geometry::ViewTreeSnapshot& snapshot,
zx_koid_t view_ref_koid) {
if (!snapshot.has_views()) {
return false;
}
auto snapshot_count = std::count_if(
snapshot.views().begin(), snapshot.views().end(),
[view_ref_koid](const auto& view) { return view.view_ref_koid() == view_ref_koid; });
return snapshot_count > 0;
}
bool CheckViewExistsInUpdates(
const std::vector<fuchsia::ui::observation::geometry::ViewTreeSnapshot>& updates,
zx_koid_t view_ref_koid) {
auto update_count =
std::count_if(updates.begin(), updates.end(), [view_ref_koid](auto& snapshot) {
return CheckViewExistsInSnapshot(snapshot, view_ref_koid);
});
return update_count > 0;
}
zx_koid_t ExtractKoid(const zx::object_base& object) {
zx_info_handle_basic_t info{};
if (object.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr) != ZX_OK) {
return ZX_KOID_INVALID; // no info
}
return info.koid;
}
zx_koid_t ExtractKoid(const fuchsia::ui::views::ViewRef& view_ref) {
return ExtractKoid(view_ref.reference);
}
// Returns the iterator to the first fuchsia::ui::observation::geometry::ViewTreeSnapshot in
// |updates| having |view_ref_koid| present.
std::vector<fuchsia::ui::observation::geometry::ViewTreeSnapshot>::const_iterator
GetFirstSnapshotWithView(
const std::vector<fuchsia::ui::observation::geometry::ViewTreeSnapshot>& updates,
zx_koid_t view_ref_koid) {
return std::find_if(updates.begin(), updates.end(), [view_ref_koid](auto& snapshot) {
return CheckViewExistsInSnapshot(snapshot, view_ref_koid);
});
}
// This component implements the test.touch.ResponseListener protocol
// and the interface for a RealmBuilder MockComponent. A MockComponent
// 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 ResponseListener, public LocalComponent {
public:
explicit ResponseListenerServer(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {}
// |test::touch::ResponseListener|
void Respond(test::touch::PointerData pointer_data) override {
FX_CHECK(respond_callback_) << "Expected callback to be set for test.touch.Respond().";
respond_callback_(std::move(pointer_data));
}
// |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.touch.ResponseListener
// protocol to this component's outgoing directory.
FX_CHECK(mock_handles->outgoing()->AddPublicService(
fidl::InterfaceRequestHandler<test::touch::ResponseListener>([this](auto request) {
bindings_.AddBinding(this, std::move(request), dispatcher_);
})) == ZX_OK);
mock_handles_.emplace_back(std::move(mock_handles));
}
void SetRespondCallback(fit::function<void(test::touch::PointerData)> callback) {
respond_callback_ = std::move(callback);
}
private:
async_dispatcher_t* dispatcher_ = nullptr;
std::vector<std::unique_ptr<LocalComponentHandles>> mock_handles_;
fidl::BindingSet<test::touch::ResponseListener> bindings_;
fit::function<void(test::touch::PointerData)> respond_callback_;
};
class TouchInputBase : public gtest::RealLoopFixture {
protected:
TouchInputBase()
: realm_builder_(std::make_unique<RealmBuilder>(RealmBuilder::Create())), realm_() {}
~TouchInputBase() 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);
BuildRealm(this->GetTestComponents(), this->GetTestRoutes(), this->GetTestV2Components());
scenic_ = realm()->Connect<fuchsia::ui::scenic::Scenic>();
observer_registry_ptr_ = realm()->Connect<fuchsia::ui::observation::test::Registry>();
observer_registry_ptr_.set_error_handler([](zx_status_t status) {
FAIL() << "Lost connection to Observer Registry Protocol: " << zx_status_get_string(status);
});
}
// 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 capability routes to the test
// realm next to the base ones added.
virtual std::vector<Route> GetTestRoutes() { return {}; }
// Subclass should implement this method to add components to the test realm
// next to the base ones added.
virtual std::vector<std::pair<ChildName, std::string>> GetTestV2Components() { return {}; }
// Launches the test client by connecting to fuchsia.ui.app.ViewProvider protocol.
// This method should only be invoked if this protocol has been exposed from
// the root of the test realm. After establishing a connection, this method uses
// |fuchsia.ui.observation.geometry.Provider| to get notified when the client view gets attached
// to the view tree.
void LaunchClient(std::string debug_name) {
// Use |fuchsia.ui.observation.test.Registry| to register the view observer endpoint with
// scenic.
fuchsia::ui::observation::geometry::ProviderPtr geometry_provider;
std::optional<bool> geometry_available;
observer_registry_ptr_->RegisterGlobalGeometryProvider(
geometry_provider.NewRequest(), [&geometry_available] { geometry_available = true; });
RunLoopUntil([&geometry_available] { return geometry_available.has_value(); });
auto tokens = scenic::ViewTokenPair::New(); // Root Presenter -> Client
// Instruct Root Presenter to present test's View.
auto root_presenter = realm()->Connect<fuchsia::ui::policy::Presenter>();
root_presenter->PresentOrReplaceView(std::move(tokens.view_holder_token),
/* presentation */ nullptr);
// Start client app inside the test environment.
// Note well. There is a significant difference in how ViewProvider is
// vended and used, between CF v1 and CF v2. This test follows the CF v2
// style: the realm specifies a component C that can serve ViewProvider, and
// when the test runner asks for that protocol, C is launched by Component
// Manager. In contrast, production uses CF v1 style, where a parent
// component P launches a child component C directly, and P connects to C's
// ViewProvider directly. However, this difference does not impact the
// testing logic.
auto [view_ref_control, view_ref] = scenic::ViewRefPair::New();
const auto view_ref_koid = ExtractKoid(view_ref);
auto view_provider = realm()->Connect<fuchsia::ui::app::ViewProvider>();
view_provider->CreateViewWithViewRef(std::move(tokens.view_token.value),
std::move(view_ref_control), std::move(view_ref));
// Wait for the client view to get attached to the view tree.
std::optional<fuchsia::ui::observation::geometry::ProviderWatchResponse> watch_response;
RunLoopUntil([this, &geometry_provider, &watch_response, &view_ref_koid] {
return HasViewConnected(geometry_provider, watch_response, view_ref_koid);
});
// Get the display height and width from the view's extent in context as the bounding box of the
// view in root view's coordinate system will be the same as the display size.
auto snapshot_iter = GetFirstSnapshotWithView(watch_response->updates(), view_ref_koid);
auto view_descriptor = std::find_if(
snapshot_iter->views().begin(), snapshot_iter->views().end(),
[&view_ref_koid](const auto& view) { return view.view_ref_koid() == view_ref_koid; });
// As the view is rotated by 90 degrees, the width of the bounding box is actually the height of
// the physical display and vici versa.
display_height_ = static_cast<uint32_t>(view_descriptor->extent_in_context().width);
display_width_ = static_cast<uint32_t>(view_descriptor->extent_in_context().height);
}
// Helper method for checking the test.touch.ResponseListener response from the client app.
void SetResponseExpectations(float expected_x, float expected_y,
zx::basic_time<ZX_CLOCK_MONOTONIC>& input_injection_time,
std::string component_name, bool& injection_complete) {
response_listener()->SetRespondCallback([expected_x, expected_y, component_name,
&input_injection_time, &injection_complete](
test::touch::PointerData pointer_data) {
EXPECT_EQ(pointer_data.component_name(), component_name);
FX_LOGS(INFO) << "Client received tap at (" << pointer_data.local_x() << ", "
<< pointer_data.local_y() << ").";
FX_LOGS(INFO) << "Expected tap is at approximately (" << expected_x << ", " << expected_y
<< ").";
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.
EXPECT_NEAR(pointer_data.local_x(), expected_x, 1);
EXPECT_NEAR(pointer_data.local_y(), expected_y, 1);
injection_complete = true;
});
}
// Calls test.touch.TestAppLauncher::Launch.
// Only works if we've already launched a client that serves test.touch.TestAppLauncher.
void LaunchEmbeddedClient(std::string debug_name) {
// Set up an empty session, only used for synchronization in this method.
auto session_pair = scenic::CreateScenicSessionPtrAndListenerRequest(scenic_.get());
session_ = std::make_unique<scenic::Session>(std::move(session_pair.first),
std::move(session_pair.second));
// Launch the embedded app.
auto test_app_launcher = realm()->Connect<test::touch::TestAppLauncher>();
bool child_launched = false;
test_app_launcher->Launch(debug_name, [&child_launched] { child_launched = true; });
RunLoopUntil([&child_launched] { return child_launched; });
// TODO(fxb/101748) : Use fuchsia.ui.observation.geometry.Provider for synchronization.
// Waits an extra frame to avoid any flakes from the child launching signal firing slightly
// early.
bool frame_presented = false;
session_->set_on_frame_presented_handler([&frame_presented](auto) { frame_presented = true; });
session_->Present2(/*when*/ zx::clock::get_monotonic().get(), /*span*/ 0, [](auto) {});
RunLoopUntil([&frame_presented] { return frame_presented; });
session_->set_on_frame_presented_handler([](auto) {});
}
// Inject directly into Root Presenter, using fuchsia.ui.input FIDLs.
// Returns the timestamp on the first injected InputReport.
template <typename TimeT>
TimeT InjectInput(TapLocation tap_location) {
using fuchsia::ui::input::InputReport;
// Device parameters
auto parameters = std::make_unique<fuchsia::ui::input::TouchscreenDescriptor>();
*parameters = {.x = {.range = {.min = -1000, .max = 1000}},
.y = {.range = {.min = -1000, .max = 1000}},
.max_finger_id = 10};
// Register it against Root Presenter.
fuchsia::ui::input::DeviceDescriptor device{.touchscreen = std::move(parameters)};
auto registry = realm()->Connect<fuchsia::ui::input::InputDeviceRegistry>();
fuchsia::ui::input::InputDevicePtr connection;
registry->RegisterDevice(std::move(device), connection.NewRequest());
FX_LOGS(INFO) << "Registered touchscreen with x touch range = (-1000, 1000) "
<< "and y touch range = (-1000, 1000).";
TimeT injection_time;
{
// Inject one input report, then a conclusion (empty) report.
//
// 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).
//
// Hence, a tap in the center of the display's top-right quadrant is observed by the child
// view as a tap in the center of its top-left quadrant.
auto touch = std::make_unique<fuchsia::ui::input::TouchscreenReport>();
switch (tap_location) {
case TapLocation::kTopLeft:
// center of top right quadrant -> ends up as center of top left quadrant
*touch = {.touches = {{.finger_id = 1, .x = 500, .y = -500}}};
break;
case TapLocation::kTopRight:
// center of bottom right quadrant -> ends up as center of top right quadrant
*touch = {.touches = {{.finger_id = 1, .x = 500, .y = 500}}};
break;
default:
FX_NOTREACHED();
}
// Use system clock, instead of dispatcher clock, for measurement purposes.
injection_time = RealNow<TimeT>();
InputReport report{.event_time = TimeToUint(injection_time), .touchscreen = std::move(touch)};
connection->DispatchReport(std::move(report));
FX_LOGS(INFO) << "Dispatching touch report at (500, -500)";
}
{
auto touch = std::make_unique<fuchsia::ui::input::TouchscreenReport>();
InputReport report{.event_time = TimeToUint(RealNow<TimeT>()),
.touchscreen = std::move(touch)};
connection->DispatchReport(std::move(report));
}
++injection_count_;
FX_LOGS(INFO) << "*** Tap injected, count: " << injection_count_;
return injection_time;
}
// Checks whether the view with |view_ref_koid| has connected to the view tree. The response of a
// f.u.o.g.Provider.Watch call is stored in |watch_response| if it contains |view_ref_koid|.
bool HasViewConnected(
const fuchsia::ui::observation::geometry::ProviderPtr& geometry_provider,
std::optional<fuchsia::ui::observation::geometry::ProviderWatchResponse>& watch_response,
zx_koid_t view_ref_koid) {
std::optional<fuchsia::ui::observation::geometry::ProviderWatchResponse> geometry_result;
geometry_provider->Watch(
[&geometry_result](auto response) { geometry_result = std::move(response); });
RunLoopUntil([&geometry_result] { return geometry_result.has_value(); });
if (CheckViewExistsInUpdates(geometry_result->updates(), view_ref_koid)) {
watch_response = std::move(geometry_result);
};
return watch_response.has_value();
}
// Guaranteed to be initialized after SetUp().
uint32_t display_width() const { return display_width_; }
uint32_t display_height() const { return display_height_; }
fuchsia::sys::ComponentControllerPtr& client_component() { return client_component_; }
sys::ServiceDirectory& child_services() { return *child_services_; }
RealmBuilder* builder() { return realm_builder_.get(); }
RealmRoot* realm() { return realm_.get(); }
ResponseListenerServer* response_listener() { return response_listener_.get(); }
private:
void BuildRealm(const std::vector<std::pair<ChildName, LegacyUrl>>& components,
const std::vector<Route>& routes,
const std::vector<std::pair<ChildName, std::string>>& v2_components) {
// Key part of service setup: have this test component vend the
// |ResponseListener| service in the constructed realm.
response_listener_ = std::make_unique<ResponseListenerServer>(dispatcher());
builder()->AddLocalChild(kMockResponseListener, response_listener_.get());
// Add all components shared by each test to the realm.
AddBaseComponents(builder());
// Add components specific for this test case to the realm.
for (const auto& [name, component] : components) {
builder()->AddLegacyChild(name, component);
}
for (const auto& [name, component] : v2_components) {
builder()->AddChild(name, component);
}
// Add the necessary routing for each of the base components added above.
AddBaseRoutes(builder());
// Add the necessary routing for each of the extra components added above.
for (const auto& route : routes) {
builder()->AddRoute(route);
}
// Finally, build the realm using the provided components and routes.
realm_ = std::make_unique<RealmRoot>(builder()->Build());
}
template <typename TimeT>
TimeT RealNow();
template <>
zx::time RealNow() {
return zx::clock::get_monotonic();
}
template <>
time_utc RealNow() {
zx::unowned_clock utc_clock(zx_utc_reference_get());
zx_time_t now;
FX_CHECK(utc_clock->read(&now) == ZX_OK);
return time_utc(now);
}
template <typename TimeT>
uint64_t TimeToUint(const TimeT& time) {
FX_CHECK(time.get() >= 0);
return static_cast<uint64_t>(time.get());
}
std::unique_ptr<RealmBuilder> realm_builder_;
std::unique_ptr<RealmRoot> realm_;
std::unique_ptr<scenic::Session> session_;
fuchsia::ui::scenic::ScenicPtr scenic_;
std::unique_ptr<ResponseListenerServer> response_listener_;
int injection_count_ = 0;
fuchsia::ui::observation::test::RegistryPtr observer_registry_ptr_;
uint32_t display_width_ = 0;
uint32_t display_height_ = 0;
fuchsia::sys::ComponentControllerPtr client_component_;
std::shared_ptr<sys::ServiceDirectory> child_services_;
};
class FlutterInputTest : public TouchInputBase {
protected:
std::vector<std::pair<ChildName, LegacyUrl>> GetTestV2Components() override {
return {
std::make_pair(kFlutterRealm, kFlutterRealmUrl),
std::make_pair(kMemoryPressureProvider, kMemoryPressureProviderUrl),
std::make_pair(kNetstack, kNetstackUrl),
};
}
std::vector<Route> GetTestRoutes() override {
return merge({GetFlutterRoutes(ChildRef{kFlutterRealm}),
{
{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
.source = ChildRef{kFlutterRealm},
.targets = {ParentRef()}},
}});
}
// Routes needed to setup Flutter client.
static std::vector<Route> GetFlutterRoutes(ChildRef target) {
return {{.capabilities = {Protocol{test::touch::ResponseListener::Name_}},
.source = ChildRef{kMockResponseListener},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_}},
.source = ChildRef{kScenicTestRealm},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::logger::LogSink::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 kFlutterRealm = "flutter_realm";
static constexpr auto kFlutterRealmUrl = "#meta/one-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, FlutterTap) {
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
LaunchClient("FlutterTap");
bool injection_complete = false;
SetResponseExpectations(/*expected_x=*/static_cast<float>(display_height()) / 4.f,
/*expected_y=*/static_cast<float>(display_width()) / 4.f,
input_injection_time,
/*component_name=*/"one-flutter", injection_complete);
input_injection_time = InjectInput<zx::basic_time<ZX_CLOCK_MONOTONIC>>(TapLocation::kTopLeft);
RunLoopUntil([&injection_complete] { return injection_complete; });
}
class GfxInputTest : public TouchInputBase {
protected:
std::vector<std::pair<ChildName, LegacyUrl>> GetTestV2Components() override {
return {std::make_pair(kCppGfxClient, kCppGfxClientUrl)};
}
std::vector<Route> GetTestRoutes() override {
return {
{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
.source = ChildRef{kCppGfxClient},
.targets = {ParentRef()}},
{.capabilities = {Protocol{test::touch::ResponseListener::Name_}},
.source = ChildRef{kMockResponseListener},
.targets = {ChildRef{kCppGfxClient}}},
{.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_}},
.source = ChildRef{kScenicTestRealm},
.targets = {ChildRef{kCppGfxClient}}},
};
}
private:
static constexpr auto kCppGfxClient = "gfx_client";
static constexpr auto kCppGfxClientUrl = "#meta/touch-gfx-client.cm";
};
TEST_F(GfxInputTest, CppGfxClientTap) {
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
LaunchClient("CppGfxClientTap");
bool injection_complete = false;
SetResponseExpectations(/*expected_x=*/static_cast<float>(display_height()) / 4.f,
/*expected_y=*/static_cast<float>(display_width()) / 4.f,
input_injection_time,
/*component_name=*/"touch-gfx-client", injection_complete);
input_injection_time = InjectInput<zx::basic_time<ZX_CLOCK_MONOTONIC>>(TapLocation::kTopLeft);
RunLoopUntil([&injection_complete] { return injection_complete; });
}
class WebEngineTest : public TouchInputBase {
protected:
std::vector<std::pair<ChildName, LegacyUrl>> GetTestComponents() override {
return {
std::make_pair(kWebContextProvider, kWebContextProviderUrl),
};
}
std::vector<std::pair<ChildName, LegacyUrl>> GetTestV2Components() override {
return {
std::make_pair(kBuildInfoProvider, kBuildInfoProviderUrl),
std::make_pair(kFontsProvider, kFontsProviderUrl),
std::make_pair(kIntl, kIntlUrl),
std::make_pair(kMemoryPressureProvider, kMemoryPressureProviderUrl),
std::make_pair(kNetstack, kNetstackUrl),
std::make_pair(kSemanticsManager, kSemanticsManagerUrl),
std::make_pair(kTextManager, kTextManagerUrl),
std::make_pair(kOneChromiumClient, kOneChromiumUrl),
};
}
std::vector<Route> GetTestRoutes() override {
return merge({GetWebEngineRoutes(ChildRef{kOneChromiumClient}),
{
{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
.source = ChildRef{kOneChromiumClient},
.targets = {ParentRef()}},
}});
}
// Injects an input event, and posts a task to retry after `kTapRetryInterval`.
//
// We post the retry task because the first input event we send to WebEngine may be lost.
// The reason the first event may be lost is that there is a race condition as the WebEngine
// starts up.
//
// More specifically: in order for our web app's JavaScript code (see kAppCode in
// one-chromium.cc)
// to receive the injected input, two things must be true before we inject the input:
// * The WebEngine must have installed its `render_node_`, and
// * The WebEngine must have set the shape of its `input_node_`
//
// The problem we have is that the fuchsia.ui.observation.Provider gives a guarantee that
// the `render_node_` is ready. If the `input_node_` is not ready at that time, Scenic will
// find that no node was hit by the touch, and drop the touch event.
//
// As for why f.u.o.g.Provider notifies that the view is connected before there's any hittable
// element, that falls out of the way WebEngine constructs its scene graph. Namely, the
// `render_node_` has a shape, so that node renders content as soon as it is `Present()`-ed
// causing it to be present in f.u.o.g.Provider's response.
//
// For more detals, see fxbug.dev/57268.
//
// TODO(fxbug.dev/58322): Improve synchronization when we move to Flatland.
void TryInject(time_utc* input_injection_time) {
*input_injection_time = InjectInput<time_utc>(TapLocation::kTopLeft);
async::PostDelayedTask(
dispatcher(), [this, input_injection_time] { TryInject(input_injection_time); },
kTapRetryInterval);
}
// Helper method for checking the test.touch.ResponseListener response from a web app.
void SetResponseExpectationsWeb(float expected_x, float expected_y,
time_utc& input_injection_time, std::string component_name,
bool& injection_complete) {
response_listener()->SetRespondCallback(
[expected_x, expected_y, component_name, &injection_complete,
&input_injection_time](test::touch::PointerData pointer_data) {
EXPECT_EQ(pointer_data.component_name(), component_name);
// Convert Chromium's position, which is in logical pixels, to a position in physical
// pixels. Note that Chromium reports integer values, so this conversion introduces an
// error of up to `device_pixel_ratio`.
auto device_pixel_ratio = pointer_data.device_pixel_ratio();
auto chromium_x = pointer_data.local_x();
auto chromium_y = pointer_data.local_y();
auto device_x = chromium_x * device_pixel_ratio;
auto device_y = chromium_y * device_pixel_ratio;
FX_LOGS(INFO) << "Chromium reported tap at (" << chromium_x << ", " << chromium_y << ").";
FX_LOGS(INFO) << "Tap scaled to (" << device_x << ", " << device_y << ").";
FX_LOGS(INFO) << "Expected tap is at approximately (" << expected_x << ", " << expected_y
<< ").";
zx::duration elapsed_time = time_utc(pointer_data.time_received()) - input_injection_time;
EXPECT_NE(elapsed_time.get(), ZX_TIME_INFINITE);
FX_LOGS(INFO) << "Input Injection Time (ns): " << input_injection_time.get();
FX_LOGS(INFO) << "Chromium Received Time (ns): " << pointer_data.time_received();
FX_LOGS(INFO) << "Elapsed Time (ns): " << elapsed_time.to_nsecs();
// Allow for minor rounding differences in coordinates. As noted above, `device_x` and
// `device_y` may have an error of up to `device_pixel_ratio` physical pixels.
EXPECT_NEAR(device_x, expected_x, device_pixel_ratio);
EXPECT_NEAR(device_y, expected_y, device_pixel_ratio);
injection_complete = true;
});
}
// Routes needed to setup Chromium client.
static std::vector<Route> GetWebEngineRoutes(ChildRef target) {
return {
{.capabilities = {Protocol{test::touch::ResponseListener::Name_}},
.source = ChildRef{kMockResponseListener},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::logger::LogSink::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kFontsProvider}}},
{.capabilities = {Protocol{fuchsia::fonts::Provider::Name_}},
.source = ChildRef{kFontsProvider},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::ui::input::ImeService::Name_}},
.source = ChildRef{kTextManager},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::intl::PropertyProvider::Name_}},
.source = ChildRef{kIntl},
.targets = {target, ChildRef{kSemanticsManager}}},
{.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 = ChildRef{kSemanticsManager},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::web::ContextProvider::Name_}},
.source = ChildRef{kWebContextProvider},
.targets = {target}},
{.capabilities = {Protocol{fuchsia::tracing::provider::Registry::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kFontsProvider}, ChildRef{kSemanticsManager}}},
{.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_}},
.source = ChildRef{kScenicTestRealm},
.targets = {ChildRef{kSemanticsManager}}},
{.capabilities = {Protocol{fuchsia::cobalt::LoggerFactory::Name_}},
.source = ChildRef{kScenicTestRealm},
.targets = {ChildRef{kMemoryPressureProvider}}},
{.capabilities = {Protocol{fuchsia::sysmem::Allocator::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kMemoryPressureProvider}, ChildRef{kOneChromiumClient}}},
{.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 = ChildRef{kScenicTestRealm},
.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}}},
};
}
static constexpr auto kOneChromiumClient = "chromium_client";
static constexpr auto kOneChromiumUrl = "#meta/one-chromium.cm";
private:
static constexpr auto kFontsProvider = "fonts_provider";
static constexpr auto kFontsProviderUrl = "#meta/fake_fonts.cm";
static constexpr auto kTextManager = "text_manager";
static constexpr auto kTextManagerUrl = "#meta/text_manager.cm";
static constexpr auto kIntl = "intl";
static constexpr auto kIntlUrl = "#meta/intl_property_manager.cm";
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 kWebContextProvider = "web_context_provider";
static constexpr auto kWebContextProviderUrl =
"fuchsia-pkg://fuchsia.com/web_engine#meta/context_provider.cmx";
static constexpr auto kSemanticsManager = "semantics_manager";
static constexpr auto kSemanticsManagerUrl = "#meta/fake-a11y-manager.cm";
static constexpr auto kBuildInfoProvider = "build_info_provider";
static constexpr auto kBuildInfoProviderUrl = "#meta/fake_build_info.cm";
// The typical latency on devices we've tested is ~60 msec. The retry interval is chosen to be
// a) Long enough that it's unlikely that we send a new tap while a previous tap is still being
// processed. That is, it should be far more likely that a new tap is sent because the first
// tap was lost, than because the system is just running slowly.
// b) Short enough that we don't slow down tryjobs.
//
// The first property is important to avoid skewing the latency metrics that we collect.
// For an explanation of why a tap might be lost, see the documentation for TryInject().
static constexpr auto kTapRetryInterval = zx::sec(1);
};
TEST_F(WebEngineTest, ChromiumTap) {
// Use a UTC time for compatibility with the time reported by `Date.now()` in web-engine.
time_utc input_injection_time(0);
// Note well: unlike one-flutter and cpp-gfx-client, the web app may be rendering before
// it is hittable. Nonetheless, waiting for rendering is better than injecting the touch
// immediately. In the event that the app is not hittable, `TryInject()` will retry.
LaunchClient("ChromiumTap");
client_component().events().OnTerminated = [](int64_t return_code,
fuchsia::sys::TerminationReason reason) {
// Unlike the Flutter and C++ apps, the process hosting the web app's logic doesn't retain
// the view token for the life of the app (the process passes that token on to the web
// engine process). Consequently, we can't just rely on the IsViewDisconnected message to
// detect early termination of the app.
if (return_code != 0) {
FX_LOGS(FATAL) << "One-Chromium terminated abnormally with return_code=" << return_code
<< ", reason="
<< static_cast<std::underlying_type_t<decltype(reason)>>(reason);
}
};
bool injection_complete = false;
SetResponseExpectationsWeb(/*expected_x=*/static_cast<float>(display_height()) / 4.f,
/*expected_y=*/static_cast<float>(display_width()) / 4.f,
input_injection_time,
/*component_name=*/"one-chromium", injection_complete);
TryInject(&input_injection_time);
RunLoopUntil([&injection_complete] { return injection_complete; });
}
// Tests that rely on Embedding Flutter component. It provides convenience
// static routes that subclass can inherit.
class EmbeddingFlutterTest {
protected:
// Components needed for Embedding Flutter to be in realm.
static std::vector<std::pair<ChildName, LegacyUrl>> GetEmbeddingFlutterComponents() {
return {
std::make_pair(kEmbeddingFlutter, kEmbeddingFlutterUrl),
};
}
// Routes needed for Embedding Flutter to run.
static std::vector<Route> GetEmbeddingFlutterRoutes() {
return {
{.capabilities = {Protocol{test::touch::TestAppLauncher::Name_}},
.source = ChildRef{kEmbeddingFlutter},
.targets = {ParentRef()}},
{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
.source = ChildRef{kEmbeddingFlutter},
.targets = {ParentRef()}},
{.capabilities = {Protocol{test::touch::ResponseListener::Name_}},
.source = ChildRef{kMockResponseListener},
.targets = {ChildRef{kEmbeddingFlutter}}},
{.capabilities = {Protocol{fuchsia::ui::scenic::Scenic::Name_}},
.source = ChildRef{kScenicTestRealm},
.targets = {ChildRef{kEmbeddingFlutter}}},
// Needed for Flutter runner.
{.capabilities = {Protocol{fuchsia::logger::LogSink::Name_},
Protocol{fuchsia::vulkan::loader::Loader::Name_},
Protocol{fuchsia::tracing::provider::Registry::Name_},
Protocol{fuchsia::sysmem::Allocator::Name_}},
.source = ParentRef(),
.targets = {ChildRef{kEmbeddingFlutter}}},
};
}
static constexpr auto kEmbeddingFlutter = "embedding_flutter";
static constexpr auto kEmbeddingFlutterUrl = "#meta/embedding-flutter-realm.cm";
};
class FlutterInFlutterTest : public FlutterInputTest, public EmbeddingFlutterTest {
protected:
std::vector<std::pair<ChildName, LegacyUrl>> GetTestV2Components() override {
return merge({EmbeddingFlutterTest::GetEmbeddingFlutterComponents(),
FlutterInputTest::GetTestV2Components()});
}
std::vector<Route> GetTestRoutes() override {
return merge({EmbeddingFlutterTest::GetEmbeddingFlutterRoutes(),
FlutterInputTest::GetFlutterRoutes(ChildRef{kEmbeddingFlutter}),
FlutterInputTest::GetFlutterRoutes(ChildRef{kFlutterRealm}),
{
{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
.source = ChildRef{kFlutterRealm},
.targets = {ChildRef{kEmbeddingFlutter}}},
}});
}
};
TEST_F(FlutterInFlutterTest, FlutterInFlutterTap) {
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
// Launch the embedding app.
LaunchClient("FlutterInFlutterTap");
// Launch the embedded app.
LaunchEmbeddedClient("one-flutter");
// Embedded app takes up the left half of the screen. Expect response from it
// when injecting to the left.
{
bool injection_complete = false;
SetResponseExpectations(/*expected_x=*/static_cast<float>(display_height()) / 4.f,
/*expected_y=*/static_cast<float>(display_width()) / 4.f,
input_injection_time,
/*component_name=*/"one-flutter", injection_complete);
input_injection_time = InjectInput<zx::basic_time<ZX_CLOCK_MONOTONIC>>(TapLocation::kTopLeft);
RunLoopUntil([&injection_complete] { return injection_complete; });
}
// Parent app takes up the right half of the screen. Expect response from it
// when injecting to the right.
{
bool injection_complete = false;
SetResponseExpectations(/*expected_x=*/static_cast<float>(display_height()) * (3.f / 4.f),
/*expected_y=*/static_cast<float>(display_width()) / 4.f,
input_injection_time,
/*component_name=*/"embedding-flutter", injection_complete);
input_injection_time = InjectInput<zx::basic_time<ZX_CLOCK_MONOTONIC>>(TapLocation::kTopRight);
RunLoopUntil([&injection_complete] { return injection_complete; });
}
}
class WebInFlutterTest : public WebEngineTest, public EmbeddingFlutterTest {
protected:
std::vector<std::pair<ChildName, LegacyUrl>> GetTestComponents() override {
return WebEngineTest::GetTestComponents();
}
std::vector<std::pair<ChildName, LegacyUrl>> GetTestV2Components() override {
return merge({EmbeddingFlutterTest::GetEmbeddingFlutterComponents(),
WebEngineTest::GetTestV2Components()});
}
std::vector<Route> GetTestRoutes() override {
return merge({EmbeddingFlutterTest::GetEmbeddingFlutterRoutes(),
WebEngineTest::GetWebEngineRoutes(ChildRef{kEmbeddingFlutter}),
WebEngineTest::GetWebEngineRoutes(ChildRef{kOneChromiumClient}),
{
{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}},
.source = ChildRef{kOneChromiumClient},
.targets = {ChildRef{kEmbeddingFlutter}}},
}});
}
};
TEST_F(WebInFlutterTest, WebInFlutterTap) {
// Launch the embedding app.
LaunchClient("WebInFlutterTap");
// Launch the embedded app.
LaunchEmbeddedClient("one-chromium");
// Parent app takes up the right half of the screen. Expect response from it
// when injecting to the right.
{
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
bool injection_complete = false;
SetResponseExpectations(/*expected_x=*/static_cast<float>(display_height()) * (3.f / 4.f),
/*expected_y=*/static_cast<float>(display_width()) / 4.f,
input_injection_time,
/*component_name=*/"embedding-flutter", injection_complete);
input_injection_time = InjectInput<zx::basic_time<ZX_CLOCK_MONOTONIC>>(TapLocation::kTopRight);
RunLoopUntil([&injection_complete] { return injection_complete; });
}
// Embedded app takes up the left half of the screen. Expect response from it
// when injecting to the left.
{
// Use a UTC time for compatibility with the time reported by `Date.now()` in web-engine.
time_utc input_injection_time(0);
bool injection_complete = false;
SetResponseExpectationsWeb(/*expected_x=*/static_cast<float>(display_height()) / 4.f,
/*expected_y=*/static_cast<float>(display_width()) / 4.f,
input_injection_time,
/*component_name=*/"one-chromium", injection_complete);
TryInject(&input_injection_time);
RunLoopUntil([&injection_complete] { return injection_complete; });
}
}
} // namespace