| |
| // 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 |