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.
This commit is contained in:
Timothy Flynn 2025-06-06 15:42:12 -04:00 committed by Tim Flynn
parent 3894d8efec
commit c011dc766f
Notes: github-actions[bot] 2025-06-10 16:06:04 +00:00
28 changed files with 535 additions and 494 deletions

View file

@ -6,22 +6,21 @@
#pragma once
#include <AK/Error.h>
#include <LibIPC/Forward.h>
#include <LibMain/Main.h>
#include <LibWebView/Forward.h>
#include <LibWebView/Application.h>
#import <Cocoa/Cocoa.h>
namespace Ladybird {
class WebViewBridge;
class Application final : public WebView::Application {
WEB_VIEW_APPLICATION(Application)
private:
virtual Optional<ByteString> ask_user_for_download_folder() const override;
virtual NonnullOwnPtr<Core::EventLoop> create_platform_event_loop() override;
};
}
@interface Application : NSApplication
- (void)setupWebViewApplication:(Main::Arguments&)arguments;
- (ErrorOr<void>)launchServices;
@end

View file

@ -4,10 +4,9 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Interface/LadybirdWebViewBridge.h>
#include <LibCore/EventLoop.h>
#include <LibCore/ThreadEventQueue.h>
#include <LibWebView/Application.h>
#include <LibWebView/EventLoop/EventLoopImplementationMacOS.h>
#include <Utilities/Conversions.h>
#import <Application/Application.h>
@ -18,56 +17,41 @@
namespace Ladybird {
class ApplicationBridge : public WebView::Application {
WEB_VIEW_APPLICATION(ApplicationBridge)
private:
virtual Optional<ByteString> 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<WebView::Application>, Main::Arguments&)
Application::Application(Badge<WebView::Application>, Main::Arguments&)
{
}
Optional<ByteString> 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<Core::EventLoop> 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<Ladybird::ApplicationBridge> m_application_bridge;
RefPtr<Requests::RequestClient> m_request_server_client;
RefPtr<ImageDecoderClient::Client> m_image_decoder_client;
}
@end
@implementation Application
#pragma mark - Public methods
- (void)setupWebViewApplication:(Main::Arguments&)arguments
{
m_application_bridge = Ladybird::ApplicationBridge::create(arguments);
}
- (ErrorOr<void>)launchServices
{
TRY(m_application_bridge->launch_services());
return {};
}
#pragma mark - NSApplication
- (void)terminate:(id)sender

View file

@ -5,14 +5,10 @@
*/
#include <AK/Enumerate.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibMain/Main.h>
#include <LibURL/Parser.h>
#include <LibWebView/Application.h>
#include <LibWebView/BrowserProcess.h>
#include <LibWebView/EventLoop/EventLoopImplementationMacOS.h>
#include <LibWebView/MachPortServer.h>
#include <LibWebView/Settings.h>
#include <LibWebView/URL.h>
#include <LibWebView/Utilities.h>
#include <LibWebView/ViewImplementation.h>
@ -47,48 +43,44 @@ ErrorOr<int> 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::MachPortServer>();
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();
}

View file

@ -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)

View file

@ -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<void> Application::launch_test_fixtures()

View file

@ -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 };
};
}

View file

@ -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<u64> 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<Web::DevicePixels>());
client().async_did_update_window_rect(m_client_state.page_index);
};
on_resize_window = [this](auto size) {
m_viewport_size = size.template to_type<Web::DevicePixels>();
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> 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> HeadlessWebView::create(Core::AnonymousBuffer the
return view;
}
NonnullOwnPtr<HeadlessWebView> 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);
}
}

View file

@ -11,17 +11,15 @@
#include <LibCore/Forward.h>
#include <LibCore/Promise.h>
#include <LibGfx/Forward.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/PixelUnits.h>
#include <LibWebView/ViewImplementation.h>
#include <LibWebView/HeadlessWebView.h>
#include <UI/Headless/Test.h>
namespace Ladybird {
class HeadlessWebView final : public WebView::ViewImplementation {
class HeadlessWebView final : public WebView::HeadlessWebView {
public:
static NonnullOwnPtr<HeadlessWebView> create(Core::AnonymousBuffer theme, Web::DevicePixelSize window_size);
static NonnullOwnPtr<HeadlessWebView> 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<WebView::WebContentClient>, Gfx::ShareableBitmap const& screenshot) override;
Core::AnonymousBuffer m_theme;
Web::DevicePixelSize m_viewport_size;
RefPtr<Core::Promise<RefPtr<Gfx::Bitmap const>>> m_pending_screenshot;
NonnullRefPtr<TestPromise> m_test_promise;
Web::Page::PendingDialog m_pending_dialog { Web::Page::PendingDialog::None };
Optional<String> m_pending_prompt_text;
// FIXME: We should implement UI-agnostic platform APIs to interact with the system clipboard.
Optional<Web::Clipboard::SystemClipboardItem> m_clipboard;
Vector<NonnullOwnPtr<HeadlessWebView>> m_child_web_views;
};
}

View file

@ -490,7 +490,7 @@ static void set_ui_callbacks_for_tests(HeadlessWebView& view)
};
}
ErrorOr<void> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size)
ErrorOr<int> 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<void> 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<void> 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<void> 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<void> 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;
}
}

View file

@ -88,7 +88,7 @@ struct TestCompletion {
using TestPromise = Core::Promise<TestCompletion>;
ErrorOr<void> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size);
ErrorOr<int> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size);
void run_dump_test(HeadlessWebView&, Test&, URL::URL const&, int timeout_in_milliseconds);
}

View file

@ -1,61 +1,21 @@
/*
* Copyright (c) 2022, Dex <dexes.ttp@gmail.com>
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
* Copyright (c) 2023-2025, Tim Flynn <trflynn89@ladybird.org>
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023-2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteString.h>
#include <AK/LexicalPath.h>
#include <AK/Platform.h>
#include <AK/String.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/Promise.h>
#include <LibCore/ResourceImplementationFile.h>
#include <LibCore/Timer.h>
#include <LibFileSystem/FileSystem.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/ImageFormats/PNGWriter.h>
#include <LibGfx/SystemTheme.h>
#include <LibURL/URL.h>
#include <LibWebView/Utilities.h>
#include <UI/Headless/Application.h>
#include <UI/Headless/HeadlessWebView.h>
#include <UI/Headless/Test.h>
static ErrorOr<NonnullRefPtr<Core::Timer>> 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<int> serenity_main(Main::Arguments arguments)
{
WebView::platform_init();
@ -68,32 +28,13 @@ ErrorOr<int> 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<Core::Timer> 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);
}

View file

@ -5,6 +5,7 @@
*/
#include <LibCore/ArgsParser.h>
#include <LibWebView/EventLoop/EventLoopImplementationQt.h>
#include <LibWebView/URL.h>
#include <UI/Qt/Application.h>
#include <UI/Qt/Settings.h>
@ -20,12 +21,23 @@ Application::Application(Badge<WebView::Application>, 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<Core::EventLoop> 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<WebView::EventLoopImplementationQt&>(event_loop->impl()).set_main_loop();
return event_loop;
}
bool Application::event(QEvent* event)
{

View file

@ -35,6 +35,7 @@ public:
private:
virtual void create_platform_options(WebView::BrowserOptions&, WebView::WebContentOptions&) override;
virtual NonnullOwnPtr<Core::EventLoop> create_platform_event_loop() override;
virtual Optional<ByteString> ask_user_for_download_folder() const override;

View file

@ -4,23 +4,16 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Process.h>
#include <LibCore/System.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibMain/Main.h>
#include <LibWebView/Application.h>
#include <LibWebView/BrowserProcess.h>
#include <LibWebView/EventLoop/EventLoopImplementationQt.h>
#include <LibWebView/HelperProcess.h>
#include <LibWebView/ProcessManager.h>
#include <LibWebView/URL.h>
#include <LibWebView/Utilities.h>
#include <UI/Qt/Application.h>
#include <UI/Qt/BrowserWindow.h>
#include <UI/Qt/Settings.h>
#include <UI/Qt/WebContentView.h>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
# include <QStyleHints>
@ -71,40 +64,12 @@ ErrorOr<int> 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<WebView::EventLoopImplementationQt&>(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<WebView::MachPortServer>();
@ -119,27 +84,52 @@ ErrorOr<int> 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();
}