From c011dc766f19dae5c7007f71cea298a4c993f5ca Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 6 Jun 2025 15:42:12 -0400 Subject: [PATCH] LibWebView+WebDriver+UI: Migrate headless browsing to main Ladybird exe We currently create a separate headless-browser application to serve two purposes: 1. Allow headless browsing to take a screenshot of a page or print its layout tree / internal text. 2. Run the LibWeb test framework. This patch migrates (1) to the main Ladybird executable. The --headless flag enables this mode. This matches the behavior of other browsers, and means we have one less executable to ship at distribution time. We want to avoid creating too many AppKit / Qt facilities in headless mode. So this involves some shuffling of application init to ensure we don't create them until after we've parsed the command line arguments. Namely, we avoid creating the NSApp in AppKit and QCoreApplication in Qt. Doing so also requires that we don't create the application event loop until we've parsed the command line as well, because the loop we create depends on whether we're creating those UI facilities. --- Documentation/Testing.md | 8 +- Libraries/LibWebView/Application.cpp | 129 ++++++++++++++++- Libraries/LibWebView/Application.h | 7 +- Libraries/LibWebView/CMakeLists.txt | 1 + Libraries/LibWebView/HeadlessWebView.cpp | 177 +++++++++++++++++++++++ Libraries/LibWebView/HeadlessWebView.h | 44 ++++++ Libraries/LibWebView/HelperProcess.cpp | 6 +- Libraries/LibWebView/Options.h | 16 +- Meta/WPT.sh | 7 +- Services/WebDriver/Client.cpp | 12 +- Services/WebDriver/Client.h | 13 +- Services/WebDriver/Session.cpp | 12 +- Services/WebDriver/Session.h | 5 +- Services/WebDriver/main.cpp | 30 ++-- UI/AppKit/Application/Application.h | 17 +-- UI/AppKit/Application/Application.mm | 68 ++++----- UI/AppKit/main.mm | 58 ++++---- UI/CMakeLists.txt | 2 +- UI/Headless/Application.cpp | 30 +--- UI/Headless/Application.h | 7 - UI/Headless/HeadlessWebView.cpp | 159 +------------------- UI/Headless/HeadlessWebView.h | 24 +-- UI/Headless/Test.cpp | 13 +- UI/Headless/Test.h | 2 +- UI/Headless/main.cpp | 73 +--------- UI/Qt/Application.cpp | 14 +- UI/Qt/Application.h | 1 + UI/Qt/main.cpp | 94 ++++++------ 28 files changed, 535 insertions(+), 494 deletions(-) create mode 100644 Libraries/LibWebView/HeadlessWebView.cpp create mode 100644 Libraries/LibWebView/HeadlessWebView.h diff --git a/Documentation/Testing.md b/Documentation/Testing.md index f5eeb99de68..491cf95705b 100644 --- a/Documentation/Testing.md +++ b/Documentation/Testing.md @@ -141,10 +141,14 @@ For Text or Layout tests, you can "rebaseline" the tests to regenerate the expec ``` For Ref and Screenshot tests, you will need to supply the equivalently rendering HTML manually. Though for Screenshot -tests, you can generate the reference screenshot itself by running headless-browser in test mode: +tests, you can generate the reference screenshot itself by running Ladybird in headless mode: ```bash -./Meta/ladybird.py run headless-browser --layout-test-mode Tests/LibWeb/Screenshot/input/your-new-test-name.html --screenshot-path Tests/LibWeb/Screenshot/images/your-new-test-name.png +./Meta/ladybird.py run ladybird --headless --layout-test-mode Tests/LibWeb/Screenshot/input/your-new-test-name.html + +# This will log something like: "Saved screenshot to: ~/Downloads/screenshot-2025-06-07-08-37-45.png" + +mv ~/Downloads/screenshot-2025-06-07-08-37-45.png Tests/LibWeb/Screenshot/images/your-new-test-name.png ``` ### Text tests diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index fceb9c07ee3..18c744b8f99 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -17,9 +17,11 @@ #include #include #include +#include #include #include #include +#include #include namespace WebView { @@ -91,6 +93,9 @@ void Application::initialize(Main::Arguments const& arguments) Vector raw_urls; Vector certificates; + Optional headless_mode; + Optional window_width; + Optional window_height; bool new_window = false; bool force_new_process = false; bool allow_popups = false; @@ -104,6 +109,7 @@ void Application::initialize(Main::Arguments const& arguments) Optional dns_server_address; Optional dns_server_port; bool use_dns_over_tls = false; + bool layout_test_mode = false; bool log_all_js_exceptions = false; bool disable_site_isolation = false; bool enable_idl_tracing = false; @@ -118,6 +124,29 @@ void Application::initialize(Main::Arguments const& arguments) Core::ArgsParser args_parser; args_parser.set_general_help("The Ladybird web browser :^)"); args_parser.add_positional_argument(raw_urls, "URLs to open", "url", Core::ArgsParser::Required::No); + + args_parser.add_option(Core::ArgsParser::Option { + .argument_mode = Core::ArgsParser::OptionArgumentMode::Optional, + .help_string = "Run Ladybird without a browser window. Mode may be 'screenshot' (default), 'layout-tree', or 'text'.", + .long_name = "headless", + .value_name = "mode", + .accept_value = [&](StringView value) { + if (headless_mode.has_value()) + return false; + + if (value.is_empty() || value.equals_ignoring_ascii_case("screenshot"sv)) + headless_mode = HeadlessMode::Screenshot; + else if (value.equals_ignoring_ascii_case("layout-tree"sv)) + headless_mode = HeadlessMode::LayoutTree; + else if (value.equals_ignoring_ascii_case("text"sv)) + headless_mode = HeadlessMode::Text; + + return headless_mode.has_value(); + }, + }); + + args_parser.add_option(window_width, "Set viewport width in pixels (default: 800) (currently only supported for headless mode)", "window-width", 0, "pixels"); + args_parser.add_option(window_height, "Set viewport height in pixels (default: 600) (currently only supported for headless mode)", "window-height", 0, "pixels"); args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate"); args_parser.add_option(new_window, "Force opening in a new window", "new-window", 'n'); args_parser.add_option(force_new_process, "Force creation of a new browser process", "force-new-process"); @@ -128,6 +157,7 @@ void Application::initialize(Main::Arguments const& arguments) args_parser.add_option(profile_process, "Enable callgrind profiling of the given process name (WebContent, RequestServer, etc.)", "profile-process", 0, "process-name"); args_parser.add_option(webdriver_content_ipc_path, "Path to WebDriver IPC for WebContent", "webdriver-content-path", 0, "path", Core::ArgsParser::OptionHideMode::CommandLineAndMarkdown); args_parser.add_option(devtools_port, "Set the Firefox DevTools server port ", "devtools-port", 0, "port"); + args_parser.add_option(layout_test_mode, "Enable layout test mode", "layout-test-mode"); args_parser.add_option(log_all_js_exceptions, "Log all JavaScript exceptions", "log-all-js-exceptions"); args_parser.add_option(disable_site_isolation, "Disable site isolation", "disable-site-isolation"); args_parser.add_option(enable_idl_tracing, "Enable IDL tracing", "enable-idl-tracing"); @@ -141,6 +171,7 @@ void Application::initialize(Main::Arguments const& arguments) args_parser.add_option(dns_server_address, "Set the DNS server address", "dns-server", 0, "host|address"); args_parser.add_option(dns_server_port, "Set the DNS server port", "dns-port", 0, "port (default: 53 or 853 if --dot)"); args_parser.add_option(use_dns_over_tls, "Use DNS over TLS", "dot"); + args_parser.add_option(Core::ArgsParser::Option { .argument_mode = Core::ArgsParser::OptionArgumentMode::Required, .help_string = "Name of the User-Agent preset to use in place of the default User-Agent", @@ -178,6 +209,7 @@ void Application::initialize(Main::Arguments const& arguments) m_browser_options = { .urls = sanitize_urls(raw_urls, m_settings.new_tab_page_url()), .raw_urls = move(raw_urls), + .headless_mode = headless_mode, .certificates = move(certificates), .new_window = new_window ? NewWindow::Yes : NewWindow::No, .force_new_process = force_new_process ? ForceNewProcess::Yes : ForceNewProcess::No, @@ -194,6 +226,11 @@ void Application::initialize(Main::Arguments const& arguments) .devtools_port = devtools_port, }; + if (window_width.has_value()) + m_browser_options.window_width = *window_width; + if (window_height.has_value()) + m_browser_options.window_height = *window_height; + if (webdriver_content_ipc_path.has_value()) m_browser_options.webdriver_content_ipc_path = *webdriver_content_ipc_path; @@ -201,6 +238,7 @@ void Application::initialize(Main::Arguments const& arguments) .command_line = MUST(String::join(' ', arguments.strings)), .executable_path = MUST(String::from_byte_string(MUST(Core::System::current_executable_path()))), .user_agent_preset = move(user_agent_preset), + .is_layout_test_mode = layout_test_mode ? IsLayoutTestMode::Yes : IsLayoutTestMode::No, .log_all_js_exceptions = log_all_js_exceptions ? LogAllJSExceptions::Yes : LogAllJSExceptions::No, .disable_site_isolation = disable_site_isolation ? DisableSiteIsolation::Yes : DisableSiteIsolation::No, .enable_idl_tracing = enable_idl_tracing ? EnableIDLTracing::Yes : EnableIDLTracing::No, @@ -215,12 +253,7 @@ void Application::initialize(Main::Arguments const& arguments) create_platform_options(m_browser_options, m_web_content_options); - if (m_browser_options.disable_sql_database == DisableSQLDatabase::No) { - m_database = Database::create().release_value_but_fixme_should_propagate_errors(); - m_cookie_jar = CookieJar::create(*m_database).release_value_but_fixme_should_propagate_errors(); - } else { - m_cookie_jar = CookieJar::create(); - } + m_event_loop = create_platform_event_loop(); } static ErrorOr> create_web_content_client(Optional view) @@ -278,6 +311,13 @@ void Application::launch_spare_web_content_process() ErrorOr Application::launch_services() { + if (m_browser_options.disable_sql_database == DisableSQLDatabase::No) { + m_database = Database::create().release_value_but_fixme_should_propagate_errors(); + m_cookie_jar = CookieJar::create(*m_database).release_value_but_fixme_should_propagate_errors(); + } else { + m_cookie_jar = CookieJar::create(); + } + TRY(launch_request_server()); TRY(launch_image_decoder_server()); return {}; @@ -329,13 +369,86 @@ ErrorOr Application::launch_devtools_server() return {}; } -int Application::execute() +static NonnullRefPtr load_page_for_screenshot_and_exit(Core::EventLoop& event_loop, HeadlessWebView& view, URL::URL const& url, int screenshot_timeout) { - int ret = m_event_loop.exec(); + outln("Taking screenshot after {} seconds", screenshot_timeout); + + auto timer = Core::Timer::create_single_shot( + screenshot_timeout * 1000, + [&]() { + view.take_screenshot(ViewImplementation::ScreenshotType::Full) + ->when_resolved([&event_loop](auto const& path) { + outln("Saved screenshot to: {}", path); + event_loop.quit(0); + }) + .when_rejected([&event_loop](auto const& error) { + warnln("Unable to take screenshot: {}", error); + event_loop.quit(0); + }); + }); + + view.load(url); + timer->start(); + + return timer; +} + +static void load_page_for_info_and_exit(Core::EventLoop& event_loop, HeadlessWebView& view, URL::URL const& url, WebView::PageInfoType type) +{ + view.on_load_finish = [&view, &event_loop, url, type](auto const& loaded_url) { + if (!url.equals(loaded_url, URL::ExcludeFragment::Yes)) + return; + + view.request_internal_page_info(type)->when_resolved([&event_loop](auto const& text) { + outln("{}", text); + event_loop.quit(0); + }); + }; + + view.load(url); +} + +ErrorOr Application::execute() +{ + OwnPtr view; + RefPtr screenshot_timer; + + if (m_browser_options.headless_mode.has_value()) { + auto theme_path = LexicalPath::join(WebView::s_ladybird_resource_root, "themes"sv, "Default.ini"sv); + auto theme = TRY(Gfx::load_system_theme(theme_path.string())); + + view = HeadlessWebView::create(move(theme), { m_browser_options.window_width, m_browser_options.window_height }); + + if (!m_browser_options.webdriver_content_ipc_path.has_value()) { + if (m_browser_options.urls.size() != 1) + return Error::from_string_literal("Headless mode currently only supports exactly one URL"); + + switch (*m_browser_options.headless_mode) { + case HeadlessMode::Screenshot: + screenshot_timer = load_page_for_screenshot_and_exit(*m_event_loop, *view, m_browser_options.urls.first(), 1); + break; + case HeadlessMode::LayoutTree: + load_page_for_info_and_exit(*m_event_loop, *view, m_browser_options.urls.first(), WebView::PageInfoType::LayoutTree | WebView::PageInfoType::PaintTree); + break; + case HeadlessMode::Text: + load_page_for_info_and_exit(*m_event_loop, *view, m_browser_options.urls.first(), WebView::PageInfoType::Text); + break; + case HeadlessMode::Test: + VERIFY_NOT_REACHED(); + } + } + } + + int ret = m_event_loop->exec(); m_in_shutdown = true; return ret; } +NonnullOwnPtr Application::create_platform_event_loop() +{ + return make(); +} + void Application::add_child_process(WebView::Process&& process) { m_process_manager.add_process(move(process)); diff --git a/Libraries/LibWebView/Application.h b/Libraries/LibWebView/Application.h index 0d1b0ae84da..3078b2be04b 100644 --- a/Libraries/LibWebView/Application.h +++ b/Libraries/LibWebView/Application.h @@ -34,7 +34,7 @@ class Application : public DevTools::DevToolsDelegate { public: virtual ~Application(); - int execute(); + ErrorOr execute(); static Application& the() { return *s_the; } @@ -50,8 +50,6 @@ public: static ProcessManager& process_manager() { return the().m_process_manager; } - Core::EventLoop& event_loop() { return m_event_loop; } - ErrorOr> launch_web_content_process(ViewImplementation&); ErrorOr launch_services(); @@ -88,6 +86,7 @@ protected: virtual void create_platform_arguments(Core::ArgsParser&) { } virtual void create_platform_options(BrowserOptions&, WebContentOptions&) { } + virtual NonnullOwnPtr create_platform_event_loop(); virtual Optional ask_user_for_download_folder() const { return {}; } @@ -149,7 +148,7 @@ private: OwnPtr m_time_zone_watcher; - Core::EventLoop m_event_loop; + OwnPtr m_event_loop; ProcessManager m_process_manager; bool m_in_shutdown { false }; diff --git a/Libraries/LibWebView/CMakeLists.txt b/Libraries/LibWebView/CMakeLists.txt index 2c2c764e621..66b5028a0fd 100644 --- a/Libraries/LibWebView/CMakeLists.txt +++ b/Libraries/LibWebView/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES CookieJar.cpp Database.cpp DOMNodeProperties.cpp + HeadlessWebView.cpp HelperProcess.cpp Mutation.cpp Plugins/FontPlugin.cpp diff --git a/Libraries/LibWebView/HeadlessWebView.cpp b/Libraries/LibWebView/HeadlessWebView.cpp new file mode 100644 index 00000000000..fd432c33bc9 --- /dev/null +++ b/Libraries/LibWebView/HeadlessWebView.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2024-2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace WebView { + +static Web::DevicePixelRect const screen_rect { 0, 0, 1920, 1080 }; + +NonnullOwnPtr HeadlessWebView::create(Core::AnonymousBuffer theme, Web::DevicePixelSize window_size) +{ + auto view = adopt_own(*new HeadlessWebView(move(theme), window_size)); + view->initialize_client(CreateNewClient::Yes); + + return view; +} + +NonnullOwnPtr HeadlessWebView::create_child(HeadlessWebView& parent, u64 page_index) +{ + auto view = adopt_own(*new HeadlessWebView(parent.m_theme, parent.m_viewport_size)); + + view->m_client_state.client = parent.client(); + view->m_client_state.page_index = page_index; + view->initialize_client(CreateNewClient::No); + + return view; +} + +HeadlessWebView::HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewport_size) + : m_theme(move(theme)) + , m_viewport_size(viewport_size) +{ + on_new_web_view = [this](auto, auto, Optional page_index) { + auto web_view = page_index.has_value() + ? HeadlessWebView::create_child(*this, *page_index) + : HeadlessWebView::create(m_theme, m_viewport_size); + + m_child_web_views.append(move(web_view)); + return m_child_web_views.last()->handle(); + }; + + on_reposition_window = [this](auto position) { + client().async_set_window_position(m_client_state.page_index, position.template to_type()); + + client().async_did_update_window_rect(m_client_state.page_index); + }; + + on_resize_window = [this](auto size) { + m_viewport_size = size.template to_type(); + + client().async_set_window_size(m_client_state.page_index, m_viewport_size); + client().async_set_viewport_size(m_client_state.page_index, m_viewport_size); + + client().async_did_update_window_rect(m_client_state.page_index); + }; + + on_restore_window = [this]() { + set_system_visibility_state(Web::HTML::VisibilityState::Visible); + }; + + on_minimize_window = [this]() { + set_system_visibility_state(Web::HTML::VisibilityState::Hidden); + }; + + on_maximize_window = [this]() { + m_viewport_size = screen_rect.size(); + + client().async_set_window_position(m_client_state.page_index, screen_rect.location()); + client().async_set_window_size(m_client_state.page_index, screen_rect.size()); + client().async_set_viewport_size(m_client_state.page_index, screen_rect.size()); + + client().async_did_update_window_rect(m_client_state.page_index); + }; + + on_fullscreen_window = [this]() { + m_viewport_size = screen_rect.size(); + + client().async_set_window_position(m_client_state.page_index, screen_rect.location()); + client().async_set_window_size(m_client_state.page_index, screen_rect.size()); + client().async_set_viewport_size(m_client_state.page_index, screen_rect.size()); + + client().async_did_update_window_rect(m_client_state.page_index); + }; + + on_request_alert = [this](auto const&) { + m_pending_dialog = Web::Page::PendingDialog::Alert; + }; + + on_request_confirm = [this](auto const&) { + m_pending_dialog = Web::Page::PendingDialog::Confirm; + }; + + on_request_prompt = [this](auto const&, auto const& prompt_text) { + m_pending_dialog = Web::Page::PendingDialog::Prompt; + m_pending_prompt_text = prompt_text; + }; + + on_request_set_prompt_text = [this](auto const& prompt_text) { + m_pending_prompt_text = prompt_text; + }; + + on_request_accept_dialog = [this]() { + switch (m_pending_dialog) { + case Web::Page::PendingDialog::None: + VERIFY_NOT_REACHED(); + break; + case Web::Page::PendingDialog::Alert: + alert_closed(); + break; + case Web::Page::PendingDialog::Confirm: + confirm_closed(true); + break; + case Web::Page::PendingDialog::Prompt: + prompt_closed(move(m_pending_prompt_text)); + break; + } + + m_pending_dialog = Web::Page::PendingDialog::None; + }; + + on_request_dismiss_dialog = [this]() { + switch (m_pending_dialog) { + case Web::Page::PendingDialog::None: + VERIFY_NOT_REACHED(); + break; + case Web::Page::PendingDialog::Alert: + alert_closed(); + break; + case Web::Page::PendingDialog::Confirm: + confirm_closed(false); + break; + case Web::Page::PendingDialog::Prompt: + prompt_closed({}); + break; + } + + m_pending_dialog = Web::Page::PendingDialog::None; + m_pending_prompt_text.clear(); + }; + + on_insert_clipboard_entry = [this](Web::Clipboard::SystemClipboardRepresentation entry, auto const&) { + Web::Clipboard::SystemClipboardItem item; + item.system_clipboard_representations.append(move(entry)); + + m_clipboard = move(item); + }; + + on_request_clipboard_entries = [this](auto request_id) { + if (m_clipboard.has_value()) + retrieved_clipboard_entries(request_id, { { *m_clipboard } }); + else + retrieved_clipboard_entries(request_id, {}); + }; + + m_system_visibility_state = Web::HTML::VisibilityState::Visible; +} + +void HeadlessWebView::initialize_client(CreateNewClient create_new_client) +{ + ViewImplementation::initialize_client(create_new_client); + + client().async_update_system_theme(m_client_state.page_index, m_theme); + client().async_set_viewport_size(m_client_state.page_index, viewport_size()); + client().async_set_window_size(m_client_state.page_index, viewport_size()); + client().async_update_screen_rects(m_client_state.page_index, { { screen_rect } }, 0); +} + +void HeadlessWebView::update_zoom() +{ + client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level); + client().async_set_viewport_size(m_client_state.page_index, m_viewport_size); +} + +} diff --git a/Libraries/LibWebView/HeadlessWebView.h b/Libraries/LibWebView/HeadlessWebView.h new file mode 100644 index 00000000000..4b68d02b5a2 --- /dev/null +++ b/Libraries/LibWebView/HeadlessWebView.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024-2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace WebView { + +class HeadlessWebView : public WebView::ViewImplementation { +public: + static NonnullOwnPtr create(Core::AnonymousBuffer theme, Web::DevicePixelSize window_size); + static NonnullOwnPtr create_child(HeadlessWebView&, u64 page_index); + +protected: + HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewport_size); + + void initialize_client(CreateNewClient) override; + void update_zoom() override; + + virtual Web::DevicePixelSize viewport_size() const override { return m_viewport_size; } + virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override { return widget_position; } + virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override { return content_position; } + + Core::AnonymousBuffer m_theme; + Web::DevicePixelSize m_viewport_size; + + Web::Page::PendingDialog m_pending_dialog { Web::Page::PendingDialog::None }; + Optional m_pending_prompt_text; + + // FIXME: We should implement UI-agnostic platform APIs to interact with the system clipboard. + Optional m_clipboard; + + Vector> m_child_web_views; +}; + +} diff --git a/Libraries/LibWebView/HelperProcess.cpp b/Libraries/LibWebView/HelperProcess.cpp index c4d104e1b8a..5f398f1a399 100644 --- a/Libraries/LibWebView/HelperProcess.cpp +++ b/Libraries/LibWebView/HelperProcess.cpp @@ -85,6 +85,7 @@ static ErrorOr> launch_web_content_proc Optional request_server_socket, ClientArguments&&... client_arguments) { + auto const& browser_options = WebView::Application::browser_options(); auto const& web_content_options = WebView::Application::web_content_options(); Vector arguments { @@ -94,6 +95,9 @@ static ErrorOr> launch_web_content_proc web_content_options.executable_path.to_byte_string(), }; + if (browser_options.headless_mode.has_value()) + arguments.append("--headless"sv); + if (web_content_options.config_path.has_value()) { arguments.append("--config-path"sv); arguments.append(web_content_options.config_path.value()); @@ -116,8 +120,6 @@ static ErrorOr> launch_web_content_proc arguments.append("--force-fontconfig"sv); if (web_content_options.collect_garbage_on_every_allocation == WebView::CollectGarbageOnEveryAllocation::Yes) arguments.append("--collect-garbage-on-every-allocation"sv); - if (web_content_options.is_headless == WebView::IsHeadless::Yes) - arguments.append("--headless"sv); if (web_content_options.paint_viewport_scrollbars == PaintViewportScrollbars::No) arguments.append("--disable-scrollbar-painting"sv); diff --git a/Libraries/LibWebView/Options.h b/Libraries/LibWebView/Options.h index cc848a663ab..c6ed4d8b622 100644 --- a/Libraries/LibWebView/Options.h +++ b/Libraries/LibWebView/Options.h @@ -15,6 +15,13 @@ namespace WebView { +enum class HeadlessMode { + Screenshot, + LayoutTree, + Text, + Test, +}; + enum class NewWindow { No, Yes, @@ -62,6 +69,9 @@ constexpr inline u16 default_devtools_port = 6000; struct BrowserOptions { Vector urls; Vector raw_urls; + Optional headless_mode; + int window_width { 800 }; + int window_height { 600 }; Vector certificates {}; NewWindow new_window { NewWindow::No }; ForceNewProcess force_new_process { ForceNewProcess::No }; @@ -120,11 +130,6 @@ enum class CollectGarbageOnEveryAllocation { Yes, }; -enum class IsHeadless { - No, - Yes, -}; - enum class PaintViewportScrollbars { Yes, No, @@ -146,7 +151,6 @@ struct WebContentOptions { EnableAutoplay enable_autoplay { EnableAutoplay::No }; CollectGarbageOnEveryAllocation collect_garbage_on_every_allocation { CollectGarbageOnEveryAllocation::No }; Optional echo_server_port {}; - IsHeadless is_headless { IsHeadless::No }; PaintViewportScrollbars paint_viewport_scrollbars { PaintViewportScrollbars::Yes }; }; diff --git a/Meta/WPT.sh b/Meta/WPT.sh index 9407ceac873..56ea0ce91a3 100755 --- a/Meta/WPT.sh +++ b/Meta/WPT.sh @@ -79,7 +79,9 @@ WPT_CERTIFICATES=( "tools/certs/cacert.pem" "${BUILD_DIR}/Lagom/cacert.pem" ) -WPT_ARGS=( "--webdriver-binary=${WEBDRIVER_BINARY}" +WPT_ARGS=( + "--binary=${LADYBIRD_BINARY}" + "--webdriver-binary=${WEBDRIVER_BINARY}" "--install-webdriver" "--webdriver-arg=--force-cpu-painting" "--no-pause-after-test" @@ -207,10 +209,7 @@ while [[ "$ARG" =~ ^(--show-window|--debug-process|--parallel-instances|(--log(- done if [ $headless -eq 1 ]; then - WPT_ARGS+=( "--binary=${HEADLESS_BROWSER_BINARY}" ) WPT_ARGS+=( "--webdriver-arg=--headless" ) -else - WPT_ARGS+=( "--binary=${LADYBIRD_BINARY}" ) fi exit_if_running_as_root "Do not run WPT.sh as root" diff --git a/Services/WebDriver/Client.cpp b/Services/WebDriver/Client.cpp index 54e1a71132e..5052e77f713 100644 --- a/Services/WebDriver/Client.cpp +++ b/Services/WebDriver/Client.cpp @@ -21,18 +21,18 @@ namespace WebDriver { -ErrorOr> Client::try_create(NonnullOwnPtr socket, LaunchBrowserCallbacks callbacks, Core::EventReceiver* parent) +ErrorOr> Client::try_create(NonnullOwnPtr socket, LaunchBrowserCallback launch_browser_callback, Core::EventReceiver* parent) { - if (!callbacks.launch_browser || !callbacks.launch_headless_browser) - return Error::from_string_literal("All callbacks to launch a browser must be provided"); + if (!launch_browser_callback) + return Error::from_string_literal("The callback to launch the browser must be provided"); TRY(socket->set_blocking(true)); - return adopt_nonnull_ref_or_enomem(new (nothrow) Client(move(socket), move(callbacks), parent)); + return adopt_nonnull_ref_or_enomem(new (nothrow) Client(move(socket), move(launch_browser_callback), parent)); } -Client::Client(NonnullOwnPtr socket, LaunchBrowserCallbacks callbacks, Core::EventReceiver* parent) +Client::Client(NonnullOwnPtr socket, LaunchBrowserCallback launch_browser_callback, Core::EventReceiver* parent) : Web::WebDriver::Client(move(socket), parent) - , m_callbacks(move(callbacks)) + , m_launch_browser_callback(move(launch_browser_callback)) { } diff --git a/Services/WebDriver/Client.h b/Services/WebDriver/Client.h index 3faa63e9842..73eb10b2d31 100644 --- a/Services/WebDriver/Client.h +++ b/Services/WebDriver/Client.h @@ -17,22 +17,19 @@ namespace WebDriver { -struct LaunchBrowserCallbacks { - Function(ByteString const&)> launch_browser; - Function(ByteString const&)> launch_headless_browser; -}; +using LaunchBrowserCallback = Function(ByteString const& socket_path, bool headless)>; class Client final : public Web::WebDriver::Client { C_OBJECT_ABSTRACT(Client); public: - static ErrorOr> try_create(NonnullOwnPtr, LaunchBrowserCallbacks, Core::EventReceiver* parent); + static ErrorOr> try_create(NonnullOwnPtr, LaunchBrowserCallback, Core::EventReceiver* parent); virtual ~Client() override; - LaunchBrowserCallbacks const& launch_browser_callbacks() const { return m_callbacks; } + LaunchBrowserCallback const& launch_browser_callback() const { return m_launch_browser_callback; } private: - Client(NonnullOwnPtr, LaunchBrowserCallbacks, Core::EventReceiver* parent); + Client(NonnullOwnPtr, LaunchBrowserCallback, Core::EventReceiver* parent); virtual Web::WebDriver::Response new_session(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response delete_session(Web::WebDriver::Parameters parameters, JsonValue payload) override; @@ -97,7 +94,7 @@ private: virtual Web::WebDriver::Response take_element_screenshot(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response print_page(Web::WebDriver::Parameters parameters, JsonValue payload) override; - LaunchBrowserCallbacks m_callbacks; + LaunchBrowserCallback m_launch_browser_callback; }; } diff --git a/Services/WebDriver/Session.cpp b/Services/WebDriver/Session.cpp index 85ddcccfabe..6e37e875561 100644 --- a/Services/WebDriver/Session.cpp +++ b/Services/WebDriver/Session.cpp @@ -8,8 +8,6 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include "Session.h" -#include "Client.h" #include #include #include @@ -19,6 +17,7 @@ #include #include #include +#include #include namespace WebDriver { @@ -34,7 +33,7 @@ ErrorOr> Session::create(NonnullRefPtr client, Js // 2. Let session be a new session with session ID session id, and HTTP flag flags contains "http". auto session = adopt_ref(*new Session(client, capabilities, move(session_id), flags)); - TRY(session->start(client->launch_browser_callbacks())); + TRY(session->start(client->launch_browser_callback())); // 3. Let proxy be the result of getting property "proxy" from capabilities and run the substeps of the first matching statement: // -> proxy is a proxy configuration object @@ -251,17 +250,14 @@ ErrorOr> Session::create_server(NonnullRefPtr Session::start(LaunchBrowserCallbacks const& callbacks) +ErrorOr Session::start(LaunchBrowserCallback const& launch_browser_callback) { auto promise = TRY(ServerPromise::try_create()); m_web_content_socket_path = ByteString::formatted("{}/webdriver/session_{}_{}", TRY(Core::StandardPaths::runtime_directory()), getpid(), m_session_id); m_web_content_server = TRY(create_server(promise)); - if (m_options.headless) - m_browser_process = TRY(callbacks.launch_headless_browser(*m_web_content_socket_path)); - else - m_browser_process = TRY(callbacks.launch_browser(*m_web_content_socket_path)); + m_browser_process = TRY(launch_browser_callback(*m_web_content_socket_path, m_options.headless)); // FIXME: Allow this to be more asynchronous. For now, this at least allows us to propagate // errors received while accepting the Browser and WebContent sockets. diff --git a/Services/WebDriver/Session.h b/Services/WebDriver/Session.h index 437d28b87e6..89e3e513af1 100644 --- a/Services/WebDriver/Session.h +++ b/Services/WebDriver/Session.h @@ -20,13 +20,12 @@ #include #include #include +#include #include #include namespace WebDriver { -struct LaunchBrowserCallbacks; - class Session : public RefCounted { public: static ErrorOr> create(NonnullRefPtr client, JsonObject& capabilities, Web::WebDriver::SessionFlags flags); @@ -87,7 +86,7 @@ public: private: Session(NonnullRefPtr client, JsonObject const& capabilities, String session_id, Web::WebDriver::SessionFlags flags); - ErrorOr start(LaunchBrowserCallbacks const&); + ErrorOr start(LaunchBrowserCallback const&); using ServerPromise = Core::Promise>; ErrorOr> create_server(NonnullRefPtr promise); diff --git a/Services/WebDriver/main.cpp b/Services/WebDriver/main.cpp index a17316bd6f8..3eafb6e2800 100644 --- a/Services/WebDriver/main.cpp +++ b/Services/WebDriver/main.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, Tim Flynn + * Copyright (c) 2022-2025, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -33,7 +33,7 @@ static ErrorOr launch_process(StringView application, ReadonlySpa return result; } -static Vector create_arguments(ByteString const& socket_path, bool force_cpu_painting, Optional debug_process) +static Vector create_arguments(ByteString const& socket_path, bool headless, bool force_cpu_painting, Optional debug_process) { Vector arguments { "--webdriver-content-path"sv, @@ -46,6 +46,9 @@ static Vector create_arguments(ByteString const& socket_path, bool f arguments.append(certificate_args.last().view().characters_without_null_termination()); } + if (headless) + arguments.append("--headless"sv); + arguments.append("--allow-popups"sv); arguments.append("--force-new-process"sv); arguments.append("--enable-autoplay"sv); @@ -63,18 +66,6 @@ static Vector create_arguments(ByteString const& socket_path, bool f return arguments; } -static ErrorOr launch_browser(ByteString const& socket_path, bool force_cpu_painting, Optional debug_process) -{ - auto arguments = create_arguments(socket_path, force_cpu_painting, move(debug_process)); - return launch_process("Ladybird"sv, arguments.span()); -} - -static ErrorOr launch_headless_browser(ByteString const& socket_path, bool force_cpu_painting, Optional debug_process) -{ - auto arguments = create_arguments(socket_path, force_cpu_painting, move(debug_process)); - return launch_process("headless-browser"sv, arguments.span()); -} - ErrorOr serenity_main(Main::Arguments arguments) { AK::set_rich_debug_enabled(true); @@ -129,15 +120,12 @@ ErrorOr serenity_main(Main::Arguments arguments) return; } - auto launch_browser_callback = [&](ByteString const& socket_path) { - return launch_browser(socket_path, force_cpu_painting, debug_process); + auto launch_browser_callback = [&](ByteString const& socket_path, bool headless) { + auto arguments = create_arguments(socket_path, headless, force_cpu_painting, debug_process); + return launch_process("Ladybird"sv, arguments.span()); }; - auto launch_headless_browser_callback = [&](ByteString const& socket_path) { - return launch_headless_browser(socket_path, force_cpu_painting, debug_process); - }; - - auto maybe_client = WebDriver::Client::try_create(maybe_buffered_socket.release_value(), { move(launch_browser_callback), move(launch_headless_browser_callback) }, server); + auto maybe_client = WebDriver::Client::try_create(maybe_buffered_socket.release_value(), move(launch_browser_callback), server); if (maybe_client.is_error()) { warnln("Could not create a WebDriver client: {}", maybe_client.error()); return; diff --git a/UI/AppKit/Application/Application.h b/UI/AppKit/Application/Application.h index 8d81487c916..c44aafbe4a7 100644 --- a/UI/AppKit/Application/Application.h +++ b/UI/AppKit/Application/Application.h @@ -6,22 +6,21 @@ #pragma once -#include -#include -#include -#include +#include #import namespace Ladybird { -class WebViewBridge; +class Application final : public WebView::Application { + WEB_VIEW_APPLICATION(Application) + +private: + virtual Optional ask_user_for_download_folder() const override; + virtual NonnullOwnPtr create_platform_event_loop() override; +}; } @interface Application : NSApplication - -- (void)setupWebViewApplication:(Main::Arguments&)arguments; -- (ErrorOr)launchServices; - @end diff --git a/UI/AppKit/Application/Application.mm b/UI/AppKit/Application/Application.mm index 16c0530192c..7b6f75c13a1 100644 --- a/UI/AppKit/Application/Application.mm +++ b/UI/AppKit/Application/Application.mm @@ -4,10 +4,9 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include #include #include -#include +#include #include #import @@ -18,56 +17,41 @@ namespace Ladybird { -class ApplicationBridge : public WebView::Application { - WEB_VIEW_APPLICATION(ApplicationBridge) - -private: - virtual Optional ask_user_for_download_folder() const override - { - auto* panel = [NSOpenPanel openPanel]; - [panel setAllowsMultipleSelection:NO]; - [panel setCanChooseDirectories:YES]; - [panel setCanChooseFiles:NO]; - [panel setMessage:@"Select download directory"]; - - if ([panel runModal] != NSModalResponseOK) - return {}; - - return Ladybird::ns_string_to_byte_string([[panel URL] path]); - } -}; - -ApplicationBridge::ApplicationBridge(Badge, Main::Arguments&) +Application::Application(Badge, Main::Arguments&) { } +Optional Application::ask_user_for_download_folder() const +{ + auto* panel = [NSOpenPanel openPanel]; + [panel setAllowsMultipleSelection:NO]; + [panel setCanChooseDirectories:YES]; + [panel setCanChooseFiles:NO]; + [panel setMessage:@"Select download directory"]; + + if ([panel runModal] != NSModalResponseOK) + return {}; + + return Ladybird::ns_string_to_byte_string([[panel URL] path]); +} + +NonnullOwnPtr Application::create_platform_event_loop() +{ + if (!browser_options().headless_mode.has_value()) { + Core::EventLoopManager::install(*new WebView::EventLoopManagerMacOS); + [::Application sharedApplication]; + } + + return WebView::Application::create_platform_event_loop(); +} + } @interface Application () -{ - OwnPtr m_application_bridge; - - RefPtr m_request_server_client; - RefPtr m_image_decoder_client; -} - @end @implementation Application -#pragma mark - Public methods - -- (void)setupWebViewApplication:(Main::Arguments&)arguments -{ - m_application_bridge = Ladybird::ApplicationBridge::create(arguments); -} - -- (ErrorOr)launchServices -{ - TRY(m_application_bridge->launch_services()); - return {}; -} - #pragma mark - NSApplication - (void)terminate:(id)sender diff --git a/UI/AppKit/main.mm b/UI/AppKit/main.mm index 37d2cced577..0922a1b64bf 100644 --- a/UI/AppKit/main.mm +++ b/UI/AppKit/main.mm @@ -5,14 +5,10 @@ */ #include -#include #include -#include #include #include -#include #include -#include #include #include #include @@ -47,48 +43,44 @@ ErrorOr serenity_main(Main::Arguments arguments) { AK::set_rich_debug_enabled(true); - Application* application = [Application sharedApplication]; - - Core::EventLoopManager::install(*new WebView::EventLoopManagerMacOS); - - [application setupWebViewApplication:arguments]; - + auto app = Ladybird::Application::create(arguments); WebView::platform_init(); - WebView::BrowserProcess browser_process; - - if (auto const& browser_options = WebView::Application::browser_options(); browser_options.force_new_process == WebView::ForceNewProcess::No) { - auto disposition = TRY(browser_process.connect(browser_options.raw_urls, browser_options.new_window)); - - if (disposition == WebView::BrowserProcess::ProcessDisposition::ExitProcess) { - outln("Opening in existing process"); - return 0; - } - } - - browser_process.on_new_tab = [&](auto const& raw_urls) { - open_urls_from_client(raw_urls, WebView::NewWindow::No); - }; - - browser_process.on_new_window = [&](auto const& raw_urls) { - open_urls_from_client(raw_urls, WebView::NewWindow::Yes); - }; - auto mach_port_server = make(); WebView::set_mach_server_name(mach_port_server->server_port_name()); mach_port_server->on_receive_child_mach_port = [&](auto pid, auto port) { - WebView::Application::the().set_process_mach_port(pid, move(port)); + app->set_process_mach_port(pid, move(port)); }; mach_port_server->on_receive_backing_stores = [](WebView::MachPortServer::BackingStoresMessage message) { if (auto view = WebView::WebContentClient::view_for_pid_and_page_id(message.pid, message.page_id); view.has_value()) view->did_allocate_iosurface_backing_stores(message.front_backing_store_id, move(message.front_backing_store_port), message.back_backing_store_id, move(message.back_backing_store_port)); }; - TRY([application launchServices]); + WebView::BrowserProcess browser_process; + TRY(app->launch_services()); - auto* delegate = [[ApplicationDelegate alloc] init]; - [NSApp setDelegate:delegate]; + if (auto const& browser_options = WebView::Application::browser_options(); !browser_options.headless_mode.has_value()) { + if (browser_options.force_new_process == WebView::ForceNewProcess::No) { + auto disposition = TRY(browser_process.connect(browser_options.raw_urls, browser_options.new_window)); + + if (disposition == WebView::BrowserProcess::ProcessDisposition::ExitProcess) { + outln("Opening in existing process"); + return 0; + } + } + + browser_process.on_new_tab = [&](auto const& raw_urls) { + open_urls_from_client(raw_urls, WebView::NewWindow::No); + }; + + browser_process.on_new_window = [&](auto const& raw_urls) { + open_urls_from_client(raw_urls, WebView::NewWindow::Yes); + }; + + auto* delegate = [[ApplicationDelegate alloc] init]; + [NSApp setDelegate:delegate]; + } return WebView::Application::the().execute(); } diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 93fb5d06a0f..a7c1298ebaa 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -110,7 +110,7 @@ set(ladybird_helper_processes ImageDecoder RequestServer WebContent WebWorker) add_dependencies(ladybird ${ladybird_helper_processes}) add_dependencies(headless-browser ${ladybird_helper_processes} ladybird_build_resource_files) -add_dependencies(WebDriver ladybird headless-browser) +add_dependencies(WebDriver ladybird) set_helper_process_properties(${ladybird_helper_processes}) if (APPLE) diff --git a/UI/Headless/Application.cpp b/UI/Headless/Application.cpp index 72fabdce8b8..83594ea81c9 100644 --- a/UI/Headless/Application.cpp +++ b/UI/Headless/Application.cpp @@ -29,10 +29,6 @@ Application::~Application() void Application::create_platform_arguments(Core::ArgsParser& args_parser) { - args_parser.add_option(screenshot_timeout, "Take a screenshot after [n] seconds (default: 1)", "screenshot", 's', "n"); - args_parser.add_option(screenshot_path, "Path the save the screenshot (default: 'output.png')", "screenshot-path", 'p', "path"); - args_parser.add_option(dump_layout_tree, "Dump layout tree and exit", "dump-layout-tree", 'd'); - args_parser.add_option(dump_text, "Dump text and exit", "dump-text", 'T'); args_parser.add_option(test_concurrency, "Maximum number of tests to run at once", "test-concurrency", 'j', "jobs"); args_parser.add_option(python_executable_path, "Path to python3", "python-executable", 'P', "path"); args_parser.add_option(test_globs, "Only run tests matching the given glob", "filter", 'f', "glob"); @@ -40,11 +36,8 @@ void Application::create_platform_arguments(Core::ArgsParser& args_parser) args_parser.add_option(dump_failed_ref_tests, "Dump screenshots of failing ref tests", "dump-failed-ref-tests", 'D'); args_parser.add_option(dump_gc_graph, "Dump GC graph", "dump-gc-graph", 'G'); args_parser.add_option(resources_folder, "Path of the base resources folder (defaults to /res)", "resources", 'r', "resources-root-path"); - args_parser.add_option(is_layout_test_mode, "Enable layout test mode", "layout-test-mode"); args_parser.add_option(rebaseline, "Rebaseline any executed layout or text tests", "rebaseline"); args_parser.add_option(per_test_timeout_in_seconds, "Per-test timeout (default: 30)", "per-test-timeout", 't', "seconds"); - args_parser.add_option(width, "Set viewport width in pixels (default: 800)", "width", 'W', "pixels"); - args_parser.add_option(height, "Set viewport height in pixels (default: 600)", "height", 'H', "pixels"); args_parser.add_option(Core::ArgsParser::Option { .argument_mode = Core::ArgsParser::OptionArgumentMode::Optional, @@ -85,29 +78,22 @@ void Application::create_platform_arguments(Core::ArgsParser& args_parser) void Application::create_platform_options(WebView::BrowserOptions& browser_options, WebView::WebContentOptions& web_content_options) { - if (!test_root_path.is_empty()) { - // --run-tests implies --layout-test-mode. - is_layout_test_mode = true; - } + browser_options.headless_mode = WebView::HeadlessMode::Test; + web_content_options.is_layout_test_mode = WebView::IsLayoutTestMode::Yes; - if (is_layout_test_mode) { - // Allow window.open() to succeed for tests. - browser_options.allow_popups = WebView::AllowPopups::Yes; + // Allow window.open() to succeed for tests. + browser_options.allow_popups = WebView::AllowPopups::Yes; - // Ensure consistent font rendering between operating systems. - web_content_options.force_fontconfig = WebView::ForceFontconfig::Yes; + // Ensure consistent font rendering between operating systems. + web_content_options.force_fontconfig = WebView::ForceFontconfig::Yes; - // Ensure tests are resilient to minor changes to the viewport scrollbar. - web_content_options.paint_viewport_scrollbars = WebView::PaintViewportScrollbars::No; - } + // Ensure tests are resilient to minor changes to the viewport scrollbar. + web_content_options.paint_viewport_scrollbars = WebView::PaintViewportScrollbars::No; if (dump_gc_graph) { // Force all tests to run in serial if we are interested in the GC graph. test_concurrency = 1; } - - web_content_options.is_layout_test_mode = is_layout_test_mode ? WebView::IsLayoutTestMode::Yes : WebView::IsLayoutTestMode::No; - web_content_options.is_headless = WebView::IsHeadless::Yes; } ErrorOr Application::launch_test_fixtures() diff --git a/UI/Headless/Application.h b/UI/Headless/Application.h index 4386bfd07b9..84a6645ee79 100644 --- a/UI/Headless/Application.h +++ b/UI/Headless/Application.h @@ -33,14 +33,9 @@ public: static constexpr u8 VERBOSITY_LEVEL_LOG_SLOWEST_TESTS = 2; static constexpr u8 VERBOSITY_LEVEL_LOG_SKIPPED_TESTS = 3; - int screenshot_timeout { 1 }; - ByteString screenshot_path { "output.png"sv }; ByteString resources_folder; bool dump_failed_ref_tests { false }; - bool dump_layout_tree { false }; - bool dump_text { false }; bool dump_gc_graph { false }; - bool is_layout_test_mode { false }; size_t test_concurrency { 1 }; ByteString python_executable_path; ByteString test_root_path; @@ -49,8 +44,6 @@ public: bool rebaseline { false }; u8 verbosity { 0 }; int per_test_timeout_in_seconds { 30 }; - int width { 800 }; - int height { 600 }; }; } diff --git a/UI/Headless/HeadlessWebView.cpp b/UI/Headless/HeadlessWebView.cpp index 47cb9a26bbe..91dd8c2a365 100644 --- a/UI/Headless/HeadlessWebView.cpp +++ b/UI/Headless/HeadlessWebView.cpp @@ -10,138 +10,6 @@ namespace Ladybird { -static Web::DevicePixelRect const screen_rect { 0, 0, 1920, 1080 }; - -HeadlessWebView::HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewport_size) - : m_theme(move(theme)) - , m_viewport_size(viewport_size) - , m_test_promise(TestPromise::construct()) -{ - on_new_web_view = [this](auto, auto, Optional page_index) { - auto web_view = page_index.has_value() - ? HeadlessWebView::create_child(*this, *page_index) - : HeadlessWebView::create(m_theme, m_viewport_size); - - m_child_web_views.append(move(web_view)); - return m_child_web_views.last()->handle(); - }; - - on_reposition_window = [this](auto position) { - client().async_set_window_position(m_client_state.page_index, position.template to_type()); - - client().async_did_update_window_rect(m_client_state.page_index); - }; - - on_resize_window = [this](auto size) { - m_viewport_size = size.template to_type(); - - client().async_set_window_size(m_client_state.page_index, m_viewport_size); - client().async_set_viewport_size(m_client_state.page_index, m_viewport_size); - - client().async_did_update_window_rect(m_client_state.page_index); - }; - - on_restore_window = [this]() { - set_system_visibility_state(Web::HTML::VisibilityState::Visible); - }; - - on_minimize_window = [this]() { - set_system_visibility_state(Web::HTML::VisibilityState::Hidden); - }; - - on_maximize_window = [this]() { - m_viewport_size = screen_rect.size(); - - client().async_set_window_position(m_client_state.page_index, screen_rect.location()); - client().async_set_window_size(m_client_state.page_index, screen_rect.size()); - client().async_set_viewport_size(m_client_state.page_index, screen_rect.size()); - - client().async_did_update_window_rect(m_client_state.page_index); - }; - - on_fullscreen_window = [this]() { - m_viewport_size = screen_rect.size(); - - client().async_set_window_position(m_client_state.page_index, screen_rect.location()); - client().async_set_window_size(m_client_state.page_index, screen_rect.size()); - client().async_set_viewport_size(m_client_state.page_index, screen_rect.size()); - - client().async_did_update_window_rect(m_client_state.page_index); - }; - - on_request_alert = [this](auto const&) { - m_pending_dialog = Web::Page::PendingDialog::Alert; - }; - - on_request_confirm = [this](auto const&) { - m_pending_dialog = Web::Page::PendingDialog::Confirm; - }; - - on_request_prompt = [this](auto const&, auto const& prompt_text) { - m_pending_dialog = Web::Page::PendingDialog::Prompt; - m_pending_prompt_text = prompt_text; - }; - - on_request_set_prompt_text = [this](auto const& prompt_text) { - m_pending_prompt_text = prompt_text; - }; - - on_request_accept_dialog = [this]() { - switch (m_pending_dialog) { - case Web::Page::PendingDialog::None: - VERIFY_NOT_REACHED(); - break; - case Web::Page::PendingDialog::Alert: - alert_closed(); - break; - case Web::Page::PendingDialog::Confirm: - confirm_closed(true); - break; - case Web::Page::PendingDialog::Prompt: - prompt_closed(move(m_pending_prompt_text)); - break; - } - - m_pending_dialog = Web::Page::PendingDialog::None; - }; - - on_request_dismiss_dialog = [this]() { - switch (m_pending_dialog) { - case Web::Page::PendingDialog::None: - VERIFY_NOT_REACHED(); - break; - case Web::Page::PendingDialog::Alert: - alert_closed(); - break; - case Web::Page::PendingDialog::Confirm: - confirm_closed(false); - break; - case Web::Page::PendingDialog::Prompt: - prompt_closed({}); - break; - } - - m_pending_dialog = Web::Page::PendingDialog::None; - m_pending_prompt_text.clear(); - }; - - on_insert_clipboard_entry = [this](Web::Clipboard::SystemClipboardRepresentation entry, auto const&) { - Web::Clipboard::SystemClipboardItem item; - item.system_clipboard_representations.append(move(entry)); - - m_clipboard = move(item); - }; - - on_request_clipboard_entries = [this](auto request_id) { - if (m_clipboard.has_value()) - retrieved_clipboard_entries(request_id, { { *m_clipboard } }); - else - retrieved_clipboard_entries(request_id, {}); - }; - - m_system_visibility_state = Web::HTML::VisibilityState::Visible; -} - NonnullOwnPtr HeadlessWebView::create(Core::AnonymousBuffer theme, Web::DevicePixelSize window_size) { auto view = adopt_own(*new HeadlessWebView(move(theme), window_size)); @@ -150,25 +18,10 @@ NonnullOwnPtr HeadlessWebView::create(Core::AnonymousBuffer the return view; } -NonnullOwnPtr HeadlessWebView::create_child(HeadlessWebView& parent, u64 page_index) +HeadlessWebView::HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewport_size) + : WebView::HeadlessWebView(move(theme), viewport_size) + , m_test_promise(TestPromise::construct()) { - auto view = adopt_own(*new HeadlessWebView(parent.m_theme, parent.m_viewport_size)); - - view->m_client_state.client = parent.client(); - view->m_client_state.page_index = page_index; - view->initialize_client(CreateNewClient::No); - - return view; -} - -void HeadlessWebView::initialize_client(CreateNewClient create_new_client) -{ - ViewImplementation::initialize_client(create_new_client); - - client().async_update_system_theme(m_client_state.page_index, m_theme); - client().async_set_viewport_size(m_client_state.page_index, viewport_size()); - client().async_set_window_size(m_client_state.page_index, viewport_size()); - client().async_update_screen_rects(m_client_state.page_index, { { screen_rect } }, 0); } void HeadlessWebView::clear_content_filters() @@ -203,10 +56,4 @@ void HeadlessWebView::on_test_complete(TestCompletion completion) m_test_promise->resolve(move(completion)); } -void HeadlessWebView::update_zoom() -{ - client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level); - client().async_set_viewport_size(m_client_state.page_index, m_viewport_size); -} - } diff --git a/UI/Headless/HeadlessWebView.h b/UI/Headless/HeadlessWebView.h index ef2bd71f769..237f88e312b 100644 --- a/UI/Headless/HeadlessWebView.h +++ b/UI/Headless/HeadlessWebView.h @@ -11,17 +11,15 @@ #include #include #include -#include #include -#include +#include #include namespace Ladybird { -class HeadlessWebView final : public WebView::ViewImplementation { +class HeadlessWebView final : public WebView::HeadlessWebView { public: static NonnullOwnPtr create(Core::AnonymousBuffer theme, Web::DevicePixelSize window_size); - static NonnullOwnPtr create_child(HeadlessWebView&, u64 page_index); void clear_content_filters(); @@ -33,29 +31,11 @@ public: private: HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewport_size); - void update_zoom() override; - void initialize_client(CreateNewClient) override; - - virtual Web::DevicePixelSize viewport_size() const override { return m_viewport_size; } - virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override { return widget_position; } - virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override { return content_position; } - virtual void did_receive_screenshot(Badge, Gfx::ShareableBitmap const& screenshot) override; - Core::AnonymousBuffer m_theme; - Web::DevicePixelSize m_viewport_size; - RefPtr>> m_pending_screenshot; NonnullRefPtr m_test_promise; - - Web::Page::PendingDialog m_pending_dialog { Web::Page::PendingDialog::None }; - Optional m_pending_prompt_text; - - // FIXME: We should implement UI-agnostic platform APIs to interact with the system clipboard. - Optional m_clipboard; - - Vector> m_child_web_views; }; } diff --git a/UI/Headless/Test.cpp b/UI/Headless/Test.cpp index 78aa85fcd97..c80c584e3d1 100644 --- a/UI/Headless/Test.cpp +++ b/UI/Headless/Test.cpp @@ -490,7 +490,7 @@ static void set_ui_callbacks_for_tests(HeadlessWebView& view) }; } -ErrorOr run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size) +ErrorOr run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size) { auto& app = Application::the(); TRY(load_test_config(app.test_root_path)); @@ -527,7 +527,7 @@ ErrorOr run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize for (auto const& [i, test] : enumerate(tests)) outln("{}/{}: {}", i + 1, tests.size(), test.relative_path); - return {}; + return 0; } if (tests.is_empty()) { @@ -560,7 +560,6 @@ ErrorOr run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize size_t timeout_count = 0; size_t crashed_count = 0; size_t skipped_count = 0; - bool all_tests_ok = true; // Keep clearing and reusing the same line if stdout is a TTY. bool log_on_one_line = app.verbosity < Application::VERBOSITY_LEVEL_LOG_TEST_DURATION && TRY(Core::System::isatty(STDOUT_FILENO)); @@ -613,15 +612,12 @@ ErrorOr run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize ++pass_count; break; case TestResult::Fail: - all_tests_ok = false; ++fail_count; break; case TestResult::Timeout: - all_tests_ok = false; ++timeout_count; break; case TestResult::Crashed: - all_tests_ok = false; ++crashed_count; break; case TestResult::Skipped: @@ -685,10 +681,7 @@ ErrorOr run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize } } - if (all_tests_ok) - return {}; - - return Error::from_string_literal("Failed LibWeb tests"); + return fail_count + timeout_count + crashed_count; } } diff --git a/UI/Headless/Test.h b/UI/Headless/Test.h index 3aaeaf1fbf6..8f5bd3b82c9 100644 --- a/UI/Headless/Test.h +++ b/UI/Headless/Test.h @@ -88,7 +88,7 @@ struct TestCompletion { using TestPromise = Core::Promise; -ErrorOr run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size); +ErrorOr run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size); void run_dump_test(HeadlessWebView&, Test&, URL::URL const&, int timeout_in_milliseconds); } diff --git a/UI/Headless/main.cpp b/UI/Headless/main.cpp index f6a99c5ee49..e0245745d9e 100644 --- a/UI/Headless/main.cpp +++ b/UI/Headless/main.cpp @@ -1,61 +1,21 @@ /* * Copyright (c) 2022, Dex♪ - * Copyright (c) 2023-2024, Tim Flynn + * Copyright (c) 2023-2025, Tim Flynn * Copyright (c) 2023, Andreas Kling * Copyright (c) 2023-2024, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ -#include #include -#include #include -#include -#include -#include #include -#include #include -#include -#include #include -#include #include #include -#include #include -static ErrorOr> load_page_for_screenshot_and_exit(Core::EventLoop& event_loop, Ladybird::HeadlessWebView& view, URL::URL const& url, int screenshot_timeout, ByteString const& screenshot_path) -{ - if (FileSystem::exists(screenshot_path)) - TRY(FileSystem::remove(screenshot_path, FileSystem::RecursionMode::Disallowed)); - - outln("Taking screenshot after {} seconds", screenshot_timeout); - - auto timer = Core::Timer::create_single_shot( - screenshot_timeout * 1000, - [&]() { - auto promise = view.take_screenshot(); - - if (auto screenshot = MUST(promise->await())) { - outln("Saving screenshot to {}", screenshot_path); - - auto output_file = MUST(Core::File::open(screenshot_path, Core::File::OpenMode::Write)); - auto image_buffer = MUST(Gfx::PNGWriter::encode(*screenshot)); - MUST(output_file->write_until_depleted(image_buffer.bytes())); - } else { - warnln("No screenshot available"); - } - - event_loop.quit(0); - }); - - view.load(url); - timer->start(); - return timer; -} - ErrorOr serenity_main(Main::Arguments arguments) { WebView::platform_init(); @@ -68,32 +28,13 @@ ErrorOr serenity_main(Main::Arguments arguments) auto theme_path = LexicalPath::join(app->resources_folder, "themes"sv, "Default.ini"sv); auto theme = TRY(Gfx::load_system_theme(theme_path.string())); - static Web::DevicePixelSize window_size { app->width, app->height }; + auto const& browser_options = Ladybird::Application::browser_options(); + Web::DevicePixelSize window_size { browser_options.window_width, browser_options.window_height }; - if (!app->test_root_path.is_empty()) { - app->test_root_path = LexicalPath::absolute_path(TRY(FileSystem::current_working_directory()), app->test_root_path); - TRY(app->launch_test_fixtures()); - TRY(Ladybird::run_tests(theme, window_size)); + VERIFY(!app->test_root_path.is_empty()); - return 0; - } + app->test_root_path = LexicalPath::absolute_path(TRY(FileSystem::current_working_directory()), app->test_root_path); + TRY(app->launch_test_fixtures()); - auto view = Ladybird::HeadlessWebView::create(move(theme), window_size); - - VERIFY(!WebView::Application::browser_options().urls.is_empty()); - auto const& url = WebView::Application::browser_options().urls.first(); - - if (app->dump_layout_tree || app->dump_text) { - Ladybird::Test test { app->dump_layout_tree ? Ladybird::TestMode::Layout : Ladybird::TestMode::Text }; - Ladybird::run_dump_test(*view, test, url, app->per_test_timeout_in_seconds * 1000); - - auto completion = MUST(view->test_promise().await()); - return completion.result == Ladybird::TestResult::Pass ? 0 : 1; - } - - RefPtr timer; - if (!WebView::Application::browser_options().webdriver_content_ipc_path.has_value()) - timer = TRY(load_page_for_screenshot_and_exit(Core::EventLoop::current(), *view, url, app->screenshot_timeout, app->screenshot_path)); - - return app->execute(); + return Ladybird::run_tests(theme, window_size); } diff --git a/UI/Qt/Application.cpp b/UI/Qt/Application.cpp index 3f1c57c40df..7d33858df62 100644 --- a/UI/Qt/Application.cpp +++ b/UI/Qt/Application.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -20,12 +21,23 @@ Application::Application(Badge, Main::Arguments& arguments { } +Application::~Application() = default; + void Application::create_platform_options(WebView::BrowserOptions&, WebView::WebContentOptions& web_content_options) { web_content_options.config_path = Settings::the()->directory(); } -Application::~Application() = default; +NonnullOwnPtr Application::create_platform_event_loop() +{ + Core::EventLoopManager::install(*new WebView::EventLoopManagerQt); + auto event_loop = WebView::Application::create_platform_event_loop(); + + if (!browser_options().headless_mode.has_value()) + static_cast(event_loop->impl()).set_main_loop(); + + return event_loop; +} bool Application::event(QEvent* event) { diff --git a/UI/Qt/Application.h b/UI/Qt/Application.h index 6daa98c7333..60c306f932b 100644 --- a/UI/Qt/Application.h +++ b/UI/Qt/Application.h @@ -35,6 +35,7 @@ public: private: virtual void create_platform_options(WebView::BrowserOptions&, WebView::WebContentOptions&) override; + virtual NonnullOwnPtr create_platform_event_loop() override; virtual Optional ask_user_for_download_folder() const override; diff --git a/UI/Qt/main.cpp b/UI/Qt/main.cpp index 1aba5f6559d..b4d39d9bd57 100644 --- a/UI/Qt/main.cpp +++ b/UI/Qt/main.cpp @@ -4,23 +4,16 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include #include #include -#include #include #include #include -#include -#include -#include #include #include #include #include #include -#include #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) # include @@ -71,40 +64,12 @@ ErrorOr serenity_main(Main::Arguments arguments) { AK::set_rich_debug_enabled(true); - Core::EventLoopManager::install(*new WebView::EventLoopManagerQt); - auto app = Ladybird::Application::create(arguments); - static_cast(Core::EventLoop::current().impl()).set_main_loop(); TRY(handle_attached_debugger()); WebView::platform_init(); - - WebView::BrowserProcess browser_process; - - if (app->browser_options().force_new_process == WebView::ForceNewProcess::No) { - auto disposition = TRY(browser_process.connect(app->browser_options().raw_urls, app->browser_options().new_window)); - - if (disposition == WebView::BrowserProcess::ProcessDisposition::ExitProcess) { - outln("Opening in existing process"); - return 0; - } - } - - browser_process.on_new_tab = [&](auto const& urls) { - auto& window = app->active_window(); - for (size_t i = 0; i < urls.size(); ++i) { - window.new_tab_from_url(urls[i], (i == 0) ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No); - } - window.show(); - window.activateWindow(); - window.raise(); - }; - - app->on_open_file = [&](auto file_url) { - auto& window = app->active_window(); - window.view().load(file_url); - }; + WebView::copy_default_config_files(Ladybird::Settings::the()->directory()); #if defined(AK_OS_MACOS) auto mach_port_server = make(); @@ -119,27 +84,52 @@ ErrorOr serenity_main(Main::Arguments arguments) }; #endif - WebView::copy_default_config_files(Ladybird::Settings::the()->directory()); - + WebView::BrowserProcess browser_process; TRY(app->launch_services()); - browser_process.on_new_window = [&](auto const& urls) { - app->new_window(urls); - }; + if (auto const& browser_options = Ladybird::Application::browser_options(); !browser_options.headless_mode.has_value()) { + if (browser_options.force_new_process == WebView::ForceNewProcess::No) { + auto disposition = TRY(browser_process.connect(browser_options.raw_urls, browser_options.new_window)); - auto& window = app->new_window(app->browser_options().urls); - window.setWindowTitle("Ladybird"); + if (disposition == WebView::BrowserProcess::ProcessDisposition::ExitProcess) { + outln("Opening in existing process"); + return 0; + } + } - if (Ladybird::Settings::the()->is_maximized()) { - window.showMaximized(); - } else { - auto last_position = Ladybird::Settings::the()->last_position(); - if (last_position.has_value()) - window.move(last_position.value()); - window.resize(Ladybird::Settings::the()->last_size()); + app->on_open_file = [&](auto const& file_url) { + auto& window = app->active_window(); + window.view().load(file_url); + }; + + browser_process.on_new_tab = [&](auto const& urls) { + auto& window = app->active_window(); + for (size_t i = 0; i < urls.size(); ++i) { + window.new_tab_from_url(urls[i], (i == 0) ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No); + } + window.show(); + window.activateWindow(); + window.raise(); + }; + + browser_process.on_new_window = [&](auto const& urls) { + app->new_window(urls); + }; + + auto& window = app->new_window(browser_options.urls); + window.setWindowTitle("Ladybird"); + + if (Ladybird::Settings::the()->is_maximized()) { + window.showMaximized(); + } else { + auto last_position = Ladybird::Settings::the()->last_position(); + if (last_position.has_value()) + window.move(last_position.value()); + window.resize(Ladybird::Settings::the()->last_size()); + } + + window.show(); } - window.show(); - return app->execute(); }