| // 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. |
| |
| // The contents of this web application are heavily borrowed from prior work |
| // such as mouse-input-chromium.cc, virtual-keyboard-test.cc and other |
| // similar efforts. |
| |
| #include <fuchsia/ui/app/cpp/fidl.h> |
| #include <fuchsia/ui/test/input/cpp/fidl.h> |
| #include <fuchsia/ui/views/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 <unistd.h> |
| #include <zircon/errors.h> |
| #include <zircon/status.h> |
| |
| #include <string> |
| |
| #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; |
| } |
| |
| // Listens to navigation events forwarded from the web page into this web app. |
| // The navigation events are used for simplistic communication about the |
| // web page's lifecycle through modifying the title of the displayed page. |
| // Modifying the title is used for boolean events, while anything that |
| // requires more complex communication uses a port. |
| 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().find("about:blank") != std::string::npos) { |
| loaded_about_blank_ = true; |
| } |
| if (nav_state.title().find("window_resized") != std::string::npos) { |
| window_resized_ = true; |
| } |
| if (nav_state.title().find("text_input_focused") != std::string::npos) { |
| text_input_focused_ = true; |
| } |
| } |
| |
| send_ack(); |
| } |
| bool loaded_about_blank_ = false; |
| bool is_main_document_loaded_ = false; |
| bool window_resized_ = false; |
| bool text_input_focused_ = false; |
| }; |
| |
| // Implements a simple web app, which responds to tap and keyboard events. |
| class WebApp : public fuchsia::ui::app::ViewProvider { |
| public: |
| WebApp() |
| : loop_(&kAsyncLoopConfigAttachToCurrentThread), |
| context_(sys::ComponentContext::CreateAndServeOutgoingDirectory()), |
| view_provider_binding_(this) { |
| SetUpViewProvider(); |
| SetUpWebEngine(); |
| } |
| |
| 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 injecting JS to |
| // the wrong page. |
| RunLoopUntil([&navigation_event_listener] { |
| return navigation_event_listener.loaded_about_blank_ && |
| navigation_event_listener.is_main_document_loaded_; |
| }); |
| |
| // Load the javascript which sets up the test HTML page. The test HTML page is instrumented |
| // with event handlers that know how to report back to the web app. |
| 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; }); |
| |
| // Register a port for web communication. |
| fuchsia::web::MessagePortPtr message_port; |
| bool is_port_registered = false; |
| bool is_window_resized = false; |
| SendMessageToWebPage(message_port.NewRequest(), "REGISTER_PORT"); |
| message_port->ReceiveMessage([&is_port_registered, &is_window_resized](auto web_message) { |
| auto message = StringFromBuffer(web_message.data()); |
| if (message == "PORT_REGISTERED") { |
| is_port_registered = true; |
| } else if (message == "PORT_REGISTERED_WINDOW_RESIZED") { |
| is_window_resized = true; |
| is_port_registered = true; |
| } else { |
| FX_LOGS(FATAL) << "Unexpected message from web page: " << message; |
| } |
| }); |
| |
| // Wait until various lifecycle stages in the web engine are reached before proceeding. |
| FX_LOGS(INFO) << "Wait for is_port_registered"; |
| RunLoopUntil([&] { return is_port_registered; }); |
| FX_LOGS(INFO) << "Wait for window_resized"; |
| if (!is_window_resized) { |
| RunLoopUntil( |
| [&navigation_event_listener] { return navigation_event_listener.window_resized_; }); |
| } |
| FX_LOGS(INFO) << "Wait for text_input_focused"; |
| RunLoopUntil( |
| [&navigation_event_listener] { return navigation_event_listener.text_input_focused_; }); |
| |
| // Send `ReportReady` to the test fixture, and wait until the call is |
| // acknowledged. |
| fuchsia::ui::test::input::KeyboardInputListenerPtr response_listener_proxy; |
| context_->svc()->Connect(response_listener_proxy.NewRequest()); |
| bool report_ready_acked = false; |
| response_listener_proxy->ReportReady([&report_ready_acked]() { report_ready_acked = true; }); |
| FX_LOGS(INFO) << "Wait for report_ready_acked"; |
| RunLoopUntil([&report_ready_acked] { return report_ready_acked; }); |
| |
| // Watch for any changes in the text area, and forward repeatedly to the |
| // response listener in the test fixture. |
| for (;;) { |
| // This WebMessage comes from the Javascript code (below). |
| std::optional<fuchsia::web::WebMessage> received; |
| message_port->ReceiveMessage( |
| [&received](fuchsia::web::WebMessage web_message) { received = std::move(web_message); }); |
| RunLoopUntil([&received] { return received.has_value(); }); |
| |
| // Forward the message to the test fixture. |
| auto str = StringFromBuffer(received.value().data()); |
| fuchsia::ui::test::input::KeyboardInputListenerReportTextInputRequest request; |
| request.set_text(std::move(str)); |
| response_listener_proxy->ReportTextInput(std::move(request)); |
| } |
| } |
| |
| private: |
| // The application code that will be loaded up. |
| static constexpr auto kAppCode = R"JS( |
| let port; |
| |
| // Report a window resize event by changing the document title. |
| window.onresize = function(event) { |
| if (window.innerWidth != 0) { |
| console.info('size: ', window.innerWidth, window.innerHeight); |
| document.title = [document.title, 'window_resized'].join(' '); |
| } |
| }; |
| |
| // Registers a port for sending messages between the web engine and this |
| // web app. |
| function receiveMessage(event) { |
| if (event.data == "REGISTER_PORT") { |
| console.log("received REGISTER_PORT"); |
| port = event.ports[0]; |
| if (window.innerWidth != 0) { |
| // If the window was resized before JS loaded, notify the test |
| // fixture so that it skips waiting for the resize to happen. |
| port.postMessage('PORT_REGISTERED_WINDOW_RESIZED'); |
| } else { |
| port.postMessage('PORT_REGISTERED'); |
| } |
| } else { |
| console.error('received unexpected message: ' + event.data); |
| } |
| }; |
| |
| function sendMessageEvent(messageObj) { |
| let message = JSON.stringify(messageObj); |
| port.postMessage(message); |
| } |
| |
| const headHtml = ` |
| <style> |
| body { |
| height: 100%; |
| background-color: #000077; /* dark blue */ |
| color: white; |
| } |
| #text-input { |
| height: 100%; |
| width: 100%; |
| background-color: #ca2c92; /* royal fuchsia */ |
| font-size: 36pt; |
| } |
| </style> |
| `; |
| |
| // Installs a large text field. The text field occupies most of the |
| // screen for easy navigation. |
| const bodyHtml = ` |
| <p id='some-text'>Some text below:</p> |
| <textarea id="text-input" rows="3" cols="20"></textarea> |
| `; |
| |
| document.head.innerHTML += headHtml; |
| document.body.innerHTML = bodyHtml; |
| |
| /** @type HTMLInputElement */ |
| let $input = document.querySelector("#text-input"); |
| |
| // Every time a keyup event happens on input, relay the key to the web app. |
| // "keyup" is selected instead of "keydown" because "keydown" will show us |
| // the *previous* state of the text area. |
| $input.addEventListener("keyup", function (e) { |
| sendMessageEvent({ |
| text: $input.value, |
| }); |
| }); |
| |
| // Sends a signal that the text area is focused, when that happens. The |
| // easiest way to do that is to change the document title. There is a |
| // navigation listener which will get notified of the title change. |
| $input.addEventListener('focus', function (e) { |
| document.title = [document.title, 'text_input_focused'].join(' '); |
| }); |
| |
| 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(ERROR) << "web_context_provider: " << zx_status_get_string(status); |
| }); |
| FX_CHECK(incoming_service_clone.is_valid()); |
| |
| fuchsia::web::CreateContextParams params; |
| // Enables chrome remote devtools if needed. |
| // params.set_remote_debugging_port(12342); |
| params.set_service_directory(std::move(incoming_service_clone)); |
| // Enable Vulkan to allow WebEngine run on Flatland. Also, enable |
| // keyboard events. |
| params.set_features(fuchsia::web::ContextFeatureFlags::VULKAN | |
| fuchsia::web::ContextFeatureFlags::NETWORK | |
| fuchsia::web::ContextFeatureFlags::KEYBOARD); |
| auto web_context_request = web_context_.NewRequest(); |
| web_context_.set_error_handler([](zx_status_t status) { |
| FX_LOGS(ERROR) << "web_context_ error: " << zx_status_get_string(status); |
| }); |
| web_context_provider->Create(std::move(params), std::move(web_context_request)); |
| fuchsia::web::CreateFrameParams frame_params; |
| frame_params.set_debug_name("text-input-chromium"); |
| // frame_params.set_enable_remote_debugging(false); |
| auto web_frame_request = web_frame_.NewRequest(); |
| web_frame_.set_error_handler([](zx_status_t status) { |
| FX_LOGS(ERROR) << "web_frame_ error: " << zx_status_get_string(status); |
| }); |
| web_context_->CreateFrameWithParams(std::move(frame_params), std::move(web_frame_request)); |
| |
| // Setup log level in JS to get logs. |
| web_frame_->SetJavaScriptLogLevel(fuchsia::web::ConsoleLogLevel::DEBUG); |
| } |
| |
| 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)); |
| } |
| |
| // This is a GFX API call, not a flatland call, so it is not implemented. |
| // This test does not work under GFX. |
| // |
| // |fuchsia::ui::app::ViewProvider| |
| void CreateViewWithViewRef(zx::eventpair, fuchsia::ui::views::ViewRefControl, |
| fuchsia::ui::views::ViewRef) override { |
| FX_LOGS(FATAL) << "CreateViewWithViewRef() is not implemented."; |
| } |
| |
| // This API call is a Flatland API call, so we must implement it to create |
| // a Flatland view. |
| 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)); |
| FX_LOGS(DEBUG) << "View created"; |
| } |
| |
| 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); |
| } |
| } |
| |
| 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(); } |