text: // 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 <lib/async-loop/cpp/loop.h>
  #include <lib/async-loop/default.h>
  #include <lib/async/cpp/task.h>
  #include <lib/fdio/spawn.h>
  #include <lib/fit/function.h>
  #include <lib/fostr/fidl/fuchsia/ui/input/formatting.h>
  #include <lib/gtest/real_loop_fixture.h>
  #include <lib/sys/cpp/component_context.h>
  #include <lib/ui/scenic/cpp/session.h>
  #include <lib/ui/scenic/cpp/view_token_pair.h>
  #include <lib/zx/clock.h>
  #include <zircon/status.h>
  
  #include <memory>
  #include <string>
  #include <vector>
  
  #include "gtest/gtest.h"
  #include "src/lib/fxl/logging.h"
  #include "src/lib/ui/base_view/base_view.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::InputEvent;
  using PointerPhase = fuchsia::ui::input::PointerEventPhase;
  using KeyboardPhase = fuchsia::ui::input::KeyboardEventPhase;
  
  // Shared context for all tests in this process.
  // Set it up once, never delete it.
  sys::ComponentContext* 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);
  
  // A very small Scenic client. Puts up a fuchsia-colored rectangle, and stores
  // input events for examination.
  class ImeClientView : public scenic::BaseView {
   public:
    ImeClientView(scenic::ViewContext context, fit::function<void()> on_view_attached_to_scene,
                  fit::function<void()> on_tap_release,
                  fit::function<void(const std::vector<InputEvent>&)> on_terminate,
                  async_dispatcher_t* dispatcher)
        : scenic::BaseView(std::move(context), "ImeClientView"),
          on_view_attached_to_scene_(std::move(on_view_attached_to_scene)),
          on_tap_release_(std::move(on_tap_release)),
          on_terminate_(std::move(on_terminate)),
          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);
    }
  
    // |scenic::BaseView|
    void OnPropertiesChanged(fuchsia::ui::gfx::ViewProperties old_properties) override {
      if (has_logical_size()) {
        CreateScene(logical_size().x, logical_size().y);
        InvalidateScene([](auto info) { FXL_LOG(INFO) << "Client: scene created."; });
      }
    }
  
    // |scenic::BaseView|
    void OnScenicEvent(fuchsia::ui::scenic::Event event) override {
      if (event.is_gfx() && event.gfx().is_view_attached_to_scene()) {
        // TODO(fxb/41382): Remove this extra InvalidateScene() call. Today we need it to ensure the
        // ViewTree connection gets flushed on time.
        InvalidateScene([this](auto info) {
          // When view is connected to scene (a proxy for "has rendered"), trigger input injection.
          FXL_LOG(INFO) << "Client: view attached to scene.";
          FXL_CHECK(on_view_attached_to_scene_) << "on_view_attached_to_scene_ was not set!";
          on_view_attached_to_scene_();
        });
      }
    }
  
    // |scenic::BaseView|
    void OnInputEvent(InputEvent event) override {
      // Trigger text events after tap gesture finishes; view has focus.
      if (event.is_pointer() && event.pointer().phase == PointerPhase::REMOVE) {
        async::PostTask(dispatcher_, [this] {
          FXL_LOG(INFO) << "Client: tap finished.";
          FXL_CHECK(on_tap_release_) << "on_tap_release_ was not set!";
          on_tap_release_();
        });
      }
  
      // Simple termination condition: when key up event is received.
      if (event.is_keyboard() && event.keyboard().phase == KeyboardPhase::RELEASED) {
        async::PostTask(dispatcher_, [this] {
          FXL_LOG(INFO) << "Client: all expected inputs received.";
          FXL_CHECK(on_terminate_) << "on_terminate_ was not set!";
          on_terminate_(observed_);
        });
      }
  
      // Store inputs for checking later.
      observed_.push_back(std::move(event));
    }
  
   private:
    // |scenic::SessionListener|
    void OnScenicError(std::string error) override { FXL_LOG(FATAL) << error; }
  
    fit::function<void()> on_view_attached_to_scene_;
    fit::function<void()> on_tap_release_;
    fit::function<void(const std::vector<InputEvent>&)> on_terminate_;
  
    std::vector<InputEvent> observed_;
  
    async_dispatcher_t* dispatcher_ = nullptr;
  };
  
  class ImeInputTest : public gtest::RealLoopFixture {
   protected:
    ImeInputTest() {
      // 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 = sys::ComponentContext::Create().release();
      }
    }
  
    ~ImeInputTest() override {
      FXL_CHECK(injection_count_ == 2) << "Oops, didn't inject all inputs.";
    }
  
    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;
    }
  
    fuchsia::ui::policy::PresenterPtr root_presenter_;
    fuchsia::ui::scenic::ScenicPtr scenic_;
  
    std::unique_ptr<ImeClientView> view_;
    uint32_t display_width_ = 0;
    uint32_t display_height_ = 0;
  
    uint32_t injection_count_ = 0;
  };
  
  TEST_F(ImeInputTest, Keyboard) {
    // Inject tap. Fires when client view's content is connected to the scene.
    fit::function<void()> on_view_attached_to_scene = [this] {
      FXL_LOG(INFO) << "Client: injecting tap.";
      FXL_CHECK(display_width_ > 0 && display_height_ > 0) << "precondition";
      {
        std::string x = std::to_string(display_width_ / 2);
        std::string y = std::to_string(display_height_ / 2);
        std::string width = "--width=" + std::to_string(display_width_);
        std::string height = "--height=" + std::to_string(display_height_);
        InjectInput({"tap",  // Tap at the center of the display
                     x.c_str(), y.c_str(), width.c_str(), height.c_str(), nullptr});
      }
      ++injection_count_;
    };
  
    // Inject text events. Fires when tap sequence finishes.
    fit::function<void()> on_tap_release = [this] {
      FXL_LOG(INFO) << "Client: injecting text.";
      // Send the Esc key(hid usage code: 41)
      InjectInput({"keyevent", "41", nullptr});
      ++injection_count_;
    };
  
    // Set up expectations. Fires when we see the "quit" condition.
    fit::function<void(const std::vector<InputEvent>&)> on_terminate =
        [this](const std::vector<InputEvent>& observed) {
          if (FXL_VLOG_IS_ON(2)) {
            for (const auto& event : observed) {
              FXL_LOG(INFO) << "Input event observed: " << event;
            }
          }
  
          ASSERT_EQ(observed.size(), 7u);
  
          ASSERT_TRUE(observed[0].is_pointer());
          EXPECT_EQ(observed[0].pointer().phase, PointerPhase::ADD);
  
          ASSERT_TRUE(observed[1].is_focus());
          EXPECT_TRUE(observed[1].focus().focused);
  
          ASSERT_TRUE(observed[2].is_pointer());
          EXPECT_EQ(observed[2].pointer().phase, PointerPhase::DOWN);
  
          ASSERT_TRUE(observed[3].is_pointer());
          EXPECT_EQ(observed[3].pointer().phase, PointerPhase::UP);
  
          ASSERT_TRUE(observed[4].is_pointer());
          EXPECT_EQ(observed[4].pointer().phase, PointerPhase::REMOVE);
  
          ASSERT_TRUE(observed[5].is_keyboard());
          EXPECT_EQ(observed[5].keyboard().phase, KeyboardPhase::PRESSED);
  
          ASSERT_TRUE(observed[6].is_keyboard());
          EXPECT_EQ(observed[6].keyboard().phase, KeyboardPhase::RELEASED);
  
          QuitLoop();
          // Today, we can't quietly break the View/ViewHolder connection.
        };
  
    // Connect to Scenic, park a callback to obtain display dimensions.
    scenic_ = g_context->svc()->Connect<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_->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_
          << ").";
    });
  
    auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();
  
    // Connect to RootPresenter, create a ViewHolder.
    root_presenter_ = g_context->svc()->Connect<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);
    });
    root_presenter_->PresentView(std::move(view_holder_token), nullptr);
  
    // Create a View.
    scenic::ViewContext view_context = {
        .enable_ime = true,
        .session_and_listener_request =
            scenic::CreateScenicSessionPtrAndListenerRequest(scenic_.get()),
        .view_token = std::move(view_token),
        .component_context = g_context,
    };
    view_ = std::make_unique<ImeClientView>(
        std::move(view_context), std::move(on_view_attached_to_scene), std::move(on_tap_release),
        std::move(on_terminate), dispatcher());
  
    // 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);
  
    RunLoop();  // Go!
  }
  
  }  // namespace
  
  // NOTE: We link in FXL's gtest_main to enable proper logging.
  