blob: 8b7dbb110a9122727032c038cc0d3cbebf8ddef9 [file] [log] [blame]
// Copyright 2020 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.
#ifndef SRC_UI_TESTS_E2E_FLUTTER_TESTS_EMBEDDER_FLUTTER_EMBEDDER_TEST_H_
#define SRC_UI_TESTS_E2E_FLUTTER_TESTS_EMBEDDER_FLUTTER_EMBEDDER_TEST_H_
#include <fuchsia/ui/policy/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/sys/cpp/testing/test_with_environment.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <lib/zx/clock.h>
#include <zircon/status.h>
#include <zircon/time.h>
#include "src/lib/ui/base_view/embedded_view_utils.h"
#include "src/ui/testing/views/color.h"
#include "src/ui/testing/views/embedder_view.h"
namespace flutter_embedder_test {
/// Defines a list of services that are injected into the test environment. Unlike the
/// injected-services in CMX which are injected per test package, these are injected per test and
/// result in a more hermetic test environment.
constexpr std::size_t NUM_SERVICES = 13;
constexpr std::array<std::pair<const char*, const char*>, NUM_SERVICES> kInjectedServices = {{
// clang-format off
{
"fuchsia.accessibility.semantics.SemanticsManager",
"fuchsia-pkg://fuchsia.com/a11y_manager#meta/a11y_manager.cmx"
},{
"fuchsia.deprecatedtimezone.Timezone",
"fuchsia-pkg://fuchsia.com/timezone#meta/timezone.cmx"
},{
"fuchsia.fonts.Provider",
"fuchsia-pkg://fuchsia.com/fonts#meta/fonts.cmx"
},{
"fuchsia.intl.PropertyProvider",
"fuchsia-pkg://fuchsia.com/intl_property_manager#meta/intl_property_manager.cmx"
},{
"fuchsia.netstack.Netstack",
"fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx"
},{
"fuchsia.posix.socket.Provider",
"fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx"
},{
"fuchsia.tracing.provider.Registry",
"fuchsia-pkg://fuchsia.com/trace_manager#meta/trace_manager.cmx"
},{
"fuchsia.ui.input.ImeService",
"fuchsia-pkg://fuchsia.com/ime_service#meta/ime_service.cmx"
},{
"fuchsia.ui.input.ImeVisibilityService",
"fuchsia-pkg://fuchsia.com/ime_service#meta/ime_service.cmx"
},{
"fuchsia.ui.scenic.Scenic",
"fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cmx"
},{
"fuchsia.ui.pointerinjector.Registry",
"fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cmx"
},{
"fuchsia.ui.policy.Presenter",
"fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx"
},{
"fuchsia.ui.input.InputDeviceRegistry",
"fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx"
},
// clang-format on
}};
// Timeout when waiting on Scenic API calls like |GetDisplayInfo|.
constexpr zx::duration kCallTimeout = zx::sec(5);
// Timeout for Scenic's |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);
class FlutterEmbedderTestsBase : public sys::testing::TestWithEnvironment {
public:
explicit FlutterEmbedderTestsBase(
const std::array<std::pair<const char*, const char*>, NUM_SERVICES> injected_services)
: injected_services_(injected_services) {}
// |testing::Test|
void SetUp() override {
TestWithEnvironment::SetUp();
// This is done in |SetUp| as opposed to the constructor to allow subclasses the opportunity to
// override |CreateServices()|.
auto services = TestWithEnvironment::CreateServices();
CreateServices(services);
// Add test-specific launchable services.
for (const auto& service_info : injected_services_) {
zx_status_t status =
services->AddServiceWithLaunchInfo({.url = service_info.second}, service_info.first);
FX_CHECK(status == ZX_OK) << "Failed to add service " << service_info.first;
}
environment_ = CreateNewEnclosingEnvironment("flutter-embedder-tests", std::move(services),
{.inherit_parent_services = true});
WaitForEnclosingEnvToStart(environment());
FX_VLOGS(1) << "Created test environment.";
// Post a "just in case" quit task, if the test hangs.
async::PostDelayedTask(
dispatcher(),
[] { FX_LOGS(FATAL) << "\n\n>> Test did not complete in time, terminating. <<\n\n"; },
kTestTimeout);
}
// Configures services available to the test environment. This method is called by |SetUp()|. It
// shadows but calls |TestWithEnvironment::CreateServices()|.
virtual void CreateServices(std::unique_ptr<sys::testing::EnvironmentServices>& services) {}
sys::testing::EnclosingEnvironment* environment() { return environment_.get(); }
fuchsia::ui::views::ViewToken CreatePresentationViewToken() {
auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();
auto presenter = environment()->ConnectToService<fuchsia::ui::policy::Presenter>();
presenter.set_error_handler(
[](zx_status_t status) { FAIL() << "presenter: " << zx_status_get_string(status); });
presenter->PresentView(std::move(view_holder_token), nullptr);
return std::move(view_token);
}
private:
const std::array<std::pair<const char*, const char*>, NUM_SERVICES> injected_services_;
std::unique_ptr<sys::ComponentContext> const component_context_;
std::unique_ptr<sys::testing::EnclosingEnvironment> environment_;
};
class FlutterEmbedderTests : public FlutterEmbedderTestsBase {
public:
FlutterEmbedderTests() : FlutterEmbedderTestsBase(kInjectedServices) {}
// |testing::Test|
void SetUp() override {
ASSERT_NO_FATAL_FAILURE(FlutterEmbedderTestsBase::SetUp());
// Connect to scenic to ensure it is up and running.
auto scenic = environment()->ConnectToService<fuchsia::ui::scenic::Scenic>();
scenic->GetDisplayInfo([this](fuchsia::ui::gfx::DisplayInfo info) { QuitLoop(); });
RunLoopWithTimeout(kCallTimeout);
}
void RunAppWithArgs(const std::string& component_url,
const std::vector<std::string>& component_args = {}) {
fuchsia::ui::scenic::ScenicPtr scenic;
environment()->ConnectToService(scenic.NewRequest());
scenic.set_error_handler([](zx_status_t status) {
FAIL() << "Lost connection to Scenic: " << zx_status_get_string(status);
});
scenic::EmbeddedViewInfo flutter_runner = scenic::LaunchComponentAndCreateView(
environment()->launcher_ptr(), component_url, component_args);
flutter_runner.controller.events().OnTerminated = [](auto...) { FAIL(); };
// Present the view.
embedder_view_.emplace(scenic::ViewContext{
.session_and_listener_request =
scenic::CreateScenicSessionPtrAndListenerRequest(scenic.get()),
.view_token = CreatePresentationViewToken(),
});
// Embed the view.
bool is_rendering = false;
embedder_view_->EmbedView(std::move(flutter_runner),
[&is_rendering](fuchsia::ui::gfx::ViewState view_state) {
is_rendering = view_state.is_rendering;
});
ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&is_rendering] { return is_rendering; }, kCallTimeout));
FX_LOGS(INFO) << "Launched component: " << component_url;
}
scenic::Screenshot TakeScreenshot() {
fuchsia::ui::scenic::ScenicPtr scenic;
environment()->ConnectToService(scenic.NewRequest());
scenic.set_error_handler([](zx_status_t status) {
FAIL() << "Lost connection to Scenic: " << zx_status_get_string(status);
});
FX_LOGS(INFO) << "Taking screenshot... ";
fuchsia::ui::scenic::ScreenshotData screenshot_out;
scenic->TakeScreenshot(
[this, &screenshot_out](fuchsia::ui::scenic::ScreenshotData screenshot, bool status) {
EXPECT_TRUE(status) << "Failed to take screenshot";
screenshot_out = std::move(screenshot);
QuitLoop();
});
EXPECT_FALSE(RunLoopWithTimeout(kScreenshotTimeout)) << "Timed out waiting for screenshot.";
FX_LOGS(INFO) << "Screenshot captured.";
return scenic::Screenshot(screenshot_out);
}
bool TakeScreenshotUntil(scenic::Color color,
fit::function<void(std::map<scenic::Color, size_t>)> callback = nullptr,
zx::duration timeout = kTestTimeout) {
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);
}
// Inject directly into Root Presenter, using fuchsia.ui.input FIDLs.
void InjectInput() {
using fuchsia::ui::input::InputReport;
// Device parameters
auto parameters = fuchsia::ui::input::TouchscreenDescriptor::New();
*parameters = {.x = {.range = {.min = -1000, .max = 1000}},
.y = {.range = {.min = -1000, .max = 1000}},
.max_finger_id = 10};
FX_LOGS(INFO) << "Injecting input... ";
// Register it against Root Presenter.
fuchsia::ui::input::DeviceDescriptor device{.touchscreen = std::move(parameters)};
auto registry = environment()->ConnectToService<fuchsia::ui::input::InputDeviceRegistry>();
fuchsia::ui::input::InputDevicePtr connection;
registry->RegisterDevice(std::move(device), connection.NewRequest());
{
// Inject one input report, then a conclusion (empty) report.
auto touch = fuchsia::ui::input::TouchscreenReport::New();
*touch = {.touches = {{.finger_id = 1, .x = 0, .y = 0}}}; // center of display
InputReport report{.event_time = TimeToUint(RealNow<zx::basic_time<ZX_CLOCK_MONOTONIC>>()),
.touchscreen = std::move(touch)};
connection->DispatchReport(std::move(report));
}
{
auto touch = fuchsia::ui::input::TouchscreenReport::New();
InputReport report{.event_time = TimeToUint(RealNow<zx::basic_time<ZX_CLOCK_MONOTONIC>>()),
.touchscreen = std::move(touch)};
connection->DispatchReport(std::move(report));
}
FX_LOGS(INFO) << "Input dispatched.";
}
private:
template <typename TimeT>
TimeT RealNow() {
TimeT now(ZX_TIME_INFINITE_PAST);
FX_CHECK(zx::clock::get(&now) == ZX_OK);
return now;
};
template <typename TimeT>
uint64_t TimeToUint(const TimeT& time) {
FX_CHECK(time.get() >= 0);
return static_cast<uint64_t>(time.get());
};
// Wrapped in optional since the view is not created until the middle of SetUp
std::optional<scenic::EmbedderView> embedder_view_;
};
} // namespace flutter_embedder_test
#endif // SRC_UI_TESTS_E2E_FLUTTER_TESTS_EMBEDDER_FLUTTER_EMBEDDER_TEST_H_