blob: 2452ef1b7548136ba7616957d2b3642a20954151 [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/accessibility/semantics/cpp/fidl.h>
#include <fuchsia/fonts/cpp/fidl.h>
#include <fuchsia/hardware/display/cpp/fidl.h>
#include <fuchsia/input/injection/cpp/fidl.h>
#include <fuchsia/intl/cpp/fidl.h>
#include <fuchsia/memorypressure/cpp/fidl.h>
#include <fuchsia/net/interfaces/cpp/fidl.h>
#include <fuchsia/netstack/cpp/fidl.h>
#include <fuchsia/sys/cpp/fidl.h>
#include <fuchsia/ui/app/cpp/fidl.h>
#include <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/pointerinjector/cpp/fidl.h>
#include <fuchsia/ui/policy/cpp/fidl.h>
#include <fuchsia/vulkan/loader/cpp/fidl.h>
#include <fuchsia/web/cpp/fidl.h>
#include <lib/async/cpp/task.h>
#include <lib/fostr/fidl/fuchsia/ui/gfx/formatting.h>
#include <lib/gtest/real_loop_fixture.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/sys/cpp/testing/enclosing_environment.h>
#include <lib/sys/cpp/testing/test_with_environment.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/ui/scenic/cpp/resources.h>
#include <lib/ui/scenic/cpp/session.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <lib/zx/clock.h>
#include <lib/zx/time.h>
#include <zircon/types.h>
#include <zircon/utc.h>
#include <iostream>
#include <type_traits>
#include <gtest/gtest.h>
#include <test/touch/cpp/fidl.h>
#include "src/ui/input/testing/fake_input_report_device/fake.h"
#include "src/ui/input/testing/fake_input_report_device/reports_reader.h"
// This test exercises the touch input dispatch path from Input Pipeline to a Scenic client. It is a
// multi-component test, and carefully avoids sleeping or polling for component coordination.
// - It runs real Root Presenter, Input Pipeline, and Scenic components.
// - It uses a fake display controller; the physical device is unused.
//
// Components involved
// - This test program
// - Input Pipeline
// - Root Presenter
// - Scenic
// - Child view, a Scenic client
//
// Touch dispatch path
// - Test program's injection -> Input Pipeline -> Scenic -> Child view
//
// Setup sequence
// - The test sets up a view hierarchy with three views:
// - Top level scene, owned by Root Presenter.
// - Middle view, owned by this test.
// - Bottom view, owned by the child view.
// - 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.
namespace {
using ScenicEvent = fuchsia::ui::scenic::Event;
using GfxEvent = fuchsia::ui::gfx::Event;
// 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);
// Common services for each test.
const std::map<std::string, std::string> LocalServices() {
return {
// Test-only variants of the input pipeline and root presenter are included in this tests's
// package for component hermeticity, and to avoid reading /dev/class/input-report. Reading
// the input device driver in a test can cause conflicts with real input devices. This also
// allows root presenter to share /config/data with the test. This allows the test to control
// the display rotation read by root presenter.
{"fuchsia.input.injection.InputDeviceRegistry",
"fuchsia-pkg://fuchsia.com/touch-input-test-ip#meta/input-pipeline.cmx"},
{"fuchsia.ui.policy.Presenter",
"fuchsia-pkg://fuchsia.com/touch-input-test-ip#meta/root_presenter.cmx"},
{"fuchsia.ui.pointerinjector.configuration.Setup",
"fuchsia-pkg://fuchsia.com/touch-input-test-ip#meta/root_presenter.cmx"},
// Scenic protocols.
{"fuchsia.ui.scenic.Scenic", "fuchsia-pkg://fuchsia.com/touch-input-test-ip#meta/scenic.cmx"},
{"fuchsia.ui.pointerinjector.Registry",
"fuchsia-pkg://fuchsia.com/touch-input-test-ip#meta/scenic.cmx"},
{"fuchsia.ui.focus.FocusChainListenerRegistry",
"fuchsia-pkg://fuchsia.com/touch-input-test-ip#meta/scenic.cmx"},
// Misc protocols.
{"fuchsia.cobalt.LoggerFactory",
"fuchsia-pkg://fuchsia.com/mock_cobalt#meta/mock_cobalt.cmx"},
{"fuchsia.hardware.display.Provider",
"fuchsia-pkg://fuchsia.com/fake-hardware-display-controller-provider#meta/hdcp.cmx"},
};
}
// Allow these global services from outside the test environment.
const std::vector<std::string> GlobalServices() {
return {"fuchsia.vulkan.loader.Loader", "fuchsia.sysmem.Allocator",
"fuchsia.scheduler.ProfileProvider"};
}
class TouchInputBase : public sys::testing::TestWithEnvironment,
public test::touch::ResponseListener {
protected:
struct LaunchableService {
std::string url;
std::string name;
};
explicit TouchInputBase(const std::vector<LaunchableService>& extra_services)
: response_listener_(this) {
auto services = TestWithEnvironment::CreateServices();
// Key part of service setup: have this test component vend the |ResponseListener| service in
// the constructed environment.
zx_status_t is_ok = services->AddService<ResponseListener>(
[this](fidl::InterfaceRequest<ResponseListener> request) {
response_listener_.Bind(std::move(request));
});
FX_CHECK(is_ok == ZX_OK);
// Add common services.
for (const auto& [name, url] : LocalServices()) {
const zx_status_t is_ok = services->AddServiceWithLaunchInfo({.url = url}, name);
FX_CHECK(is_ok == ZX_OK) << "Failed to add service " << name;
}
// Enable services from outside this test.
for (const auto& service : GlobalServices()) {
const zx_status_t is_ok = services->AllowParentService(service);
FX_CHECK(is_ok == ZX_OK) << "Failed to add service " << service;
}
// Add test-specific launchable services.
for (const auto& service_info : extra_services) {
const zx_status_t is_ok =
services->AddServiceWithLaunchInfo({.url = service_info.url}, service_info.name);
FX_CHECK(is_ok == ZX_OK) << "Failed to add service " << service_info.name;
}
test_env_ = CreateNewEnclosingEnvironment("touch_input_test_env", std::move(services));
WaitForEnclosingEnvToStart(test_env_.get());
FX_VLOGS(1) << "Created test environment.";
RegisterInjectionDevice();
// 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);
}
~TouchInputBase() override {
FX_CHECK(injection_count_ > 0) << "injection expected but didn't happen.";
}
sys::testing::EnclosingEnvironment* test_env() { return test_env_.get(); }
scenic::Session* session() { return session_.get(); }
void MakeSession(fuchsia::ui::scenic::SessionPtr session,
fidl::InterfaceRequest<fuchsia::ui::scenic::SessionListener> session_listener) {
session_ = std::make_unique<scenic::Session>(std::move(session), std::move(session_listener));
}
scenic::ViewHolder* view_holder() { return view_holder_.get(); }
void MakeViewHolder(fuchsia::ui::views::ViewHolderToken token, const std::string& name) {
FX_CHECK(session_);
view_holder_ = std::make_unique<scenic::ViewHolder>(session_.get(), std::move(token), name);
}
void SetRespondCallback(fit::function<void(test::touch::PointerData)> callback) {
respond_callback_ = std::move(callback);
}
// |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));
}
void RegisterInjectionDevice() {
registry_ = test_env()->ConnectToService<fuchsia::input::injection::InputDeviceRegistry>();
// Create a FakeInputDevice
fake_input_device_ = std::make_unique<fake_input_report_device::FakeInputDevice>(
input_device_ptr_.NewRequest(), dispatcher());
// Set descriptor
auto device_descriptor = std::make_unique<fuchsia::input::report::DeviceDescriptor>();
auto touch = device_descriptor->mutable_touch()->mutable_input();
touch->set_touch_type(fuchsia::input::report::TouchType::TOUCHSCREEN);
touch->set_max_contacts(10);
fuchsia::input::report::Axis axis;
axis.unit.type = fuchsia::input::report::UnitType::NONE;
axis.unit.exponent = 0;
axis.range.min = -1000;
axis.range.max = 1000;
fuchsia::input::report::ContactInputDescriptor contact;
contact.set_position_x(axis);
contact.set_position_y(axis);
contact.set_pressure(axis);
touch->mutable_contacts()->push_back(std::move(contact));
fake_input_device_->SetDescriptor(std::move(device_descriptor));
// Register the FakeInputDevice
registry_->Register(std::move(input_device_ptr_));
}
// Inject directly into Input Pipeline, using fuchsia.input.injection FIDLs.
template <typename TimeT>
TimeT InjectInput() {
// Set InputReports to inject. One contact at the center of the top right quadrant, followed
// by no contacts.
fuchsia::input::report::ContactInputReport contact_input_report;
contact_input_report.set_contact_id(1);
contact_input_report.set_position_x(500);
contact_input_report.set_position_y(-500);
fuchsia::input::report::TouchInputReport touch_input_report;
auto contacts = touch_input_report.mutable_contacts();
contacts->push_back(std::move(contact_input_report));
fuchsia::input::report::InputReport input_report;
input_report.set_touch(std::move(touch_input_report));
std::vector<fuchsia::input::report::InputReport> input_reports;
input_reports.push_back(std::move(input_report));
fuchsia::input::report::TouchInputReport remove_touch_input_report;
fuchsia::input::report::InputReport remove_input_report;
remove_input_report.set_touch(std::move(remove_touch_input_report));
input_reports.push_back(std::move(remove_input_report));
fake_input_device_->SetReports(std::move(input_reports));
++injection_count_;
FX_LOGS(INFO) << "*** Tap injected, count: " << injection_count_;
return RealNow<TimeT>();
}
int injection_count() const { return injection_count_; }
private:
template <typename TimeT>
TimeT RealNow();
template <>
zx::time RealNow() {
return zx::clock::get_monotonic();
}
template <>
zx::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 zx::time_utc(now);
}
template <typename TimeT>
uint64_t TimeToUint(const TimeT& time) {
FX_CHECK(time.get() >= 0);
return static_cast<uint64_t>(time.get());
};
fidl::Binding<test::touch::ResponseListener> response_listener_;
std::unique_ptr<sys::testing::EnclosingEnvironment> test_env_;
std::unique_ptr<scenic::Session> session_;
fuchsia::input::injection::InputDeviceRegistryPtr registry_;
std::unique_ptr<fake_input_report_device::FakeInputDevice> fake_input_device_;
fuchsia::input::report::InputDevicePtr input_device_ptr_;
int injection_count_ = 0;
// Child view's ViewHolder.
std::unique_ptr<scenic::ViewHolder> view_holder_;
fit::function<void(test::touch::PointerData)> respond_callback_;
};
class TouchInputTest_IP : public TouchInputBase {
protected:
TouchInputTest_IP() : TouchInputBase({}) {}
};
TEST_F(TouchInputTest_IP, FlutterTap) {
const std::string kOneFlutter = "fuchsia-pkg://fuchsia.com/one-flutter#meta/one-flutter.cmx";
uint32_t display_width = 0;
uint32_t display_height = 0;
// Get the display dimensions
auto scenic = test_env()->ConnectToService<fuchsia::ui::scenic::Scenic>();
scenic->GetDisplayInfo(
[&display_width, &display_height](fuchsia::ui::gfx::DisplayInfo display_info) {
display_width = display_info.width_in_px;
display_height = display_info.height_in_px;
FX_LOGS(INFO) << "Got display_width = " << display_width
<< " and display_height = " << display_height;
});
RunLoopUntil(
[&display_width, &display_height] { return display_width != 0 && display_height != 0; });
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
// Define test expectations for when Flutter calls back with "Respond()".
SetRespondCallback([this, display_width, display_height,
&input_injection_time](test::touch::PointerData pointer_data) {
// 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.
float expected_x = static_cast<float>(display_height) / 4.f;
float expected_y = static_cast<float>(display_width) / 4.f;
FX_LOGS(INFO) << "Flutter 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) << "Flutter 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);
FX_LOGS(INFO) << "*** PASS ***";
QuitLoop();
});
// Define when to set size for Flutter's view, and when to inject input against Flutter's view.
scenic::Session::EventHandler handler =
[this, &input_injection_time](const std::vector<fuchsia::ui::scenic::Event>& events) {
for (const auto& event : events) {
if (!event.is_gfx())
continue; // skip non-gfx events
if (event.gfx().is_view_properties_changed()) {
auto properties = event.gfx().view_properties_changed().properties;
FX_VLOGS(1) << "Test received its view properties; transfer to child view: "
<< properties;
FX_CHECK(view_holder()) << "Expect that view holder is already set up.";
view_holder()->SetViewProperties(properties);
session()->Present2(/*when*/ zx::clock::get_monotonic().get(), /*span*/ 0, [](auto) {});
} else if (event.gfx().is_view_state_changed()) {
bool hittable = event.gfx().view_state_changed().state.is_rendering;
FX_VLOGS(1) << "Child's view content is hittable: " << std::boolalpha << hittable;
if (hittable) {
input_injection_time = InjectInput<zx::basic_time<ZX_CLOCK_MONOTONIC>>();
}
} else if (event.gfx().is_view_disconnected()) {
// Save time, terminate the test immediately if we know that Flutter's view is borked.
FX_CHECK(injection_count() > 0)
<< "Expected to have completed input injection, but Flutter view terminated early.";
}
}
};
auto tokens_rt = scenic::ViewTokenPair::New(); // Root Presenter -> Test
auto tokens_tf = scenic::ViewTokenPair::New(); // Test -> Flutter
// Instruct Root Presenter to present test's View.
auto root_presenter = test_env()->ConnectToService<fuchsia::ui::policy::Presenter>();
root_presenter->PresentOrReplaceView(std::move(tokens_rt.view_holder_token),
/* presentation */ nullptr);
// Set up test's View, to harvest Flutter view's view_state.is_rendering signal.
auto session_pair = scenic::CreateScenicSessionPtrAndListenerRequest(scenic.get());
MakeSession(std::move(session_pair.first), std::move(session_pair.second));
session()->set_event_handler(std::move(handler));
session()->SetDebugName("flutter-tap-test");
scenic::View view(session(), std::move(tokens_rt.view_token), "test's view");
MakeViewHolder(std::move(tokens_tf.view_holder_token), "test's viewholder for flutter");
view.AddChild(*view_holder());
// Request to make test's view; this will trigger dispatch of view properties.
session()->Present2(/*when*/ zx::clock::get_monotonic().get(), /*span*/ 0, [](auto) {
FX_VLOGS(1) << "test's view and view holder created by Scenic.";
});
// Start Flutter app inside the test environment.
// Note well. We launch the flutter component directly, and ask for its ViewProvider service
// directly, to closely model production setup.
fuchsia::sys::ComponentControllerPtr one_flutter_component;
{
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = kOneFlutter;
// Create a point-to-point offer-use connection between parent and child.
auto child_services = sys::ServiceDirectory::CreateWithRequest(&launch_info.directory_request);
one_flutter_component = test_env()->CreateComponent(std::move(launch_info));
auto view_provider = child_services->Connect<fuchsia::ui::app::ViewProvider>();
view_provider->CreateView(std::move(tokens_tf.view_token.value), /* in */ nullptr,
/* out */ nullptr);
}
RunLoop(); // Go!
}
TEST_F(TouchInputTest_IP, CppGfxClientTap) {
const std::string kCppGfxClient =
"fuchsia-pkg://fuchsia.com/touch-gfx-client#meta/touch-gfx-client.cmx";
uint32_t display_width = 0;
uint32_t display_height = 0;
// Get the display dimensions
auto scenic = test_env()->ConnectToService<fuchsia::ui::scenic::Scenic>();
scenic->GetDisplayInfo(
[&display_width, &display_height](fuchsia::ui::gfx::DisplayInfo display_info) {
display_width = display_info.width_in_px;
display_height = display_info.height_in_px;
FX_LOGS(INFO) << "Got display_width = " << display_width
<< " and display_height = " << display_height;
});
RunLoopUntil(
[&display_width, &display_height] { return display_width != 0 && display_height != 0; });
// Use `ZX_CLOCK_MONOTONIC` to avoid complications due to wall-clock time changes.
zx::basic_time<ZX_CLOCK_MONOTONIC> input_injection_time(0);
// Define test expectations for when CppGfxClient calls back with "Respond()".
SetRespondCallback([this, display_width, display_height,
&input_injection_time](test::touch::PointerData pointer_data) {
// 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.
float expected_x = static_cast<float>(display_height) / 4.f;
float expected_y = static_cast<float>(display_width) / 4.f;
FX_LOGS(INFO) << "CppGfxClient 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) << "CppGfxClient 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);
FX_LOGS(INFO) << "*** PASS ***";
QuitLoop();
});
// Define when to set size for CppGfxClient's view, and when to inject input against
// CppGfxClient's view.
scenic::Session::EventHandler handler =
[this, &input_injection_time](const std::vector<fuchsia::ui::scenic::Event>& events) {
for (const auto& event : events) {
if (!event.is_gfx())
continue; // skip non-gfx events
if (event.gfx().is_view_properties_changed()) {
auto properties = event.gfx().view_properties_changed().properties;
FX_VLOGS(1) << "Test received its view properties; transfer to child view: "
<< properties;
FX_CHECK(view_holder()) << "Expect that view holder is already set up.";
view_holder()->SetViewProperties(properties);
session()->Present2(/*when*/ zx::clock::get_monotonic().get(), /*span*/ 0, [](auto) {});
} else if (event.gfx().is_view_state_changed()) {
bool hittable = event.gfx().view_state_changed().state.is_rendering;
FX_VLOGS(1) << "Child's view content is hittable: " << std::boolalpha << hittable;
if (hittable) {
input_injection_time = InjectInput<zx::basic_time<ZX_CLOCK_MONOTONIC>>();
}
} else if (event.gfx().is_view_disconnected()) {
// Save time, terminate the test immediately if we know that CppGfxClient's view is
// borked.
FX_CHECK(injection_count() > 0) << "Expected to have completed input injection, but "
"CppGfxClient's view terminated early.";
}
}
};
auto tokens_rt = scenic::ViewTokenPair::New(); // Root Presenter -> Test
auto tokens_tf = scenic::ViewTokenPair::New(); // Test -> CppGfxClient
// Instruct Root Presenter to present test's View.
auto root_presenter = test_env()->ConnectToService<fuchsia::ui::policy::Presenter>();
root_presenter->PresentOrReplaceView(std::move(tokens_rt.view_holder_token),
/* presentation */ nullptr);
// Set up test's View, to harvest CppGfxClient view's view_state.is_rendering signal.
auto session_pair = scenic::CreateScenicSessionPtrAndListenerRequest(scenic.get());
MakeSession(std::move(session_pair.first), std::move(session_pair.second));
session()->set_event_handler(std::move(handler));
session()->SetDebugName("touch-gfx-client-tap-test");
scenic::View view(session(), std::move(tokens_rt.view_token), "test's view");
MakeViewHolder(std::move(tokens_tf.view_holder_token), "test's viewholder for CppGfxClient");
view.AddChild(*view_holder());
// Request to make test's view; this will trigger dispatch of view properties.
session()->Present2(/*when*/ zx::clock::get_monotonic().get(), /*span*/ 0, [](auto) {
FX_LOGS(INFO) << "test's view and view holder created by Scenic.";
});
// Start CppGfxClient app inside the test environment.
// Note well. We launch the CppGfxClient component directly, and ask for its ViewProvider service
// directly, to closely model production setup.
fuchsia::sys::ComponentControllerPtr cpp_gfx_client_component;
{
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = kCppGfxClient;
// Create a point-to-point offer-use connection between parent and child.
auto child_services = sys::ServiceDirectory::CreateWithRequest(&launch_info.directory_request);
cpp_gfx_client_component = test_env()->CreateComponent(std::move(launch_info));
auto view_provider = child_services->Connect<fuchsia::ui::app::ViewProvider>();
view_provider->CreateView(std::move(tokens_tf.view_token.value), /* in */ nullptr,
/* out */ nullptr);
}
RunLoop(); // Go!
}
class WebEngineTest_IP : public TouchInputBase {
public:
WebEngineTest_IP()
: TouchInputBase({
{.url = kFontsProvider, .name = fuchsia::fonts::Provider::Name_},
{.url = kImeService, .name = fuchsia::ui::input::ImeService::Name_},
{.url = kImeService, .name = fuchsia::ui::input::ImeVisibilityService::Name_},
{.url = kIntl, .name = fuchsia::intl::PropertyProvider::Name_},
{.url = kMemoryPressureProvider, .name = fuchsia::memorypressure::Provider::Name_},
{.url = kNetstack, .name = fuchsia::netstack::Netstack::Name_},
{.url = kNetstack, .name = fuchsia::net::interfaces::State::Name_},
{.url = kSemanticsManager,
.name = fuchsia::accessibility::semantics::SemanticsManager::Name_},
{.url = kWebContextProvider, .name = fuchsia::web::ContextProvider::Name_},
}) {}
protected:
// 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 `is_rendering` signal that we monitor only guarantees us
// 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 `is_rendering` triggers 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 `is_rendering` as soon as it is `Present()`-ed. Walking transitively up the
// scene graph, that causes our `Session` to receive the `is_rendering` signal.
//
// For more detals, see fxbug.dev/57268.
//
// TODO(fxbug.dev/58322): Improve synchronization when we move to Flatland.
void TryInject(zx::basic_time<ZX_CLOCK_UTC>* input_injection_time) {
*input_injection_time = InjectInput<zx::basic_time<ZX_CLOCK_UTC>>();
async::PostDelayedTask(
dispatcher(), [this, input_injection_time] { TryInject(input_injection_time); },
kTapRetryInterval);
};
private:
static constexpr char kFontsProvider[] = "fuchsia-pkg://fuchsia.com/fonts#meta/fonts.cmx";
static constexpr char kImeService[] =
"fuchsia-pkg://fuchsia.com/ime_service#meta/ime_service.cmx";
static constexpr char kIntl[] =
"fuchsia-pkg://fuchsia.com/intl_property_manager#meta/intl_property_manager.cmx";
static constexpr char kMemoryPressureProvider[] =
"fuchsia-pkg://fuchsia.com/memory_monitor#meta/memory_monitor.cmx";
static constexpr char kNetstack[] = "fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx";
static constexpr char kWebContextProvider[] =
"fuchsia-pkg://fuchsia.com/web_engine#meta/context_provider.cmx";
static constexpr char kSemanticsManager[] =
"fuchsia-pkg://fuchsia.com/a11y-manager#meta/a11y-manager.cmx";
// 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_IP, ChromiumTap) {
const std::string kOneChromium = "fuchsia-pkg://fuchsia.com/one-chromium#meta/one-chromium.cmx";
uint32_t display_width = 0;
uint32_t display_height = 0;
// Get the display dimensions
auto scenic = test_env()->ConnectToService<fuchsia::ui::scenic::Scenic>();
scenic->GetDisplayInfo(
[&display_width, &display_height](fuchsia::ui::gfx::DisplayInfo display_info) {
display_width = display_info.width_in_px;
display_height = display_info.height_in_px;
FX_LOGS(INFO) << "Got display_width = " << display_width
<< " and display_height = " << display_height;
});
RunLoopUntil(
[&display_width, &display_height] { return display_width != 0 && display_height != 0; });
// Use `ZX_CLOCK_UTC` for compatibility with the time reported by `Date.now()` in web-engine.
zx::basic_time<ZX_CLOCK_UTC> input_injection_time(0);
// Define test expectations for when Chromium calls back with "Respond()".
SetRespondCallback([this, display_width, display_height,
&input_injection_time](test::touch::PointerData pointer_data) {
// 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.
float expected_x = static_cast<float>(display_height) / 4.f;
float expected_y = static_cast<float>(display_width) / 4.f;
// 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 =
zx::basic_time<ZX_CLOCK_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);
QuitLoop();
});
// Define when to set size for Chromium's view, and when to inject input against Chromium's view.
scenic::Session::EventHandler handler = [this, &input_injection_time](
const std::vector<fuchsia::ui::scenic::Event>&
events) {
for (const auto& event : events) {
if (!event.is_gfx())
continue; // skip non-gfx events
if (event.gfx().is_view_properties_changed()) {
auto properties = event.gfx().view_properties_changed().properties;
FX_VLOGS(1) << "Test received its view properties; transfer to child view: " << properties;
FX_CHECK(view_holder()) << "Expect that view holder is already set up.";
view_holder()->SetViewProperties(properties);
session()->Present2(/*when*/ zx::clock::get_monotonic().get(), /*span*/ 0, [](auto) {});
} else if (event.gfx().is_view_state_changed()) {
// Note well: unlike one-flutter and touch-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.
bool rendering = event.gfx().view_state_changed().state.is_rendering;
FX_VLOGS(1) << "Child's view content is rendering: " << std::boolalpha << rendering;
if (rendering) {
TryInject(&input_injection_time);
}
} else if (event.gfx().is_view_disconnected()) {
// Save time, terminate the test immediately if we know that Chromium's view is borked.
FX_CHECK(injection_count() > 0)
<< "Expected to have completed input injection, but Chromium view terminated early.";
}
}
};
auto tokens_rt = scenic::ViewTokenPair::New(); // Root Presenter -> Test
auto tokens_tc = scenic::ViewTokenPair::New(); // Test -> Chromium
// Instruct Root Presenter to present test's View.
auto root_presenter = test_env()->ConnectToService<fuchsia::ui::policy::Presenter>();
root_presenter->PresentOrReplaceView(std::move(tokens_rt.view_holder_token),
/* presentation */ nullptr);
// Set up test's View, to harvest Chromium view's view_state.is_rendering signal.
auto session_pair = scenic::CreateScenicSessionPtrAndListenerRequest(scenic.get());
MakeSession(std::move(session_pair.first), std::move(session_pair.second));
session()->set_event_handler(std::move(handler));
session()->SetDebugName("chromium-tap-test");
scenic::View view(session(), std::move(tokens_rt.view_token), "test's view");
MakeViewHolder(std::move(tokens_tc.view_holder_token), "test's viewholder for chromium");
view.AddChild(*view_holder());
// Request to make test's view; this will trigger dispatch of view properties.
session()->Present2(/*when*/ zx::clock::get_monotonic().get(), /*span*/ 0, [](auto) {
FX_VLOGS(1) << "test's view and view holder created by Scenic.";
});
// Start Chromium app inside the test environment.
fuchsia::sys::ComponentControllerPtr one_chromium_component;
{
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = kOneChromium;
// Create a point-to-point offer-use connection between parent and child.
auto child_services = sys::ServiceDirectory::CreateWithRequest(&launch_info.directory_request);
one_chromium_component = test_env()->CreateComponent(std::move(launch_info));
one_chromium_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);
}
};
auto view_provider = child_services->Connect<fuchsia::ui::app::ViewProvider>();
view_provider->CreateView(std::move(tokens_tc.view_token.value), /* in */ nullptr,
/* out */ nullptr);
}
RunLoop(); // Go!
}
} // namespace