blob: 1656d7986fd26511f3f504fbcecec11cd731586b [file] [log] [blame]
// 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::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.
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);
// 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, async_dispatcher_t* dispatcher)
: scenic::BaseView(std::move(context), "ImeClientView"),
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);
});
}
// |scenic::BaseView|
void OnInputEvent(InputEvent event) override {
if (on_input_) {
on_input_(std::move(event));
}
}
void SetOnInputCallback(fit::function<void(InputEvent)> on_input) {
on_input_ = std::move(on_input);
}
private:
// |scenic::SessionListener|
void OnScenicError(std::string error) override { FXL_LOG(FATAL) << error; }
async_dispatcher_t* dispatcher_ = nullptr;
fit::function<void(InputEvent)> on_input_;
};
class ImeInputTest : public gtest::RealLoopFixture {
protected:
// Mildly complex ctor, but we don't throw and we don't call virtual methods.
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 = 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 = {
.enable_ime = true,
.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<ImeClientView>(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);
});
root_presenter_->PresentView(std::move(view_holder_token), nullptr);
// 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);
}
~ImeInputTest() 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()> on_terminate) {
on_terminate_ = std::move(on_terminate);
}
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;
std::vector<InputEvent> observed_;
fit::function<void()> inject_input_;
fit::function<void()> on_terminate_;
bool test_was_run_ = false;
};
TEST_F(ImeInputTest, Keyboard) {
// Handle input. Fires for every input event received.
view_->SetOnInputCallback([this](InputEvent event) {
// Store inputs for checking later.
observed_.push_back(std::move(event));
// Inject text events after tap gesture is done and view has focus.
if (event.is_pointer() && event.pointer().phase == PointerPhase::REMOVE) {
async::PostTask(dispatcher(), [this] {
// Send the Esc key(hid usage code: 41)
InjectInput({"keyevent", "41", nullptr});
});
}
// Simple termination condition: when key up event is received.
if (event.is_keyboard() &&
event.keyboard().phase == KeyboardPhase::RELEASED) {
async::PostTask(dispatcher(), [this] {
FXL_CHECK(on_terminate_) << "on_terminate_ was not set!";
on_terminate_();
});
}
});
// Inject tap. Fires when display and content are available.
SetInjectInputCallback([this] {
InjectInput({"tap", // Tap at the center of the display
std::to_string(display_width_ / 2).c_str(),
std::to_string(display_height_ / 2).c_str(), nullptr});
});
// Set up expectations. Fires when we see the "quit" message.
SetOnTerminateCallback([this]() {
if (FXL_VLOG_IS_ON(2)) {
for (const auto& event : observed_) {
FXL_LOG(INFO) << "Input event observed: " << event;
}
}
EXPECT_EQ(observed_.size(), 7u);
EXPECT_EQ(observed_[0].pointer().phase, PointerPhase::ADD);
EXPECT_TRUE(observed_[1].focus().focused);
EXPECT_EQ(observed_[2].pointer().phase, PointerPhase::DOWN);
EXPECT_EQ(observed_[3].pointer().phase, PointerPhase::UP);
EXPECT_EQ(observed_[4].pointer().phase, PointerPhase::REMOVE);
EXPECT_EQ(observed_[5].keyboard().phase, KeyboardPhase::PRESSED);
EXPECT_EQ(observed_[6].keyboard().phase, KeyboardPhase::RELEASED);
QuitLoop();
// Today, we can't quietly break the View/ViewHolder connection.
});
RunLoop(); // Go!
}
} // namespace
// NOTE: We link in FXL's gtest_main to enable proper logging.