LibWebView+Tests+UI: Migrate headless-browser to test-web

Now that headless mode is built into the main Ladybird executable, the
headless-browser's only purpose is to run tests. So let's move it to the
testing directory and rename it to test-web (a la test-js / test-wasm).
This commit is contained in:
Timothy Flynn 2025-06-06 20:11:13 -04:00 committed by Tim Flynn
commit 9a5b31ccd1
Notes: github-actions[bot] 2025-06-10 16:05:57 +00:00
21 changed files with 166 additions and 189 deletions

View file

@ -200,8 +200,8 @@ BUILD_PRESET=Debug ./Meta/ladybird.py run
Note that debug symbols are available in both Release and Debug builds. Note that debug symbols are available in both Release and Debug builds.
If you want to run other applications, such as the headless-browser, the JS REPL, or the WebAssembly REPL, specify an If you want to run other applications, such as the the JS REPL or the WebAssembly REPL, specify an executable with
executable with `./Meta/ladybird.py run <executable_name>`. `./Meta/ladybird.py run <executable_name>`.
### The User Interfaces ### The User Interfaces

View file

@ -13,11 +13,10 @@ There are two types of ports that can be made to Ladybird:
### UI Ports ### UI Ports
There are currently three supported UI ports: There are currently two supported UI ports:
- Qt6: The generic UI port. - Qt6: The generic UI port.
- AppKit/Cocoa: The macOS native port, which uses the AppKit framework. - AppKit/Cocoa: The macOS native port, which uses the AppKit framework.
- Headless: A headless port that does not have a UI, used for testing.
### Platform Ports ### Platform Ports

View file

@ -13,8 +13,8 @@ Tests of internal C++ code go in their own `TestFoo.cpp` file in `Tests/LibWeb`.
The easiest way to run tests is to use the `ladybird.py` script. The LibWeb tests are registered with CMake as a test in The easiest way to run tests is to use the `ladybird.py` script. The LibWeb tests are registered with CMake as a test in
`UI/CMakeLists.txt`. Using the built-in test filtering, you can run all tests with `Meta/ladybird.py test` or run `UI/CMakeLists.txt`. Using the built-in test filtering, you can run all tests with `Meta/ladybird.py test` or run
just the LibWeb tests with `Meta/ladybird.py test LibWeb`. The second way is to invoke the headless browser test runner just the LibWeb tests with `Meta/ladybird.py test LibWeb`. The second way is to invoke the `test-web` test runner
directly. See the invocation in `UI/CMakeLists.txt` for the expected command line arguments. directly with `Meta/ladybird.py run test-web`.
A third way is to invoke `ctest` directly. The simplest method is to use the `default` preset from `CMakePresets.json`: A third way is to invoke `ctest` directly. The simplest method is to use the `default` preset from `CMakePresets.json`:
@ -137,7 +137,7 @@ you will need to regenerate the corresponding expectations file to match the act
For Text or Layout tests, you can "rebaseline" the tests to regenerate the expectation file: For Text or Layout tests, you can "rebaseline" the tests to regenerate the expectation file:
```bash ```bash
./Meta/ladybird.py run headless-browser --run-tests "./Tests/LibWeb" --rebaseline -f Text/input/your-new-test-name.html ./Meta/ladybird.py run test-web --rebaseline -f Text/input/your-new-test-name.html
``` ```
For Ref and Screenshot tests, you will need to supply the equivalently rendering HTML manually. Though for Screenshot For Ref and Screenshot tests, you will need to supply the equivalently rendering HTML manually. Though for Screenshot

View file

@ -28,6 +28,7 @@ static constexpr auto libexec_path = "libexec"sv;
#endif #endif
ByteString s_ladybird_resource_root; ByteString s_ladybird_resource_root;
static Optional<ByteString> s_ladybird_binary_path;
Optional<ByteString> s_mach_server_name; Optional<ByteString> s_mach_server_name;
@ -43,8 +44,11 @@ void set_mach_server_name(ByteString name)
s_mach_server_name = move(name); s_mach_server_name = move(name);
} }
ErrorOr<ByteString> application_directory() static ErrorOr<ByteString> application_directory()
{ {
if (s_ladybird_binary_path.has_value())
return *s_ladybird_binary_path;
auto current_executable_path = TRY(Core::System::current_executable_path()); auto current_executable_path = TRY(Core::System::current_executable_path());
return LexicalPath::dirname(current_executable_path); return LexicalPath::dirname(current_executable_path);
} }
@ -61,8 +65,10 @@ static LexicalPath find_prefix(LexicalPath const& application_directory)
return application_directory.parent(); return application_directory.parent();
} }
void platform_init() void platform_init(Optional<ByteString> ladybird_binary_path)
{ {
s_ladybird_binary_path = move(ladybird_binary_path);
s_ladybird_resource_root = [] { s_ladybird_resource_root = [] {
auto home = Core::Environment::get("XDG_CONFIG_HOME"sv) auto home = Core::Environment::get("XDG_CONFIG_HOME"sv)
.value_or_lazy_evaluated_optional([]() { return Core::Environment::get("HOME"sv); }); .value_or_lazy_evaluated_optional([]() { return Core::Environment::get("HOME"sv); });
@ -78,6 +84,7 @@ void platform_init()
return find_prefix(LexicalPath(app_dir)).append("share/Lagom"sv).string(); return find_prefix(LexicalPath(app_dir)).append("share/Lagom"sv).string();
#endif #endif
}(); }();
Core::ResourceImplementation::install(make<Core::ResourceImplementationFile>(MUST(String::from_byte_string(s_ladybird_resource_root)))); Core::ResourceImplementation::install(make<Core::ResourceImplementationFile>(MUST(String::from_byte_string(s_ladybird_resource_root))));
} }

View file

@ -14,9 +14,8 @@
namespace WebView { namespace WebView {
void platform_init(); void platform_init(Optional<ByteString> ladybird_binary_path = {});
void copy_default_config_files(StringView config_path); void copy_default_config_files(StringView config_path);
ErrorOr<ByteString> application_directory();
ErrorOr<Vector<ByteString>> get_paths_for_helper_process(StringView process_name); ErrorOr<Vector<ByteString>> get_paths_for_helper_process(StringView process_name);
extern ByteString s_ladybird_resource_root; extern ByteString s_ladybird_resource_root;

View file

@ -73,7 +73,7 @@ ensure_run_dir() {
LADYBIRD_BINARY=${LADYBIRD_BINARY:-"$(default_binary_path)/Ladybird"} LADYBIRD_BINARY=${LADYBIRD_BINARY:-"$(default_binary_path)/Ladybird"}
WEBDRIVER_BINARY=${WEBDRIVER_BINARY:-"$(default_binary_path)/WebDriver"} WEBDRIVER_BINARY=${WEBDRIVER_BINARY:-"$(default_binary_path)/WebDriver"}
HEADLESS_BROWSER_BINARY=${HEADLESS_BROWSER_BINARY:-"$(default_binary_path)/headless-browser"} TEST_WEB_BINARY=${TEST_WEB_BINARY:-"${BUILD_DIR}/bin/test-web"}
WPT_PROCESSES=${WPT_PROCESSES:-$(get_number_of_processing_units)} WPT_PROCESSES=${WPT_PROCESSES:-$(get_number_of_processing_units)}
WPT_CERTIFICATES=( WPT_CERTIFICATES=(
"tools/certs/cacert.pem" "tools/certs/cacert.pem"
@ -596,14 +596,14 @@ import_wpt()
done < <(printf "%s\n" "${RAW_TESTS[@]}" | sort -u) done < <(printf "%s\n" "${RAW_TESTS[@]}" | sort -u)
pushd "${LADYBIRD_SOURCE_DIR}" > /dev/null pushd "${LADYBIRD_SOURCE_DIR}" > /dev/null
./Meta/ladybird.py build headless-browser ./Meta/ladybird.py build test-web
set +e set +e
for path in "${TESTS[@]}"; do for path in "${TESTS[@]}"; do
echo "Importing test from ${path}" echo "Importing test from ${path}"
if ! ./Meta/import-wpt-test.py https://wpt.live/"${path}"; then if ! ./Meta/import-wpt-test.py https://wpt.live/"${path}"; then
continue continue
fi fi
"${HEADLESS_BROWSER_BINARY}" --run-tests ./Tests/LibWeb --rebaseline -f "$path" "${TEST_WEB_BINARY}" --rebaseline -f "$path"
done done
set -e set -e
popd > /dev/null popd > /dev/null

View file

@ -341,7 +341,6 @@ def run_main(host_system: HostSystem, build_dir: Path, target: str, args: list[s
run_args = [] run_args = []
if host_system == HostSystem.macOS and target in ( if host_system == HostSystem.macOS and target in (
"headless-browser",
"ImageDecoder", "ImageDecoder",
"Ladybird", "Ladybird",
"RequestServer", "RequestServer",

View file

@ -1,8 +1,8 @@
set(TEST_SOURCES set(TEST_SOURCES
TestCSSIDSpeed.cpp TestCSSIDSpeed.cpp
TestCSSInheritedProperty.cpp
TestCSSPixels.cpp TestCSSPixels.cpp
TestCSSTokenStream.cpp TestCSSTokenStream.cpp
TestCSSInheritedProperty.cpp
TestFetchInfrastructure.cpp TestFetchInfrastructure.cpp
TestFetchURL.cpp TestFetchURL.cpp
TestHTMLTokenizer.cpp TestHTMLTokenizer.cpp
@ -33,3 +33,5 @@ if (ENABLE_SWIFT)
target_link_libraries(TestLibWebSwift PRIVATE AK LibWeb LibGC SwiftTesting::SwiftTesting) target_link_libraries(TestLibWebSwift PRIVATE AK LibWeb LibGC SwiftTesting::SwiftTesting)
add_test(NAME TestLibWebSwift COMMAND TestLibWebSwift) add_test(NAME TestLibWebSwift COMMAND TestLibWebSwift)
endif() endif()
add_subdirectory("test-web")

View file

@ -87,8 +87,7 @@ def create_test(test_name: str, test_type: str, is_async: bool = False) -> None:
elif test_type == "Layout": elif test_type == "Layout":
input_boilerplate = generic_boilerplate input_boilerplate = generic_boilerplate
expected_boilerplate = f"""run expected_boilerplate = f"""run
./Meta/ladybird.py run headless-browser --run-tests ./Meta/ladybird.py run test-web --rebaseline -f {input_file}
"${{LADYBIRD_SOURCE_DIR}}/Tests/LibWeb" --rebaseline -f {input_file}
to produce the expected output for this test to produce the expected output for this test
""" """
print("Delete <!DOCTYPE html> and replace it with <!--Quirks mode--> if test should run in quirks mode") print("Delete <!DOCTYPE html> and replace it with <!--Quirks mode--> if test should run in quirks mode")

View file

@ -4,21 +4,22 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include "Application.h"
#include "Fixture.h"
#include <LibCore/ArgsParser.h> #include <LibCore/ArgsParser.h>
#include <LibCore/Environment.h> #include <LibCore/Environment.h>
#include <LibCore/System.h> #include <LibCore/System.h>
#include <LibWebView/Utilities.h> #include <LibWebView/Utilities.h>
#include <UI/Headless/Application.h>
#include <UI/Headless/Fixture.h>
namespace Ladybird { namespace TestWeb {
Application::Application(Badge<WebView::Application>, Main::Arguments&) Application::Application(Badge<WebView::Application>, Main::Arguments&)
: resources_folder(WebView::s_ladybird_resource_root) : test_concurrency(Core::System::hardware_concurrency())
, test_concurrency(Core::System::hardware_concurrency())
, python_executable_path("python3") , python_executable_path("python3")
{ {
if (auto ladybird_source_dir = Core::Environment::get("LADYBIRD_SOURCE_DIR"sv); ladybird_source_dir.has_value())
test_root_path = LexicalPath::join(*ladybird_source_dir, "Tests"sv, "LibWeb"sv).string();
} }
Application::~Application() Application::~Application()
@ -29,37 +30,16 @@ Application::~Application()
void Application::create_platform_arguments(Core::ArgsParser& args_parser) void Application::create_platform_arguments(Core::ArgsParser& args_parser)
{ {
args_parser.add_option(test_root_path, "Path containing the tests to run", "test-path", 0, "path");
args_parser.add_option(test_concurrency, "Maximum number of tests to run at once", "test-concurrency", 'j', "jobs"); 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"); args_parser.add_option(test_globs, "Only run tests matching the given glob", "filter", 'f', "glob");
args_parser.add_option(test_dry_run, "List the tests that would be run, without running them", "dry-run"); args_parser.add_option(python_executable_path, "Path to python3", "python-executable", 'P', "path");
args_parser.add_option(dump_failed_ref_tests, "Dump screenshots of failing ref tests", "dump-failed-ref-tests", 'D'); 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(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(test_dry_run, "List the tests that would be run, without running them", "dry-run");
args_parser.add_option(rebaseline, "Rebaseline any executed layout or text tests", "rebaseline"); 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(per_test_timeout_in_seconds, "Per-test timeout (default: 30)", "per-test-timeout", 't', "seconds");
args_parser.add_option(Core::ArgsParser::Option {
.argument_mode = Core::ArgsParser::OptionArgumentMode::Optional,
.help_string = "Run tests. If a path is provided, tests are loaded from that path. Otherwise, LADYBIRD_SOURCE_DIR must be set.",
.long_name = "run-tests",
.short_name = 'R',
.value_name = "test-root-path",
.accept_value = [&](StringView value) -> ErrorOr<bool> {
if (!value.is_empty()) {
test_root_path = value;
return true;
}
if (auto ladybird_source_dir = Core::Environment::get("LADYBIRD_SOURCE_DIR"sv); ladybird_source_dir.has_value()) {
test_root_path = LexicalPath::join(*ladybird_source_dir, "Tests"sv, "LibWeb"sv).string();
return true;
}
return false;
},
});
args_parser.add_option(Core::ArgsParser::Option { args_parser.add_option(Core::ArgsParser::Option {
.argument_mode = Core::ArgsParser::OptionArgumentMode::Optional, .argument_mode = Core::ArgsParser::OptionArgumentMode::Optional,
.help_string = "Log extra information about test results (use multiple times for more information)", .help_string = "Log extra information about test results (use multiple times for more information)",

View file

@ -11,7 +11,7 @@
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibWebView/Application.h> #include <LibWebView/Application.h>
namespace Ladybird { namespace TestWeb {
class Application : public WebView::Application { class Application : public WebView::Application {
WEB_VIEW_APPLICATION(Application) WEB_VIEW_APPLICATION(Application)
@ -33,17 +33,21 @@ public:
static constexpr u8 VERBOSITY_LEVEL_LOG_SLOWEST_TESTS = 2; static constexpr u8 VERBOSITY_LEVEL_LOG_SLOWEST_TESTS = 2;
static constexpr u8 VERBOSITY_LEVEL_LOG_SKIPPED_TESTS = 3; static constexpr u8 VERBOSITY_LEVEL_LOG_SKIPPED_TESTS = 3;
ByteString resources_folder; ByteString test_root_path;
size_t test_concurrency { 1 };
Vector<ByteString> test_globs;
ByteString python_executable_path;
bool dump_failed_ref_tests { false }; bool dump_failed_ref_tests { false };
bool dump_gc_graph { false }; bool dump_gc_graph { false };
size_t test_concurrency { 1 };
ByteString python_executable_path;
ByteString test_root_path;
Vector<ByteString> test_globs;
bool test_dry_run { false }; bool test_dry_run { false };
bool rebaseline { false }; bool rebaseline { false };
u8 verbosity { 0 };
int per_test_timeout_in_seconds { 30 }; int per_test_timeout_in_seconds { 30 };
u8 verbosity { 0 };
}; };
} }

View file

@ -0,0 +1,25 @@
set(SOURCES
Application.cpp
Fixture.cpp
TestWebView.cpp
main.cpp
)
add_executable(test-web ${SOURCES})
add_dependencies(test-web ladybird_build_resource_files ImageDecoder RequestServer WebContent WebWorker)
target_link_libraries(test-web PRIVATE AK LibCore LibDiff LibFileSystem LibGfx LibImageDecoderClient LibIPC LibJS LibMain LibRequests LibURL LibWeb LibWebView)
if (APPLE)
target_compile_definitions(test-web PRIVATE LADYBIRD_BINARY_PATH="$<TARGET_FILE_DIR:ladybird>")
endif()
if (BUILD_TESTING)
find_package(Python3 REQUIRED)
add_test(
NAME LibWeb
COMMAND $<TARGET_FILE:test-web> --python-executable ${Python3_EXECUTABLE} --dump-failed-ref-tests --per-test-timeout 120 --verbose
)
set_tests_properties(LibWeb PROPERTIES ENVIRONMENT LADYBIRD_SOURCE_DIR=${SERENITY_PROJECT_ROOT})
endif()

View file

@ -4,14 +4,15 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include "Fixture.h"
#include "Application.h"
#include <AK/ByteBuffer.h> #include <AK/ByteBuffer.h>
#include <AK/LexicalPath.h> #include <AK/LexicalPath.h>
#include <LibCore/Process.h> #include <LibCore/Process.h>
#include <LibCore/StandardPaths.h> #include <LibCore/StandardPaths.h>
#include <UI/Headless/Application.h>
#include <UI/Headless/Fixture.h>
namespace Ladybird { namespace TestWeb {
static ByteString s_fixtures_path; static ByteString s_fixtures_path;
@ -45,22 +46,24 @@ private:
Optional<Core::Process> m_process; Optional<Core::Process> m_process;
}; };
#ifdef AK_OS_WINDOWS #if defined(AK_OS_WINDOWS)
// FIXME: Implement Ladybird::HttpEchoServerFixture::setup on Windows
ErrorOr<void> HttpEchoServerFixture::setup(WebView::WebContentOptions&) ErrorOr<void> HttpEchoServerFixture::setup(WebView::WebContentOptions&)
{ {
VERIFY(0 && "Ladybird::HttpEchoServerFixture::setup is not implemented"); VERIFY(0 && "HttpEchoServerFixture::setup is not implemented");
} }
// FIXME: Implement Ladybird::HttpEchoServerFixture::teardown_impl on Windows
void HttpEchoServerFixture::teardown_impl() void HttpEchoServerFixture::teardown_impl()
{ {
VERIFY(0 && "Ladybird::HttpEchoServerFixture::teardown_impl is not implemented"); VERIFY(0 && "HttpEchoServerFixture::teardown_impl is not implemented");
} }
#else #else
ErrorOr<void> HttpEchoServerFixture::setup(WebView::WebContentOptions& web_content_options) ErrorOr<void> HttpEchoServerFixture::setup(WebView::WebContentOptions& web_content_options)
{ {
auto const script_path = LexicalPath::join(s_fixtures_path, m_script_path); auto const script_path = LexicalPath::join(s_fixtures_path, m_script_path);
auto const arguments = Vector { script_path.string(), "--directory", Ladybird::Application::the().test_root_path }; auto const arguments = Vector { script_path.string(), "--directory", Application::the().test_root_path };
// FIXME: Pick a more reasonable log path that is more observable // FIXME: Pick a more reasonable log path that is more observable
auto const log_path = LexicalPath::join(Core::StandardPaths::tempfile_directory(), "http-test-server.log"sv).string(); auto const log_path = LexicalPath::join(Core::StandardPaths::tempfile_directory(), "http-test-server.log"sv).string();
@ -68,7 +71,7 @@ ErrorOr<void> HttpEchoServerFixture::setup(WebView::WebContentOptions& web_conte
auto stdout_fds = TRY(Core::System::pipe2(0)); auto stdout_fds = TRY(Core::System::pipe2(0));
auto const process_options = Core::ProcessSpawnOptions { auto const process_options = Core::ProcessSpawnOptions {
.executable = Ladybird::Application::the().python_executable_path, .executable = Application::the().python_executable_path,
.search_for_executable_in_path = true, .search_for_executable_in_path = true,
.arguments = arguments, .arguments = arguments,
.file_actions = { .file_actions = {
@ -111,11 +114,12 @@ void HttpEchoServerFixture::teardown_impl()
m_process = {}; m_process = {};
} }
#endif #endif
void Fixture::initialize_fixtures() void Fixture::initialize_fixtures()
{ {
s_fixtures_path = LexicalPath::join(Ladybird::Application::the().test_root_path, "Fixtures"sv).string(); s_fixtures_path = LexicalPath::join(Application::the().test_root_path, "Fixtures"sv).string();
auto& registry = all(); auto& registry = all();
registry.append(make<HttpEchoServerFixture>()); registry.append(make<HttpEchoServerFixture>());

View file

@ -11,8 +11,9 @@
#include <AK/Optional.h> #include <AK/Optional.h>
#include <AK/StringView.h> #include <AK/StringView.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibWebView/Forward.h>
namespace Ladybird { namespace TestWeb {
class Fixture { class Fixture {
public: public:

View file

@ -1,27 +1,19 @@
/* /*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org> * Copyright (c) 2024-2025, Tim Flynn <trflynn89@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#pragma once #pragma once
#include <AK/Assertions.h>
#include <AK/ByteString.h> #include <AK/ByteString.h>
#include <AK/Error.h>
#include <AK/RefPtr.h> #include <AK/RefPtr.h>
#include <AK/String.h> #include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Time.h> #include <AK/Time.h>
#include <LibCore/Forward.h>
#include <LibCore/Promise.h> #include <LibCore/Promise.h>
#include <LibGfx/Forward.h> #include <LibGfx/Forward.h>
#include <LibURL/Forward.h>
#include <LibWeb/PixelUnits.h>
namespace Ladybird { namespace TestWeb {
class HeadlessWebView;
enum class TestMode { enum class TestMode {
Layout, Layout,
@ -43,23 +35,6 @@ enum class RefTestExpectationType {
Mismatch, Mismatch,
}; };
static constexpr StringView test_result_to_string(TestResult result)
{
switch (result) {
case TestResult::Pass:
return "Pass"sv;
case TestResult::Fail:
return "Fail"sv;
case TestResult::Skipped:
return "Skipped"sv;
case TestResult::Timeout:
return "Timeout"sv;
case TestResult::Crashed:
return "Crashed"sv;
}
VERIFY_NOT_REACHED();
}
struct Test { struct Test {
TestMode mode; TestMode mode;
@ -88,7 +63,4 @@ struct TestCompletion {
using TestPromise = Core::Promise<TestCompletion>; using TestPromise = Core::Promise<TestCompletion>;
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

@ -4,32 +4,33 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include "TestWebView.h"
#include <LibGfx/Bitmap.h> #include <LibGfx/Bitmap.h>
#include <LibGfx/ShareableBitmap.h> #include <LibGfx/ShareableBitmap.h>
#include <UI/Headless/HeadlessWebView.h>
namespace Ladybird { namespace TestWeb {
NonnullOwnPtr<HeadlessWebView> HeadlessWebView::create(Core::AnonymousBuffer theme, Web::DevicePixelSize window_size) NonnullOwnPtr<TestWebView> TestWebView::create(Core::AnonymousBuffer theme, Web::DevicePixelSize window_size)
{ {
auto view = adopt_own(*new HeadlessWebView(move(theme), window_size)); auto view = adopt_own(*new TestWebView(move(theme), window_size));
view->initialize_client(CreateNewClient::Yes); view->initialize_client(CreateNewClient::Yes);
return view; return view;
} }
HeadlessWebView::HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewport_size) TestWebView::TestWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewport_size)
: WebView::HeadlessWebView(move(theme), viewport_size) : WebView::HeadlessWebView(move(theme), viewport_size)
, m_test_promise(TestPromise::construct()) , m_test_promise(TestPromise::construct())
{ {
} }
void HeadlessWebView::clear_content_filters() void TestWebView::clear_content_filters()
{ {
client().async_set_content_filters(m_client_state.page_index, {}); client().async_set_content_filters(m_client_state.page_index, {});
} }
NonnullRefPtr<Core::Promise<RefPtr<Gfx::Bitmap const>>> HeadlessWebView::take_screenshot() NonnullRefPtr<Core::Promise<RefPtr<Gfx::Bitmap const>>> TestWebView::take_screenshot()
{ {
VERIFY(!m_pending_screenshot); VERIFY(!m_pending_screenshot);
@ -39,7 +40,7 @@ NonnullRefPtr<Core::Promise<RefPtr<Gfx::Bitmap const>>> HeadlessWebView::take_sc
return *m_pending_screenshot; return *m_pending_screenshot;
} }
void HeadlessWebView::did_receive_screenshot(Badge<WebView::WebContentClient>, Gfx::ShareableBitmap const& screenshot) void TestWebView::did_receive_screenshot(Badge<WebView::WebContentClient>, Gfx::ShareableBitmap const& screenshot)
{ {
VERIFY(m_pending_screenshot); VERIFY(m_pending_screenshot);
@ -47,7 +48,7 @@ void HeadlessWebView::did_receive_screenshot(Badge<WebView::WebContentClient>, G
pending_screenshot->resolve(screenshot.bitmap()); pending_screenshot->resolve(screenshot.bitmap());
} }
void HeadlessWebView::on_test_complete(TestCompletion completion) void TestWebView::on_test_complete(TestCompletion completion)
{ {
m_pending_screenshot.clear(); m_pending_screenshot.clear();
m_pending_dialog = Web::Page::PendingDialog::None; m_pending_dialog = Web::Page::PendingDialog::None;

View file

@ -6,6 +6,8 @@
#pragma once #pragma once
#include "TestWeb.h"
#include <AK/Badge.h> #include <AK/Badge.h>
#include <AK/RefPtr.h> #include <AK/RefPtr.h>
#include <LibCore/Forward.h> #include <LibCore/Forward.h>
@ -13,13 +15,12 @@
#include <LibGfx/Forward.h> #include <LibGfx/Forward.h>
#include <LibWeb/PixelUnits.h> #include <LibWeb/PixelUnits.h>
#include <LibWebView/HeadlessWebView.h> #include <LibWebView/HeadlessWebView.h>
#include <UI/Headless/Test.h>
namespace Ladybird { namespace TestWeb {
class HeadlessWebView final : public WebView::HeadlessWebView { class TestWebView final : public WebView::HeadlessWebView {
public: public:
static NonnullOwnPtr<HeadlessWebView> create(Core::AnonymousBuffer theme, Web::DevicePixelSize window_size); static NonnullOwnPtr<TestWebView> create(Core::AnonymousBuffer theme, Web::DevicePixelSize window_size);
void clear_content_filters(); void clear_content_filters();
@ -29,7 +30,7 @@ public:
void on_test_complete(TestCompletion); void on_test_complete(TestCompletion);
private: private:
HeadlessWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewport_size); TestWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewport_size);
virtual void did_receive_screenshot(Badge<WebView::WebContentClient>, Gfx::ShareableBitmap const& screenshot) override; virtual void did_receive_screenshot(Badge<WebView::WebContentClient>, Gfx::ShareableBitmap const& screenshot) override;

View file

@ -1,11 +1,17 @@
/* /*
* Copyright (c) 2024-2025, Tim Flynn <trflynn89@ladybird.org> * Copyright (c) 2022, Dex <dexes.ttp@gmail.com>
* 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 * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include "Application.h"
#include "TestWeb.h"
#include "TestWebView.h"
#include <AK/ByteBuffer.h> #include <AK/ByteBuffer.h>
#include <AK/ByteString.h>
#include <AK/Enumerate.h> #include <AK/Enumerate.h>
#include <AK/LexicalPath.h> #include <AK/LexicalPath.h>
#include <AK/QuickSort.h> #include <AK/QuickSort.h>
@ -21,16 +27,32 @@
#include <LibFileSystem/FileSystem.h> #include <LibFileSystem/FileSystem.h>
#include <LibGfx/Bitmap.h> #include <LibGfx/Bitmap.h>
#include <LibGfx/ImageFormats/PNGWriter.h> #include <LibGfx/ImageFormats/PNGWriter.h>
#include <LibGfx/SystemTheme.h>
#include <LibURL/URL.h> #include <LibURL/URL.h>
#include <LibWeb/HTML/SelectedFile.h> #include <LibWeb/HTML/SelectedFile.h>
#include <UI/Headless/Application.h> #include <LibWebView/Utilities.h>
#include <UI/Headless/HeadlessWebView.h>
#include <UI/Headless/Test.h>
namespace Ladybird { namespace TestWeb {
static Vector<ByteString> s_skipped_tests; static Vector<ByteString> s_skipped_tests;
static constexpr StringView test_result_to_string(TestResult result)
{
switch (result) {
case TestResult::Pass:
return "Pass"sv;
case TestResult::Fail:
return "Fail"sv;
case TestResult::Skipped:
return "Skipped"sv;
case TestResult::Timeout:
return "Timeout"sv;
case TestResult::Crashed:
return "Crashed"sv;
}
VERIFY_NOT_REACHED();
}
static ErrorOr<void> load_test_config(StringView test_root_path) static ErrorOr<void> load_test_config(StringView test_root_path)
{ {
auto config_path = LexicalPath::join(test_root_path, "TestConfig.ini"sv); auto config_path = LexicalPath::join(test_root_path, "TestConfig.ini"sv);
@ -127,14 +149,14 @@ static ErrorOr<void> collect_crash_tests(Application const& app, Vector<Test>& t
return {}; return {};
} }
static void clear_test_callbacks(HeadlessWebView& view) static void clear_test_callbacks(TestWebView& view)
{ {
view.on_load_finish = {}; view.on_load_finish = {};
view.on_test_finish = {}; view.on_test_finish = {};
view.on_web_content_crashed = {}; view.on_web_content_crashed = {};
} }
void run_dump_test(HeadlessWebView& view, Test& test, URL::URL const& url, int timeout_in_milliseconds) static void run_dump_test(TestWebView& view, Test& test, URL::URL const& url, int timeout_in_milliseconds)
{ {
auto timer = Core::Timer::create_single_shot(timeout_in_milliseconds, [&view, &test]() { auto timer = Core::Timer::create_single_shot(timeout_in_milliseconds, [&view, &test]() {
view.on_load_finish = {}; view.on_load_finish = {};
@ -307,7 +329,7 @@ if (!hasReftestWaitClass()) {
} }
)"_string; )"_string;
static void run_ref_test(HeadlessWebView& view, Test& test, URL::URL const& url, int timeout_in_milliseconds) static void run_ref_test(TestWebView& view, Test& test, URL::URL const& url, int timeout_in_milliseconds)
{ {
auto timer = Core::Timer::create_single_shot(timeout_in_milliseconds, [&view, &test]() { auto timer = Core::Timer::create_single_shot(timeout_in_milliseconds, [&view, &test]() {
view.on_load_finish = {}; view.on_load_finish = {};
@ -404,7 +426,7 @@ static void run_ref_test(HeadlessWebView& view, Test& test, URL::URL const& url,
timer->start(); timer->start();
} }
static void run_test(HeadlessWebView& view, Test& test, Application& app) static void run_test(TestWebView& view, Test& test, Application& app)
{ {
// Clear the current document. // Clear the current document.
// FIXME: Implement a debug-request to do this more thoroughly. // FIXME: Implement a debug-request to do this more thoroughly.
@ -441,7 +463,7 @@ static void run_test(HeadlessWebView& view, Test& test, Application& app)
view.load(URL::about_blank()); view.load(URL::about_blank());
} }
static void set_ui_callbacks_for_tests(HeadlessWebView& view) static void set_ui_callbacks_for_tests(TestWebView& view)
{ {
view.on_request_file_picker = [&](auto const& accepted_file_types, auto allow_multiple_files) { view.on_request_file_picker = [&](auto const& accepted_file_types, auto allow_multiple_files) {
// Create some dummy files for tests. // Create some dummy files for tests.
@ -490,7 +512,7 @@ static void set_ui_callbacks_for_tests(HeadlessWebView& view)
}; };
} }
ErrorOr<int> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size) static ErrorOr<int> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size)
{ {
auto& app = Application::the(); auto& app = Application::the();
TRY(load_test_config(app.test_root_path)); TRY(load_test_config(app.test_root_path));
@ -539,11 +561,11 @@ ErrorOr<int> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize
auto concurrency = min(app.test_concurrency, tests.size()); auto concurrency = min(app.test_concurrency, tests.size());
size_t loaded_web_views = 0; size_t loaded_web_views = 0;
Vector<NonnullOwnPtr<HeadlessWebView>> views; Vector<NonnullOwnPtr<TestWebView>> views;
views.ensure_capacity(concurrency); views.ensure_capacity(concurrency);
for (size_t i = 0; i < concurrency; ++i) { for (size_t i = 0; i < concurrency; ++i) {
auto view = HeadlessWebView::create(theme, window_size); auto view = TestWebView::create(theme, window_size);
view->on_load_finish = [&](auto const&) { ++loaded_web_views; }; view->on_load_finish = [&](auto const&) { ++loaded_web_views; };
views.unchecked_append(move(view)); views.unchecked_append(move(view));
@ -685,3 +707,28 @@ ErrorOr<int> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize
} }
} }
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
#if defined(LADYBIRD_BINARY_PATH)
WebView::platform_init(LADYBIRD_BINARY_PATH);
#else
WebView::platform_init();
#endif
auto app = TestWeb::Application::create(arguments);
TRY(app->launch_services());
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()));
auto const& browser_options = TestWeb::Application::browser_options();
Web::DevicePixelSize window_size { browser_options.window_width, browser_options.window_height };
VERIFY(!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());
return TestWeb::run_tests(theme, window_size);
}

View file

@ -104,17 +104,14 @@ else()
) )
endif() endif()
add_subdirectory(Headless)
set(ladybird_helper_processes ImageDecoder RequestServer WebContent WebWorker) set(ladybird_helper_processes ImageDecoder RequestServer WebContent WebWorker)
add_dependencies(ladybird ${ladybird_helper_processes}) add_dependencies(ladybird ${ladybird_helper_processes})
add_dependencies(headless-browser ${ladybird_helper_processes} ladybird_build_resource_files)
add_dependencies(WebDriver ladybird) add_dependencies(WebDriver ladybird)
set_helper_process_properties(${ladybird_helper_processes}) set_helper_process_properties(${ladybird_helper_processes})
if (APPLE) if (APPLE)
set_helper_process_properties(headless-browser WebDriver) set_helper_process_properties(WebDriver)
endif() endif()
if(NOT CMAKE_SKIP_INSTALL_RULES) if(NOT CMAKE_SKIP_INSTALL_RULES)

View file

@ -1,20 +0,0 @@
set(SOURCES
Application.cpp
Fixture.cpp
HeadlessWebView.cpp
Test.cpp
main.cpp
)
add_executable(headless-browser ${SOURCES})
target_include_directories(headless-browser PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(headless-browser PRIVATE ${LADYBIRD_SOURCE_DIR})
target_link_libraries(headless-browser PRIVATE ${LADYBIRD_LIBS} LibDiff)
if (BUILD_TESTING)
find_package(Python3 REQUIRED)
add_test(
NAME LibWeb
COMMAND $<TARGET_FILE:headless-browser> --run-tests ${LADYBIRD_SOURCE_DIR}/Tests/LibWeb --python-executable ${Python3_EXECUTABLE} --dump-failed-ref-tests --per-test-timeout 120 --verbose
)
endif()

View file

@ -1,40 +0,0 @@
/*
* Copyright (c) 2022, Dex <dexes.ttp@gmail.com>
* 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/LexicalPath.h>
#include <AK/String.h>
#include <LibCore/ResourceImplementationFile.h>
#include <LibFileSystem/FileSystem.h>
#include <LibGfx/SystemTheme.h>
#include <LibWebView/Utilities.h>
#include <UI/Headless/Application.h>
#include <UI/Headless/Test.h>
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
WebView::platform_init();
auto app = Ladybird::Application::create(arguments);
TRY(app->launch_services());
Core::ResourceImplementation::install(make<Core::ResourceImplementationFile>(MUST(String::from_byte_string(app->resources_folder))));
auto theme_path = LexicalPath::join(app->resources_folder, "themes"sv, "Default.ini"sv);
auto theme = TRY(Gfx::load_system_theme(theme_path.string()));
auto const& browser_options = Ladybird::Application::browser_options();
Web::DevicePixelSize window_size { browser_options.window_width, browser_options.window_height };
VERIFY(!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());
return Ladybird::run_tests(theme, window_size);
}