mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-22 10:19:20 +00:00
headless-browser: Make the browser Application more accessible
We currently pass around the individual fields of the Application class to a bunch of free functions. This makes adding a new field, and passing it all the way to e.g. run_dump_test pretty annoying, as we have to go through about 5 function calls. This will get much worse in an upcoming patch to run LibWeb tests concurrently. There, we will have to further pass these flags around as async lambda value captures. To make this nicer, just access the flags from Application::the(), which is how the "real" UIs access their application objects as well.
This commit is contained in:
parent
b1718f5691
commit
377d1f22ae
Notes:
github-actions[bot]
2024-10-06 17:25:55 +00:00
Author: https://github.com/trflynn89
Commit: 377d1f22ae
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1627
Reviewed-by: https://github.com/ADKaster
Reviewed-by: https://github.com/shannonbooth
1 changed files with 89 additions and 83 deletions
|
@ -10,6 +10,7 @@
|
||||||
#include <AK/Badge.h>
|
#include <AK/Badge.h>
|
||||||
#include <AK/ByteBuffer.h>
|
#include <AK/ByteBuffer.h>
|
||||||
#include <AK/ByteString.h>
|
#include <AK/ByteString.h>
|
||||||
|
#include <AK/Enumerate.h>
|
||||||
#include <AK/LexicalPath.h>
|
#include <AK/LexicalPath.h>
|
||||||
#include <AK/NonnullOwnPtr.h>
|
#include <AK/NonnullOwnPtr.h>
|
||||||
#include <AK/Platform.h>
|
#include <AK/Platform.h>
|
||||||
|
@ -49,6 +50,61 @@ constexpr int DEFAULT_TIMEOUT_MS = 30000; // 30sec
|
||||||
|
|
||||||
static StringView s_current_test_path;
|
static StringView s_current_test_path;
|
||||||
|
|
||||||
|
struct Application : public WebView::Application {
|
||||||
|
WEB_VIEW_APPLICATION(Application)
|
||||||
|
|
||||||
|
static Application& the()
|
||||||
|
{
|
||||||
|
return static_cast<Application&>(WebView::Application::the());
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void create_platform_arguments(Core::ArgsParser& args_parser) override
|
||||||
|
{
|
||||||
|
args_parser.add_option(screenshot_timeout, "Take a screenshot after [n] seconds (default: 1)", "screenshot", 's', "n");
|
||||||
|
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_root_path, "Run tests in path", "run-tests", 'R', "test-root-path");
|
||||||
|
args_parser.add_option(test_glob, "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(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void create_platform_options(WebView::ChromeOptions& chrome_options, WebView::WebContentOptions& web_content_options) override
|
||||||
|
{
|
||||||
|
if (!test_root_path.is_empty()) {
|
||||||
|
// --run-tests implies --layout-test-mode.
|
||||||
|
is_layout_test_mode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_layout_test_mode) {
|
||||||
|
// Allow window.open() to succeed for tests.
|
||||||
|
chrome_options.allow_popups = WebView::AllowPopups::Yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
web_content_options.is_layout_test_mode = is_layout_test_mode ? WebView::IsLayoutTestMode::Yes : WebView::IsLayoutTestMode::No;
|
||||||
|
}
|
||||||
|
|
||||||
|
int screenshot_timeout { 1 };
|
||||||
|
ByteString resources_folder { s_ladybird_resource_root };
|
||||||
|
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 };
|
||||||
|
ByteString test_root_path;
|
||||||
|
ByteString test_glob;
|
||||||
|
bool test_dry_run { false };
|
||||||
|
bool rebaseline { false };
|
||||||
|
};
|
||||||
|
|
||||||
|
Application::Application(Badge<WebView::Application>, Main::Arguments&)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
class HeadlessWebContentView final : public WebView::ViewImplementation {
|
class HeadlessWebContentView final : public WebView::ViewImplementation {
|
||||||
public:
|
public:
|
||||||
static ErrorOr<NonnullOwnPtr<HeadlessWebContentView>> create(Core::AnonymousBuffer theme, Gfx::IntSize const& window_size, StringView resources_folder)
|
static ErrorOr<NonnullOwnPtr<HeadlessWebContentView>> create(Core::AnonymousBuffer theme, Gfx::IntSize const& window_size, StringView resources_folder)
|
||||||
|
@ -201,7 +257,7 @@ static StringView test_result_to_string(TestResult result)
|
||||||
VERIFY_NOT_REACHED();
|
VERIFY_NOT_REACHED();
|
||||||
}
|
}
|
||||||
|
|
||||||
static ErrorOr<TestResult> run_dump_test(HeadlessWebContentView& view, URL::URL const& url, StringView expectation_path, TestMode mode, bool rebaseline = false, int timeout_in_milliseconds = DEFAULT_TIMEOUT_MS)
|
static ErrorOr<TestResult> run_dump_test(HeadlessWebContentView& view, URL::URL const& url, StringView expectation_path, TestMode mode, int timeout_in_milliseconds = DEFAULT_TIMEOUT_MS)
|
||||||
{
|
{
|
||||||
Core::EventLoop loop;
|
Core::EventLoop loop;
|
||||||
bool did_timeout = false;
|
bool did_timeout = false;
|
||||||
|
@ -265,7 +321,7 @@ static ErrorOr<TestResult> run_dump_test(HeadlessWebContentView& view, URL::URL
|
||||||
return TestResult::Skipped;
|
return TestResult::Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto expectation_file_or_error = Core::File::open(expectation_path, rebaseline ? Core::File::OpenMode::Write : Core::File::OpenMode::Read);
|
auto expectation_file_or_error = Core::File::open(expectation_path, Application::the().rebaseline ? Core::File::OpenMode::Write : Core::File::OpenMode::Read);
|
||||||
if (expectation_file_or_error.is_error()) {
|
if (expectation_file_or_error.is_error()) {
|
||||||
warnln("Failed opening '{}': {}", expectation_path, expectation_file_or_error.error());
|
warnln("Failed opening '{}': {}", expectation_path, expectation_file_or_error.error());
|
||||||
return expectation_file_or_error.release_error();
|
return expectation_file_or_error.release_error();
|
||||||
|
@ -273,7 +329,7 @@ static ErrorOr<TestResult> run_dump_test(HeadlessWebContentView& view, URL::URL
|
||||||
|
|
||||||
auto expectation_file = expectation_file_or_error.release_value();
|
auto expectation_file = expectation_file_or_error.release_value();
|
||||||
|
|
||||||
if (rebaseline) {
|
if (Application::the().rebaseline) {
|
||||||
TRY(expectation_file->write_until_depleted(result));
|
TRY(expectation_file->write_until_depleted(result));
|
||||||
return TestResult::Pass;
|
return TestResult::Pass;
|
||||||
}
|
}
|
||||||
|
@ -304,7 +360,7 @@ static ErrorOr<TestResult> run_dump_test(HeadlessWebContentView& view, URL::URL
|
||||||
return TestResult::Fail;
|
return TestResult::Fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
static ErrorOr<TestResult> run_ref_test(HeadlessWebContentView& view, URL::URL const& url, bool dump_failed_ref_tests, int timeout_in_milliseconds = DEFAULT_TIMEOUT_MS)
|
static ErrorOr<TestResult> run_ref_test(HeadlessWebContentView& view, URL::URL const& url, int timeout_in_milliseconds = DEFAULT_TIMEOUT_MS)
|
||||||
{
|
{
|
||||||
Core::EventLoop loop;
|
Core::EventLoop loop;
|
||||||
bool did_timeout = false;
|
bool did_timeout = false;
|
||||||
|
@ -342,7 +398,7 @@ static ErrorOr<TestResult> run_ref_test(HeadlessWebContentView& view, URL::URL c
|
||||||
if (actual_screenshot->visually_equals(*expectation_screenshot))
|
if (actual_screenshot->visually_equals(*expectation_screenshot))
|
||||||
return TestResult::Pass;
|
return TestResult::Pass;
|
||||||
|
|
||||||
if (dump_failed_ref_tests) {
|
if (Application::the().dump_failed_ref_tests) {
|
||||||
warnln("\033[33;1mRef test {} failed; dumping screenshots\033[0m", url);
|
warnln("\033[33;1mRef test {} failed; dumping screenshots\033[0m", url);
|
||||||
auto title = LexicalPath::title(URL::percent_decode(url.serialize_path()));
|
auto title = LexicalPath::title(URL::percent_decode(url.serialize_path()));
|
||||||
auto dump_screenshot = [&](Gfx::Bitmap& bitmap, StringView path) -> ErrorOr<void> {
|
auto dump_screenshot = [&](Gfx::Bitmap& bitmap, StringView path) -> ErrorOr<void> {
|
||||||
|
@ -363,7 +419,7 @@ static ErrorOr<TestResult> run_ref_test(HeadlessWebContentView& view, URL::URL c
|
||||||
return TestResult::Fail;
|
return TestResult::Fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
static ErrorOr<TestResult> run_test(HeadlessWebContentView& view, StringView input_path, StringView expectation_path, TestMode mode, bool dump_failed_ref_tests, bool rebaseline)
|
static ErrorOr<TestResult> run_test(HeadlessWebContentView& view, StringView input_path, StringView expectation_path, TestMode mode)
|
||||||
{
|
{
|
||||||
// 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.
|
||||||
|
@ -423,9 +479,9 @@ static ErrorOr<TestResult> run_test(HeadlessWebContentView& view, StringView inp
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case TestMode::Text:
|
case TestMode::Text:
|
||||||
case TestMode::Layout:
|
case TestMode::Layout:
|
||||||
return run_dump_test(view, url, expectation_path, mode, rebaseline);
|
return run_dump_test(view, url, expectation_path, mode);
|
||||||
case TestMode::Ref:
|
case TestMode::Ref:
|
||||||
return run_ref_test(view, url, dump_failed_ref_tests);
|
return run_ref_test(view, url);
|
||||||
default:
|
default:
|
||||||
VERIFY_NOT_REACHED();
|
VERIFY_NOT_REACHED();
|
||||||
}
|
}
|
||||||
|
@ -498,51 +554,54 @@ static ErrorOr<void> collect_ref_tests(Vector<Test>& tests, StringView path)
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
static ErrorOr<int> run_tests(HeadlessWebContentView* view, StringView test_root_path, StringView test_glob, bool dump_failed_ref_tests, bool dump_gc_graph, bool dry_run, bool rebaseline)
|
static ErrorOr<int> run_tests(HeadlessWebContentView* view)
|
||||||
{
|
{
|
||||||
if (view)
|
if (view)
|
||||||
view->clear_content_filters();
|
view->clear_content_filters();
|
||||||
|
|
||||||
TRY(load_test_config(test_root_path));
|
auto& app = Application::the();
|
||||||
|
TRY(load_test_config(app.test_root_path));
|
||||||
|
|
||||||
Vector<Test> tests;
|
Vector<Test> tests;
|
||||||
TRY(collect_dump_tests(tests, ByteString::formatted("{}/Layout", test_root_path), "."sv, TestMode::Layout));
|
auto test_glob = ByteString::formatted("*{}*", app.test_glob);
|
||||||
TRY(collect_dump_tests(tests, ByteString::formatted("{}/Text", test_root_path), "."sv, TestMode::Text));
|
|
||||||
TRY(collect_ref_tests(tests, ByteString::formatted("{}/Ref", test_root_path)));
|
TRY(collect_dump_tests(tests, ByteString::formatted("{}/Layout", app.test_root_path), "."sv, TestMode::Layout));
|
||||||
|
TRY(collect_dump_tests(tests, ByteString::formatted("{}/Text", app.test_root_path), "."sv, TestMode::Text));
|
||||||
|
TRY(collect_ref_tests(tests, ByteString::formatted("{}/Ref", app.test_root_path)));
|
||||||
#ifndef AK_OS_MACOS
|
#ifndef AK_OS_MACOS
|
||||||
TRY(collect_ref_tests(tests, ByteString::formatted("{}/Screenshot", test_root_path)));
|
TRY(collect_ref_tests(tests, ByteString::formatted("{}/Screenshot", app.test_root_path)));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
tests.remove_all_matching([&](auto const& test) {
|
tests.remove_all_matching([&](auto const& test) {
|
||||||
return !test.input_path.matches(test_glob, CaseSensitivity::CaseSensitive);
|
return !test.input_path.matches(test_glob, CaseSensitivity::CaseSensitive);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (app.test_dry_run) {
|
||||||
|
outln("Found {} tests...", tests.size());
|
||||||
|
|
||||||
|
for (auto const& [i, test] : enumerate(tests))
|
||||||
|
outln("{}/{}: {}", i + 1, tests.size(), LexicalPath::relative_path(test.input_path, app.test_root_path));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
size_t pass_count = 0;
|
size_t pass_count = 0;
|
||||||
size_t fail_count = 0;
|
size_t fail_count = 0;
|
||||||
size_t timeout_count = 0;
|
size_t timeout_count = 0;
|
||||||
size_t skipped_count = 0;
|
size_t skipped_count = 0;
|
||||||
|
|
||||||
bool is_tty = isatty(STDOUT_FILENO);
|
bool is_tty = isatty(STDOUT_FILENO);
|
||||||
|
outln("Running {} tests...", tests.size());
|
||||||
if (dry_run)
|
|
||||||
outln("Found {} tests...", tests.size());
|
|
||||||
else
|
|
||||||
outln("Running {} tests...", tests.size());
|
|
||||||
|
|
||||||
for (size_t i = 0; i < tests.size(); ++i) {
|
for (size_t i = 0; i < tests.size(); ++i) {
|
||||||
auto& test = tests[i];
|
auto& test = tests[i];
|
||||||
|
|
||||||
if (is_tty && !dry_run) {
|
if (is_tty) {
|
||||||
// Keep clearing and reusing the same line if stdout is a TTY.
|
// Keep clearing and reusing the same line if stdout is a TTY.
|
||||||
out("\33[2K\r");
|
out("\33[2K\r");
|
||||||
}
|
}
|
||||||
|
|
||||||
out("{}/{}: {}", i + 1, tests.size(), LexicalPath::relative_path(test.input_path, test_root_path));
|
out("{}/{}: {}", i + 1, tests.size(), LexicalPath::relative_path(test.input_path, app.test_root_path));
|
||||||
|
|
||||||
if (dry_run) {
|
|
||||||
outln("");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_tty)
|
if (is_tty)
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
|
@ -555,7 +614,7 @@ static ErrorOr<int> run_tests(HeadlessWebContentView* view, StringView test_root
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
test.result = TRY(run_test(*view, test.input_path, test.expectation_path, test.mode, dump_failed_ref_tests, rebaseline));
|
test.result = TRY(run_test(*view, test.input_path, test.expectation_path, test.mode));
|
||||||
switch (*test.result) {
|
switch (*test.result) {
|
||||||
case TestResult::Pass:
|
case TestResult::Pass:
|
||||||
++pass_count;
|
++pass_count;
|
||||||
|
@ -572,9 +631,6 @@ static ErrorOr<int> run_tests(HeadlessWebContentView* view, StringView test_root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dry_run)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (is_tty)
|
if (is_tty)
|
||||||
outln("\33[2K\rDone!");
|
outln("\33[2K\rDone!");
|
||||||
|
|
||||||
|
@ -587,7 +643,7 @@ static ErrorOr<int> run_tests(HeadlessWebContentView* view, StringView test_root
|
||||||
outln("{}: {}", test_result_to_string(*test.result), test.input_path);
|
outln("{}: {}", test_result_to_string(*test.result), test.input_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dump_gc_graph) {
|
if (app.dump_gc_graph) {
|
||||||
auto path = view->dump_gc_graph();
|
auto path = view->dump_gc_graph();
|
||||||
if (path.is_error()) {
|
if (path.is_error()) {
|
||||||
warnln("Failed to dump GC graph: {}", path.error());
|
warnln("Failed to dump GC graph: {}", path.error());
|
||||||
|
@ -601,56 +657,6 @@ static ErrorOr<int> run_tests(HeadlessWebContentView* view, StringView test_root
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Application : public WebView::Application {
|
|
||||||
WEB_VIEW_APPLICATION(Application)
|
|
||||||
|
|
||||||
virtual void create_platform_arguments(Core::ArgsParser& args_parser) override
|
|
||||||
{
|
|
||||||
args_parser.add_option(screenshot_timeout, "Take a screenshot after [n] seconds (default: 1)", "screenshot", 's', "n");
|
|
||||||
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_root_path, "Run tests in path", "run-tests", 'R', "test-root-path");
|
|
||||||
args_parser.add_option(test_glob, "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(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");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void create_platform_options(WebView::ChromeOptions& chrome_options, WebView::WebContentOptions& web_content_options) override
|
|
||||||
{
|
|
||||||
if (!test_root_path.is_empty()) {
|
|
||||||
// --run-tests implies --layout-test-mode.
|
|
||||||
is_layout_test_mode = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_layout_test_mode) {
|
|
||||||
// Allow window.open() to succeed for tests.
|
|
||||||
chrome_options.allow_popups = WebView::AllowPopups::Yes;
|
|
||||||
}
|
|
||||||
|
|
||||||
web_content_options.is_layout_test_mode = is_layout_test_mode ? WebView::IsLayoutTestMode::Yes : WebView::IsLayoutTestMode::No;
|
|
||||||
}
|
|
||||||
|
|
||||||
int screenshot_timeout { 1 };
|
|
||||||
ByteString resources_folder { s_ladybird_resource_root };
|
|
||||||
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 };
|
|
||||||
StringView test_root_path;
|
|
||||||
ByteString test_glob;
|
|
||||||
bool test_dry_run { false };
|
|
||||||
bool rebaseline { false };
|
|
||||||
};
|
|
||||||
|
|
||||||
Application::Application(Badge<WebView::Application>, Main::Arguments&)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
{
|
{
|
||||||
platform_init();
|
platform_init();
|
||||||
|
@ -672,8 +678,8 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
|
|
||||||
auto absolute_test_root_path = LexicalPath::absolute_path(TRY(FileSystem::current_working_directory()), app->test_root_path);
|
auto absolute_test_root_path = LexicalPath::absolute_path(TRY(FileSystem::current_working_directory()), app->test_root_path);
|
||||||
app->test_root_path = absolute_test_root_path;
|
app->test_root_path = absolute_test_root_path;
|
||||||
auto test_glob = ByteString::formatted("*{}*", app->test_glob);
|
|
||||||
return run_tests(view, app->test_root_path, test_glob, app->dump_failed_ref_tests, app->dump_gc_graph, app->test_dry_run, app->rebaseline);
|
return run_tests(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto view = TRY(HeadlessWebContentView::create(move(theme), window_size, app->resources_folder));
|
auto view = TRY(HeadlessWebContentView::create(move(theme), window_size, app->resources_folder));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue