blob: 3f9c8e5dc8c7f239828ac83ad17179f3a0adaf7c [file]
// Copyright 2021 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 <fuchsia/sys/cpp/fidl.h>
#include <fuchsia/ui/app/cpp/fidl.h>
#include <fuchsia/ui/focus/cpp/fidl.h>
#include <fuchsia/ui/policy/cpp/fidl.h>
#include <lib/async/cpp/task.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/sys/cpp/testing/enclosing_environment.h>
#include <lib/sys/cpp/testing/test_with_environment.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/ui/scenic/cpp/resources.h>
#include <lib/ui/scenic/cpp/session.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <lib/zx/clock.h>
#include <lib/zx/time.h>
#include <optional>
#include <gtest/gtest.h>
#include <test/focus/cpp/fidl.h>
// This test exercises the client-side view-focus machinery managed by Scenic:
// - fuchsia.ui.views.Focuser (giving focus to a particular view)
// - fuchsia.ui.views.ViewRefFocused (learning when your view gained/lost focus)
// as well as the focus contract offered by Root Presenter.
//
// This test uses the following components: Root Presenter, Scenic, this test
// component itself, and a C++ GFX client.
//
// Synchronization: Underneath Root Presenter, the test component installs a test view to monitor
// the "real" child view. One test checks that Root Presenter transfers focus to the test view upon
// connection.
//
// The test waits for the child view to spin up and become connected to the view
// tree. Then, after the test view receives focus from Root Presenter, the test will transfer focus
// down to the child view. The child view will report back to the test that it received focus.
namespace {
// Max timeout in failure cases.
// Set this as low as you can that still works across all test platforms.
constexpr zx::duration kTimeout = zx::min(5);
// Common services for each test.
const std::map<std::string, std::string> LocalServices() {
return {
// Root Presenter is bundled with the test package to ensure version hermeticity and driver
// hermeticity.
{"fuchsia.ui.policy.Presenter",
"fuchsia-pkg://fuchsia.com/focus-input-test#meta/root_presenter.cmx"},
// Scenic protocols.
{"fuchsia.ui.scenic.Scenic", "fuchsia-pkg://fuchsia.com/focus-input-test#meta/scenic.cmx"},
{"fuchsia.ui.focus.FocusChainListenerRegistry",
"fuchsia-pkg://fuchsia.com/focus-input-test#meta/scenic.cmx"},
// Misc protocols.
{"fuchsia.cobalt.LoggerFactory",
"fuchsia-pkg://fuchsia.com/mock_cobalt#meta/mock_cobalt.cmx"},
{"fuchsia.hardware.display.Provider",
"fuchsia-pkg://fuchsia.com/fake-hardware-display-controller-provider#meta/hdcp.cmx"},
};
}
// Allow these global services from outside the test environment.
const std::vector<std::string> GlobalServices() {
return {"fuchsia.vulkan.loader.Loader", "fuchsia.sysmem.Allocator",
"fuchsia.scheduler.ProfileProvider"};
}
class FocusInputTest : public sys::testing::TestWithEnvironment,
public test::focus::ResponseListener {
protected:
FocusInputTest() : response_listener_(this) {
auto services = TestWithEnvironment::CreateServices();
// Key part of service setup: have this test component vend the |ResponseListener| service to
// the constructed environment.
{
zx_status_t is_ok = services->AddService<test::focus::ResponseListener>(
[this](fidl::InterfaceRequest<test::focus::ResponseListener> request) {
response_listener_.Bind(std::move(request));
});
FX_CHECK(is_ok == ZX_OK);
}
// Add common services.
for (const auto& [name, url] : LocalServices()) {
const zx_status_t is_ok = services->AddServiceWithLaunchInfo({.url = url}, name);
FX_CHECK(is_ok == ZX_OK) << "Failed to add service " << name;
}
// Enable services from outside this test.
for (const auto& service : GlobalServices()) {
const zx_status_t is_ok = services->AllowParentService(service);
FX_CHECK(is_ok == ZX_OK) << "Failed to add service " << service;
}
test_env_ = CreateNewEnclosingEnvironment("focus_input_test_env", std::move(services));
WaitForEnclosingEnvToStart(test_env_.get());
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"; },
kTimeout);
}
void CreateScenicClientAndTestView(fuchsia::ui::views::ViewToken view_token,
scenic::ViewRefPair view_ref_pair) {
fuchsia::ui::scenic::SessionEndpoints endpoints;
fuchsia::ui::scenic::SessionPtr client_endpoint;
fidl::InterfaceHandle<fuchsia::ui::scenic::SessionListener> listener_handle;
fidl::InterfaceRequest<fuchsia::ui::scenic::SessionListener> listener_request =
listener_handle.NewRequest(); // client side
endpoints.set_session(client_endpoint.NewRequest())
.set_session_listener(std::move(listener_handle))
.set_view_ref_focused(test_view_focus_watcher_.NewRequest())
.set_view_focuser(test_view_focuser_control_.NewRequest());
test_env_->ConnectToService<fuchsia::ui::scenic::Scenic>()->CreateSessionT(
std::move(endpoints), [] { /* don't block, feed forward */ });
session_ =
std::make_unique<scenic::Session>(std::move(client_endpoint), std::move(listener_request));
session_->SetDebugName("focus-input-test");
test_view_ = std::make_unique<scenic::View>(session_.get(), std::move(view_token),
std::move(view_ref_pair.control_ref),
std::move(view_ref_pair.view_ref), "test view");
session_->Present2(/* when */ zx::clock::get_monotonic().get(), /* span */ 0,
[](auto) { FX_LOGS(INFO) << "test view created by Scenic."; });
}
// |test.focus.ResponseListener|
void Respond(test::focus::Data focus_data) override {
FX_CHECK(respond_callback_) << "Expected callback to be set for test.focus.Respond().";
respond_callback_(std::move(focus_data));
}
// FIELDS
std::unique_ptr<sys::testing::EnclosingEnvironment> test_env_;
// Protocols used.
fuchsia::ui::views::ViewRefFocusedPtr test_view_focus_watcher_;
fuchsia::ui::views::FocuserPtr test_view_focuser_control_;
// Protocols vended.
fidl::Binding<test::focus::ResponseListener> response_listener_;
// Scenic state.
std::unique_ptr<scenic::Session> session_;
std::unique_ptr<scenic::View> test_view_;
// Per-test action for |test.focus.ResponseListener.Respond|.
fit::function<void(test::focus::Data)> respond_callback_;
};
// This test exercises the focus contract with Root Presenter: the view offered to Root
// Presenter will have focus transferred to it. The test itself offers such a view to Root
// Presenter.
// NOTE. This test does not use test.focus.ResponseListener. There's not a client that listens to
// ViewRefFocused.
TEST_F(FocusInputTest, TestView_ReceivesFocusTransfer_FromRootPresenter) {
auto tokens_rt = scenic::ViewTokenPair::New(); // Root Presenter -> Test
auto refs_rt = scenic::ViewRefPair::New();
fuchsia::ui::views::ViewRef test_view_ref;
refs_rt.view_ref.Clone(&test_view_ref);
// Instruct Root Presenter to present test view.
auto root_presenter = test_env_->ConnectToService<fuchsia::ui::policy::Presenter>();
root_presenter->PresentOrReplaceView2(std::move(tokens_rt.view_holder_token),
std::move(test_view_ref),
/* presentation */ nullptr);
// Set up test view, to harvest focus signal. Root Presenter will ask Scenic to transfer focus
// to this View's ViewRef.
CreateScenicClientAndTestView(std::move(tokens_rt.view_token), std::move(refs_rt));
std::optional<bool> focus_status;
test_view_focus_watcher_->Watch(
[&focus_status](fuchsia::ui::views::FocusState state) { focus_status = state.focused(); });
RunLoopUntil([&focus_status] { return focus_status.has_value(); });
ASSERT_TRUE(focus_status.value()) << "test view should initially receive focus";
FX_LOGS(INFO) << "*** PASS ***";
}
// This test exercises the focus contract between a parent view and child view: upon focus transfer
// from parent view (this test, under Root Presenter) to child view (a simple C++ client), the
// parent view will receive a focus event with "focus=false", and the child view will receive a
// focus event with "focus=true".
TEST_F(FocusInputTest, TestView_TransfersFocus_ToChildView) {
{ // Link test view under Root Presenter's view.
auto tokens_rt = scenic::ViewTokenPair::New();
auto refs_rt = scenic::ViewRefPair::New();
fuchsia::ui::views::ViewRef test_view_ref;
refs_rt.view_ref.Clone(&test_view_ref);
// Instruct Root Presenter to present test view.
auto root_presenter = test_env_->ConnectToService<fuchsia::ui::policy::Presenter>();
root_presenter->PresentOrReplaceView2(std::move(tokens_rt.view_holder_token),
std::move(test_view_ref),
/* presentation */ nullptr);
// Set up test view, to harvest focus signal. Root Presenter will ask Scenic to transfer focus
// to test view's ViewRef.
CreateScenicClientAndTestView(std::move(tokens_rt.view_token), std::move(refs_rt));
}
{ // Wait for test view to receive focus.
std::optional<bool> focus_status;
test_view_focus_watcher_->Watch(
[&focus_status](fuchsia::ui::views::FocusState state) { focus_status = state.focused(); });
RunLoopUntil([&focus_status] { return focus_status.has_value(); });
ASSERT_TRUE(focus_status.value()) << "test view should initially receive focus";
}
auto tokens_tc = scenic::ViewTokenPair::New(); // connect test view to child view
auto refs_tc = scenic::ViewRefPair::New(); // view ref for child view
fuchsia::ui::views::ViewRef child_view_ref;
refs_tc.view_ref.Clone(&child_view_ref);
// Set up data collection from child view.
std::optional<test::focus::Data> child_focus_status;
respond_callback_ = [&child_focus_status](test::focus::Data data) {
child_focus_status = std::move(data);
};
bool child_connected = false; // condition variable
{ // Set up view holder for child view. Set up notification for when child view connects.
scenic::ViewHolder view_holder_for_child(session_.get(), std::move(tokens_tc.view_holder_token),
"test's view holder for gfx child");
const uint32_t vh_id = view_holder_for_child.id();
test_view_->AddChild(view_holder_for_child);
session_->Present2(/* when */ zx::clock::get_monotonic().get(), /* span */ 0, [](auto) {
FX_LOGS(INFO) << "test's viewholder for gfx child created by Scenic.";
});
session_->set_event_handler(
[vh_id, &child_connected](const std::vector<fuchsia::ui::scenic::Event>& events) {
for (const auto& event : events) {
if (event.is_gfx() && event.gfx().is_view_connected() &&
event.gfx().view_connected().view_holder_id == vh_id) {
child_connected = true;
}
}
});
}
fuchsia::sys::ComponentControllerPtr focus_gfx_child; // Keep child alive on stack.
{ // Launch child component to vend child view. Wait until child view connects.
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = "fuchsia-pkg://fuchsia.com/focus-input-test#meta/focus-gfx-client.cmx";
// Create a point-to-point offer-use connection between parent and child.
auto child_services = sys::ServiceDirectory::CreateWithRequest(&launch_info.directory_request);
focus_gfx_child = test_env_->CreateComponent(std::move(launch_info));
auto view_provider = child_services->Connect<fuchsia::ui::app::ViewProvider>();
view_provider->CreateViewWithViewRef(std::move(tokens_tc.view_token.value),
std::move(refs_tc.control_ref),
std::move(refs_tc.view_ref));
RunLoopUntil([&child_connected] { return child_connected; });
}
const zx::time request_time = zx::clock::get_monotonic();
{ // Transfer focus to child view and watch for change in test view's focus status.
test_view_focuser_control_->RequestFocus(std::move(child_view_ref),
[](auto) { /* don't block, feed forward */ });
FX_LOGS(INFO) << "Test requested focus transfer to child view at time " << request_time.get();
std::optional<bool> focus_status;
test_view_focus_watcher_->Watch(
[&focus_status](fuchsia::ui::views::FocusState state) { focus_status = state.focused(); });
RunLoopUntil([&focus_status] { return focus_status.has_value(); });
EXPECT_FALSE(focus_status.value()) << "test view should lose focus";
}
{ // Wait for child view's version of focus data.
RunLoopUntil([&child_focus_status] { return child_focus_status.has_value(); });
FX_CHECK(child_focus_status.value().has_time_received()) << "contract with child view";
FX_CHECK(child_focus_status.value().has_focus_status()) << "contract with child view";
const zx::time receive_time = zx::time(child_focus_status.value().time_received());
FX_LOGS(INFO) << "Child view received focus event at time " << receive_time.get();
zx::duration latency = receive_time - request_time;
FX_LOGS(INFO) << "JFYI focus latency: " << latency.to_usecs() << " us";
ASSERT_TRUE(child_focus_status.value().focus_status()) << "child view should gain focus";
FX_LOGS(INFO) << "*** PASS ***";
}
}
// This test ensures that multiple clients can connect to the FocusChainListenerRegistry.
// It does not set up a scene; these "early" listeners should observe an empty focus chain.
// NOTE. This test does not use test.focus.ResponseListener. There's not a client that listens to
// ViewRefFocused.
TEST_F(FocusInputTest, SimultaneousCallsTo_FocusChainListenerRegistry) {
using fuchsia::ui::focus::FocusChain;
// Miniature FocusChainListener, just for this one test.
class FocusChainListenerImpl : public fuchsia::ui::focus::FocusChainListener {
public:
FocusChainListenerImpl(sys::testing::EnclosingEnvironment* env,
std::vector<FocusChain>& collector, bool& error_fired)
: listener_impl_(this), collector_(collector), error_fired_(error_fired) {
env->ConnectToService(listener_registry_.NewRequest());
listener_registry_.set_error_handler([this](zx_status_t) { error_fired_ = true; });
listener_registry_->Register(listener_impl_.NewBinding());
}
// |fuchsia.ui.focus.FocusChainListener|
void OnFocusChange(FocusChain focus_chain, OnFocusChangeCallback callback) {
collector_.push_back(std::move(focus_chain));
callback();
}
private:
fuchsia::ui::focus::FocusChainListenerRegistryPtr listener_registry_;
fidl::Binding<fuchsia::ui::focus::FocusChainListener> listener_impl_;
std::vector<FocusChain>& collector_;
bool& error_fired_;
};
std::vector<FocusChain> collected_a;
bool error_fired_a = false;
FocusChainListenerImpl listener_a(test_env_.get(), collected_a, error_fired_a);
std::vector<FocusChain> collected_b;
bool error_fired_b = false;
FocusChainListenerImpl listener_b(test_env_.get(), collected_b, error_fired_b);
RunLoopUntil([&error_fired_a, &error_fired_b, &collected_a, &collected_b] {
// Wait until an error fired, or both listeners see their first report.
return error_fired_a || error_fired_b || (collected_a.size() > 0 && collected_b.size() > 0);
});
// Client "a" is clean, and collected a focus chain.
EXPECT_FALSE(error_fired_a);
ASSERT_EQ(collected_a.size(), 1u);
// It's empty, since there's no scene at time of connection.
EXPECT_FALSE(collected_a[0].has_focus_chain());
// Client "b" is clean, and collected a focus chain.
EXPECT_FALSE(error_fired_b);
ASSERT_EQ(collected_b.size(), 1u);
// It's empty, since there's no scene at time of connection.
EXPECT_FALSE(collected_b[0].has_focus_chain());
}
} // namespace