blob: b94c07b2b15b14f57a32ebe8bdee986f6a8348d3 [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/ui_test_manager/ui_test_manager.h"
#include <fuchsia/accessibility/cpp/fidl.h>
#include <fuchsia/accessibility/scene/cpp/fidl.h>
#include <fuchsia/accessibility/semantics/cpp/fidl.h>
#include <fuchsia/input/injection/cpp/fidl.h>
#include <fuchsia/input/virtualkeyboard/cpp/fidl.h>
#include <fuchsia/logger/cpp/fidl.h>
#include <fuchsia/scheduler/cpp/fidl.h>
#include <fuchsia/session/scene/cpp/fidl.h>
#include <fuchsia/sys/cpp/fidl.h>
#include <fuchsia/tracing/provider/cpp/fidl.h>
#include <fuchsia/ui/accessibility/view/cpp/fidl.h>
#include <fuchsia/ui/app/cpp/fidl.h>
#include <fuchsia/ui/composition/cpp/fidl.h>
#include <fuchsia/ui/focus/cpp/fidl.h>
#include <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/pointerinjector/configuration/cpp/fidl.h>
#include <fuchsia/ui/pointerinjector/cpp/fidl.h>
#include <fuchsia/ui/policy/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <fuchsia/vulkan/loader/cpp/fidl.h>
#include <lib/sys/component/cpp/testing/realm_builder.h>
#include <lib/sys/component/cpp/testing/realm_builder_types.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <test/accessibility/cpp/fidl.h>
#include <test/inputsynthesis/cpp/fidl.h>
#include "sdk/lib/syslog/cpp/macros.h"
#include "src/lib/fsl/handles/object_info.h"
namespace ui_testing {
namespace {
using component_testing::Capability;
using component_testing::ChildRef;
using component_testing::LocalComponent;
using component_testing::LocalComponentHandles;
using component_testing::ParentRef;
using component_testing::Protocol;
using component_testing::RealmBuilder;
using component_testing::Ref;
using component_testing::Route;
// Base realm urls.
constexpr auto kScenicOnlyUrl = "#meta/scenic_only.cm";
constexpr auto kRootPresenterSceneUrl = "#meta/root_presenter_scene.cm";
constexpr auto kRootPresenterSceneWithInputUrl = "#meta/root_presenter_scene_with_input.cm";
constexpr auto kSceneManagerSceneUrl = "#meta/scene_manager_scene.cm";
constexpr auto kSceneManagerSceneWithInputUrl = "#meta/scene_manager_scene_with_input.cm";
// System component urls.
constexpr auto kRealA11yManagerUrl = "#meta/a11y-manager.cm";
constexpr auto kFakeA11yManagerUrl = "#meta/fake-a11y-manager.cm";
constexpr auto kClientSubrealmName = "client-subrealm";
// Component names.
// NOTE: These names must match the names in meta/*.cml.
constexpr auto kA11yManagerName = "a11y-manager";
constexpr auto kScenicName = "scenic";
constexpr auto kRootPresenterName = "root-presenter";
constexpr auto kSceneManagerName = "scene-manager";
constexpr auto kInputPipelineName = "input-pipeline";
constexpr auto kTextManagerName = "text-manager";
// Contents of config file used to force scenic to use flatland.
constexpr auto kUseFlatlandScenicConfig = R"(
{
"flatland_buffer_collection_import_mode": "renderer_only",
"i_can_haz_flatland": true
}
)";
constexpr auto kDefaultScale = 0.f;
// Set of low-level system services that components in the realm can consume
// from parent (test_manager).
std::vector<std::string> DefaultSystemServices() {
return {fuchsia::logger::LogSink::Name_, fuchsia::scheduler::ProfileProvider::Name_,
fuchsia::sysmem::Allocator::Name_, fuchsia::tracing::provider::Registry::Name_,
fuchsia::vulkan::loader::Loader::Name_};
}
std::optional<fuchsia::ui::observation::geometry::ViewDescriptor> ViewDescriptorFromSnapshot(
const fuchsia::ui::observation::geometry::ViewTreeSnapshot& snapshot, zx_koid_t view_ref_koid) {
if (!snapshot.has_views()) {
return std::nullopt;
}
auto it = std::find_if(snapshot.views().begin(), snapshot.views().end(),
[view_ref_koid](const auto& view) {
return view.has_view_ref_koid() && view.view_ref_koid() == view_ref_koid;
});
if (it == snapshot.views().end()) {
return std::nullopt;
}
return fidl::Clone(*it);
}
// Returns the name of the scene owner component (if any).
std::string SceneOwnerName(const UITestManager::Config& config) {
if (!config.scene_owner) {
return "";
}
switch (*config.scene_owner) {
case UITestManager::SceneOwnerType::ROOT_PRESENTER:
return kRootPresenterName;
case UITestManager::SceneOwnerType::SCENE_MANAGER:
return kSceneManagerName;
default:
return "";
}
}
// Returns the name of the input owner component (if any).
std::string InputOwnerName(const UITestManager::Config& config) {
if (!config.use_input) {
return "";
}
switch (*config.scene_owner) {
case UITestManager::SceneOwnerType::ROOT_PRESENTER:
return kInputPipelineName;
case UITestManager::SceneOwnerType::SCENE_MANAGER:
return kSceneManagerName;
default:
return "";
}
}
// List of scenic services available in the test realm.
std::vector<std::string> ScenicServices(const UITestManager::Config& config) {
if (config.use_flatland) {
// Note that we expose FlatlandDisplay to the client subrealm for now, since
// we only have in-tree test clients at the moment. Once UITestManager is
// used for out-of-tree tests, we'll want to add a flag to
// UITestManager::Config to control whether we expose internal-only APIs to
// the client subrealm.
return {fuchsia::ui::observation::test::Registry::Name_,
fuchsia::ui::composition::Allocator::Name_, fuchsia::ui::composition::Flatland::Name_,
fuchsia::ui::composition::FlatlandDisplay::Name_, fuchsia::ui::scenic::Scenic::Name_};
} else {
return {fuchsia::ui::observation::test::Registry::Name_,
fuchsia::ui::focus::FocusChainListenerRegistry::Name_,
fuchsia::ui::scenic::Scenic::Name_, fuchsia::ui::views::ViewRefInstalled::Name_};
}
}
// List of a11y services available in the test realm.
std::vector<std::string> AccessibilityServices(const UITestManager::Config& config) {
if (!config.accessibility_owner) {
return {};
}
return {fuchsia::accessibility::semantics::SemanticsManager::Name_,
fuchsia::accessibility::Magnifier::Name_};
}
// List of scene owner services available in the test realm.
std::vector<std::string> SceneOwnerServices(const UITestManager::Config& config) {
if (!config.scene_owner)
return {};
if (config.scene_owner == UITestManager::SceneOwnerType::ROOT_PRESENTER) {
return {fuchsia::ui::accessibility::view::Registry::Name_,
fuchsia::input::virtualkeyboard::Manager::Name_,
fuchsia::input::virtualkeyboard::ControllerCreator::Name_,
fuchsia::ui::pointerinjector::configuration::Setup::Name_,
fuchsia::ui::policy::Presenter::Name_};
} else if (config.scene_owner == UITestManager::SceneOwnerType::SCENE_MANAGER) {
return {fuchsia::session::scene::Manager::Name_,
fuchsia::ui::accessibility::view::Registry::Name_};
}
return {};
}
// List of input services available in the test realm.
std::vector<std::string> InputServices(const UITestManager::Config& config) {
if (!config.use_input) {
return {};
}
if (config.scene_owner) {
return {fuchsia::input::injection::InputDeviceRegistry::Name_,
fuchsia::ui::policy::DeviceListenerRegistry::Name_};
} else {
return {fuchsia::ui::pointerinjector::Registry::Name_};
}
}
// Returns a mapping from ui service name to the component that vends the
// service.
std::map<std::string, std::string> GetServiceToComponentMap(UITestManager::Config config) {
std::map<std::string, std::string> service_to_component;
for (const auto& service : ScenicServices(config)) {
service_to_component[service] = kScenicName;
}
for (const auto& service : AccessibilityServices(config)) {
service_to_component[service] = kA11yManagerName;
}
for (const auto& service : SceneOwnerServices(config)) {
service_to_component[service] = SceneOwnerName(config);
}
for (const auto& service : InputServices(config)) {
service_to_component[service] = InputOwnerName(config);
}
// Additional input services.
if (config.use_input) {
service_to_component[fuchsia::ui::input::ImeService::Name_] = kTextManagerName;
service_to_component[fuchsia::ui::input3::Keyboard::Name_] = kTextManagerName;
}
return service_to_component;
}
} // namespace
UITestManager::UITestManager(UITestManager::Config config)
: config_(config), focus_chain_listener_binding_(this) {}
std::string UITestManager::CalculateBaseRealmUrl() {
if (config_.scene_owner == UITestManager::SceneOwnerType::ROOT_PRESENTER) {
return config_.use_input ? kRootPresenterSceneWithInputUrl : kRootPresenterSceneUrl;
} else if (config_.scene_owner == UITestManager::SceneOwnerType::SCENE_MANAGER) {
return config_.use_input ? kSceneManagerSceneWithInputUrl : kSceneManagerSceneUrl;
} else {
// If we have exhausted all potential scene owner options, then scene_owner
// should be std::nullopt.
FX_CHECK(!config_.scene_owner) << "Unrecognized scene owner";
// If no scene owner is specified, use the scenic-only realm.
return kScenicOnlyUrl;
}
}
void UITestManager::RouteServices(std::vector<std::string> services, Ref source,
std::vector<Ref> targets) {
if (services.empty()) {
return;
}
std::vector<Capability> protocols;
for (const auto& service : services) {
protocols.emplace_back(Protocol{service});
}
realm_builder_.AddRoute(
Route{.capabilities = protocols, .source = std::move(source), .targets = std::move(targets)});
}
component_testing::Realm UITestManager::AddSubrealm() {
has_client_subrealm_ = true;
return realm_builder_.AddChildRealm(kClientSubrealmName);
}
void UITestManager::ConfigureClientSubrealm() {
if (!has_client_subrealm_) {
return;
}
// Route default system services to test subrealm.
RouteServices(DefaultSystemServices(), /* source = */ ParentRef(),
/* targets = */ {ChildRef{kClientSubrealmName}});
// Route services to parent that client requested to expose.
RouteServices(config_.exposed_client_services, /* source = */ ChildRef{kClientSubrealmName},
/* targets = */ {ParentRef()});
// Route services client requested from ui subrealm.
auto service_to_component = GetServiceToComponentMap(config_);
for (const auto& service : config_.ui_to_client_services) {
auto it = service_to_component.find(service);
FX_CHECK(it != service_to_component.end())
<< "Service is not available for the specified realm configuration: " << service;
RouteServices({service}, /* source = */ ChildRef{it->second},
/* targets = */ {ChildRef{kClientSubrealmName}});
}
// Route ViewProvider to parent if the client specifies a scene owner.
if (config_.scene_owner) {
RouteServices({fuchsia::ui::app::ViewProvider::Name_},
/* source = */ ChildRef{kClientSubrealmName}, /* targets = */ {ParentRef()});
}
}
void UITestManager::ConfigureAccessibility() {
std::string a11y_manager_url;
// Add real a11y manager to the test realm, if requested.
// Otherwise, add fake a11y manager if it's requested, OR if the test uses
// `FlatlandSceneManager` (which will only render a client view if the a11y
// view is present).
if (config_.accessibility_owner == UITestManager::AccessibilityOwnerType::REAL) {
a11y_manager_url = kRealA11yManagerUrl;
} else if (config_.accessibility_owner == UITestManager::AccessibilityOwnerType::FAKE ||
(config_.scene_owner == UITestManager::SceneOwnerType::SCENE_MANAGER &&
config_.use_flatland)) {
a11y_manager_url = kFakeA11yManagerUrl;
} else {
return;
}
realm_builder_.AddChild(kA11yManagerName, a11y_manager_url);
RouteServices({fuchsia::logger::LogSink::Name_},
/* source = */ ParentRef(),
/* targets = */ {ChildRef{kA11yManagerName}});
RouteServices({fuchsia::ui::composition::Flatland::Name_, fuchsia::ui::scenic::Scenic::Name_},
/* source = */ ChildRef{kScenicName},
/* targets = */ {ChildRef{kA11yManagerName}});
RouteServices({fuchsia::accessibility::semantics::SemanticsManager::Name_,
test::accessibility::Magnifier::Name_},
/* source = */ ChildRef{kA11yManagerName},
/* targets = */ {ParentRef()});
if (config_.scene_owner) {
if (config_.use_flatland) {
RouteServices({fuchsia::accessibility::scene::Provider::Name_},
/* source = */ ChildRef{kA11yManagerName},
/* targets = */ {ChildRef{SceneOwnerName(config_)}});
} else {
RouteServices({fuchsia::accessibility::Magnifier::Name_},
/* source = */ ChildRef{kA11yManagerName},
/* targets = */ {ChildRef{SceneOwnerName(config_)}});
}
}
}
void UITestManager::RouteConfigData() {
auto config_directory_contents = component_testing::DirectoryContents();
std::vector<Ref> targets;
// Override scenic's Override "i_can_haz_flatland" if necessary.
if (config_.use_flatland) {
config_directory_contents.AddFile("scenic_config", kUseFlatlandScenicConfig);
targets.push_back(ChildRef{kScenicName});
}
// Supply a default display rotation.
std::string scene_owner_name = SceneOwnerName(config_);
if (config_.scene_owner) {
config_directory_contents.AddFile("display_rotation", std::to_string(config_.display_rotation));
if (config_.display_pixel_density > 0) {
config_directory_contents.AddFile("display_pixel_density",
std::to_string(config_.display_pixel_density));
}
targets.push_back(ChildRef{scene_owner_name});
}
if (!targets.empty()) {
realm_builder_.RouteReadOnlyDirectory("config-data", std::move(targets),
std::move(config_directory_contents));
}
}
void UITestManager::BuildRealm() {
// Set up a11y manager, if requested, and route semantics manager service to
// client subrealm.
//
// NOTE: We opt to configure accessibility dynamically, rather then in the
// .cml for the base realms, because there are three different a11y
// configurations (fake, real, none), which can each apply to scenes
// with/without input. The a11y service routing is also different for gfx and
// flatland, so it would be unwieldy to create a separate static declaration
// for every a11y configuration tested.
ConfigureAccessibility();
// Route config data directories to appropriate recipients (currently, scenic,
// scene manager, and root presenter are the only use cases for config files.
RouteConfigData();
// This step needs to come after ConfigureAccessibility(), because the a11y
// manager component needs to be added to the realm first.
ConfigureClientSubrealm();
realm_root_ = std::make_unique<component_testing::RealmRoot>(realm_builder_.Build());
}
std::unique_ptr<sys::ServiceDirectory> UITestManager::TakeExposedServicesDirectory() {
return std::make_unique<sys::ServiceDirectory>(realm_root_->CloneRoot());
}
void UITestManager::InitializeScene() {
FX_CHECK(realm_root_) << "BuildRealm() must be called before InitializeScene()";
FX_CHECK(config_.scene_owner.has_value()) << "Scene owner must be specified";
FX_CHECK(!observer_registry_ && !geometry_provider_) << "InitializeScene() called twice";
// Register geometry observer. We should do this before attaching the client
// view, so that we see all the view tree snapshots.
realm_root_->Connect<fuchsia::ui::observation::test::Registry>(observer_registry_.NewRequest());
observer_registry_->RegisterGlobalGeometryProvider(geometry_provider_.NewRequest());
// Register focus chain listener.
auto focus_chain_listener_registry =
realm_root_->Connect<fuchsia::ui::focus::FocusChainListenerRegistry>();
focus_chain_listener_registry->Register(focus_chain_listener_binding_.NewBinding());
if (*config_.scene_owner == UITestManager::SceneOwnerType::ROOT_PRESENTER) {
root_presenter_ = realm_root_->Connect<fuchsia::ui::policy::Presenter>();
auto client_view_tokens = scenic::ViewTokenPair::New();
auto [client_control_ref, client_view_ref] = scenic::ViewRefPair::New();
client_view_ref_ = fidl::Clone(client_view_ref);
root_presenter_->PresentOrReplaceView2(std::move(client_view_tokens.view_holder_token),
fidl::Clone(client_view_ref),
/* presentation */ nullptr);
auto client_view_provider = realm_root_->Connect<fuchsia::ui::app::ViewProvider>();
client_view_provider->CreateViewWithViewRef(std::move(client_view_tokens.view_token.value),
std::move(client_control_ref),
std::move(client_view_ref));
} else if (config_.scene_owner == UITestManager::SceneOwnerType::SCENE_MANAGER) {
scene_manager_ = realm_root_->Connect<fuchsia::session::scene::Manager>();
auto view_provider = realm_root_->Connect<fuchsia::ui::app::ViewProvider>();
scene_manager_->SetRootView(
std::move(view_provider),
[this](fuchsia::ui::views::ViewRef view_ref) { client_view_ref_ = std::move(view_ref); });
} else {
FX_LOGS(FATAL) << "Unsupported scene owner";
}
WatchViewTree();
}
void UITestManager::WatchViewTree() {
FX_CHECK(geometry_provider_)
<< "Geometry observer must be registered before calling WatchViewTree()";
geometry_provider_->Watch([this](auto response) {
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());
}
WatchViewTree();
return;
}
const auto& error = response.error();
if (error.has_channel_overflow() && error.channel_overflow()) {
FX_LOGS(FATAL) << "Geometry provider channel overflowed";
} else if (error.has_buffer_overflow() && error.buffer_overflow()) {
FX_LOGS(FATAL) << "Geometry provider buffer overflowed";
} else if (error.has_views_overflow() && error.views_overflow()) {
FX_LOGS(FATAL) << "Geometry provider attempted to report too many views";
}
});
}
void UITestManager::OnFocusChange(fuchsia::ui::focus::FocusChain focus_chain,
OnFocusChangeCallback callback) {
last_focus_chain_ = std::move(focus_chain);
callback();
}
bool UITestManager::ClientViewIsRendering() {
if (!last_view_tree_snapshot_) {
return false;
}
auto client_view_ref_koid = ClientViewRefKoid();
if (!client_view_ref_koid) {
return false;
}
return ViewDescriptorFromSnapshot(*last_view_tree_snapshot_, *client_view_ref_koid) !=
std::nullopt;
}
bool UITestManager::ClientViewIsFocused() {
if (!last_focus_chain_) {
return false;
}
auto client_view_ref_koid = ClientViewRefKoid();
if (!client_view_ref_koid) {
return false;
}
if (!last_focus_chain_->has_focus_chain()) {
return false;
}
if (last_focus_chain_->focus_chain().empty()) {
return false;
}
return fsl::GetKoid(last_focus_chain_->focus_chain().back().reference.get()) ==
client_view_ref_koid;
}
std::optional<zx_koid_t> UITestManager::ClientViewRefKoid() {
if (!client_view_ref_) {
return std::nullopt;
}
return fsl::GetKoid(client_view_ref_->reference.get());
}
float UITestManager::ClientViewScaleFactor() {
if (!last_view_tree_snapshot_) {
return kDefaultScale;
}
auto client_view_ref_koid = ClientViewRefKoid();
if (!client_view_ref_koid) {
return kDefaultScale;
}
const auto client_view_descriptor =
ViewDescriptorFromSnapshot(*last_view_tree_snapshot_, *client_view_ref_koid);
if (!client_view_descriptor || !client_view_descriptor->has_layout()) {
return kDefaultScale;
}
const auto& pixel_scale = client_view_descriptor->layout().pixel_scale;
return std::max(pixel_scale[0], pixel_scale[1]);
}
} // namespace ui_testing