blob: 445b2c642a6f001c66ba3299298e759edc0e08f7 [file] [log] [blame]
// Copyright 2022 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 "src/ui/testing/util/portable_ui_test.h"
#include <fidl/fuchsia.input.report/cpp/fidl.h>
#include <fidl/fuchsia.input.virtualkeyboard/cpp/fidl.h>
#include <fidl/fuchsia.logger/cpp/fidl.h>
#include <fidl/fuchsia.scheduler/cpp/fidl.h>
#include <fidl/fuchsia.sysmem/cpp/fidl.h>
#include <fidl/fuchsia.tracing.provider/cpp/fidl.h>
#include <fidl/fuchsia.ui.app/cpp/fidl.h>
#include <fidl/fuchsia.ui.display.singleton/cpp/fidl.h>
#include <fidl/fuchsia.ui.focus/cpp/fidl.h>
#include <fidl/fuchsia.ui.test.input/cpp/fidl.h>
#include <fidl/fuchsia.vulkan.loader/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_types.h>
#include <lib/syslog/cpp/macros.h>
#include <utility>
#include <src/ui/testing/util/fidl_cpp_helpers.h>
namespace ui_testing {
namespace {
// Types imported for the realm_builder library.
using component_testing::ConfigValue;
using component_testing::Dictionary;
using component_testing::ParentRef;
using component_testing::Protocol;
using component_testing::RealmRoot;
using component_testing::Route;
bool CheckViewExistsInSnapshot(const fuchsia_ui_observation_geometry::ViewTreeSnapshot& snapshot,
zx_koid_t view_ref_koid) {
if (!snapshot.views().has_value()) {
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;
}
} // namespace
void PortableUITest::SetUpRealmBase() {
FX_LOGS(INFO) << "Setting up realm base.";
// Add test UI stack component.
realm_builder_.AddChild(kTestUIStack, GetTestUIStackUrl());
// Route base system services to flutter and the test UI stack.
realm_builder_.AddRoute(Route{
.capabilities = {Protocol{fidl::DiscoverableProtocolName<fuchsia_logger::LogSink>},
Dictionary{"diagnostics"},
Protocol{fidl::DiscoverableProtocolName<fuchsia_scheduler::RoleManager>},
Protocol{fidl::DiscoverableProtocolName<fuchsia_sysmem::Allocator>},
Protocol{fidl::DiscoverableProtocolName<fuchsia_sysmem2::Allocator>},
Protocol{fidl::DiscoverableProtocolName<fuchsia_vulkan_loader::Loader>},
Protocol{
fidl::DiscoverableProtocolName<fuchsia_tracing_provider::Registry>}},
.source = ParentRef{},
.targets = {kTestUIStackRef}});
// Route UI capabilities from test-ui-stack to test driver.
realm_builder_.AddRoute(Route{
.capabilities =
{
Protocol{fidl::DiscoverableProtocolName<fuchsia_ui_composition::Screenshot>},
Protocol{fidl::DiscoverableProtocolName<fuchsia_ui_display_singleton::Info>},
Protocol{
fidl::DiscoverableProtocolName<fuchsia_ui_focus::FocusChainListenerRegistry>},
Protocol{fidl::DiscoverableProtocolName<fuchsia_ui_test_input::Registry>},
Protocol{fidl::DiscoverableProtocolName<fuchsia_ui_test_scene::Controller>},
Protocol{fidl::DiscoverableProtocolName<test_accessibility::Magnifier>},
Protocol{fidl::DiscoverableProtocolName<fuchsia_input_virtualkeyboard::Manager>},
Protocol{
fidl::DiscoverableProtocolName<fuchsia_input_virtualkeyboard::ControllerCreator>},
},
.source = kTestUIStackRef,
.targets = {ParentRef{}}});
// Configure test-ui-stack.
realm_builder_.InitMutableConfigFromPackage(kTestUIStack);
realm_builder_.SetConfigValue(kTestUIStack, "display_rotation",
ConfigValue::Uint32(display_rotation()));
FX_CHECK((display_width_px() == 0) == (display_height_px() == 0))
<< "display_width_px() and display_height_px() must be either both zero or both non-zero.";
realm_builder_.SetConfigValue(kTestUIStack, "display_width_px",
ConfigValue::Uint32(display_width_px()));
realm_builder_.SetConfigValue(kTestUIStack, "display_height_px",
ConfigValue::Uint32(display_height_px()));
realm_builder_.SetConfigValue(kTestUIStack, "display_refresh_rate_millihertz",
ConfigValue::Uint32(display_refresh_rate_millihertz()));
realm_builder_.SetConfigValue(kTestUIStack, "device_pixel_ratio",
ConfigValue(std::to_string(device_pixel_ratio())));
realm_builder_.SetConfigValue(kTestUIStack, "suspend_enabled",
ConfigValue::Bool(suspend_enabled()));
}
void PortableUITest::SetUp() {
SetUpRealmBase();
// Add additional components configured by the subclass.
for (const auto& [name, component] : GetEagerTestComponents()) {
realm_builder_.AddChild(
name, component,
component_testing::ChildOptions{.startup_mode = component_testing::StartupMode::EAGER});
}
for (const auto& [name, component] : GetTestComponents()) {
realm_builder_.AddChild(name, component);
}
ExtendRealm();
// Add the necessary routing for each of the extra components added above, including any in
// ExtendRealm.
for (const auto& route : GetTestRoutes()) {
realm_builder_.AddRoute(route);
}
realm_ = realm_builder_.Build();
// Get the display dimensions. This is not only a log output, it ensures display info is ready.
FX_LOGS(INFO) << "Got display_width = " << display_width()
<< " and display_height = " << display_height();
}
void PortableUITest::TearDown() {
begin_tear_down_ = true;
bool complete = false;
realm_->Teardown([&](fit::result<fuchsia::component::Error> result) { complete = true; });
RunLoopUntil([&]() { return complete; });
}
void PortableUITest::ProcessViewGeometryResponse(
fuchsia_ui_observation_geometry::WatchResponse response) {
// Process update if no error.
if (!response.error().has_value()) {
if (response.updates().has_value() && !response.updates()->empty()) {
last_view_tree_snapshot_ = std::move(response.updates()->back());
}
} else {
// Otherwise, process error.
const auto& error = response.error().value();
if (error & fuchsia_ui_observation_geometry::Error::kChannelOverflow) {
FX_LOGS(DEBUG) << "View Tree watcher channel overflowed";
} else if (error & fuchsia_ui_observation_geometry::Error::kBufferOverflow) {
FX_LOGS(DEBUG) << "View Tree watcher buffer overflowed";
} else if (error & fuchsia_ui_observation_geometry::Error::kViewsOverflow) {
// This one indicates some possible data loss, so we log with a high severity.
FX_LOGS(WARNING) << "View Tree watcher attempted to report too many views";
}
}
}
void PortableUITest::SetUpSceneProvider() {
ASSERT_TRUE(realm_.has_value());
auto scene_provider_connect = realm_->component().Connect<fuchsia_ui_test_scene::Controller>();
ZX_ASSERT_OK(scene_provider_connect);
scene_provider_ = fidl::SyncClient(std::move(scene_provider_connect.value()));
}
void PortableUITest::WatchViewGeometry() {
FX_CHECK(view_tree_watcher_.is_valid())
<< "View Tree watcher must be registered before calling Watch()";
view_tree_watcher_->Watch().Then([this](auto response) {
if (!begin_tear_down_) {
ZX_ASSERT_OK(response);
ProcessViewGeometryResponse(std::move(response.value()));
WatchViewGeometry();
}
});
}
void PortableUITest::WaitForViewPresentation() {
SetUpSceneProvider();
// WatchViewPresentation() will hang until a ClientView has `Present()`
ZX_ASSERT_OK(scene_provider_->WatchViewPresentation());
}
bool PortableUITest::HasViewConnected(zx_koid_t view_ref_koid) {
return last_view_tree_snapshot_.has_value() &&
CheckViewExistsInSnapshot(*last_view_tree_snapshot_, view_ref_koid);
}
void PortableUITest::RegisterViewTreeWatcher() {
auto [watcher_client, watcher_server] =
fidl::Endpoints<fuchsia_ui_observation_geometry::ViewTreeWatcher>::Create();
view_tree_watcher_ = fidl::Client(std::move(watcher_client), dispatcher());
ZX_ASSERT_OK(scene_provider_->RegisterViewTreeWatcher({std::move(watcher_server)}));
}
void PortableUITest::LaunchClient() {
SetUpSceneProvider();
RegisterViewTreeWatcher();
auto view_provider_connect = realm_->component().Connect<fuchsia_ui_app::ViewProvider>();
ZX_ASSERT_OK(view_provider_connect);
fuchsia_ui_test_scene::ControllerAttachClientViewRequest request;
request.view_provider() = std::move(view_provider_connect.value());
auto attach_client_view_res = scene_provider_->AttachClientView(std::move(request));
ZX_ASSERT_OK(attach_client_view_res);
client_root_view_ref_koid_ = attach_client_view_res->view_ref_koid();
FX_LOGS(INFO) << "Waiting for client view ref koid";
RunLoopUntil([this] { return client_root_view_ref_koid_.has_value(); });
WatchViewGeometry();
FX_LOGS(INFO) << "Waiting for client view to connect";
RunLoopUntil([this] { return HasViewConnected(*client_root_view_ref_koid_); });
FX_LOGS(INFO) << "Client view has rendered";
}
void PortableUITest::LaunchClientWithEmbeddedView() {
LaunchClient();
// At this point, the parent view must have rendered, so we just need to wait
// for the embedded view.
RunLoopUntil([this] {
if (!last_view_tree_snapshot_.has_value() || !last_view_tree_snapshot_->views().has_value()) {
return false;
}
if (!client_root_view_ref_koid_.has_value()) {
return false;
}
for (const auto& view : last_view_tree_snapshot_->views().value()) {
if (!view.view_ref_koid().has_value() ||
view.view_ref_koid().value() != *client_root_view_ref_koid_) {
continue;
}
if (view.children()->empty()) {
return false;
}
// NOTE: We can't rely on the presence of the child view in
// `view.children()` to guarantee that it has rendered. The child view
// also needs to be present in `last_view_tree_snapshot_->views`.
return std::count_if(last_view_tree_snapshot_->views()->begin(),
last_view_tree_snapshot_->views()->end(),
[view_to_find = view.children()->back()](const auto& view_to_check) {
return view_to_check.view_ref_koid().has_value() &&
view_to_check.view_ref_koid().value() == view_to_find;
}) > 0;
}
return false;
});
FX_LOGS(INFO) << "Embedded view has rendered";
}
Screenshot PortableUITest::TakeScreenshot(ScreenshotFormat format) {
if (!screenshotter_.is_valid()) {
auto connect = realm_root()->component().Connect<fuchsia_ui_composition::Screenshot>();
ZX_ASSERT_OK(connect);
screenshotter_ = fidl::SyncClient(std::move(connect.value()));
}
FX_LOGS(INFO) << "Taking screenshot... ";
auto res = screenshotter_->Take({{.format = format}});
ZX_ASSERT_OK(res);
FX_LOGS(INFO) << "Screenshot captured.";
if (format == ScreenshotFormat::kPng) {
return Screenshot(res->vmo().value());
}
return Screenshot(res->vmo().value(), display_size().width(), display_size().height(),
display_rotation());
}
bool PortableUITest::TakeScreenshotUntil(
fit::function<bool(const Screenshot&)> screenshot_predicate, zx::duration predicate_timeout,
zx::duration step, ScreenshotFormat format) {
return RunLoopWithTimeoutOrUntil(
[this, &screenshot_predicate, &format] {
return screenshot_predicate(TakeScreenshot(format));
},
predicate_timeout, step);
}
fuchsia_math::SizeU PortableUITest::display_size() {
if (display_size_)
return display_size_.value();
auto display_info_connect =
realm_root()->component().Connect<fuchsia_ui_display_singleton::Info>();
ZX_ASSERT_OK(display_info_connect);
fidl::SyncClient<fuchsia_ui_display_singleton::Info> display_info(
std::move(display_info_connect.value()));
auto get_metrics_res = display_info->GetMetrics();
ZX_ASSERT_OK(get_metrics_res);
display_size_ = get_metrics_res->info().extent_in_px();
return display_size_.value();
}
uint32_t PortableUITest::display_width() { return display_size().width(); }
uint32_t PortableUITest::display_height() { return display_size().height(); }
void PortableUITest::RegisterTouchScreen() {
FX_LOGS(INFO) << "Registering fake touch screen";
ConnectInputRegistry();
auto [register_touch_client, register_touch_server] =
fidl::Endpoints<fuchsia_ui_test_input::TouchScreen>::Create();
fake_touchscreen_ = fidl::SyncClient(std::move(register_touch_client));
ZX_ASSERT_OK(input_registry_->RegisterTouchScreen(
{{.device = std::move(register_touch_server),
.coordinate_unit = fuchsia_ui_test_input::CoordinateUnit::kPhysicalPixels}}));
FX_LOGS(INFO) << "Touchscreen registered";
}
void PortableUITest::InjectTap(int32_t x, int32_t y) {
fuchsia_ui_test_input::TouchScreenSimulateTapRequest tap_request;
tap_request.tap_location() = fuchsia_math::Vec{{.x = x, .y = y}};
FX_LOGS(INFO) << "Injecting tap at (" << tap_request.tap_location()->x() << ", "
<< tap_request.tap_location()->y() << ")";
fake_touchscreen_->SimulateTap(tap_request);
++touch_injection_request_count_;
FX_LOGS(INFO) << "*** Tap injected, count: " << touch_injection_request_count_;
}
void PortableUITest::InjectTapWithRetry(int32_t x, int32_t y) {
InjectTap(x, y);
async::PostDelayedTask(
dispatcher(), [this, x, y] { InjectTapWithRetry(x, y); }, kTapRetryInterval);
}
void PortableUITest::InjectSwipe(int start_x, int start_y, int end_x, int end_y,
int move_event_count) {
fuchsia_ui_test_input::TouchScreenSimulateSwipeRequest swipe_request;
swipe_request.start_location() = {{.x = start_x, .y = start_y}};
swipe_request.end_location() = {{.x = end_x, .y = end_y}};
swipe_request.move_event_count() = move_event_count;
FX_LOGS(INFO) << "Injecting swipe from (" << swipe_request.start_location()->x() << ", "
<< swipe_request.start_location()->y() << ") to ("
<< swipe_request.end_location()->x() << ", " << swipe_request.end_location()->y()
<< ") with move_event_count = " << swipe_request.move_event_count().value();
fake_touchscreen_->SimulateSwipe(swipe_request);
touch_injection_request_count_++;
FX_LOGS(INFO) << "*** Swipe injected";
}
void PortableUITest::InjectTouchEvent(fuchsia_input_report::TouchInputReport report) {
fuchsia_ui_test_input::TouchScreenSimulateTouchEventRequest request;
request.report() = std::move(report);
fake_touchscreen_->SimulateTouchEvent(request);
touch_injection_request_count_++;
FX_LOGS(INFO) << "*** Touch event injected";
}
void PortableUITest::RegisterMouse() {
FX_LOGS(INFO) << "Registering fake mouse";
ConnectInputRegistry();
auto [register_mouse_client, register_mouse_server] =
fidl::Endpoints<fuchsia_ui_test_input::Mouse>::Create();
fake_mouse_ = fidl::SyncClient(std::move(register_mouse_client));
ZX_ASSERT_OK(input_registry_->RegisterMouse({{.device = std::move(register_mouse_server)}}));
FX_LOGS(INFO) << "Mouse registered";
}
void PortableUITest::SimulateMouseEvent(
const std::vector<fuchsia_ui_test_input::MouseButton>& pressed_buttons, int movement_x,
int movement_y) {
FX_LOGS(INFO) << "Requesting mouse event";
ZX_ASSERT_OK(fake_mouse_->SimulateMouseEvent(
{{.pressed_buttons = pressed_buttons, .movement_x = movement_x, .movement_y = movement_y}}));
}
void PortableUITest::SimulateMouseScroll(
const std::vector<fuchsia_ui_test_input::MouseButton>& pressed_buttons, int scroll_x,
int scroll_y, bool use_physical_units) {
FX_LOGS(INFO) << "Requesting mouse scroll";
fuchsia_ui_test_input::MouseSimulateMouseEventRequest request({
.pressed_buttons = pressed_buttons,
});
if (use_physical_units) {
request.scroll_h_physical_pixel() = scroll_x;
request.scroll_v_physical_pixel() = scroll_y;
} else {
request.scroll_h_detent() = scroll_x;
request.scroll_v_detent() = scroll_y;
}
ZX_ASSERT_OK(fake_mouse_->SimulateMouseEvent(request));
}
void PortableUITest::RegisterKeyboard() {
FX_LOGS(INFO) << "Registering fake keyboard";
ConnectInputRegistry();
auto [register_kb_client, register_kb_server] =
fidl::Endpoints<fuchsia_ui_test_input::Keyboard>::Create();
fake_keyboard_ = fidl::SyncClient(std::move(register_kb_client));
ZX_ASSERT_OK(input_registry_->RegisterKeyboard({{.device = std::move(register_kb_server)}}));
FX_LOGS(INFO) << "Keyboard registered";
}
void PortableUITest::SimulateUsAsciiTextEntry(const std::string& str) {
FX_LOGS(INFO) << "Requesting key event";
ZX_ASSERT_OK(fake_keyboard_->SimulateUsAsciiTextEntry({{.text = str}}));
}
void PortableUITest::ConnectInputRegistry() {
if (!input_registry_.is_valid()) {
auto res = realm_->component().Connect<fuchsia_ui_test_input::Registry>();
ZX_ASSERT_OK(res);
input_registry_ = fidl::SyncClient(std::move(res.value()));
}
}
} // namespace ui_testing