blob: 1b747cd05dbd36c7d528184029e9d46407b044d4 [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 <fuchsia/ui/app/cpp/fidl.h>
#include <fuchsia/ui/test/input/cpp/fidl.h>
#include <fuchsia/web/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/sys/cpp/service_directory.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/clock.h>
#include <lib/zx/time.h>
#include <zircon/errors.h>
#include <zircon/status.h>
#include <string>
#include "fuchsia/ui/views/cpp/fidl.h"
#include "src/lib/fsl/vmo/vector.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/lib/json_parser/json_parser.h"
namespace {
fuchsia::mem::Buffer BufferFromString(const std::string& script) {
fuchsia::mem::Buffer buffer;
uint64_t num_bytes = script.size();
zx::vmo vmo;
zx_status_t status = zx::vmo::create(num_bytes, 0u, &vmo);
FX_CHECK(status >= 0);
status = vmo.write(script.data(), 0, num_bytes);
FX_CHECK(status >= 0);
buffer.vmo = std::move(vmo);
buffer.size = num_bytes;
return buffer;
}
std::string StringFromBuffer(const fuchsia::mem::Buffer& buffer) {
size_t num_bytes = buffer.size;
std::string str(num_bytes, 'x');
buffer.vmo.read(str.data(), 0, num_bytes);
return str;
}
std::vector<fuchsia::ui::test::input::MouseButton> GetPressedButtons(int buttons) {
std::vector<fuchsia::ui::test::input::MouseButton> pressed_buttons;
if (buttons & 0x1) {
pressed_buttons.push_back(fuchsia::ui::test::input::MouseButton::FIRST);
}
if (buttons & 0x1 >> 1) {
pressed_buttons.push_back(fuchsia::ui::test::input::MouseButton::SECOND);
}
if (buttons & 0x1 >> 2) {
pressed_buttons.push_back(fuchsia::ui::test::input::MouseButton::THIRD);
}
return pressed_buttons;
}
fuchsia::ui::test::input::MouseEventPhase GetPhase(std::string type) {
if (type == "add") {
return fuchsia::ui::test::input::MouseEventPhase::ADD;
} else if (type == "hover") {
return fuchsia::ui::test::input::MouseEventPhase::HOVER;
} else if (type == "mousedown") {
return fuchsia::ui::test::input::MouseEventPhase::DOWN;
} else if (type == "mousemove") {
return fuchsia::ui::test::input::MouseEventPhase::MOVE;
} else if (type == "mouseup") {
return fuchsia::ui::test::input::MouseEventPhase::UP;
} else if (type == "wheel") {
return fuchsia::ui::test::input::MouseEventPhase::WHEEL;
} else {
FX_LOGS(FATAL) << "Invalid mouse event type: " << type;
exit(1);
}
}
class NavListener : public fuchsia::web::NavigationEventListener {
public:
// |fuchsia::web::NavigationEventListener|
void OnNavigationStateChanged(fuchsia::web::NavigationState nav_state,
OnNavigationStateChangedCallback send_ack) override {
if (nav_state.has_is_main_document_loaded()) {
FX_LOGS(INFO) << "nav_state.is_main_document_loaded = "
<< nav_state.is_main_document_loaded();
is_main_document_loaded_ = nav_state.is_main_document_loaded();
}
if (nav_state.has_page_type()) {
FX_CHECK(nav_state.page_type() != fuchsia::web::PageType::ERROR);
}
if (nav_state.has_title()) {
FX_LOGS(INFO) << "nav_state.title = " << nav_state.title();
if (nav_state.title() == "about:blank") {
loaded_about_blank_ = true;
}
if (nav_state.title() == "window_resized") {
window_resized_ = true;
}
}
send_ack();
}
bool loaded_about_blank_ = false;
bool is_main_document_loaded_ = false;
bool window_resized_ = false;
};
// Implements a simple web app, which responds to mouse events.
class WebApp : public fuchsia::ui::app::ViewProvider {
public:
WebApp()
: loop_(&kAsyncLoopConfigAttachToCurrentThread),
context_(sys::ComponentContext::CreateAndServeOutgoingDirectory()),
view_provider_binding_(this) {
SetupWebEngine();
SetupViewProvider();
}
void Run() {
FX_LOGS(INFO) << "Loading web app";
fuchsia::web::NavigationControllerPtr navigation_controller;
NavListener navigation_event_listener;
fidl::Binding<fuchsia::web::NavigationEventListener> navigation_event_listener_binding(
&navigation_event_listener);
web_frame_->SetNavigationEventListener(navigation_event_listener_binding.NewBinding());
web_frame_->GetNavigationController(navigation_controller.NewRequest());
navigation_controller->LoadUrl("about:blank", fuchsia::web::LoadUrlParams(), [](auto result) {
if (result.is_err()) {
FX_LOGS(FATAL) << "Error while loading URL: " << static_cast<uint32_t>(result.err());
}
});
// Wait for navigation loaded "about:blank" page then inject JS code, to avoid inject JS to
// wrong page.
RunLoopUntil([&navigation_event_listener] {
return navigation_event_listener.loaded_about_blank_ &&
navigation_event_listener.is_main_document_loaded_;
});
bool is_js_loaded = false;
web_frame_->ExecuteJavaScript({"*"}, BufferFromString(kAppCode), [&is_js_loaded](auto result) {
if (result.is_err()) {
FX_LOGS(FATAL) << "Error while executing JavaScript: "
<< static_cast<uint32_t>(result.err());
} else {
is_js_loaded = true;
}
});
RunLoopUntil([&] { return is_js_loaded; });
fuchsia::web::MessagePortPtr message_port;
bool is_port_registered = false;
bool window_resized = false;
SendMessageToWebPage(message_port.NewRequest(), "REGISTER_PORT");
message_port->ReceiveMessage([&is_port_registered, &window_resized](auto web_message) {
auto message = StringFromBuffer(web_message.data());
// JS already saw window has size, don't wait for resize.
if (message == "PORT_REGISTERED WINDOW_RESIZED") {
window_resized = true;
} else {
FX_CHECK(message == "PORT_REGISTERED") << "Expected PORT_REGISTERED but got " << message;
}
is_port_registered = true;
});
RunLoopUntil([&] { return is_port_registered; });
if (!window_resized) {
RunLoopUntil(
[&navigation_event_listener] { return navigation_event_listener.window_resized_; });
}
fuchsia::ui::test::input::MouseInputListenerSyncPtr mouse_input_listener;
context_->svc()->Connect(mouse_input_listener.NewRequest());
RunLoopForMouseReponse(mouse_input_listener, message_port);
}
private:
static constexpr auto kAppCode = R"JS(
let port;
document.body.onmousemove = function(event) {
console.assert(port != null);
let response = JSON.stringify({
type: event.type,
epoch_msec: Date.now(),
x: event.clientX,
y: event.clientY,
wheel_h: event.deltaX,
wheel_v: event.deltaY,
device_scale_factor: window.devicePixelRatio,
buttons: event.buttons
});
console.info('Reporting mouse move event ', response);
port.postMessage(response);
};
document.body.onmousedown = function(event) {
console.assert(port != null);
let response = JSON.stringify({
type: event.type,
epoch_msec: Date.now(),
x: event.clientX,
y: event.clientY,
wheel_h: event.deltaX,
wheel_v: event.deltaY,
device_scale_factor: window.devicePixelRatio,
buttons: event.buttons
});
console.info('Reporting mouse down event ', response);
port.postMessage(response);
};
document.body.onmouseup = function(event) {
console.assert(port != null);
let response = JSON.stringify({
type: event.type,
epoch_msec: Date.now(),
x: event.clientX,
y: event.clientY,
wheel_h: event.deltaX,
wheel_v: event.deltaY,
device_scale_factor: window.devicePixelRatio,
buttons: event.buttons
});
console.info('Reporting mouse up event ', response);
port.postMessage(response);
};
document.body.onwheel = function(event) {
console.assert(port != null);
let response = JSON.stringify({
type: event.type,
epoch_msec: Date.now(),
x: event.clientX,
y: event.clientY,
wheel_h: event.deltaX,
wheel_v: event.deltaY,
device_scale_factor: window.devicePixelRatio,
buttons: event.buttons
});
console.info('Reporting mouse wheel event ', response);
port.postMessage(response);
};
window.onresize = function(event) {
if (window.innerWidth != 0) {
console.info('size: ', window.innerWidth, window.innerHeight);
document.title = 'window_resized';
}
};
function receiveMessage(event) {
if (event.data == "REGISTER_PORT") {
console.log("received REGISTER_PORT");
port = event.ports[0];
if (window.innerWidth != 0 && window.innerHeight != 0) {
console.info('size when REGISTER_PORT: ', window.innerWidth, window.innerHeight);
port.postMessage('PORT_REGISTERED WINDOW_RESIZED');
} else {
port.postMessage('PORT_REGISTERED');
}
} else {
console.error('received unexpected message: ' + event.data);
}
};
window.addEventListener('message', receiveMessage, false);
console.info('JS loaded');
)JS";
void SetupWebEngine() {
auto web_context_provider = context_->svc()->Connect<fuchsia::web::ContextProvider>();
auto incoming_service_clone = context_->svc()->CloneChannel();
web_context_provider.set_error_handler([](zx_status_t status) {
FX_LOGS(WARNING) << "web_context_provider: " << zx_status_get_string(status);
});
FX_CHECK(incoming_service_clone.is_valid());
fuchsia::web::CreateContextParams params;
params.set_service_directory(std::move(incoming_service_clone));
// Enable Vulkan to allow WebEngine run on Flatland.
params.set_features(fuchsia::web::ContextFeatureFlags::VULKAN |
fuchsia::web::ContextFeatureFlags::NETWORK);
web_context_provider->Create(std::move(params), web_context_.NewRequest());
web_context_.set_error_handler([](zx_status_t status) {
FX_LOGS(WARNING) << "web_context_: " << zx_status_get_string(status);
});
web_context_->CreateFrame(web_frame_.NewRequest());
web_frame_.set_error_handler([](zx_status_t status) {
FX_LOGS(WARNING) << "web_frame_: " << zx_status_get_string(status);
});
// Setup log level in JS to get logs.
web_frame_->SetJavaScriptLogLevel(fuchsia::web::ConsoleLogLevel::INFO);
}
void SetupViewProvider() {
fidl::InterfaceRequestHandler<fuchsia::ui::app::ViewProvider> handler =
[&](fidl::InterfaceRequest<fuchsia::ui::app::ViewProvider> request) {
if (view_provider_binding_.is_bound()) {
request.Close(ZX_ERR_ALREADY_BOUND);
return;
}
view_provider_binding_.Bind(std::move(request));
};
context_->outgoing()->AddPublicService(std::move(handler));
}
// |fuchsia::ui::app::ViewProvider|
void CreateViewWithViewRef(zx::eventpair token,
fuchsia::ui::views::ViewRefControl view_ref_control,
fuchsia::ui::views::ViewRef view_ref) override {
// Flatland only use |CreateView2|.
FX_LOGS(FATAL) << "CreateViewWithViewRef() is not implemented.";
}
void CreateView2(fuchsia::ui::app::CreateView2Args args) override {
fuchsia::web::CreateView2Args args2;
fuchsia::ui::views::ViewCreationToken token;
args2.set_view_creation_token(std::move(*args.mutable_view_creation_token()));
web_frame_->CreateView2(std::move(args2));
}
void SendMessageToWebPage(fidl::InterfaceRequest<fuchsia::web::MessagePort> message_port,
const std::string& message) {
fuchsia::web::WebMessage web_message;
web_message.set_data(BufferFromString(message));
std::vector<fuchsia::web::OutgoingTransferable> outgoing;
outgoing.emplace_back(
fuchsia::web::OutgoingTransferable::WithMessagePort(std::move(message_port)));
web_message.set_outgoing_transfer(std::move(outgoing));
web_frame_->PostMessage(/*target_origin=*/"*", std::move(web_message),
[](auto result) { FX_CHECK(!result.is_err()); });
}
template <typename PredicateT>
void RunLoopUntil(PredicateT predicate) {
while (!predicate()) {
loop_.Run(zx::time::infinite(), true);
}
}
void RunLoopForMouseReponse(
fuchsia::ui::test::input::MouseInputListenerSyncPtr& mouse_input_listener,
fuchsia::web::MessagePortPtr& message_port) {
while (true) {
bool got_mouse_event = false;
FX_LOGS(INFO) << "Waiting for mouse response message";
message_port->ReceiveMessage([&mouse_input_listener, &got_mouse_event](auto web_message) {
std::optional<rapidjson::Document> mouse_response;
mouse_response = json::JSONParser().ParseFromString(StringFromBuffer(web_message.data()),
"web-app-response");
// Validate structure of mouse response.
const auto& mouse_resp = mouse_response.value();
FX_CHECK(mouse_resp.HasMember("type"));
FX_CHECK(mouse_resp.HasMember("epoch_msec"));
FX_CHECK(mouse_resp.HasMember("x"));
FX_CHECK(mouse_resp.HasMember("y"));
FX_CHECK(mouse_resp.HasMember("device_scale_factor"));
FX_CHECK(mouse_resp.HasMember("buttons"));
FX_CHECK(mouse_resp["type"].IsString());
FX_CHECK(mouse_resp["epoch_msec"].IsInt64());
FX_CHECK(mouse_resp["x"].IsNumber());
FX_CHECK(mouse_resp["y"].IsNumber());
FX_CHECK(mouse_resp["device_scale_factor"].IsNumber());
FX_CHECK(mouse_resp["buttons"].IsNumber());
// Relay response to parent.
fuchsia::ui::test::input::MouseInputListenerReportMouseInputRequest request;
request.set_time_received(mouse_resp["epoch_msec"].GetInt64() * 1000 * 1000);
request.set_local_x(mouse_resp["x"].GetDouble());
request.set_local_y(mouse_resp["y"].GetDouble());
request.set_device_pixel_ratio(mouse_resp["device_scale_factor"].GetDouble());
request.set_phase(GetPhase(mouse_resp["type"].GetString()));
request.set_buttons(GetPressedButtons(mouse_resp["buttons"].GetInt()));
if (mouse_resp.HasMember("wheel_h")) {
FX_CHECK(mouse_resp["wheel_h"].IsNumber());
request.set_wheel_x_physical_pixel(mouse_resp["wheel_h"].GetDouble());
}
if (mouse_resp.HasMember("wheel_v")) {
FX_CHECK(mouse_resp["wheel_v"].IsNumber());
request.set_wheel_y_physical_pixel(mouse_resp["wheel_v"].GetDouble());
}
request.set_component_name("mouse-input-chromium");
FX_LOGS(INFO) << "Got mouse response message " << mouse_resp["type"].GetString();
mouse_input_listener->ReportMouseInput(std::move(request));
got_mouse_event = true;
});
RunLoopUntil([&]() { return got_mouse_event; });
}
}
async::Loop loop_;
std::unique_ptr<sys::ComponentContext> context_;
fidl::Binding<fuchsia::ui::app::ViewProvider> view_provider_binding_;
fuchsia::web::ContextPtr web_context_;
fuchsia::web::FramePtr web_frame_;
};
} // namespace
int main(int argc, const char** argv) { WebApp().Run(); }