| // Copyright 2013 The Flutter 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 "portable_ui_test.h" |
| |
| #include <fuchsia/logger/cpp/fidl.h> |
| #include <fuchsia/tracing/provider/cpp/fidl.h> |
| #include <fuchsia/ui/app/cpp/fidl.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/sys/component/cpp/testing/realm_builder.h> |
| #include <lib/sys/component/cpp/testing/realm_builder_types.h> |
| |
| #include "check_view.h" |
| #include "flutter/fml/logging.h" |
| |
| namespace fuchsia_test_utils { |
| namespace { |
| |
| // Types imported for the realm_builder library. |
| using component_testing::ChildOptions; |
| using component_testing::ChildRef; |
| using component_testing::ParentRef; |
| using component_testing::Protocol; |
| using component_testing::RealmRoot; |
| using component_testing::Route; |
| |
| using fuchsia_test_utils::CheckViewExistsInSnapshot; |
| |
| } // namespace |
| |
| void PortableUITest::SetUp(bool build_realm) { |
| SetUpRealmBase(); |
| ExtendRealm(); |
| |
| if (build_realm) { |
| BuildRealm(); |
| } |
| } |
| |
| void PortableUITest::BuildRealm() { |
| realm_ = std::make_unique<RealmRoot>(realm_builder_.Build()); |
| } |
| |
| void PortableUITest::SetUpRealmBase() { |
| FML_LOG(INFO) << "Setting up realm base"; |
| |
| // Add Flutter JIT runner as a child of the RealmBuilder |
| realm_builder_.AddChild(kFlutterJitRunner, kFlutterJitRunnerUrl); |
| |
| // Add environment providing the Flutter JIT runner |
| fuchsia::component::decl::Environment flutter_runner_environment; |
| flutter_runner_environment.set_name(kFlutterRunnerEnvironment); |
| flutter_runner_environment.set_extends( |
| fuchsia::component::decl::EnvironmentExtends::REALM); |
| flutter_runner_environment.set_runners({}); |
| auto environment_runners = flutter_runner_environment.mutable_runners(); |
| |
| // Add Flutter JIT runner to the environment |
| fuchsia::component::decl::RunnerRegistration flutter_jit_runner_reg; |
| flutter_jit_runner_reg.set_source(fuchsia::component::decl::Ref::WithChild( |
| fuchsia::component::decl::ChildRef{.name = kFlutterJitRunner})); |
| flutter_jit_runner_reg.set_source_name(kFlutterJitRunner); |
| flutter_jit_runner_reg.set_target_name(kFlutterJitRunner); |
| environment_runners->push_back(std::move(flutter_jit_runner_reg)); |
| auto realm_decl = realm_builder_.GetRealmDecl(); |
| if (!realm_decl.has_environments()) { |
| realm_decl.set_environments({}); |
| } |
| auto realm_environments = realm_decl.mutable_environments(); |
| realm_environments->push_back(std::move(flutter_runner_environment)); |
| realm_builder_.ReplaceRealmDecl(std::move(realm_decl)); |
| |
| // 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::sysmem::Allocator::Name_}, |
| Protocol{fuchsia::tracing::provider::Registry::Name_}, |
| Protocol{fuchsia::ui::input::ImeService::Name_}, |
| Protocol{kPosixSocketProviderName}, |
| Protocol{kVulkanLoaderServiceName}, |
| component_testing::Directory{"config-data"}}, |
| .source = ParentRef(), |
| .targets = {kFlutterJitRunnerRef, kTestUIStackRef}}); |
| |
| // Route UI capabilities to test driver and Flutter runner |
| realm_builder_.AddRoute(Route{ |
| .capabilities = {Protocol{fuchsia::ui::composition::Allocator::Name_}, |
| Protocol{fuchsia::ui::composition::Flatland::Name_}, |
| Protocol{fuchsia::ui::scenic::Scenic::Name_}, |
| Protocol{fuchsia::ui::test::input::Registry::Name_}, |
| Protocol{fuchsia::ui::test::scene::Controller::Name_}, |
| Protocol{fuchsia::ui::display::singleton::Info::Name_}, |
| Protocol{kPointerInjectorRegistryName}}, |
| .source = kTestUIStackRef, |
| .targets = {ParentRef(), kFlutterJitRunnerRef}}); |
| } |
| |
| 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) { |
| FML_LOG(INFO) << "View Tree watcher channel overflowed"; |
| } else if (error | |
| fuchsia::ui::observation::geometry::Error::BUFFER_OVERFLOW) { |
| FML_LOG(INFO) << "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 |
| FML_LOG(WARNING) |
| << "View Tree watcher attempted to report too many views"; |
| } |
| } |
| } |
| |
| void PortableUITest::WatchViewGeometry() { |
| FML_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(); |
| }); |
| } |
| |
| 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() { |
| scene_provider_ = |
| realm_->component().Connect<fuchsia::ui::test::scene::Controller>(); |
| scene_provider_.set_error_handler([](auto) { |
| FML_LOG(ERROR) << "Error from test scene provider: " |
| << &zx_status_get_string; |
| }); |
| |
| 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; |
| }); |
| |
| FML_LOG(INFO) << "Waiting for client view ref koid"; |
| RunLoopUntil([this] { return client_root_view_ref_koid_.has_value(); }); |
| |
| WatchViewGeometry(); |
| |
| FML_LOG(INFO) << "Waiting for client view to connect"; |
| // Wait for the client view to get attached to the view tree. |
| RunLoopUntil( |
| [this] { return HasViewConnected(*client_root_view_ref_koid_); }); |
| FML_LOG(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; |
| }); |
| |
| FML_LOG(INFO) << "Embedded view has rendered"; |
| } |
| |
| void PortableUITest::RegisterTouchScreen() { |
| FML_LOG(INFO) << "Registering fake touch screen"; |
| input_registry_ = |
| realm_->component().Connect<fuchsia::ui::test::input::Registry>(); |
| input_registry_.set_error_handler([](auto) { |
| FML_LOG(ERROR) << "Error from input helper: " << &zx_status_get_string; |
| }); |
| |
| bool touchscreen_registered = false; |
| fuchsia::ui::test::input::RegistryRegisterTouchScreenRequest request; |
| request.set_device(fake_touchscreen_.NewRequest()); |
| input_registry_->RegisterTouchScreen( |
| std::move(request), |
| [&touchscreen_registered]() { touchscreen_registered = true; }); |
| |
| RunLoopUntil([&touchscreen_registered] { return touchscreen_registered; }); |
| FML_LOG(INFO) << "Touchscreen registered"; |
| } |
| |
| void PortableUITest::RegisterMouse() { |
| FML_LOG(INFO) << "Registering fake mouse"; |
| input_registry_ = |
| realm_->component().Connect<fuchsia::ui::test::input::Registry>(); |
| input_registry_.set_error_handler([](auto) { |
| FML_LOG(ERROR) << "Error from input helper: " << &zx_status_get_string; |
| }); |
| |
| 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; }); |
| FML_LOG(INFO) << "Mouse registered"; |
| } |
| |
| void PortableUITest::RegisterKeyboard() { |
| FML_LOG(INFO) << "Registering fake keyboard"; |
| input_registry_ = |
| realm_->component().Connect<fuchsia::ui::test::input::Registry>(); |
| input_registry_.set_error_handler([](auto) { |
| FML_LOG(ERROR) << "Error from input helper: " << &zx_status_get_string; |
| }); |
| |
| bool keyboard_registered = false; |
| fuchsia::ui::test::input::RegistryRegisterKeyboardRequest request; |
| request.set_device(fake_keyboard_.NewRequest()); |
| input_registry_->RegisterKeyboard( |
| std::move(request), |
| [&keyboard_registered]() { keyboard_registered = true; }); |
| |
| RunLoopUntil([&keyboard_registered] { return keyboard_registered; }); |
| FML_LOG(INFO) << "Keyboard 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; |
| |
| FML_LOG(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_; |
| FML_LOG(INFO) << "*** Tap injected, count: " |
| << touch_injection_request_count_; |
| }); |
| } |
| |
| void PortableUITest::SimulateMouseEvent( |
| std::vector<fuchsia::ui::test::input::MouseButton> pressed_buttons, |
| int movement_x, |
| int movement_y) { |
| 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); |
| |
| FML_LOG(INFO) << "Injecting mouse input"; |
| |
| fake_mouse_->SimulateMouseEvent( |
| std::move(request), [] { FML_LOG(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) { |
| FML_LOG(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), [] { |
| FML_LOG(INFO) << "Mouse scroll event injected"; |
| }); |
| } |
| |
| void PortableUITest::SimulateTextEntry(std::string text) { |
| FML_LOG(INFO) << "Sending text request"; |
| bool done = false; |
| |
| fuchsia::ui::test::input::KeyboardSimulateUsAsciiTextEntryRequest request; |
| request.set_text(text); |
| fake_keyboard_->SimulateUsAsciiTextEntry(std::move(request), |
| [&done]() { done = true; }); |
| |
| RunLoopUntil([&] { return done; }); |
| FML_LOG(INFO) << "Text request sent"; |
| } |
| |
| } // namespace fuchsia_test_utils |