mirror of
https://github.com/SerenityOS/serenity.git
synced 2025-01-23 01:41:59 -05:00
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:
parent
582784d0cf
commit
0d66a80a0f
4 changed files with 112 additions and 25 deletions
3
Tests/LibWeb/Ref/manifest.json
Normal file
3
Tests/LibWeb/Ref/manifest.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"square-flex.html": "square-ref.html"
|
||||
}
|
11
Tests/LibWeb/Ref/square-flex.html
Normal file
11
Tests/LibWeb/Ref/square-flex.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<style>
|
||||
.box {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: pink;
|
||||
}
|
||||
</style><div class="box"><div class="item">
|
7
Tests/LibWeb/Ref/square-ref.html
Normal file
7
Tests/LibWeb/Ref/square-ref.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<style>
|
||||
.box {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: pink;
|
||||
}
|
||||
</style><div class="box"></div>
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue