headless-browser: Add ref tests support

The ref tests runner takes screenshots of both the input page and the
expected page, then compares them. Ref testing allows us to catch
painting bugs, which cannot be detected with the layout and text tests
we already have.

With ref tests, we'll likely want to reuse the same expectation page
for multiple inputs. Therefore, there's a `manifest.json` file that
describes the relationship between inputs and expected outputs.
This commit is contained in:
Aliaksandr Kalenik 2023-08-14 00:37:49 +02:00 committed by Andreas Kling
parent 582784d0cf
commit 0d66a80a0f
4 changed files with 112 additions and 25 deletions

View file

@ -0,0 +1,3 @@
{
"square-flex.html": "square-ref.html"
}

View file

@ -0,0 +1,11 @@
<style>
.box {
display: flex;
}
.item {
width: 100px;
height: 100px;
background-color: pink;
}
</style><div class="box"><div class="item">

View file

@ -0,0 +1,7 @@
<style>
.box {
width: 100px;
height: 100px;
background-color: pink;
}
</style><div class="box"></div>

View file

@ -9,6 +9,8 @@
#include <AK/Badge.h>
#include <AK/DeprecatedString.h>
#include <AK/Function.h>
#include <AK/JsonObject.h>
#include <AK/JsonParser.h>
#include <AK/LexicalPath.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/Platform.h>
@ -184,9 +186,16 @@ static ErrorOr<URL> format_url(StringView url)
enum class TestMode {
Layout,
Text,
Ref,
};
static ErrorOr<String> run_one_test(HeadlessWebContentView& view, StringView input_path, StringView expectation_path, TestMode mode, int timeout_in_milliseconds = 15000)
enum class TestResult {
Pass,
Fail,
Timeout,
};
static ErrorOr<TestResult> run_dump_test(HeadlessWebContentView& view, StringView input_path, StringView expectation_path, TestMode mode, int timeout_in_milliseconds = 15000)
{
Core::EventLoop loop;
bool did_timeout = false;
@ -197,7 +206,6 @@ static ErrorOr<String> run_one_test(HeadlessWebContentView& view, StringView inp
}));
view.load(URL::create_with_file_scheme(TRY(FileSystem::real_path(input_path)).to_deprecated_string()));
(void)expectation_path;
String result;
@ -225,25 +233,7 @@ static ErrorOr<String> run_one_test(HeadlessWebContentView& view, StringView inp
loop.exec();
if (did_timeout)
return Error::from_errno(ETIMEDOUT);
return result;
}
enum class TestResult {
Pass,
Fail,
Timeout,
};
static ErrorOr<TestResult> run_test(HeadlessWebContentView& view, StringView input_path, StringView expectation_path, TestMode mode)
{
auto result = run_one_test(view, input_path, expectation_path, mode);
if (result.is_error() && result.error().code() == ETIMEDOUT)
return TestResult::Timeout;
if (result.is_error())
return result.release_error();
auto expectation_file_or_error = Core::File::open(expectation_path, Core::File::OpenMode::Read);
if (expectation_file_or_error.is_error()) {
@ -255,7 +245,7 @@ static ErrorOr<TestResult> run_test(HeadlessWebContentView& view, StringView inp
auto expectation = TRY(String::from_utf8(StringView(TRY(expectation_file->read_until_eof()).bytes())));
auto actual = result.release_value();
auto actual = result;
auto actual_trimmed = TRY(actual.trim("\n"sv, TrimMode::Right));
auto expectation_trimmed = TRY(expectation.trim("\n"sv, TrimMode::Right));
@ -279,6 +269,58 @@ static ErrorOr<TestResult> run_test(HeadlessWebContentView& view, StringView inp
return TestResult::Fail;
}
static ErrorOr<TestResult> run_ref_test(HeadlessWebContentView& view, StringView input_path, StringView expectation_path, int timeout_in_milliseconds = 15000)
{
Core::EventLoop loop;
bool did_timeout = false;
auto timeout_timer = TRY(Core::Timer::create_single_shot(5000, [&] {
did_timeout = true;
loop.quit(0);
}));
view.load(URL::create_with_file_scheme(TRY(FileSystem::real_path(input_path)).to_deprecated_string()));
auto expectation_real_path = TRY(FileSystem::real_path(expectation_path)).to_deprecated_string();
RefPtr<Gfx::Bitmap> actual_screenshot, expectation_screenshot;
view.on_load_finish = [&](auto const&) {
if (actual_screenshot) {
expectation_screenshot = view.take_screenshot();
loop.quit(0);
} else {
actual_screenshot = view.take_screenshot();
view.load(URL::create_with_file_scheme(expectation_real_path));
}
};
timeout_timer->start(timeout_in_milliseconds);
loop.exec();
if (did_timeout)
return TestResult::Timeout;
VERIFY(actual_screenshot);
VERIFY(expectation_screenshot);
if (actual_screenshot->visually_equals(*expectation_screenshot))
return TestResult::Pass;
return TestResult::Fail;
}
static ErrorOr<TestResult> run_test(HeadlessWebContentView& view, StringView input_path, StringView expectation_path, TestMode mode)
{
switch (mode) {
case TestMode::Text:
case TestMode::Layout:
return run_dump_test(view, input_path, expectation_path, mode);
case TestMode::Ref:
return run_ref_test(view, input_path, expectation_path);
default:
VERIFY_NOT_REACHED();
}
}
struct Test {
String input_path;
String expectation_path;
@ -286,14 +328,14 @@ struct Test {
Optional<TestResult> result;
};
static ErrorOr<void> collect_tests(Vector<Test>& tests, StringView path, StringView trail, TestMode mode)
static ErrorOr<void> collect_dump_tests(Vector<Test>& tests, StringView path, StringView trail, TestMode mode)
{
Core::DirIterator it(TRY(String::formatted("{}/input/{}", path, trail)).to_deprecated_string(), Core::DirIterator::Flags::SkipDots);
while (it.has_next()) {
auto name = it.next_path();
auto input_path = TRY(FileSystem::real_path(TRY(String::formatted("{}/input/{}/{}", path, trail, name))));
if (FileSystem::is_directory(input_path)) {
TRY(collect_tests(tests, path, TRY(String::formatted("{}/{}", trail, name)), mode));
TRY(collect_dump_tests(tests, path, TRY(String::formatted("{}/{}", trail, name)), mode));
continue;
}
if (!name.ends_with(".html"sv))
@ -306,13 +348,37 @@ static ErrorOr<void> collect_tests(Vector<Test>& tests, StringView path, StringV
return {};
}
static ErrorOr<void> collect_ref_tests(Vector<Test>& tests, StringView path)
{
auto manifest_path = TRY(String::formatted("{}/manifest.json", path));
auto manifest_file_or_error = Core::File::open(manifest_path, Core::File::OpenMode::Read);
if (manifest_file_or_error.is_error()) {
warnln("Failed opening '{}': {}", manifest_path, manifest_file_or_error.error());
return manifest_file_or_error.release_error();
}
auto manifest_file = manifest_file_or_error.release_value();
auto manifest = TRY(String::from_utf8(StringView(TRY(manifest_file->read_until_eof()).bytes())));
auto manifest_json = TRY(JsonParser(manifest).parse());
TRY(manifest_json.as_object().try_for_each_member([&](DeprecatedString const& key, AK::JsonValue const& value) -> ErrorOr<void> {
TRY(String::from_deprecated_string(key));
auto input_path = TRY(String::formatted("{}/{}", path, key));
auto expectation_path = TRY(String::formatted("{}/{}", path, value.to_deprecated_string()));
tests.append({ input_path, expectation_path, TestMode::Ref, {} });
return {};
}));
return {};
}
static ErrorOr<int> run_tests(HeadlessWebContentView& view, StringView test_root_path)
{
view.clear_content_filters();
Vector<Test> tests;
TRY(collect_tests(tests, TRY(String::formatted("{}/Layout", test_root_path)), "."sv, TestMode::Layout));
TRY(collect_tests(tests, TRY(String::formatted("{}/Text", test_root_path)), "."sv, TestMode::Text));
TRY(collect_dump_tests(tests, TRY(String::formatted("{}/Layout", test_root_path)), "."sv, TestMode::Layout));
TRY(collect_dump_tests(tests, TRY(String::formatted("{}/Text", test_root_path)), "."sv, TestMode::Text));
TRY(collect_ref_tests(tests, TRY(String::formatted("{}/Ref", test_root_path))));
size_t pass_count = 0;
size_t fail_count = 0;