Prototype new test runner

Follows up on https://github.com/lightpanda-io/browser/pull/994 and replaces
the jsRunner with a new page.navigation-based test runner.

Currently only implemented for the Window tests, looking for feedback and
converting every existing test will take time - so for a while, newRunner (to be
renamed) will sit side-by-side with jsRunner.

In addition to the benefits outlined in 994, largely around code simplicity and
putting more of the actual code under tests, I think our WebAPI tests
particularly benefit from:
1 - No need to recompile when modifying the html tests
2 - Much better assertions, e.g. you can assert that something is actually an
    array, not just a string representation of an array
3 - Ability to test some edge cases (e.g. dynamic script loading)

I've put some effort into testing.js to make sure that, if the encapsulating
zig test passes, it's because it actually passed, not because it didn't run.

For the time being, console tests are removed. I think it's more useful to have
access to the console within tests, than it is to test the console (which is
just a wrapper around log, which is both tested and heavily used).
This commit is contained in:
Karl Seguin
2025-09-01 18:09:12 +08:00
parent c0f0630e17
commit c40704d2f3
8 changed files with 402 additions and 303 deletions

View File

@@ -35,10 +35,13 @@ pub var arena_instance = std.heap.ArenaAllocator.init(std.heap.c_allocator);
pub const arena_allocator = arena_instance.allocator();
pub fn reset() void {
_ = arena_instance.reset(.{ .retain_capacity = {} });
_ = arena_instance.reset(.retain_capacity);
}
const App = @import("app.zig").App;
const Env = @import("browser/env.zig").Env;
const Browser = @import("browser/browser.zig").Browser;
const Session = @import("browser/session.zig").Session;
const parser = @import("browser/netsurf.zig");
// Merged std.testing.expectEqual and std.testing.expectString
@@ -362,9 +365,7 @@ fn isJsonValue(a: std.json.Value, b: std.json.Value) bool {
pub const tracking_allocator = @import("root").tracking_allocator.allocator();
pub const JsRunner = struct {
const URL = @import("url.zig").URL;
const Env = @import("browser/env.zig").Env;
const Page = @import("browser/page.zig").Page;
const Browser = @import("browser/browser.zig").Browser;
page: *Page,
browser: *Browser,
@@ -485,13 +486,72 @@ pub fn jsRunner(alloc: Allocator, opts: RunnerOpts) !JsRunner {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
pub var test_app: *App = undefined;
pub var test_browser: Browser = undefined;
pub var test_session: *Session = undefined;
pub fn setup() !void {
test_app = try App.init(gpa.allocator(), .{
.run_mode = .serve,
.tls_verify_host = false,
});
errdefer test_app.deinit();
test_browser = try Browser.init(test_app);
errdefer test_browser.deinit();
test_session = try test_browser.newSession();
}
pub fn shutdown() void {
test_browser.deinit();
test_app.deinit();
}
pub fn newRunner(file: []const u8) !void {
defer _ = arena_instance.reset(.retain_capacity);
const page = try test_session.createPage();
defer test_session.removePage();
const js_context = page.main_context;
var try_catch: Env.TryCatch = undefined;
try_catch.init(js_context);
defer try_catch.deinit();
// if you want to make a lot of changes to testing.js, but don't want to reload
// every time, use this to dynamically load it during development
const content = try std.fs.cwd().readFileAlloc(arena_allocator, "src/browser/tests/testing.js", 1024 * 32);
// const content = @embedFile("browser/tests/testing.js");
js_context.eval(content, "testing.js") catch |err| {
const msg = try_catch.err(arena_allocator) catch @errorName(err) orelse "unknown";
std.debug.print("Failed to setup testing.js: {s}\n", .{msg});
return err;
};
const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/browser/tests/{s}", .{file});
try page.navigate(url, .{});
page.wait(2);
const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| {
const msg = try_catch.err(arena_allocator) catch @errorName(err) orelse "unknown";
std.debug.print("{s}: test failure\nError: {s}\n", .{ file, msg });
return err;
};
const status = try value.toString(arena_allocator);
if (std.mem.eql(u8, status, "ok")) {
return;
}
if (std.mem.eql(u8, status, "empty")) {
std.debug.print("{s}: No testing assertions were made\n", .{file});
return error.NoTestingAssertions;
}
if (std.mem.eql(u8, status, "fail")) {
return error.TestFail;
}
std.debug.print("{s}: Invalid test status: '{s}'\n", .{ file, status });
return error.TestFail;
}