| // Copyright 2019 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/ui/input/cpp/fidl.h> |
| #include <fuchsia/ui/policy/cpp/fidl.h> |
| #include <fuchsia/ui/scenic/cpp/fidl.h> |
| #include <gtest/gtest.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/component/cpp/startup_context.h> |
| #include <lib/fdio/spawn.h> |
| #include <lib/fit/function.h> |
| #include <lib/gtest/real_loop_fixture.h> |
| #include <lib/ui/base_view/cpp/base_view.h> |
| #include <lib/ui/input/cpp/formatting.h> |
| #include <lib/ui/scenic/cpp/session.h> |
| #include <lib/ui/scenic/cpp/view_token_pair.h> |
| #include <zircon/status.h> |
| |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "src/lib/fxl/logging.h" |
| |
| // NOTE WELL. Run each of these e2e tests in its own executable. They each |
| // consume and maintain process-global context, so it's better to keep them |
| // separate. Plus, separation means they start up components in a known good |
| // state, instead of reusing component state possibly dirtied by other tests. |
| |
| namespace { |
| |
| using fuchsia::ui::input::MediaButtonsEvent; |
| |
| // Shared context for all tests in this process. |
| // Set it up once, never delete it. |
| component::StartupContext* g_context = nullptr; |
| |
| // 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); |
| |
| // This implements the MediaButtonsListener class. Its purpose is to attach |
| // to the presentation and test that MediaButton Events are actually sent |
| // out to the Listeners. |
| class ButtonsListenerImpl : public fuchsia::ui::policy::MediaButtonsListener { |
| public: |
| ButtonsListenerImpl( |
| fidl::InterfaceRequest<fuchsia::ui::policy::MediaButtonsListener> |
| listener_request) |
| : listener_binding_(this, std::move(listener_request)) {} |
| ~ButtonsListenerImpl() = default; |
| |
| int EventsSeen() { return events_seen_; } |
| |
| void SetOnTerminateCallback( |
| fit::function<void(const std::vector<MediaButtonsEvent>&)> on_terminate, |
| int num_events_to_terminate) { |
| on_terminate_ = std::move(on_terminate); |
| num_events_to_terminate_ = num_events_to_terminate; |
| } |
| |
| private: |
| // |MediaButtonsListener| |
| void OnMediaButtonsEvent( |
| fuchsia::ui::input::MediaButtonsEvent event) override { |
| // Store inputs for checking later. |
| observed_.push_back(std::move(event)); |
| |
| events_seen_++; |
| if (events_seen_ >= num_events_to_terminate_) { |
| on_terminate_(observed_); |
| } |
| } |
| |
| int events_seen_ = 0; |
| int num_events_to_terminate_ = 0; |
| fidl::Binding<fuchsia::ui::policy::MediaButtonsListener> listener_binding_; |
| fit::function<void(const std::vector<MediaButtonsEvent>&)> on_terminate_; |
| std::vector<MediaButtonsEvent> observed_; |
| }; |
| |
| // A very small Scenic client. Puts up a fuchsia-colored rectangle. |
| class MinimalClientView : public scenic::BaseView { |
| public: |
| MinimalClientView(scenic::ViewContext context, async_dispatcher_t* dispatcher) |
| : scenic::BaseView(std::move(context), "MinimalClientView"), |
| dispatcher_(dispatcher) { |
| FXL_CHECK(dispatcher_); |
| } |
| |
| void CreateScene(uint32_t width_in_px, uint32_t height_in_px) { |
| float width = static_cast<float>(width_in_px); |
| float height = static_cast<float>(height_in_px); |
| |
| scenic::ShapeNode background(session()); |
| |
| scenic::Material material(session()); |
| material.SetColor(255, 0, 255, 255); // Fuchsia |
| background.SetMaterial(material); |
| |
| scenic::Rectangle rectangle(session(), width, height); |
| background.SetShape(rectangle); |
| background.SetTranslation(width / 2, height / 2, -10.f); |
| |
| root_node().AddChild(background); |
| } |
| |
| void Update(uint64_t present_time) { |
| session()->Present( |
| present_time, [this](fuchsia::images::PresentationInfo info) { |
| Update(info.presentation_time + info.presentation_interval); |
| }); |
| } |
| |
| private: |
| // |scenic::SessionListener| |
| void OnScenicError(std::string error) override { FXL_LOG(FATAL) << error; } |
| |
| async_dispatcher_t* dispatcher_ = nullptr; |
| }; |
| |
| class MediaButtonsListenerTest : public gtest::RealLoopFixture { |
| protected: |
| // Mildly complex ctor, but we don't throw and we don't call virtual methods. |
| MediaButtonsListenerTest() { |
| // This fixture constructor may run multiple times, but we want the context |
| // to be set up just once per process. |
| if (g_context == nullptr) { |
| g_context = component::StartupContext::CreateFromStartupInfo().release(); |
| } |
| |
| auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); |
| |
| // Connect to Scenic, create a View. |
| scenic_ = |
| g_context->ConnectToEnvironmentService<fuchsia::ui::scenic::Scenic>(); |
| scenic_.set_error_handler([](zx_status_t status) { |
| FXL_LOG(FATAL) << "Lost connection to Scenic: " |
| << zx_status_get_string(status); |
| }); |
| scenic::ViewContext view_context = { |
| .session_and_listener_request = |
| scenic::CreateScenicSessionPtrAndListenerRequest(scenic_.get()), |
| .view_token = std::move(view_token), |
| .incoming_services = nullptr, |
| .outgoing_services = nullptr, |
| .startup_context = g_context, |
| }; |
| view_ = std::make_unique<MinimalClientView>(std::move(view_context), |
| dispatcher()); |
| |
| // Connect to RootPresenter, create a ViewHolder. |
| root_presenter_ = |
| g_context |
| ->ConnectToEnvironmentService<fuchsia::ui::policy::Presenter>(); |
| root_presenter_.set_error_handler([](zx_status_t status) { |
| FXL_LOG(FATAL) << "Lost connection to RootPresenter: " |
| << zx_status_get_string(status); |
| }); |
| |
| fidl::InterfaceHandle<fuchsia::ui::policy::Presentation> presentation; |
| root_presenter_->PresentView(std::move(view_holder_token), |
| presentation.NewRequest()); |
| |
| // Connect to the MediaButtons listener. |
| fidl::InterfacePtr<fuchsia::ui::policy::Presentation> presentation_ptr; |
| fidl::InterfaceHandle<fuchsia::ui::policy::MediaButtonsListener> |
| listener_handle; |
| button_listener_impl_ = |
| std::make_unique<ButtonsListenerImpl>(listener_handle.NewRequest()); |
| |
| presentation_ptr = presentation.Bind(); |
| presentation_ptr->RegisterMediaButtonsListener(std::move(listener_handle)); |
| |
| // When display is available, create content and drive input to touchscreen. |
| scenic_->GetDisplayInfo([this](fuchsia::ui::gfx::DisplayInfo display_info) { |
| display_width_ = display_info.width_in_px; |
| display_height_ = display_info.height_in_px; |
| |
| FXL_CHECK(display_width_ > 0 && display_height_ > 0) |
| << "Display size unsuitable for this test: (" << display_width_ |
| << ", " << display_height_ << ")."; |
| |
| view_->CreateScene(display_width_, display_height_); |
| view_->Update(zx_clock_get_monotonic()); |
| |
| inject_input_(); // Display up, content ready. Send in input. |
| |
| test_was_run_ = true; // Actually did work for this test. |
| }); |
| |
| // Post a "just in case" quit task, if the test hangs. |
| async::PostDelayedTask( |
| dispatcher(), |
| [] { |
| FXL_LOG(FATAL) |
| << "\n\n>> Test did not complete in time, terminating. <<\n\n"; |
| }, |
| kTimeout); |
| } |
| |
| ~MediaButtonsListenerTest() override { |
| FXL_CHECK(test_was_run_) << "Oops, didn't actually do anything."; |
| } |
| |
| void InjectInput(std::vector<const char*> args) { |
| // Start with process name, end with nullptr. |
| args.insert(args.begin(), "input"); |
| args.push_back(nullptr); |
| |
| // Start the /bin/input process. |
| zx_handle_t proc; |
| zx_status_t status = fdio_spawn(ZX_HANDLE_INVALID, FDIO_SPAWN_CLONE_ALL, |
| "/bin/input", args.data(), &proc); |
| FXL_CHECK(status == ZX_OK) |
| << "fdio_spawn: " << zx_status_get_string(status); |
| |
| // Wait for termination. |
| status = zx_object_wait_one(proc, ZX_PROCESS_TERMINATED, |
| (zx::clock::get_monotonic() + kTimeout).get(), |
| nullptr); |
| FXL_CHECK(status == ZX_OK) |
| << "zx_object_wait_one: " << zx_status_get_string(status); |
| |
| // Check termination status. |
| zx_info_process_t info; |
| status = zx_object_get_info(proc, ZX_INFO_PROCESS, &info, sizeof(info), |
| nullptr, nullptr); |
| FXL_CHECK(status == ZX_OK) |
| << "zx_object_get_info: " << zx_status_get_string(status); |
| FXL_CHECK(info.return_code == 0) |
| << "info.return_code: " << info.return_code; |
| } |
| |
| void SetInjectInputCallback(fit::function<void()> inject_input) { |
| inject_input_ = std::move(inject_input); |
| } |
| |
| void SetOnTerminateCallback( |
| fit::function<void(const std::vector<MediaButtonsEvent>&)> on_terminate, |
| int num_events_to_terminate) { |
| button_listener_impl_->SetOnTerminateCallback(std::move(on_terminate), |
| num_events_to_terminate); |
| } |
| |
| std::unique_ptr<ButtonsListenerImpl> button_listener_impl_; |
| fuchsia::ui::policy::PresenterPtr root_presenter_; |
| fuchsia::ui::scenic::ScenicPtr scenic_; |
| |
| std::unique_ptr<MinimalClientView> view_; |
| uint32_t display_width_ = 0; |
| uint32_t display_height_ = 0; |
| |
| fit::function<void()> inject_input_; |
| bool test_was_run_ = false; |
| }; |
| |
| TEST_F(MediaButtonsListenerTest, MediaButtons) { |
| // Set up inputs. Fires when display and content are available. |
| SetInjectInputCallback([this] { |
| InjectInput({"media_button", "1", "1", "1", "1", nullptr}); |
| }); |
| |
| // Set up expectations. Terminate when we see 1 message. |
| SetOnTerminateCallback( |
| [this](const std::vector<MediaButtonsEvent>& observed) { |
| EXPECT_EQ(observed.size(), 1U); |
| QuitLoop(); |
| // TODO(SCN-1449): Cleanly break the View/ViewHolder connection. |
| }, |
| 1); |
| |
| RunLoop(); // Go! |
| } |
| |
| } // namespace |
| |
| // NOTE: We link in FXL's gtest_main to enable proper logging. |