Merge branch 'main' into agent

This commit is contained in:
Adrià Arrufat
2026-04-24 13:13:05 +02:00
15 changed files with 166 additions and 40 deletions

View File

@@ -258,7 +258,7 @@ jobs:
- name: start http
run: |
go run ws/main.go & echo $! > WS.pid
go run runner/main.go -serve & echo $! > WS.pid
sleep 2
- name: run lightpanda in cgroup

View File

@@ -36,13 +36,8 @@ jobs:
os: ${{env.OS}}
arch: ${{env.ARCH}}
- uses: ./.github/actions/v8-snapshot
with:
os: ${{env.OS}}
arch: ${{env.ARCH}}
- name: zig build release
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic
run: zig build -Dwpt_extensions -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic
- name: upload artifact
uses: actions/upload-artifact@v7

View File

@@ -722,7 +722,6 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: Allocator, request_url
if (!opts.force and URL.eqlDocument(target.url, resolved_url)) {
target.url = try target.arena.dupeZ(u8, resolved_url);
target.window._location = try Location.init(target.url, target);
target.document._location = target.window._location;
if (target.parent == null) {
try session.navigation.updateEntries(target.url, opts.kind, target, true);
}

View File

@@ -113,6 +113,9 @@ pub fn createPage(self: *Session) !*Frame {
self.page = @as(Page, undefined);
const page = &self.page.?;
errdefer self.page = null;
try Page.init(page, self, self.nextFrameId());
const frame = &page.frame;
@@ -184,6 +187,9 @@ pub fn replacePage(self: *Session) !*Frame {
self.page = @as(Page, undefined);
const page = &self.page.?;
errdefer self.page = null;
try Page.init(page, self, frame_id);
return &page.frame;
}
@@ -302,8 +308,9 @@ fn processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation)
frame.deinit(true);
frame.* = undefined;
try Frame.init(frame, frame_id, page, parent);
errdefer {
// If anything fails from this point on, frame.deinit will be called
// and we need to remove the frame from the parent's frame list.
for (parent.child_frames.items, 0..) |f, i| {
if (f == frame) {
parent.child_frames_sorted = false;
@@ -311,6 +318,10 @@ fn processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation)
break;
}
}
}
try Frame.init(frame, frame_id, page, parent);
errdefer {
if (parent_notified) {
parent._pending_loads -= 1;
}
@@ -349,11 +360,16 @@ fn processRootQueuedNavigation(self: *Session) !void {
self.page = @as(Page, undefined);
const page = &self.page.?;
errdefer self.page = null;
try Page.init(page, self, frame_id);
const new_frame = &page.frame;
// Creates a new NavigationEventTarget for this frame.
try self.navigation.onNewFrame(new_frame);
self.navigation.onNewFrame(new_frame) catch |err| {
log.err(.browser, "createPage onNewNewFrame", .{ .err = err });
};
// start JS env
// Inform CDP the main frame has been created such that additional context for other Worlds can be created as well

View File

@@ -587,6 +587,12 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
const signature = v8.v8__Signature__New(isolate, template);
// Namespace objects (e.g. console) expose their members as own properties
// of each instance rather than via the prototype, so Object.entries(...)
// returns them. See https://console.spec.whatwg.org/#console-namespace.
const own_properties = @hasDecl(JsApi.Meta, "own_properties") and JsApi.Meta.own_properties;
const member_template = if (own_properties) instance else prototype;
const declarations = @typeInfo(JsApi).@"struct".decls;
var has_named_index_getter = false;
@@ -652,7 +658,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
if (value.static) {
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
} else {
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
v8.v8__Template__Set(@ptrCast(member_template), js_name, @ptrCast(function_template), v8.None);
}
},
bridge.Indexed => {

View File

@@ -1,6 +1,16 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=entries>
{
const entries = Object.entries(console);
testing.expectEqual(true, entries.length > 10);
const log_entry = entries.find((e) => e[0] == 'log');
testing.expectEqual(true, log_entry[1] === console.log);
}
</script>
<script id="time">
// should not crash
console.time();

View File

@@ -7,6 +7,11 @@
testing.expectEqual('function', typeof CSS.supports);
</script>
<script id="entries">
const entries = Object.entries(CSS);
testing.expectEqual(true, entries.length >= 2);
</script>
<script id="escape_basic">
{
testing.expectEqual('hello', CSS.escape('hello'));

View File

@@ -69,6 +69,77 @@
testing.expectEqual(['mark', 'measure'], PerformanceObserver.supportedEntryTypes);
</script>
<script id="list_get_entries_by_type">
{
performance.clearMarks();
performance.clearMeasures();
const observer = new PerformanceObserver((list) => {
const marks = list.getEntriesByType("mark");
testing.expectEqual(true, marks instanceof Array);
testing.expectEqual(2, marks.length);
testing.expectEqual("lbt1", marks[0].name);
testing.expectEqual("mark", marks[0].entryType);
testing.expectEqual("lbt2", marks[1].name);
const measures = list.getEntriesByType("measure");
testing.expectEqual(1, measures.length);
testing.expectEqual("lbtMeasure", measures[0].name);
testing.expectEqual("measure", measures[0].entryType);
// A type that's observed but has no entries returns an empty array.
const resources = list.getEntriesByType("resource");
testing.expectEqual(true, resources instanceof Array);
testing.expectEqual(0, resources.length);
observer.disconnect();
});
observer.observe({ entryTypes: ["mark", "measure"] });
performance.mark("lbt1");
performance.mark("lbt2");
performance.measure("lbtMeasure");
}
</script>
<script id="list_get_entries_by_name">
{
performance.clearMarks();
performance.clearMeasures();
const observer = new PerformanceObserver((list) => {
// Without entryType, any type matching the name is returned.
const shared = list.getEntriesByName("lbnShared");
testing.expectEqual(true, shared instanceof Array);
testing.expectEqual(2, shared.length);
// With entryType, only entries of that type are returned.
const sharedMark = list.getEntriesByName("lbnShared", "mark");
testing.expectEqual(1, sharedMark.length);
testing.expectEqual("mark", sharedMark[0].entryType);
const sharedMeasure = list.getEntriesByName("lbnShared", "measure");
testing.expectEqual(1, sharedMeasure.length);
testing.expectEqual("measure", sharedMeasure[0].entryType);
// Name mismatch -> empty array.
const missing = list.getEntriesByName("lbnMissing");
testing.expectEqual(0, missing.length);
// Name matches but type doesn't -> empty array.
const wrongType = list.getEntriesByName("lbnOnlyMark", "measure");
testing.expectEqual(0, wrongType.length);
observer.disconnect();
});
observer.observe({ entryTypes: ["mark", "measure"] });
performance.mark("lbnShared");
performance.measure("lbnShared");
performance.mark("lbnOnlyMark");
}
</script>
<script id="buffered_option">
{
// Clear marks from previous tests so we get a precise count

View File

@@ -165,6 +165,11 @@ pub const JsApi = struct {
pub const Meta = struct {
pub const name = "Css";
// Per the CSSOM spec, CSS is a namespace object — members are own
// properties so Object.entries(CSS) returns them.
pub const own_properties = true;
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;

View File

@@ -195,6 +195,10 @@ pub const JsApi = struct {
pub const Meta = struct {
pub const name = "Console";
// Per the console spec, members are own properties of the namespace
// object, so Object.entries(console) returns them.
pub const own_properties = true;
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};

View File

@@ -197,7 +197,8 @@ pub fn getCurrentScript(self: *const HTMLDocument) ?*Element.Html.Script {
}
pub fn getLocation(self: *const HTMLDocument) ?*@import("Location.zig") {
return self._proto._location;
const frame = self._proto._frame orelse return null;
return frame.window._location;
}
pub fn setLocation(self: *HTMLDocument, url: [:0]const u8, frame: *Frame) !void {

View File

@@ -1,15 +1,16 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Frame = @import("../Frame.zig");
const datetime = @import("../../datetime.zig");
const EventCounts = @import("EventCounts.zig");
const Allocator = std.mem.Allocator;
pub fn registerTypes() []const type {
return &.{ Performance, Entry, Mark, Measure, PerformanceTiming, PerformanceNavigation };
}
const std = @import("std");
const Performance = @This();
_time_origin: u64,
@@ -188,32 +189,34 @@ pub fn getEntries(self: *const Performance) []*Entry {
}
pub fn getEntriesByType(self: *const Performance, entry_type: []const u8, frame: *Frame) ![]const *Entry {
var result: std.ArrayList(*Entry) = .empty;
for (self._entries.items) |entry| {
if (std.mem.eql(u8, entry.getEntryType(), entry_type)) {
try result.append(frame.call_arena, entry);
}
}
return result.items;
return filterEntriesByType(frame.call_arena, self._entries.items, entry_type);
}
pub fn getEntriesByName(self: *const Performance, name: []const u8, entry_type: ?[]const u8, frame: *Frame) ![]const *Entry {
return filterEntriesByName(frame.call_arena, self._entries.items, name, entry_type);
}
// Also used by PerformanceObserver
pub fn filterEntriesByType(arena: Allocator, list: []*Entry, entry_type: []const u8) ![]const *Entry {
var result: std.ArrayList(*Entry) = .empty;
for (list) |entry| {
if (std.mem.eql(u8, entry.getEntryType(), entry_type)) {
try result.append(arena, entry);
}
}
return result.items;
}
// Also used by PerformanceObserver
pub fn filterEntriesByName(arena: Allocator, list: []*Entry, name: []const u8, entry_type: ?[]const u8) ![]const *Entry {
var result: std.ArrayList(*Entry) = .empty;
for (self._entries.items) |entry| {
for (list) |entry| {
if (!std.mem.eql(u8, entry._name, name)) {
continue;
}
const et = entry_type orelse {
try result.append(frame.call_arena, entry);
continue;
};
if (std.mem.eql(u8, entry.getEntryType(), et)) {
try result.append(frame.call_arena, entry);
if (entry_type == null or std.mem.eql(u8, entry.getEntryType(), entry_type.?)) {
try result.append(arena, entry);
}
}

View File

@@ -24,6 +24,7 @@ const Frame = @import("../Frame.zig");
const Performance = @import("Performance.zig");
const log = lp.log;
const Execution = js.Execution;
pub fn registerTypes() []const type {
return &.{ PerformanceObserver, EntryList };
@@ -203,10 +204,18 @@ pub const JsApi = struct {
pub const EntryList = struct {
_entries: []*Performance.Entry,
pub fn getEntries(self: *const EntryList) []*Performance.Entry {
pub fn getEntries(self: *const EntryList) []const *Performance.Entry {
return self._entries;
}
pub fn getEntriesByType(self: *const EntryList, entry_type: []const u8, exec: *Execution) ![]const *Performance.Entry {
return Performance.filterEntriesByType(exec.call_arena, self._entries, entry_type);
}
pub fn getEntriesByName(self: *const EntryList, name: []const u8, entry_type: ?[]const u8, exec: *Execution) ![]const *Performance.Entry {
return Performance.filterEntriesByName(exec.call_arena, self._entries, name, entry_type);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(EntryList);
@@ -217,6 +226,8 @@ pub const EntryList = struct {
};
pub const getEntries = bridge.function(EntryList.getEntries, .{});
pub const getEntriesByType = bridge.function(EntryList.getEntriesByType, .{});
pub const getEntriesByName = bridge.function(EntryList.getEntriesByName, .{});
};
};

View File

@@ -19,15 +19,15 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Session = @import("../Session.zig");
const Page = @import("../Page.zig");
// This type is only included when the binary is built with the -Dwpt_extensions flag
const WebDriver = @This();
_pad: bool = false,
pub fn deleteAllCookies(_: *const WebDriver, session: *Session) void {
session.cookie_jar.clearRetainingCapacity();
pub fn deleteAllCookies(_: *const WebDriver, page: *Page) void {
page.session.cookie_jar.clearRetainingCapacity();
}
pub const JsApi = struct {

View File

@@ -170,14 +170,14 @@ pub fn getOrigin(self: *const Window) []const u8 {
return self._frame.origin orelse "null";
}
pub fn getLocation(self: *const Window) *Location {
return self._location;
}
pub fn getSelection(self: *const Window) *Selection {
return &self._document._selection;
}
pub fn getLocation(self: *const Window) *Location {
return self._location;
}
pub fn setLocation(self: *Window, url: [:0]const u8, frame: *Frame) !void {
return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._frame });
}