| // 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 <fuchsia/logger/cpp/fidl.h> |
| #include <fuchsia/tracing/provider/cpp/fidl.h> |
| #include <fuchsia/ui/app/cpp/fidl.h> |
| #include <fuchsia/ui/composition/cpp/fidl.h> |
| #include <fuchsia/ui/display/singleton/cpp/fidl.h> |
| #include <fuchsia/ui/observation/geometry/cpp/fidl.h> |
| #include <fuchsia/ui/test/input/cpp/fidl.h> |
| #include <fuchsia/ui/test/scene/cpp/fidl.h> |
| #include <lib/async-loop/testing/cpp/real_loop.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/fidl/cpp/binding_set.h> |
| #include <lib/sys/component/cpp/testing/realm_builder.h> |
| #include <lib/sys/component/cpp/testing/realm_builder_types.h> |
| #include <lib/zx/clock.h> |
| #include <zircon/status.h> |
| #include <zircon/time.h> |
| |
| #include <optional> |
| #include <vector> |
| |
| #include "flutter/fml/logging.h" |
| #include "gtest/gtest.h" |
| |
| #include "flutter/shell/platform/fuchsia/flutter/tests/integration/utils/check_view.h" |
| #include "flutter/shell/platform/fuchsia/flutter/tests/integration/utils/screenshot.h" |
| |
| namespace flutter_embedder_test { |
| namespace { |
| |
| // Types imported for the realm_builder library. |
| using component_testing::ChildOptions; |
| using component_testing::ChildRef; |
| using component_testing::DirectoryContents; |
| using component_testing::ParentRef; |
| using component_testing::Protocol; |
| using component_testing::RealmRoot; |
| using component_testing::Route; |
| using component_testing::StartupMode; |
| |
| using fuchsia_test_utils::CheckViewExistsInUpdates; |
| |
| // The FIDL bindings for this service are not exposed in the Fuchsia SDK, so we |
| // must encode the name manually here. |
| constexpr auto kVulkanLoaderServiceName = "fuchsia.vulkan.loader.Loader"; |
| |
| constexpr auto kFlutterJitRunnerUrl = |
| "fuchsia-pkg://fuchsia.com/oot_flutter_jit_runner#meta/" |
| "flutter_jit_runner.cm"; |
| constexpr auto kFlutterJitProductRunnerUrl = |
| "fuchsia-pkg://fuchsia.com/oot_flutter_jit_product_runner#meta/" |
| "flutter_jit_product_runner.cm"; |
| constexpr auto kFlutterAotRunnerUrl = |
| "fuchsia-pkg://fuchsia.com/oot_flutter_aot_runner#meta/" |
| "flutter_aot_runner.cm"; |
| constexpr auto kFlutterAotProductRunnerUrl = |
| "fuchsia-pkg://fuchsia.com/oot_flutter_aot_product_runner#meta/" |
| "flutter_aot_product_runner.cm"; |
| constexpr char kChildViewUrl[] = |
| "fuchsia-pkg://fuchsia.com/child-view#meta/child-view.cm"; |
| constexpr char kParentViewUrl[] = |
| "fuchsia-pkg://fuchsia.com/parent-view#meta/parent-view.cm"; |
| static constexpr auto kTestUiStackUrl = |
| "fuchsia-pkg://fuchsia.com/flatland-scene-manager-test-ui-stack#meta/" |
| "test-ui-stack.cm"; |
| |
| constexpr auto kFlutterRunnerEnvironment = "flutter_runner_env"; |
| constexpr auto kFlutterJitRunner = "flutter_jit_runner"; |
| constexpr auto kFlutterJitRunnerRef = ChildRef{kFlutterJitRunner}; |
| constexpr auto kFlutterJitProductRunner = "flutter_jit_product_runner"; |
| constexpr auto kFlutterJitProductRunnerRef = ChildRef{kFlutterJitProductRunner}; |
| constexpr auto kFlutterAotRunner = "flutter_aot_runner"; |
| constexpr auto kFlutterAotRunnerRef = ChildRef{kFlutterAotRunner}; |
| constexpr auto kFlutterAotProductRunner = "flutter_aot_product_runner"; |
| constexpr auto kFlutterAotProductRunnerRef = ChildRef{kFlutterAotProductRunner}; |
| constexpr auto kChildView = "child_view"; |
| constexpr auto kChildViewRef = ChildRef{kChildView}; |
| constexpr auto kParentView = "parent_view"; |
| constexpr auto kParentViewRef = ChildRef{kParentView}; |
| constexpr auto kTestUiStack = "ui"; |
| constexpr auto kTestUiStackRef = ChildRef{kTestUiStack}; |
| |
| // Background and foreground color values. |
| const fuchsia_test_utils::Pixel kParentBackgroundColor(0xFF, |
| 0x00, |
| 0x00, |
| 0xFF); // Blue |
| const fuchsia_test_utils::Pixel kChildBackgroundColor(0xFF, |
| 0x00, |
| 0xFF, |
| 0xFF); // Pink |
| const fuchsia_test_utils::Pixel kFlatlandOverlayColor(0x00, |
| 0xFF, |
| 0x00, |
| 0xFF); // Green |
| |
| static uint32_t OverlayPixelCount( |
| std::map<fuchsia_test_utils::Pixel, uint32_t>& histogram) { |
| return histogram[kFlatlandOverlayColor]; |
| } |
| |
| // Timeout for |TakeScreenshot| FIDL call. |
| constexpr zx::duration kScreenshotTimeout = zx::sec(10); |
| // Timeout to fail the test if it goes beyond this duration. |
| constexpr zx::duration kTestTimeout = zx::min(1); |
| |
| } // namespace |
| |
| class FlutterEmbedderTest : public ::loop_fixture::RealLoop, |
| public ::testing::Test { |
| public: |
| FlutterEmbedderTest() |
| : realm_builder_(component_testing::RealmBuilder::Create()) { |
| FML_VLOG(-1) << "Setting up base realm"; |
| SetUpRealmBase(); |
| |
| // Post a "just in case" quit task, if the test hangs. |
| async::PostDelayedTask( |
| dispatcher(), |
| [] { |
| FML_LOG(FATAL) |
| << "\n\n>> Test did not complete in time, terminating. <<\n\n"; |
| }, |
| kTestTimeout); |
| } |
| |
| bool HasViewConnected( |
| const fuchsia::ui::observation::geometry::ViewTreeWatcherPtr& |
| view_tree_watcher, |
| std::optional<fuchsia::ui::observation::geometry::WatchResponse>& |
| watch_response, |
| zx_koid_t view_ref_koid); |
| |
| void LaunchParentViewInRealm( |
| const std::vector<std::string>& component_args = {}); |
| |
| fuchsia_test_utils::Screenshot TakeScreenshot(); |
| |
| bool TakeScreenshotUntil( |
| fuchsia_test_utils::Pixel color, |
| fit::function<void(std::map<fuchsia_test_utils::Pixel, uint32_t>)> |
| callback = nullptr, |
| zx::duration timeout = kTestTimeout); |
| |
| private: |
| void SetUpRealmBase(); |
| |
| fuchsia::ui::test::scene::ControllerPtr scene_provider_; |
| fuchsia::ui::observation::geometry::ViewTreeWatcherPtr view_tree_watcher_; |
| fuchsia::ui::composition::ScreenshotPtr screenshot_; |
| |
| // Wrapped in optional since the view is not created until the middle of SetUp |
| component_testing::RealmBuilder realm_builder_; |
| std::unique_ptr<component_testing::RealmRoot> realm_; |
| |
| uint64_t display_width_ = 0; |
| uint64_t display_height_ = 0; |
| }; |
| |
| void FlutterEmbedderTest::SetUpRealmBase() { |
| FML_LOG(INFO) << "Setting up realm base."; |
| |
| // First, add the flutter runner(s) as children. |
| realm_builder_.AddChild(kFlutterJitRunner, kFlutterJitRunnerUrl); |
| realm_builder_.AddChild(kFlutterJitProductRunner, |
| kFlutterJitProductRunnerUrl); |
| realm_builder_.AddChild(kFlutterAotRunner, kFlutterAotRunnerUrl); |
| realm_builder_.AddChild(kFlutterAotProductRunner, |
| kFlutterAotProductRunnerUrl); |
| |
| // Then, add an environment providing them. |
| 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(); |
| 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)); |
| fuchsia::component::decl::RunnerRegistration flutter_jit_product_runner_reg; |
| flutter_jit_product_runner_reg.set_source( |
| fuchsia::component::decl::Ref::WithChild( |
| fuchsia::component::decl::ChildRef{.name = |
| kFlutterJitProductRunner})); |
| flutter_jit_product_runner_reg.set_source_name(kFlutterJitProductRunner); |
| flutter_jit_product_runner_reg.set_target_name(kFlutterJitProductRunner); |
| environment_runners->push_back(std::move(flutter_jit_product_runner_reg)); |
| fuchsia::component::decl::RunnerRegistration flutter_aot_runner_reg; |
| flutter_aot_runner_reg.set_source(fuchsia::component::decl::Ref::WithChild( |
| fuchsia::component::decl::ChildRef{.name = kFlutterAotRunner})); |
| flutter_aot_runner_reg.set_source_name(kFlutterAotRunner); |
| flutter_aot_runner_reg.set_target_name(kFlutterAotRunner); |
| environment_runners->push_back(std::move(flutter_aot_runner_reg)); |
| fuchsia::component::decl::RunnerRegistration flutter_aot_product_runner_reg; |
| flutter_aot_product_runner_reg.set_source( |
| fuchsia::component::decl::Ref::WithChild( |
| fuchsia::component::decl::ChildRef{.name = |
| kFlutterAotProductRunner})); |
| flutter_aot_product_runner_reg.set_source_name(kFlutterAotProductRunner); |
| flutter_aot_product_runner_reg.set_target_name(kFlutterAotProductRunner); |
| environment_runners->push_back(std::move(flutter_aot_product_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, kTestUiStackUrl); |
| |
| // Add embedded parent and child components. |
| realm_builder_.AddChild(kChildView, kChildViewUrl, |
| ChildOptions{ |
| .environment = kFlutterRunnerEnvironment, |
| }); |
| realm_builder_.AddChild(kParentView, kParentViewUrl, |
| ChildOptions{ |
| .environment = kFlutterRunnerEnvironment, |
| }); |
| |
| // Route base system services to flutter runners. |
| realm_builder_.AddRoute( |
| Route{.capabilities = |
| { |
| Protocol{fuchsia::logger::LogSink::Name_}, |
| Protocol{fuchsia::sysmem::Allocator::Name_}, |
| Protocol{fuchsia::tracing::provider::Registry::Name_}, |
| Protocol{kVulkanLoaderServiceName}, |
| }, |
| .source = ParentRef{}, |
| .targets = {kFlutterJitRunnerRef, kFlutterJitProductRunnerRef, |
| kFlutterAotRunnerRef, kFlutterAotProductRunnerRef}}); |
| |
| // Route base system services to 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{kVulkanLoaderServiceName}}, |
| .source = ParentRef{}, |
| .targets = {kTestUiStackRef}}); |
| |
| // Route UI capabilities from test UI stack to flutter runners. |
| realm_builder_.AddRoute(Route{ |
| .capabilities = {Protocol{fuchsia::ui::composition::Allocator::Name_}, |
| Protocol{fuchsia::ui::composition::Flatland::Name_}}, |
| .source = kTestUiStackRef, |
| .targets = {kFlutterJitRunnerRef, kFlutterJitProductRunnerRef, |
| kFlutterAotRunnerRef, kFlutterAotProductRunnerRef}}); |
| |
| // Route test capabilities from test UI stack to test driver. |
| realm_builder_.AddRoute(Route{ |
| .capabilities = {Protocol{fuchsia::ui::composition::Screenshot::Name_}, |
| Protocol{fuchsia::ui::test::input::Registry::Name_}, |
| Protocol{fuchsia::ui::test::scene::Controller::Name_}, |
| Protocol{fuchsia::ui::display::singleton::Info::Name_}}, |
| .source = kTestUiStackRef, |
| .targets = {ParentRef()}}); |
| |
| // Route ViewProvider from child to parent, and parent to test. |
| realm_builder_.AddRoute( |
| Route{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}}, |
| .source = kParentViewRef, |
| .targets = {ParentRef()}}); |
| realm_builder_.AddRoute( |
| Route{.capabilities = {Protocol{fuchsia::ui::app::ViewProvider::Name_}}, |
| .source = kChildViewRef, |
| .targets = {kParentViewRef}}); |
| } |
| |
| // Checks whether the view with |view_ref_koid| has connected to the view tree. |
| // The response of a f.u.o.g.Provider.Watch call is stored in |watch_response| |
| // if it contains |view_ref_koid|. |
| bool FlutterEmbedderTest::HasViewConnected( |
| const fuchsia::ui::observation::geometry::ViewTreeWatcherPtr& |
| view_tree_watcher, |
| std::optional<fuchsia::ui::observation::geometry::WatchResponse>& |
| watch_response, |
| zx_koid_t view_ref_koid) { |
| std::optional<fuchsia::ui::observation::geometry::WatchResponse> watch_result; |
| view_tree_watcher->Watch( |
| [&watch_result](auto response) { watch_result = std::move(response); }); |
| FML_LOG(INFO) << "Waiting for view tree watch result"; |
| RunLoopUntil([&watch_result] { return watch_result.has_value(); }); |
| FML_LOG(INFO) << "Received for view tree watch result"; |
| if (CheckViewExistsInUpdates(watch_result->updates(), view_ref_koid)) { |
| watch_response = std::move(watch_result); |
| }; |
| return watch_response.has_value(); |
| } |
| |
| void FlutterEmbedderTest::LaunchParentViewInRealm( |
| const std::vector<std::string>& component_args) { |
| FML_LOG(INFO) << "Launching parent-view"; |
| |
| if (!component_args.empty()) { |
| // Construct a args.csv file containing the specified comma-separated |
| // component args. |
| std::string csv; |
| for (const auto& arg : component_args) { |
| csv += arg + ','; |
| } |
| // Remove last comma. |
| csv.pop_back(); |
| |
| auto config_directory_contents = DirectoryContents(); |
| config_directory_contents.AddFile("args.csv", csv); |
| realm_builder_.RouteReadOnlyDirectory("config-data", {kParentViewRef}, |
| std::move(config_directory_contents)); |
| } |
| realm_ = std::make_unique<RealmRoot>(realm_builder_.Build()); |
| |
| // Get the display information using the |fuchsia.ui.display.singleton.Info|. |
| std::optional<bool> display_metrics_obtained; |
| fuchsia::ui::display::singleton::InfoPtr display_info = |
| realm_->component().Connect<fuchsia::ui::display::singleton::Info>(); |
| display_info->GetMetrics([this, &display_metrics_obtained](auto info) { |
| display_width_ = info.extent_in_px().width; |
| display_height_ = info.extent_in_px().height; |
| display_metrics_obtained = true; |
| }); |
| RunLoopUntil([&display_metrics_obtained] { |
| return display_metrics_obtained.has_value(); |
| }); |
| FML_LOG(INFO) << "Got display_width " << display_width_ << " display_height " |
| << display_height_; |
| |
| // Instruct Test UI Stack to present parent-view's View. |
| std::optional<zx_koid_t> view_ref_koid; |
| scene_provider_ = |
| realm_->component().Connect<fuchsia::ui::test::scene::Controller>(); |
| scene_provider_.set_error_handler( |
| [](auto) { FML_LOG(ERROR) << "Error from test scene provider"; }); |
| 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), [&view_ref_koid](auto client_view_ref_koid) { |
| view_ref_koid = client_view_ref_koid; |
| }); |
| |
| FML_LOG(INFO) << "Waiting for client view ref koid"; |
| RunLoopUntil([&view_ref_koid] { return view_ref_koid.has_value(); }); |
| |
| // Wait for the client view to get attached to the view tree. |
| std::optional<fuchsia::ui::observation::geometry::WatchResponse> |
| watch_response; |
| FML_LOG(INFO) << "Waiting for client view to render; koid is " |
| << (view_ref_koid.has_value() ? view_ref_koid.value() : 0); |
| RunLoopUntil([this, &watch_response, &view_ref_koid] { |
| return HasViewConnected(view_tree_watcher_, watch_response, *view_ref_koid); |
| }); |
| FML_LOG(INFO) << "Client view has rendered"; |
| |
| screenshot_ = |
| realm_->component().Connect<fuchsia::ui::composition::Screenshot>(); |
| FML_LOG(INFO) << "Launched parent-view"; |
| } |
| |
| fuchsia_test_utils::Screenshot FlutterEmbedderTest::TakeScreenshot() { |
| FML_LOG(INFO) << "Taking screenshot... "; |
| |
| fuchsia::ui::composition::ScreenshotTakeRequest request; |
| request.set_format(fuchsia::ui::composition::ScreenshotFormat::BGRA_RAW); |
| |
| std::optional<fuchsia::ui::composition::ScreenshotTakeResponse> response; |
| screenshot_->Take(std::move(request), [this, &response](auto screenshot) { |
| response = std::move(screenshot); |
| QuitLoop(); |
| }); |
| |
| EXPECT_FALSE(RunLoopWithTimeout(kScreenshotTimeout)) |
| << "Timed out waiting for screenshot."; |
| FML_LOG(INFO) << "Screenshot captured."; |
| |
| return fuchsia_test_utils::Screenshot( |
| response->vmo(), display_width_, display_height_, /*display_rotation*/ 0); |
| } |
| |
| bool FlutterEmbedderTest::TakeScreenshotUntil( |
| fuchsia_test_utils::Pixel color, |
| fit::function<void(std::map<fuchsia_test_utils::Pixel, uint32_t>)> callback, |
| zx::duration timeout) { |
| return RunLoopWithTimeoutOrUntil( |
| [this, &callback, &color] { |
| auto screenshot = TakeScreenshot(); |
| auto histogram = screenshot.Histogram(); |
| |
| bool color_found = histogram[color] > 0; |
| if (color_found && callback != nullptr) { |
| callback(std::move(histogram)); |
| } |
| return color_found; |
| }, |
| timeout); |
| } |
| |
| TEST_F(FlutterEmbedderTest, Embedding) { |
| LaunchParentViewInRealm(); |
| |
| // Take screenshot until we see the child-view's embedded color. |
| ASSERT_TRUE(TakeScreenshotUntil( |
| kChildBackgroundColor, |
| [](std::map<fuchsia_test_utils::Pixel, uint32_t> histogram) { |
| // Expect parent and child background colors, with parent color > child |
| // color. |
| EXPECT_GT(histogram[kParentBackgroundColor], 0u); |
| EXPECT_GT(histogram[kChildBackgroundColor], 0u); |
| EXPECT_GT(histogram[kParentBackgroundColor], |
| histogram[kChildBackgroundColor]); |
| })); |
| } |
| |
| TEST_F(FlutterEmbedderTest, EmbeddingWithOverlay) { |
| LaunchParentViewInRealm({"--showOverlay"}); |
| |
| // Take screenshot until we see the child-view's embedded color. |
| ASSERT_TRUE(TakeScreenshotUntil( |
| kChildBackgroundColor, |
| [](std::map<fuchsia_test_utils::Pixel, uint32_t> histogram) { |
| // Expect parent, overlay and child background colors. |
| // With parent color > child color and overlay color > child color. |
| const uint32_t overlay_pixel_count = OverlayPixelCount(histogram); |
| EXPECT_GT(histogram[kParentBackgroundColor], 0u); |
| EXPECT_GT(overlay_pixel_count, 0u); |
| EXPECT_GT(histogram[kChildBackgroundColor], 0u); |
| EXPECT_GT(histogram[kParentBackgroundColor], |
| histogram[kChildBackgroundColor]); |
| EXPECT_GT(overlay_pixel_count, histogram[kChildBackgroundColor]); |
| })); |
| } |
| |
| } // namespace flutter_embedder_test |