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(); }