blob: eaa6ef93ab15dee778c606a0701c75fb2f166fa1 [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 <fuchsia/logger/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/display/singleton/cpp/fidl.h>
#include <fuchsia/vulkan/loader/cpp/fidl.h>
#include <lib/async/cpp/task.h>
#include <lib/sys/component/cpp/testing/realm_builder_types.h>
#include <lib/syslog/cpp/macros.h>
namespace ui_testing {
namespace {
// Types imported for the realm_builder library.
using component_testing::ChildRef;
using component_testing::ConfigValue;
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.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;
}
// Timeout for taking a screenshot.
constexpr zx::duration kScreenshotTimeout = zx::sec(10);
} // 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{fuchsia::logger::LogSink::Name_},
Protocol{fuchsia::scheduler::RoleManager::Name_},
Protocol{fuchsia::sysmem::Allocator::Name_},
Protocol{fuchsia::sysmem2::Allocator::Name_},
Protocol{fuchsia::vulkan::loader::Loader::Name_},
Protocol{fuchsia::tracing::provider::Registry::Name_}},
.source = ParentRef{},
.targets = {kTestUIStackRef}});
// Route UI capabilities from test-ui-stack to test driver.
realm_builder_.AddRoute(
Route{.capabilities = {Protocol{fuchsia::ui::composition::Screenshot::Name_},
Protocol{fuchsia::ui::display::singleton::Info::Name_},
Protocol{fuchsia::ui::test::input::Registry::Name_},
Protocol{fuchsia::ui::test::scene::Controller::Name_}},
.source = kTestUIStackRef,
.targets = {ParentRef{}}});
// Configure test-ui-stack.
realm_builder_.InitMutableConfigToEmpty(kTestUIStack);
realm_builder_.SetConfigValue(kTestUIStack, "display_rotation",
ConfigValue::Uint32(display_rotation()));
realm_builder_.SetConfigValue(kTestUIStack, "device_pixel_ratio",
ConfigValue(std::to_string(device_pixel_ratio())));
}
void PortableUITest::SetUp() {
SetUpRealmBase();
ExtendRealm();
realm_ = realm_builder_.Build();
}
void PortableUITest::TearDown() {
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.has_error()) {
std::vector<fuchsia::ui::observation::geometry::ViewTreeSnapshot>* updates =
response.mutable_updates();
if (updates && !updates->empty()) {
last_view_tree_snapshot_ = std::move(updates->back());
}
} else {
// Otherwise, process error.
const auto& error = response.error();
if (error | fuchsia::ui::observation::geometry::Error::CHANNEL_OVERFLOW) {
FX_LOGS(DEBUG) << "View Tree watcher channel overflowed";
} else if (error | fuchsia::ui::observation::geometry::Error::BUFFER_OVERFLOW) {
FX_LOGS(DEBUG) << "View Tree watcher buffer overflowed";
} else if (error | fuchsia::ui::observation::geometry::Error::VIEWS_OVERFLOW) {
// 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());
scene_provider_ = realm_->component().Connect<fuchsia::ui::test::scene::Controller>();
scene_provider_.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Error from test scene provider: " << zx_status_get_string(status);
});
}
void PortableUITest::WatchViewGeometry() {
FX_CHECK(view_tree_watcher_) << "View Tree watcher must be registered before calling Watch()";
view_tree_watcher_->Watch([this](auto response) {
ProcessViewGeometryResponse(std::move(response));
WatchViewGeometry();
});
}
void PortableUITest::WaitForViewPresentation() {
SetUpSceneProvider();
bool view_presented = false;
scene_provider_->WatchViewPresentation([&]() { view_presented = true; });
RunLoopUntil([&]() { return view_presented; });
}
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::LaunchClient() {
SetUpSceneProvider();
fuchsia::ui::test::scene::ControllerAttachClientViewRequest request;
request.set_view_provider(realm_->component().Connect<fuchsia::ui::app::ViewProvider>());
scene_provider_->RegisterViewTreeWatcher(view_tree_watcher_.NewRequest(), []() {});
scene_provider_->AttachClientView(std::move(request), [this](auto client_view_ref_koid) {
client_root_view_ref_koid_ = client_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_->has_views()) {
return false;
}
if (!client_root_view_ref_koid_.has_value()) {
return false;
}
for (const auto& view : last_view_tree_snapshot_->views()) {
if (!view.has_view_ref_koid() || view.view_ref_koid() != *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.has_view_ref_koid() &&
view_to_check.view_ref_koid() == view_to_find;
}) > 0;
}
return false;
});
FX_LOGS(INFO) << "Embedded view has rendered";
}
Screenshot PortableUITest::TakeScreenshot(ScreenshotFormat format) {
if (!screenshotter_.has_value()) {
screenshotter_ = realm_root()->component().Connect<fuchsia::ui::composition::Screenshot>();
}
FX_LOGS(INFO) << "Taking screenshot... ";
fuchsia::ui::composition::ScreenshotTakeRequest request;
request.set_format(format);
std::optional<fuchsia::ui::composition::ScreenshotTakeResponse> response;
screenshotter_.value()->Take(std::move(request), [this, &response](auto screenshot) {
response = std::move(screenshot);
QuitLoop();
});
EXPECT_FALSE(RunLoopWithTimeout(kScreenshotTimeout)) << "Timed out waiting for screenshot.";
FX_LOGS(INFO) << "Screenshot captured.";
if (format == ScreenshotFormat::PNG) {
return Screenshot(response->vmo());
}
return Screenshot(response->vmo(), 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();
fuchsia::ui::display::singleton::InfoPtr display_info =
realm_root()->component().Connect<fuchsia::ui::display::singleton::Info>();
fuchsia::math::SizeU size;
display_info->GetMetrics([&size](auto info) { size = info.extent_in_px(); });
RunLoopUntil([&size] { return size.width > 0 && size.height > 0; });
display_size_ = size;
return size;
}
void PortableUITest::RegisterTouchScreen() {
FX_LOGS(INFO) << "Registering fake touch screen";
input_registry_ = realm_->component().Connect<fuchsia::ui::test::input::Registry>();
input_registry_.set_error_handler([](zx_status_t status) {
FX_LOGS(ERROR) << "Error connecting to f.ui.test.input.Registry: "
<< zx_status_get_string(status);
});
bool touchscreen_registered = false;
fuchsia::ui::test::input::RegistryRegisterTouchScreenRequest request;
request.set_device(fake_touchscreen_.NewRequest());
request.set_coordinate_unit(fuchsia::ui::test::input::CoordinateUnit::PHYSICAL_PIXELS);
input_registry_->RegisterTouchScreen(
std::move(request), [&touchscreen_registered]() { touchscreen_registered = true; });
RunLoopUntil([&touchscreen_registered] { return touchscreen_registered; });
FX_LOGS(INFO) << "Touchscreen registered";
}
void PortableUITest::InjectTap(int32_t x, int32_t y) {
fuchsia::ui::test::input::TouchScreenSimulateTapRequest tap_request;
tap_request.mutable_tap_location()->x = x;
tap_request.mutable_tap_location()->y = y;
FX_LOGS(INFO) << "Injecting tap at (" << tap_request.tap_location().x << ", "
<< tap_request.tap_location().y << ")";
fake_touchscreen_->SimulateTap(std::move(tap_request), [this]() {
++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.mutable_start_location()->x = start_x;
swipe_request.mutable_start_location()->y = start_y;
swipe_request.mutable_end_location()->x = end_x;
swipe_request.mutable_end_location()->y = end_y;
swipe_request.set_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();
fake_touchscreen_->SimulateSwipe(std::move(swipe_request), [this]() {
touch_injection_request_count_++;
FX_LOGS(INFO) << "*** Swipe injected";
});
}
void PortableUITest::RegisterMouse() {
FX_LOGS(INFO) << "Registering fake mouse";
input_registry_ = realm_->component().Connect<fuchsia::ui::test::input::Registry>();
input_registry_.set_error_handler([](auto) { FX_LOGS(ERROR) << "Error from input helper"; });
bool mouse_registered = false;
fuchsia::ui::test::input::RegistryRegisterMouseRequest request;
request.set_device(fake_mouse_.NewRequest());
input_registry_->RegisterMouse(std::move(request),
[&mouse_registered]() { mouse_registered = true; });
RunLoopUntil([&mouse_registered] { return mouse_registered; });
FX_LOGS(INFO) << "Mouse registered";
}
void PortableUITest::SimulateMouseEvent(
std::vector<fuchsia::ui::test::input::MouseButton> pressed_buttons, int movement_x,
int movement_y) {
FX_LOGS(INFO) << "Requesting mouse event";
fuchsia::ui::test::input::MouseSimulateMouseEventRequest request;
request.set_pressed_buttons(std::move(pressed_buttons));
request.set_movement_x(movement_x);
request.set_movement_y(movement_y);
fake_mouse_->SimulateMouseEvent(std::move(request),
[] { FX_LOGS(INFO) << "Mouse event injected"; });
}
void PortableUITest::SimulateMouseScroll(
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;
request.set_pressed_buttons(std::move(pressed_buttons));
if (use_physical_units) {
request.set_scroll_h_physical_pixel(scroll_x);
request.set_scroll_v_physical_pixel(scroll_y);
} else {
request.set_scroll_h_detent(scroll_x);
request.set_scroll_v_detent(scroll_y);
}
fake_mouse_->SimulateMouseEvent(std::move(request),
[] { FX_LOGS(INFO) << "Mouse scroll event injected"; });
}
} // namespace ui_testing