Merge pull request #1565 from lightpanda-io/frames

Initial support for frames
This commit is contained in:
Karl Seguin
2026-02-21 07:17:51 +08:00
committed by GitHub
21 changed files with 714 additions and 216 deletions

View File

@@ -73,6 +73,7 @@ const EventListeners = struct {
page_navigated: List = .{}, page_navigated: List = .{},
page_network_idle: List = .{}, page_network_idle: List = .{},
page_network_almost_idle: List = .{}, page_network_almost_idle: List = .{},
page_frame_created: List = .{},
http_request_fail: List = .{}, http_request_fail: List = .{},
http_request_start: List = .{}, http_request_start: List = .{},
http_request_intercept: List = .{}, http_request_intercept: List = .{},
@@ -89,6 +90,7 @@ const Events = union(enum) {
page_navigated: *const PageNavigated, page_navigated: *const PageNavigated,
page_network_idle: *const PageNetworkIdle, page_network_idle: *const PageNetworkIdle,
page_network_almost_idle: *const PageNetworkAlmostIdle, page_network_almost_idle: *const PageNetworkAlmostIdle,
page_frame_created: *const PageFrameCreated,
http_request_fail: *const RequestFail, http_request_fail: *const RequestFail,
http_request_start: *const RequestStart, http_request_start: *const RequestStart,
http_request_intercept: *const RequestIntercept, http_request_intercept: *const RequestIntercept,
@@ -129,6 +131,12 @@ pub const PageNetworkAlmostIdle = struct {
timestamp: u64, timestamp: u64,
}; };
pub const PageFrameCreated = struct {
page_id: u32,
parent_id: u32,
timestamp: u64,
};
pub const RequestStart = struct { pub const RequestStart = struct {
transfer: *Transfer, transfer: *Transfer,
}; };

View File

@@ -495,12 +495,15 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j
const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null; const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null;
// Look up the inline handler for this target // Look up the inline handler for this target
const element = switch (target._type) { const html_element = switch (target._type) {
.node => |n| n.is(Element) orelse return null, .node => |n| n.is(Element.Html) orelse return null,
else => return null, else => return null,
}; };
return self.page.getAttrListener(element, handler_type); return html_element.getAttributeFunction(handler_type, self.page) catch |err| {
log.warn(.event, "inline html callback", .{ .type = handler_type, .err = err });
return null;
};
} }
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void { fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {

View File

@@ -186,7 +186,7 @@ _notified_network_almost_idle: IdleNotification = .init,
// A navigation event that happens from a script gets scheduled to run on the // A navigation event that happens from a script gets scheduled to run on the
// next tick. // next tick.
_queued_navigation: ?QueuedNavigation = null, _queued_navigation: ?*QueuedNavigation = null,
// The URL of the current page // The URL of the current page
url: [:0]const u8 = "about:blank", url: [:0]const u8 = "about:blank",
@@ -221,16 +221,29 @@ _arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
count: usize, count: usize,
}) else void) = if (IS_DEBUG) .empty else {}, }) else void) = if (IS_DEBUG) .empty else {},
parent: ?*Page,
window: *Window, window: *Window,
document: *Document, document: *Document,
iframe: ?*Element.Html.IFrame = null,
frames: std.ArrayList(*Page) = .{},
frames_sorted: bool = true,
// DOM version used to invalidate cached state of "live" collections // DOM version used to invalidate cached state of "live" collections
version: usize = 0, version: usize = 0,
// This is maybe not great. It's a counter on the number of events that we're
// waiting on before triggering the "load" event. Essentially, we need all
// synchronous scripts and all iframes to be loaded. Scripts are handled by the
// ScriptManager, so all scripts just count as 1 pending load.
_pending_loads: u32,
_parent_notified: if (IS_DEBUG) bool else void = if (IS_DEBUG) false else {},
_type: enum { root, frame }, // only used for logs right now
_req_id: u32 = 0, _req_id: u32 = 0,
_navigated_options: ?NavigatedOpts = null, _navigated_options: ?NavigatedOpts = null,
pub fn init(self: *Page, id: u32, session: *Session) !void { pub fn init(self: *Page, id: u32, session: *Session, parent: ?*Page) !void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.page, "page.init", .{}); log.debug(.page, "page.init", .{});
} }
@@ -251,6 +264,7 @@ pub fn init(self: *Page, id: u32, session: *Session) !void {
self.* = .{ self.* = .{
.id = id, .id = id,
.js = undefined, .js = undefined,
.parent = parent,
.arena = page_arena, .arena = page_arena,
.document = document, .document = document,
.window = undefined, .window = undefined,
@@ -258,28 +272,41 @@ pub fn init(self: *Page, id: u32, session: *Session) !void {
.call_arena = call_arena, .call_arena = call_arena,
._session = session, ._session = session,
._factory = factory, ._factory = factory,
._pending_loads = 1, // always 1 for the ScriptManager
._type = if (parent == null) .root else .frame,
._script_manager = undefined, ._script_manager = undefined,
._event_manager = EventManager.init(page_arena, self), ._event_manager = EventManager.init(page_arena, self),
}; };
var screen: *Screen = undefined;
var visual_viewport: *VisualViewport = undefined;
if (parent) |p| {
screen = p.window._screen;
visual_viewport = p.window._visual_viewport;
} else {
screen = try factory.eventTarget(Screen{
._proto = undefined,
._orientation = null,
});
visual_viewport = try factory.eventTarget(VisualViewport{
._proto = undefined,
});
}
self.window = try factory.eventTarget(Window{ self.window = try factory.eventTarget(Window{
._page = self,
._proto = undefined, ._proto = undefined,
._document = self.document, ._document = self.document,
._location = &default_location, ._location = &default_location,
._performance = Performance.init(), ._performance = Performance.init(),
._screen = try factory.eventTarget(Screen{ ._screen = screen,
._proto = undefined, ._visual_viewport = visual_viewport,
._orientation = null,
}),
._visual_viewport = try factory.eventTarget(VisualViewport{
._proto = undefined,
}),
}); });
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self); self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
errdefer self._script_manager.deinit(); errdefer self._script_manager.deinit();
self.js = try browser.env.createContext(self, true); self.js = try browser.env.createContext(self);
errdefer self.js.deinit(); errdefer self.js.deinit();
if (comptime builtin.is_test == false) { if (comptime builtin.is_test == false) {
@@ -295,8 +322,12 @@ pub fn init(self: *Page, id: u32, session: *Session) !void {
} }
pub fn deinit(self: *Page) void { pub fn deinit(self: *Page) void {
for (self.frames.items) |frame| {
frame.deinit();
}
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.page, "page.deinit", .{ .url = self.url }); log.debug(.page, "page.deinit", .{ .url = self.url, .type = self._type });
// Uncomment if you want slab statistics to print. // Uncomment if you want slab statistics to print.
// const stats = self._factory._slab.getStats(self.arena) catch unreachable; // const stats = self._factory._slab.getStats(self.arena) catch unreachable;
@@ -305,6 +336,10 @@ pub fn deinit(self: *Page) void {
// stats.print(&stream) catch unreachable; // stats.print(&stream) catch unreachable;
} }
if (self._queued_navigation) |qn| {
self.arena_pool.release(qn.arena);
}
const session = self._session; const session = self._session;
session.browser.env.destroyContext(self.js); session.browser.env.destroyContext(self.js);
@@ -316,7 +351,7 @@ pub fn deinit(self: *Page) void {
var it = self._arena_pool_leak_track.valueIterator(); var it = self._arena_pool_leak_track.valueIterator();
while (it.next()) |value_ptr| { while (it.next()) |value_ptr| {
if (value_ptr.count > 0) { if (value_ptr.count > 0) {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner }); log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner, .type = self._type });
} }
} }
} }
@@ -385,7 +420,7 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?; const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
if (found.count != 1) { if (found.count != 1) {
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count }); log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count, .type = self._type });
return; return;
} }
found.count = 0; found.count = 0;
@@ -410,6 +445,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.reason = opts.reason, .reason = opts.reason,
.body = opts.body != null, .body = opts.body != null,
.req_id = req_id, .req_id = req_id,
.type = self._type,
}); });
// if the url is about:blank, we load an empty HTML document in the // if the url is about:blank, we load an empty HTML document in the
@@ -506,7 +542,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.done_callback = pageDoneCallback, .done_callback = pageDoneCallback,
.error_callback = pageErrorCallback, .error_callback = pageErrorCallback,
}) catch |err| { }) catch |err| {
log.err(.page, "navigate request", .{ .url = self.url, .err = err }); log.err(.page, "navigate request", .{ .url = self.url, .err = err, .type = self._type });
return err; return err;
}; };
} }
@@ -514,23 +550,27 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// We cannot navigate immediately as navigating will delete the DOM tree, // We cannot navigate immediately as navigating will delete the DOM tree,
// which holds this event's node. // which holds this event's node.
// As such we schedule the function to be called as soon as possible. // As such we schedule the function to be called as soon as possible.
// The page.arena is safe to use here, but the transfer_arena exists
// specifically for this type of lifetime.
pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOpts, priority: NavigationPriority) !void { pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOpts, priority: NavigationPriority) !void {
if (self.canScheduleNavigation(priority) == false) { if (self.canScheduleNavigation(priority) == false) {
return; return;
} }
const arena = try self.arena_pool.acquire();
errdefer self.arena_pool.release(arena);
return self.scheduleNavigationWithArena(arena, request_url, opts, priority);
}
const session = self._session; fn scheduleNavigationWithArena(self: *Page, arena: Allocator, request_url: []const u8, opts: NavigateOpts, priority: NavigationPriority) !void {
const resolved_url = try URL.resolve( const resolved_url = try URL.resolve(
session.transfer_arena, arena,
self.base(), self.base(),
request_url, request_url,
.{ .always_dupe = true }, .{ .always_dupe = true },
); );
const session = self._session;
if (!opts.force and URL.eqlDocument(self.url, resolved_url)) { if (!opts.force and URL.eqlDocument(self.url, resolved_url)) {
self.arena_pool.release(arena);
self.url = try self.arena.dupeZ(u8, resolved_url); self.url = try self.arena.dupeZ(u8, resolved_url);
self.window._location = try Location.init(self.url, self); self.window._location = try Location.init(self.url, self);
self.document._location = self.window._location; self.document._location = self.window._location;
@@ -541,15 +581,23 @@ pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOp
.url = resolved_url, .url = resolved_url,
.reason = opts.reason, .reason = opts.reason,
.target = resolved_url, .target = resolved_url,
.type = self._type,
}); });
self._session.browser.http_client.abort(); session.browser.http_client.abort();
self._queued_navigation = .{ const qn = try arena.create(QueuedNavigation);
qn.* = .{
.opts = opts, .opts = opts,
.arena = arena,
.url = resolved_url, .url = resolved_url,
.priority = priority, .priority = priority,
}; };
if (self._queued_navigation) |existing| {
self.arena_pool.release(existing.arena);
}
self._queued_navigation = qn;
} }
// A script can have multiple competing navigation events, say it starts off // A script can have multiple competing navigation events, say it starts off
@@ -589,7 +637,7 @@ pub fn documentIsLoaded(self: *Page) void {
self._load_state = .load; self._load_state = .load;
self.document._ready_state = .interactive; self.document._ready_state = .interactive;
self._documentIsLoaded() catch |err| { self._documentIsLoaded() catch |err| {
log.err(.page, "document is loaded", .{ .err = err }); log.err(.page, "document is loaded", .{ .err = err, .type = self._type });
}; };
} }
@@ -602,6 +650,38 @@ pub fn _documentIsLoaded(self: *Page) !void {
); );
} }
pub fn scriptsCompletedLoading(self: *Page) void {
self.pendingLoadCompleted();
}
pub fn iframeCompletedLoading(self: *Page, iframe: *Element.Html.IFrame) void {
blk: {
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| {
log.err(.page, "iframe event init", .{ .err = err });
break :blk;
};
defer if (!event._v8_handoff) event.deinit(false);
self._event_manager.dispatch(iframe.asNode().asEventTarget(), event) catch |err| {
log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src });
};
}
self.pendingLoadCompleted();
}
fn pendingLoadCompleted(self: *Page) void {
const pending_loads = self._pending_loads;
if (pending_loads == 1) {
self._pending_loads = 0;
self.documentIsComplete();
} else {
self._pending_loads = pending_loads - 1;
}
}
pub fn documentIsComplete(self: *Page) void { pub fn documentIsComplete(self: *Page) void {
if (self._load_state == .complete) { if (self._load_state == .complete) {
// Ideally, documentIsComplete would only be called once, but with // Ideally, documentIsComplete would only be called once, but with
@@ -621,7 +701,7 @@ pub fn documentIsComplete(self: *Page) void {
self._load_state = .complete; self._load_state = .complete;
self._documentIsComplete() catch |err| { self._documentIsComplete() catch |err| {
log.err(.page, "document is complete", .{ .err = err }); log.err(.page, "document is complete", .{ .err = err, .type = self._type });
}; };
if (IS_DEBUG) { if (IS_DEBUG) {
@@ -675,6 +755,19 @@ fn _documentIsComplete(self: *Page) !void {
ls.toLocal(self.window._on_pageshow), ls.toLocal(self.window._on_pageshow),
.{ .context = "page show" }, .{ .context = "page show" },
); );
self.notifyParentLoadComplete();
}
fn notifyParentLoadComplete(self: *Page) void {
if (comptime IS_DEBUG) {
std.debug.assert(self._parent_notified == false);
self._parent_notified = true;
}
if (self.parent) |p| {
p.iframeCompletedLoading(self.iframe.?);
}
} }
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !bool { fn pageHeaderDoneCallback(transfer: *Http.Transfer) !bool {
@@ -692,6 +785,7 @@ fn pageHeaderDoneCallback(transfer: *Http.Transfer) !bool {
.url = self.url, .url = self.url,
.status = header.status, .status = header.status,
.content_type = header.contentType(), .content_type = header.contentType(),
.type = self._type,
}); });
} }
@@ -712,7 +806,7 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
} orelse .unknown; } orelse .unknown;
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.page, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len }); log.debug(.page, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len, .type = self._type });
} }
switch (mime.content_type) { switch (mime.content_type) {
@@ -756,18 +850,17 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
} }
fn pageDoneCallback(ctx: *anyopaque) !void { fn pageDoneCallback(ctx: *anyopaque) !void {
if (comptime IS_DEBUG) {
log.debug(.page, "navigate done", .{});
}
var self: *Page = @ptrCast(@alignCast(ctx)); var self: *Page = @ptrCast(@alignCast(ctx));
self.clearTransferArena();
if (comptime IS_DEBUG) {
log.debug(.page, "navigate done", .{ .type = self._type });
}
//We need to handle different navigation types differently. //We need to handle different navigation types differently.
try self._session.navigation.commitNavigation(self); try self._session.navigation.commitNavigation(self);
defer if (comptime IS_DEBUG) { defer if (comptime IS_DEBUG) {
log.debug(.page, "page.load.complete", .{ .url = self.url }); log.debug(.page, "page.load.complete", .{ .url = self.url, .type = self._type });
}; };
const parse_arena = try self.getArena(.{ .debug = "Page.parse" }); const parse_arena = try self.getArena(.{ .debug = "Page.parse" });
@@ -836,36 +929,19 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
} }
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void { fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
log.err(.page, "navigate failed", .{ .err = err });
var self: *Page = @ptrCast(@alignCast(ctx)); var self: *Page = @ptrCast(@alignCast(ctx));
log.err(.page, "navigate failed", .{ .err = err, .type = self._type });
self._parse_state = .{ .err = err }; self._parse_state = .{ .err = err };
// In case of error, we want to complete the page with a custom HTML // In case of error, we want to complete the page with a custom HTML
// containing the error. // containing the error.
pageDoneCallback(ctx) catch |e| { pageDoneCallback(ctx) catch |e| {
log.err(.browser, "pageErrorCallback", .{ .err = e }); log.err(.browser, "pageErrorCallback", .{ .err = e, .type = self._type });
return; return;
}; };
} }
// The transfer arena is useful and interesting, but has a weird lifetime.
// When we're transferring from one page to another (via delayed navigation)
// we need things in memory: like the URL that we're navigating to and
// optionally the body to POST. That cannot exist in the page.arena, because
// the page that we have is going to be destroyed and a new page is going
// to be created. If we used the page.arena, we'd wouldn't be able to reset
// it between navigation.
// So the transfer arena is meant to exist between a navigation event. It's
// freed when the main html navigation is complete, either in pageDoneCallback
// or pageErrorCallback. It needs to exist for this long because, if we set
// a body, CURLOPT_POSTFIELDS does not copy the body (it optionally can, but
// why would we want to) and requires the body to live until the transfer
// is complete.
fn clearTransferArena(self: *Page) void {
self.arena_pool.reset(self._session.transfer_arena, 4 * 1024);
}
pub fn wait(self: *Page, wait_ms: u32) Session.WaitResult { pub fn wait(self: *Page, wait_ms: u32) Session.WaitResult {
return self._wait(wait_ms) catch |err| { return self._wait(wait_ms) catch |err| {
switch (err) { switch (err) {
@@ -876,7 +952,7 @@ pub fn wait(self: *Page, wait_ms: u32) Session.WaitResult {
// to run this through more real-world sites and see if we need // to run this through more real-world sites and see if we need
// to expand the switch (err) to have more customized logs for // to expand the switch (err) to have more customized logs for
// specific messages. // specific messages.
log.err(.browser, "page wait", .{ .err = err }); log.err(.browser, "page wait", .{ .err = err, .type = self._type });
}, },
} }
return .done; return .done;
@@ -884,6 +960,10 @@ pub fn wait(self: *Page, wait_ms: u32) Session.WaitResult {
} }
fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult { fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
if (comptime IS_DEBUG) {
std.debug.assert(self._type == .root);
}
var timer = try std.time.Timer.start(); var timer = try std.time.Timer.start();
var ms_remaining = wait_ms; var ms_remaining = wait_ms;
@@ -1107,10 +1187,82 @@ pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Ele
log.err(.page, "page.scriptAddedCallback", .{ log.err(.page, "page.scriptAddedCallback", .{
.err = err, .err = err,
.src = script.asElement().getAttributeSafe(comptime .wrap("src")), .src = script.asElement().getAttributeSafe(comptime .wrap("src")),
.type = self._type,
}); });
}; };
} }
pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
if (self.isGoingAway()) {
// if we're planning on navigating to another page, don't load this iframe
return;
}
if (iframe._executed) {
return;
}
const src = try iframe.getSrc(self);
if (src.len == 0) {
return;
}
iframe._executed = true;
const session = self._session;
const page_id = session.nextPageId();
const page_frame = try self.arena.create(Page);
try Page.init(page_frame, page_id, session, self);
self._pending_loads += 1;
page_frame.iframe = iframe;
iframe._content_window = page_frame.window;
self._session.notification.dispatch(.page_frame_created, &.{
.page_id = page_id,
.parent_id = self.id,
.timestamp = timestamp(.monotonic),
});
page_frame.navigate(src, .{ .reason = .initialFrameNavigation }) catch |err| {
log.warn(.page, "iframe navigate failure", .{ .url = src, .err = err });
self._pending_loads -= 1;
iframe._content_window = null;
page_frame.deinit();
return error.IFrameLoadError;
};
// window[N] is based on document order. For now we'll just append the frame
// at the end of our list and set frames_sorted == false. window.getFrame
// will check this flag to decide if it needs to sort the frames or not.
// But, we can optimize this a bit. Since we expect frames to often be
// added in document order, we can do a quick check to see whether the list
// is sorted or not.
try self.frames.append(self.arena, page_frame);
const frames_len = self.frames.items.len;
if (frames_len == 1) {
// this is the only frame, it must be sorted.
return;
}
if (self.frames_sorted == false) {
// the list already wasn't sorted, it still isn't
return;
}
// So we added a frame into a sorted list. If this frame is sorted relative
// to the last frame, it's still sorted
const iframe_a = self.frames.items[frames_len - 2].iframe.?;
const iframe_b = self.frames.items[frames_len - 1].iframe.?;
if (iframe_a.asNode().compareDocumentPosition(iframe_b.asNode()) & 0x04 == 0) {
// if b followed a, then & 0x04 = 0x04
// but since we got 0, it means b does not follow a, and thus our list
// is no longer sorted.
self.frames_sorted = false;
}
}
pub fn domChanged(self: *Page) void { pub fn domChanged(self: *Page) void {
self.version += 1; self.version += 1;
@@ -1120,7 +1272,7 @@ pub fn domChanged(self: *Page) void {
self._intersection_check_scheduled = true; self._intersection_check_scheduled = true;
self.js.queueIntersectionChecks() catch |err| { self.js.queueIntersectionChecks() catch |err| {
log.err(.page, "page.schedIntersectChecks", .{ .err = err }); log.err(.page, "page.schedIntersectChecks", .{ .err = err, .type = self._type });
}; };
} }
@@ -1211,6 +1363,7 @@ pub fn setAttrListener(
log.debug(.event, "Page.setAttrListener", .{ log.debug(.event, "Page.setAttrListener", .{
.element = element, .element = element,
.listener_type = listener_type, .listener_type = listener_type,
.type = self._type,
}); });
} }
@@ -1221,18 +1374,6 @@ pub fn setAttrListener(
gop.value_ptr.* = listener_callback; gop.value_ptr.* = listener_callback;
} }
/// Returns the inline event listener by an element and listener type.
pub fn getAttrListener(
self: *const Page,
element: *Element,
listener_type: GlobalEventHandler,
) ?JS.Function.Global {
return self._element_attr_listeners.get(.{
.target = element.asEventTarget(),
.handler = listener_type,
});
}
pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void { pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void {
return self._performance_observers.append(self.arena, observer); return self._performance_observers.append(self.arena, observer);
} }
@@ -1252,7 +1393,7 @@ pub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void
for (self._performance_observers.items) |observer| { for (self._performance_observers.items) |observer| {
if (observer.interested(entry)) { if (observer.interested(entry)) {
observer._entries.append(self.arena, entry) catch |err| { observer._entries.append(self.arena, entry) catch |err| {
log.err(.page, "notifyPerformanceObservers", .{ .err = err }); log.err(.page, "notifyPerformanceObservers", .{ .err = err, .type = self._type });
}; };
} }
} }
@@ -1347,7 +1488,7 @@ pub fn performScheduledIntersectionChecks(self: *Page) void {
} }
self._intersection_check_scheduled = false; self._intersection_check_scheduled = false;
self.checkIntersections() catch |err| { self.checkIntersections() catch |err| {
log.err(.page, "page.schedIntersectChecks", .{ .err = err }); log.err(.page, "page.schedIntersectChecks", .{ .err = err, .type = self._type });
}; };
} }
@@ -1363,7 +1504,7 @@ pub fn deliverIntersections(self: *Page) void {
i -= 1; i -= 1;
const observer = self._intersection_observers.items[i]; const observer = self._intersection_observers.items[i];
observer.deliverEntries(self) catch |err| { observer.deliverEntries(self) catch |err| {
log.err(.page, "page.deliverIntersections", .{ .err = err }); log.err(.page, "page.deliverIntersections", .{ .err = err, .type = self._type });
}; };
} }
} }
@@ -1381,7 +1522,7 @@ pub fn deliverMutations(self: *Page) void {
}; };
if (self._mutation_delivery_depth > 100) { if (self._mutation_delivery_depth > 100) {
log.err(.page, "page.MutationLimit", .{}); log.err(.page, "page.MutationLimit", .{ .type = self._type });
self._mutation_delivery_depth = 0; self._mutation_delivery_depth = 0;
return; return;
} }
@@ -1390,7 +1531,7 @@ pub fn deliverMutations(self: *Page) void {
while (it) |node| : (it = node.next) { while (it) |node| : (it = node.next) {
const observer: *MutationObserver = @fieldParentPtr("node", node); const observer: *MutationObserver = @fieldParentPtr("node", node);
observer.deliverRecords(self) catch |err| { observer.deliverRecords(self) catch |err| {
log.err(.page, "page.deliverMutations", .{ .err = err }); log.err(.page, "page.deliverMutations", .{ .err = err, .type = self._type });
}; };
} }
} }
@@ -1408,7 +1549,7 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
var i: usize = 0; var i: usize = 0;
var slots = self.call_arena.alloc(*Element.Html.Slot, pending) catch |err| { var slots = self.call_arena.alloc(*Element.Html.Slot, pending) catch |err| {
log.err(.page, "deliverSlotchange.append", .{ .err = err }); log.err(.page, "deliverSlotchange.append", .{ .err = err, .type = self._type });
return; return;
}; };
@@ -1421,19 +1562,19 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
for (slots) |slot| { for (slots) |slot| {
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self) catch |err| { const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self) catch |err| {
log.err(.page, "deliverSlotchange.init", .{ .err = err }); log.err(.page, "deliverSlotchange.init", .{ .err = err, .type = self._type });
continue; continue;
}; };
defer if (!event._v8_handoff) event.deinit(false); defer if (!event._v8_handoff) event.deinit(false);
const target = slot.asNode().asEventTarget(); const target = slot.asNode().asEventTarget();
_ = target.dispatchEvent(event, self) catch |err| { _ = target.dispatchEvent(event, self) catch |err| {
log.err(.page, "deliverSlotchange.dispatch", .{ .err = err }); log.err(.page, "deliverSlotchange.dispatch", .{ .err = err, .type = self._type });
}; };
} }
} }
fn notifyNetworkIdle(self: *Page) void { pub fn notifyNetworkIdle(self: *Page) void {
lp.assert(self._notified_network_idle == .done, "Page.notifyNetworkIdle", .{}); lp.assert(self._notified_network_idle == .done, "Page.notifyNetworkIdle", .{});
self._session.notification.dispatch(.page_network_idle, &.{ self._session.notification.dispatch(.page_network_idle, &.{
.page_id = self.id, .page_id = self.id,
@@ -1442,7 +1583,7 @@ fn notifyNetworkIdle(self: *Page) void {
}); });
} }
fn notifyNetworkAlmostIdle(self: *Page) void { pub fn notifyNetworkAlmostIdle(self: *Page) void {
lp.assert(self._notified_network_almost_idle == .done, "Page.notifyNetworkAlmostIdle", .{}); lp.assert(self._notified_network_almost_idle == .done, "Page.notifyNetworkAlmostIdle", .{});
self._session.notification.dispatch(.page_network_almost_idle, &.{ self._session.notification.dispatch(.page_network_almost_idle, &.{
.page_id = self.id, .page_id = self.id,
@@ -1483,7 +1624,7 @@ pub fn appendNew(self: *Page, parent: *Node, child: Node.NodeOrText) !void {
// called from the parser when the node and all its children have been added // called from the parser when the node and all its children have been added
pub fn nodeComplete(self: *Page, node: *Node) !void { pub fn nodeComplete(self: *Page, node: *Node) !void {
Node.Build.call(node, "complete", .{ node, self }) catch |err| { Node.Build.call(node, "complete", .{ node, self }) catch |err| {
log.err(.bug, "build.complete", .{ .tag = node.getNodeName(&self.buf), .err = err }); log.err(.bug, "build.complete", .{ .tag = node.getNodeName(&self.buf), .err = err, .type = self._type });
return err; return err;
}; };
return self.nodeIsReady(true, node); return self.nodeIsReady(true, node);
@@ -2152,7 +2293,6 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
}, },
else => {}, else => {},
} }
const tag_name = try String.init(self.arena, name, .{}); const tag_name = try String.init(self.arena, name, .{});
// Check if this is a custom element (must have hyphen for HTML namespace) // Check if this is a custom element (must have hyphen for HTML namespace)
@@ -2183,7 +2323,7 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
var caught: JS.TryCatch.Caught = undefined; var caught: JS.TryCatch.Caught = undefined;
_ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| { _ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| {
log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught }); log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught, .type = self._type });
return node; return node;
}; };
@@ -2241,7 +2381,7 @@ fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespac
const node = element.asNode(); const node = element.asNode();
if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) { if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) {
@call(.auto, @field(E.Build, "created"), .{ node, self }) catch |err| { @call(.auto, @field(E.Build, "created"), .{ node, self }) catch |err| {
log.err(.page, "build.created", .{ .tag = node.getNodeName(&self.buf), .err = err }); log.err(.page, "build.created", .{ .tag = node.getNodeName(&self.buf), .err = err, .type = self._type });
return err; return err;
}; };
} }
@@ -2651,7 +2791,7 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
pub fn attributeChange(self: *Page, element: *Element, name: String, value: String, old_value: ?String) void { pub fn attributeChange(self: *Page, element: *Element, name: String, value: String, old_value: ?String) void {
_ = Element.Build.call(element, "attributeChange", .{ element, name, value, self }) catch |err| { _ = Element.Build.call(element, "attributeChange", .{ element, name, value, self }) catch |err| {
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err }); log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err, .type = self._type });
}; };
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, self); Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, self);
@@ -2660,7 +2800,7 @@ pub fn attributeChange(self: *Page, element: *Element, name: String, value: Stri
while (it) |node| : (it = node.next) { while (it) |node| : (it = node.next) {
const observer: *MutationObserver = @fieldParentPtr("node", node); const observer: *MutationObserver = @fieldParentPtr("node", node);
observer.notifyAttributeChange(element, name, old_value, self) catch |err| { observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
log.err(.page, "attributeChange.notifyObserver", .{ .err = err }); log.err(.page, "attributeChange.notifyObserver", .{ .err = err, .type = self._type });
}; };
} }
@@ -2677,7 +2817,7 @@ pub fn attributeChange(self: *Page, element: *Element, name: String, value: Stri
pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value: String) void { pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value: String) void {
_ = Element.Build.call(element, "attributeRemove", .{ element, name, self }) catch |err| { _ = Element.Build.call(element, "attributeRemove", .{ element, name, self }) catch |err| {
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err }); log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err, .type = self._type });
}; };
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, self); Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, self);
@@ -2686,7 +2826,7 @@ pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value:
while (it) |node| : (it = node.next) { while (it) |node| : (it = node.next) {
const observer: *MutationObserver = @fieldParentPtr("node", node); const observer: *MutationObserver = @fieldParentPtr("node", node);
observer.notifyAttributeChange(element, name, old_value, self) catch |err| { observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
log.err(.page, "attributeRemove.notifyObserver", .{ .err = err }); log.err(.page, "attributeRemove.notifyObserver", .{ .err = err, .type = self._type });
}; };
} }
@@ -2703,11 +2843,11 @@ pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value:
fn signalSlotChange(self: *Page, slot: *Element.Html.Slot) void { fn signalSlotChange(self: *Page, slot: *Element.Html.Slot) void {
self._slots_pending_slotchange.put(self.arena, slot, {}) catch |err| { self._slots_pending_slotchange.put(self.arena, slot, {}) catch |err| {
log.err(.page, "signalSlotChange.put", .{ .err = err }); log.err(.page, "signalSlotChange.put", .{ .err = err, .type = self._type });
return; return;
}; };
self.scheduleSlotchangeDelivery() catch |err| { self.scheduleSlotchangeDelivery() catch |err| {
log.err(.page, "signalSlotChange.schedule", .{ .err = err }); log.err(.page, "signalSlotChange.schedule", .{ .err = err, .type = self._type });
}; };
} }
@@ -2747,7 +2887,7 @@ fn updateElementAssignedSlot(self: *Page, element: *Element) void {
// Recursively search through the shadow root for a matching slot // Recursively search through the shadow root for a matching slot
if (findMatchingSlot(shadow_root.asNode(), slot_name)) |slot| { if (findMatchingSlot(shadow_root.asNode(), slot_name)) |slot| {
self._element_assigned_slots.put(self.arena, element, slot) catch |err| { self._element_assigned_slots.put(self.arena, element, slot) catch |err| {
log.err(.page, "updateElementAssignedSlot.put", .{ .err = err }); log.err(.page, "updateElementAssignedSlot.put", .{ .err = err, .type = self._type });
}; };
} }
} }
@@ -2794,7 +2934,7 @@ pub fn characterDataChange(
while (it) |node| : (it = node.next) { while (it) |node| : (it = node.next) {
const observer: *MutationObserver = @fieldParentPtr("node", node); const observer: *MutationObserver = @fieldParentPtr("node", node);
observer.notifyCharacterDataChange(target, old_value, self) catch |err| { observer.notifyCharacterDataChange(target, old_value, self) catch |err| {
log.err(.page, "cdataChange.notifyObserver", .{ .err = err }); log.err(.page, "cdataChange.notifyObserver", .{ .err = err, .type = self._type });
}; };
} }
} }
@@ -2821,7 +2961,7 @@ pub fn childListChange(
while (it) |node| : (it = node.next) { while (it) |node| : (it = node.next) {
const observer: *MutationObserver = @fieldParentPtr("node", node); const observer: *MutationObserver = @fieldParentPtr("node", node);
observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, self) catch |err| { observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, self) catch |err| {
log.err(.page, "childListChange.notifyObserver", .{ .err = err }); log.err(.page, "childListChange.notifyObserver", .{ .err = err, .type = self._type });
}; };
} }
} }
@@ -2872,7 +3012,17 @@ fn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void {
} }
self.scriptAddedCallback(from_parser, script) catch |err| { self.scriptAddedCallback(from_parser, script) catch |err| {
log.err(.page, "page.nodeIsReady", .{ .err = err }); log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "script", .type = self._type });
return err;
};
} else if (node.is(Element.Html.IFrame)) |iframe| {
if ((comptime from_parser == false) and iframe._src.len == 0) {
// iframe was added via JavaScript, but without a src
return;
}
self.iframeAddedCallback(iframe) catch |err| {
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "iframe", .type = self._type });
return err; return err;
}; };
} }
@@ -2982,6 +3132,7 @@ pub const NavigateReason = enum {
script, script,
history, history,
navigation, navigation,
initialFrameNavigation,
}; };
pub const NavigateOpts = struct { pub const NavigateOpts = struct {
@@ -3006,7 +3157,8 @@ const NavigationPriority = enum {
anchor, anchor,
}; };
const QueuedNavigation = struct { pub const QueuedNavigation = struct {
arena: Allocator,
url: [:0]const u8, url: [:0]const u8,
opts: NavigateOpts, opts: NavigateOpts,
priority: NavigationPriority, priority: NavigationPriority,
@@ -3020,6 +3172,7 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
.node = target, .node = target,
.x = x, .x = x,
.y = y, .y = y,
.type = self._type,
}); });
} }
const event = (try @import("webapi/event/MouseEvent.zig").init("click", .{ const event = (try @import("webapi/event/MouseEvent.zig").init("click", .{
@@ -3054,12 +3207,12 @@ pub fn handleClick(self: *Page, target: *Node) !void {
// Check target attribute - don't navigate if opening in new window/tab // Check target attribute - don't navigate if opening in new window/tab
const target_val = anchor.getTarget(); const target_val = anchor.getTarget();
if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) { if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
log.warn(.not_implemented, "a.target", .{}); log.warn(.not_implemented, "a.target", .{ .type = self._type });
return; return;
} }
if (try element.hasAttribute(comptime .wrap("download"), self)) { if (try element.hasAttribute(comptime .wrap("download"), self)) {
log.warn(.browser, "a.download", .{}); log.warn(.browser, "a.download", .{ .type = self._type });
return; return;
} }
@@ -3096,6 +3249,7 @@ pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
.url = self.url, .url = self.url,
.node = element, .node = element,
.key = keyboard_event._key, .key = keyboard_event._key,
.type = self._type,
}); });
} }
try self._event_manager.dispatch(element.asEventTarget(), event); try self._event_manager.dispatch(element.asEventTarget(), event);
@@ -3185,11 +3339,12 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
// I don't think this is technically correct, but FormData handles it ok // I don't think this is technically correct, but FormData handles it ok
const form_data = try FormData.init(form, submitter_, self); const form_data = try FormData.init(form, submitter_, self);
const transfer_arena = self._session.transfer_arena; const arena = try self.arena_pool.acquire();
errdefer self.arena_pool.release(arena);
const encoding = form_element.getAttributeSafe(comptime .wrap("enctype")); const encoding = form_element.getAttributeSafe(comptime .wrap("enctype"));
var buf = std.Io.Writer.Allocating.init(transfer_arena); var buf = std.Io.Writer.Allocating.init(arena);
try form_data.write(encoding, &buf.writer); try form_data.write(encoding, &buf.writer);
const method = form_element.getAttributeSafe(comptime .wrap("method")) orelse ""; const method = form_element.getAttributeSafe(comptime .wrap("method")) orelse "";
@@ -3205,9 +3360,9 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
// form_data.write currently only supports this encoding, so we know this has to be the content type // form_data.write currently only supports this encoding, so we know this has to be the content type
opts.header = "Content-Type: application/x-www-form-urlencoded"; opts.header = "Content-Type: application/x-www-form-urlencoded";
} else { } else {
action = try URL.concatQueryString(transfer_arena, action, buf.written()); action = try URL.concatQueryString(arena, action, buf.written());
} }
return self.scheduleNavigation(action, opts, .form); return self.scheduleNavigationWithArena(arena, action, opts, .form);
} }
// insertText is a shortcut to insert text into the active element. // insertText is a shortcut to insert text into the active element.
@@ -3259,6 +3414,10 @@ test "WebApi: Page" {
try testing.htmlRunner("page", .{}); try testing.htmlRunner("page", .{});
} }
test "WebApi: Frames" {
try testing.htmlRunner("frames", .{});
}
test "WebApi: Integration" { test "WebApi: Integration" {
try testing.htmlRunner("integration", .{}); try testing.htmlRunner("integration", .{});
} }

View File

@@ -83,6 +83,10 @@ imported_modules: std.StringHashMapUnmanaged(ImportedModule),
// importmap contains resolved urls. // importmap contains resolved urls.
importmap: std.StringHashMapUnmanaged([:0]const u8), importmap: std.StringHashMapUnmanaged([:0]const u8),
// have we notified the page that all scripts are loaded (used to fire the "load"
// event).
page_notified_of_completion: bool,
pub fn init(allocator: Allocator, http_client: *Http.Client, page: *Page) ScriptManager { pub fn init(allocator: Allocator, http_client: *Http.Client, page: *Page) ScriptManager {
return .{ return .{
.page = page, .page = page,
@@ -96,6 +100,7 @@ pub fn init(allocator: Allocator, http_client: *Http.Client, page: *Page) Script
.client = http_client, .client = http_client,
.static_scripts_done = false, .static_scripts_done = false,
.buffer_pool = BufferPool.init(allocator, 5), .buffer_pool = BufferPool.init(allocator, 5),
.page_notified_of_completion = false,
.script_pool = std.heap.MemoryPool(Script).init(allocator), .script_pool = std.heap.MemoryPool(Script).init(allocator),
}; };
} }
@@ -570,10 +575,9 @@ fn evaluate(self: *ScriptManager) void {
// Page makes this safe to call multiple times. // Page makes this safe to call multiple times.
page.documentIsLoaded(); page.documentIsLoaded();
if (self.async_scripts.first == null) { if (self.async_scripts.first == null and self.page_notified_of_completion == false) {
// Looks like all async scripts are done too! self.page_notified_of_completion = true;
// Page makes this safe to call multiple times. page.scriptsCompletedLoading();
page.documentIsComplete();
} }
} }

View File

@@ -18,6 +18,7 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const builtin = @import("builtin");
const log = @import("../log.zig"); const log = @import("../log.zig");
@@ -31,7 +32,7 @@ const Browser = @import("Browser.zig");
const Notification = @import("../Notification.zig"); const Notification = @import("../Notification.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;
// Session is like a browser's tab. // Session is like a browser's tab.
// It owns the js env and the loader for all the pages of the session. // It owns the js env and the loader for all the pages of the session.
@@ -45,16 +46,6 @@ notification: *Notification,
// Used to create our Inspector and in the BrowserContext. // Used to create our Inspector and in the BrowserContext.
arena: Allocator, arena: Allocator,
// The page's arena is unsuitable for data that has to existing while
// navigating from one page to another. For example, if we're clicking
// on an HREF, the URL exists in the original page (where the click
// originated) but also has to exist in the new page.
// While we could use the Session's arena, this could accumulate a lot of
// memory if we do many navigation events. The `transfer_arena` is meant to
// bridge the gap: existing long enough to store any data needed to end one
// page and start another.
transfer_arena: Allocator,
cookie_jar: storage.Cookie.Jar, cookie_jar: storage.Cookie.Jar,
storage_shed: storage.Shed, storage_shed: storage.Shed,
@@ -70,9 +61,6 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
const arena = try browser.arena_pool.acquire(); const arena = try browser.arena_pool.acquire();
errdefer browser.arena_pool.release(arena); errdefer browser.arena_pool.release(arena);
const transfer_arena = try browser.arena_pool.acquire();
errdefer browser.arena_pool.release(transfer_arena);
self.* = .{ self.* = .{
.page = null, .page = null,
.arena = arena, .arena = arena,
@@ -83,7 +71,6 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
.storage_shed = .{}, .storage_shed = .{},
.browser = browser, .browser = browser,
.notification = notification, .notification = notification,
.transfer_arena = transfer_arena,
.cookie_jar = storage.Cookie.Jar.init(allocator), .cookie_jar = storage.Cookie.Jar.init(allocator),
}; };
} }
@@ -96,7 +83,6 @@ pub fn deinit(self: *Session) void {
self.cookie_jar.deinit(); self.cookie_jar.deinit();
self.storage_shed.deinit(browser.app.allocator); self.storage_shed.deinit(browser.app.allocator);
browser.arena_pool.release(self.transfer_arena);
browser.arena_pool.release(self.arena); browser.arena_pool.release(self.arena);
} }
@@ -107,11 +93,7 @@ pub fn createPage(self: *Session) !*Page {
self.page = @as(Page, undefined); self.page = @as(Page, undefined);
const page = &self.page.?; const page = &self.page.?;
try Page.init(page, self.nextPageId(), self, null);
const id = self.page_id_gen +% 1;
self.page_id_gen = id;
try Page.init(page, id, self);
// Creates a new NavigationEventTarget for this page. // Creates a new NavigationEventTarget for this page.
try self.navigation.onNewPage(page); try self.navigation.onNewPage(page);
@@ -150,13 +132,14 @@ pub fn replacePage(self: *Session) !*Page {
var current = self.page.?; var current = self.page.?;
const page_id = current.id; const page_id = current.id;
const parent = current.parent;
current.deinit(); current.deinit();
self.browser.env.memoryPressureNotification(.moderate); self.browser.env.memoryPressureNotification(.moderate);
self.page = @as(Page, undefined); self.page = @as(Page, undefined);
const page = &self.page.?; const page = &self.page.?;
try Page.init(page, page_id, self); try Page.init(page, page_id, self, parent);
return page; return page;
} }
@@ -176,54 +159,208 @@ pub fn findPage(self: *Session, id: u32) ?*Page {
} }
pub fn wait(self: *Session, wait_ms: u32) WaitResult { pub fn wait(self: *Session, wait_ms: u32) WaitResult {
var page = &(self.page orelse return .no_page);
while (true) { while (true) {
if (self.page) |*page| { const wait_result = self._wait(page, wait_ms) catch |err| {
switch (page.wait(wait_ms)) { switch (err) {
.done => { error.JsError => {}, // already logged (with hopefully more context)
if (page._queued_navigation == null) { else => log.err(.browser, "session wait", .{
return .done; .err = err,
} }),
self.processScheduledNavigation() catch return .done;
},
else => |result| return result,
} }
} else { return .done;
return .no_page; };
switch (wait_result) {
.done => {
if (page._queued_navigation == null) {
return .done;
}
page = self.processScheduledNavigation(page) catch return .done;
},
else => |result| return result,
} }
// if we've successfull navigated, we'll give the new page another
// page.wait(wait_ms)
} }
} }
fn processScheduledNavigation(self: *Session) !void { fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
defer self.browser.arena_pool.reset(self.transfer_arena, 4 * 1024); var timer = try std.time.Timer.start();
const url, const opts, const page_id = blk: { var ms_remaining = wait_ms;
const page = self.page.?;
const qn = page._queued_navigation.?;
// qn might not be safe to use after self.removePage is called, hence
// this block;
const url = qn.url;
const opts = qn.opts;
// This was already aborted on the page, but it would be pretty const browser = self.browser;
// bad if old requests went to the new page, so let's make double sure var http_client = browser.http_client;
self.browser.http_client.abort();
// I'd like the page to know NOTHING about cdp_socket / CDP, but the
// fact is that the behavior of wait changes depending on whether or
// not we're using CDP.
// If we aren't using CDP, as soon as we think there's nothing left
// to do, we can exit - we'de done.
// But if we are using CDP, we should wait for the whole `wait_ms`
// because the http_click.tick() also monitors the CDP socket. And while
// we could let CDP poll http (like it does for HTTP requests), the fact
// is that we know more about the timing of stuff (e.g. how long to
// poll/sleep) in the page.
const exit_when_done = http_client.cdp_client == null;
while (true) {
switch (page._parse_state) {
.pre, .raw, .text, .image => {
// The main page hasn't started/finished navigating.
// There's no JS to run, and no reason to run the scheduler.
if (http_client.active == 0 and exit_when_done) {
// haven't started navigating, I guess.
return .done;
}
// Either we have active http connections, or we're in CDP
// mode with an extra socket. Either way, we're waiting
// for http traffic
if (try http_client.tick(@intCast(ms_remaining)) == .cdp_socket) {
// exit_when_done is explicitly set when there isn't
// an extra socket, so it should not be possibl to
// get an cdp_socket message when exit_when_done
// is true.
if (IS_DEBUG) {
std.debug.assert(exit_when_done == false);
}
// data on a socket we aren't handling, return to caller
return .cdp_socket;
}
},
.html, .complete => {
if (page._queued_navigation != null) {
return .done;
}
// The HTML page was parsed. We now either have JS scripts to
// download, or scheduled tasks to execute, or both.
// scheduler.run could trigger new http transfers, so do not
// store http_client.active BEFORE this call and then use
// it AFTER.
const ms_to_next_task = try browser.runMacrotasks();
const http_active = http_client.active;
const total_network_activity = http_active + http_client.intercepted;
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
page.notifyNetworkAlmostIdle();
}
if (page._notified_network_idle.check(total_network_activity == 0)) {
page.notifyNetworkIdle();
}
if (http_active == 0 and exit_when_done) {
// we don't need to consider http_client.intercepted here
// because exit_when_done is true, and that can only be
// the case when interception isn't possible.
if (comptime IS_DEBUG) {
std.debug.assert(http_client.intercepted == 0);
}
const ms = ms_to_next_task orelse blk: {
if (wait_ms - ms_remaining < 100) {
if (comptime builtin.is_test) {
return .done;
}
// Look, we want to exit ASAP, but we don't want
// to exit so fast that we've run none of the
// background jobs.
break :blk 50;
}
// No http transfers, no cdp extra socket, no
// scheduled tasks, we're done.
return .done;
};
if (ms > ms_remaining) {
// Same as above, except we have a scheduled task,
// it just happens to be too far into the future
// compared to how long we were told to wait.
return .done;
}
// We have a task to run in the not-so-distant future.
// You might think we can just sleep until that task is
// ready, but we should continue to run lowPriority tasks
// in the meantime, and that could unblock things. So
// we'll just sleep for a bit, and then restart our wait
// loop to see if anything new can be processed.
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
} else {
// We're here because we either have active HTTP
// connections, or exit_when_done == false (aka, there's
// an cdp_socket registered with the http client).
// We should continue to run lowPriority tasks, so we
// minimize how long we'll poll for network I/O.
const ms_to_wait = @min(200, @min(ms_remaining, ms_to_next_task orelse 200));
if (try http_client.tick(ms_to_wait) == .cdp_socket) {
// data on a socket we aren't handling, return to caller
return .cdp_socket;
}
}
},
.err => |err| {
page._parse_state = .{ .raw_done = @errorName(err) };
return err;
},
.raw_done => {
if (exit_when_done) {
return .done;
}
// we _could_ http_client.tick(ms_to_wait), but this has
// the same result, and I feel is more correct.
return .no_page;
},
}
const ms_elapsed = timer.lap() / 1_000_000;
if (ms_elapsed >= ms_remaining) {
return .done;
}
ms_remaining -= @intCast(ms_elapsed);
}
}
fn processScheduledNavigation(self: *Session, current_page: *Page) !*Page {
const browser = self.browser;
const qn = current_page._queued_navigation.?;
// take ownership of the page's queued navigation
current_page._queued_navigation = null;
defer browser.arena_pool.release(qn.arena);
const page_id, const parent = blk: {
const page = &self.page.?;
const page_id = page.id;
const parent = page.parent;
browser.http_client.abort();
self.removePage(); self.removePage();
break :blk .{ url, opts, page.id }; break :blk .{ page_id, parent };
}; };
const page = self.createPage() catch |err| { self.page = @as(Page, undefined);
log.err(.browser, "queued navigation page error", .{ const page = &self.page.?;
.err = err, try Page.init(page, page_id, self, parent);
.url = url,
});
return err;
};
page.id = page_id;
page.navigate(url, opts) catch |err| { // Creates a new NavigationEventTarget for this page.
log.err(.browser, "queued navigation error", .{ .err = err, .url = url }); try self.navigation.onNewPage(page);
// start JS env
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
self.notification.dispatch(.page_created, page);
page.navigate(qn.url, qn.opts) catch |err| {
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
return err; return err;
}; };
return page;
}
pub fn nextPageId(self: *Session) u32 {
const id = self.page_id_gen +% 1;
self.page_id_gen = id;
return id;
} }

View File

@@ -47,9 +47,6 @@ isolate: js.Isolate,
// from this, and we can free it when the context is done. // from this, and we can free it when the context is done.
handle: v8.Global, handle: v8.Global,
// True if the context is auto-entered,
entered: bool,
cpu_profiler: ?*v8.CpuProfiler = null, cpu_profiler: ?*v8.CpuProfiler = null,
heap_profiler: ?*v8.HeapProfiler = null, heap_profiler: ?*v8.HeapProfiler = null,
@@ -247,11 +244,6 @@ pub fn deinit(self: *Context) void {
v8.v8__Global__Reset(global); v8.v8__Global__Reset(global);
} }
} }
if (self.entered) {
v8.v8__Context__Exit(@ptrCast(v8.v8__Global__Get(&self.handle, self.isolate.handle)));
}
v8.v8__Global__Reset(&self.handle); v8.v8__Global__Reset(&self.handle);
} }
@@ -333,11 +325,14 @@ pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
const isolate = self.isolate; const isolate = self.isolate;
js.HandleScope.init(&ls.handle_scope, isolate); js.HandleScope.init(&ls.handle_scope, isolate);
const local_v8_context: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle));
v8.v8__Context__Enter(local_v8_context);
// TODO: add and init ls.hs for the handlescope // TODO: add and init ls.hs for the handlescope
ls.local = .{ ls.local = .{
.ctx = self, .ctx = self,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle)),
.isolate = isolate, .isolate = isolate,
.handle = local_v8_context,
.call_arena = self.call_arena, .call_arena = self.call_arena,
}; };
} }
@@ -364,7 +359,6 @@ pub fn stringToPersistedFunction(self: *Context, str: []const u8) !js.Function.G
extra = "(e)"; extra = "(e)";
} }
const full = try std.fmt.allocPrintSentinel(self.call_arena, "(function(e) {{ {s}{s} }})", .{ normalized, extra }, 0); const full = try std.fmt.allocPrintSentinel(self.call_arena, "(function(e) {{ {s}{s} }})", .{ normalized, extra }, 0);
const js_val = try ls.local.compileAndRun(full, null); const js_val = try ls.local.compileAndRun(full, null);
if (!js_val.isFunction()) { if (!js_val.isFunction()) {
return error.StringFunctionError; return error.StringFunctionError;

View File

@@ -175,8 +175,23 @@ pub fn init(app: *App, opts: InitOpts) !Env {
.data = null, .data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking, .flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
}); });
// I don't 100% understand this. We actually set this up in the snapshot,
// but for the global instance, it doesn't work. SetIndexedHandler and
// SetNamedHandler are set on the Instance template, and that's the key
// difference. The context has its own global instance, so we need to set
// these back up directly on it. There might be a better way to do this.
v8.v8__ObjectTemplate__SetIndexedHandler(global_template_local, &.{
.getter = Window.JsApi.index.getter,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = 0,
});
v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal); v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);
private_symbols = PrivateSymbols.init(isolate_handle); private_symbols = PrivateSymbols.init(isolate_handle);
} }
@@ -225,7 +240,7 @@ pub fn deinit(self: *Env) void {
allocator.destroy(self.isolate_params); allocator.destroy(self.isolate_params);
} }
pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context { pub fn createContext(self: *Env, page: *Page) !*Context {
const context_arena = try self.app.arena_pool.acquire(); const context_arena = try self.app.arena_pool.acquire();
errdefer self.app.arena_pool.release(context_arena); errdefer self.app.arena_pool.release(context_arena);
@@ -264,13 +279,6 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
var global_global: v8.Global = undefined; var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global); v8.v8__Global__New(isolate.handle, global_obj, &global_global);
if (enter) {
v8.v8__Context__Enter(v8_context);
}
errdefer if (enter) {
v8.v8__Context__Exit(v8_context);
};
const context_id = self.context_id; const context_id = self.context_id;
self.context_id = context_id + 1; self.context_id = context_id + 1;
@@ -279,7 +287,6 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
.env = self, .env = self,
.page = page, .page = page,
.id = context_id, .id = context_id,
.entered = enter,
.isolate = isolate, .isolate = isolate,
.arena = context_arena, .arena = context_arena,
.handle = context_global, .handle = context_global,

View File

@@ -1333,6 +1333,7 @@ pub const Scope = struct {
handle_scope: js.HandleScope, handle_scope: js.HandleScope,
pub fn deinit(self: *Scope) void { pub fn deinit(self: *Scope) void {
v8.v8__Context__Exit(self.local.handle);
self.handle_scope.deinit(); self.handle_scope.deinit();
} }

View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script>
function frame1Onload() {
window.f1_onload = true;
}
</script>
<iframe id=f1 onload="frame1Onload" src="support/sub1.html"></iframe>
<iframe id=f2 src="support/sub2.html"></iframe>
<script id="basic">
testing.eventually(() => {
testing.expectEqual(undefined, window[10]);
testing.expectEqual(window, window[0].top);
testing.expectEqual(window, window[0].parent);
testing.expectEqual(false, window === window[0]);
testing.expectEqual(window, window[1].top);
testing.expectEqual(window, window[1].parent);
testing.expectEqual(false, window === window[1]);
testing.expectEqual(false, window[0] === window[1]);
testing.expectEqual(0, $('#f1').childNodes.length);
testing.expectEqual(window[0], $('#f1').contentWindow);
testing.expectEqual(window[1], $('#f2').contentWindow);
testing.expectEqual(window[0].document, $('#f1').contentDocument);
testing.expectEqual(window[1].document, $('#f2').contentDocument);
// sibling frames share the same top
testing.expectEqual(window[0].top, window[1].top);
// child frames have no sub-frames
testing.expectEqual(0, window[0].length);
testing.expectEqual(0, window[1].length);
// self and window are self-referential on child frames
testing.expectEqual(window[0], window[0].self);
testing.expectEqual(window[0], window[0].window);
testing.expectEqual(window[1], window[1].self);
// child frame's top.parent is itself (root has no parent)
testing.expectEqual(window, window[0].top.parent);
// testing.expectEqual(true, window.sub1_loaded);
// testing.expectEqual(true, window.sub2_loaded);
});
</script>
<script id=onload>
{
let f3_load_event = false;
let f3 = document.createElement('iframe');
f3.addEventListener('load', () => {
f3_load_event = true;
});
f3.src = 'invalid'; // still fires load!
document.documentElement.appendChild(f3);
testing.eventually(() => {
testing.expectEqual(true, window.f1_onload);
testing.expectEqual(true, f3_load_event);
});
}
</script>
<script id=count>
testing.eventually(() => {
testing.expectEqual(3, window.length);
});
</script>

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<div id=div-1>sub1 div1</div>
<script>
// should not have access to the parent's JS context
window.top.sub1_loaded = window.testing == undefined;
</script>

View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<div id=div-1>sub2 div1</div>
<script>
// should not have access to the parent's JS context
window.top.sub2_loaded = window.testing == undefined;
</script>

View File

@@ -220,4 +220,8 @@
return val; return val;
}); });
} }
if (window._lightpanda_skip_auto_assert !== true) {
window.addEventListener('load', testing.assertOk);
}
})(); })();

View File

@@ -713,7 +713,7 @@ pub fn normalize(self: *Node, page: *Page) !void {
return self._normalize(page.call_arena, &buffer, page); return self._normalize(page.call_arena, &buffer, page);
} }
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, StringTooLarge, NotSupported, NotImplemented, InvalidCharacterError, CloneError }!*Node { pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, StringTooLarge, NotSupported, NotImplemented, InvalidCharacterError, CloneError, IFrameLoadError }!*Node {
const deep = deep_ orelse false; const deep = deep_ orelse false;
switch (self._type) { switch (self._type) {
.cdata => |cd| { .cdata => |cd| {

View File

@@ -52,6 +52,7 @@ const Allocator = std.mem.Allocator;
const Window = @This(); const Window = @This();
_proto: *EventTarget, _proto: *EventTarget,
_page: *Page,
_document: *Document, _document: *Document,
_css: CSS = .init, _css: CSS = .init,
_crypto: Crypto = .init, _crypto: Crypto = .init,
@@ -96,6 +97,21 @@ pub fn getWindow(self: *Window) *Window {
return self; return self;
} }
pub fn getTop(self: *Window) *Window {
var p = self._page;
while (p.parent) |parent| {
p = parent;
}
return p.window;
}
pub fn getParent(self: *Window) *Window {
if (self._page.parent) |p| {
return p.window;
}
return self;
}
pub fn getDocument(self: *Window) *Document { pub fn getDocument(self: *Window) *Document {
return self._document; return self._document;
} }
@@ -388,23 +404,31 @@ pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
return decoded; return decoded;
} }
pub fn getFrame(_: *Window, _: usize) !?*Window { pub fn getFrame(self: *Window, idx: usize) !?*Window {
// TODO return the iframe's window. const page = self._page;
return null; const frames = page.frames.items;
if (idx >= frames.len) {
return null;
}
if (page.frames_sorted == false) {
std.mem.sort(*Page, frames, {}, struct {
fn lessThan(_: void, a: *Page, b: *Page) bool {
const iframe_a = a.iframe orelse return false;
const iframe_b = b.iframe orelse return true;
const pos = iframe_a.asNode().compareDocumentPosition(iframe_b.asNode());
// Return true if a precedes b (a should come before b in sorted order)
return (pos & 0x04) != 0; // FOLLOWING bit: b follows a
}
}.lessThan);
page.frames_sorted = true;
}
return frames[idx].window;
} }
pub fn getFramesLength(self: *const Window) u32 { pub fn getFramesLength(self: *const Window) u32 {
const TreeWalker = @import("TreeWalker.zig"); return @intCast(self._page.frames.items.len);
var walker = TreeWalker.Full.init(self._document.asNode(), .{});
var ln: u32 = 0;
while (walker.next()) |node| {
if (node.is(Element.Html.IFrame) != null) {
ln += 1;
}
}
return ln;
} }
pub fn getScrollX(self: *const Window) u32 { pub fn getScrollX(self: *const Window) u32 {
@@ -716,10 +740,10 @@ pub const JsApi = struct {
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 } }); pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 } });
pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } }); pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } });
pub const top = bridge.accessor(Window.getWindow, null, .{}); pub const top = bridge.accessor(Window.getTop, null, .{});
pub const self = bridge.accessor(Window.getWindow, null, .{}); pub const self = bridge.accessor(Window.getWindow, null, .{});
pub const window = bridge.accessor(Window.getWindow, null, .{}); pub const window = bridge.accessor(Window.getWindow, null, .{});
pub const parent = bridge.accessor(Window.getWindow, null, .{}); pub const parent = bridge.accessor(Window.getParent, null, .{});
pub const navigator = bridge.accessor(Window.getNavigator, null, .{}); pub const navigator = bridge.accessor(Window.getNavigator, null, .{});
pub const screen = bridge.accessor(Window.getScreen, null, .{}); pub const screen = bridge.accessor(Window.getScreen, null, .{});
pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{}); pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{});

View File

@@ -373,13 +373,13 @@ pub fn setTabIndex(self: *HtmlElement, value: i32, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("tabindex"), .wrap(str), page); try self.asElement().setAttributeSafe(comptime .wrap("tabindex"), .wrap(str), page);
} }
fn getAttributeFunction( pub fn getAttributeFunction(
self: *HtmlElement, self: *HtmlElement,
listener_type: GlobalEventHandler, listener_type: GlobalEventHandler,
page: *Page, page: *Page,
) !?js.Function.Global { ) !?js.Function.Global {
const element = self.asElement(); const element = self.asElement();
if (page.getAttrListener(element, listener_type)) |cached| { if (page._element_attr_listeners.get(.{ .target = element.asEventTarget(), .handler = listener_type })) |cached| {
return cached; return cached;
} }

View File

@@ -16,15 +16,21 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const log = @import("../../../../log.zig");
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig"); const Page = @import("../../../Page.zig");
const Window = @import("../../Window.zig"); const Window = @import("../../Window.zig");
const Document = @import("../../Document.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
const URL = @import("../../URL.zig");
const IFrame = @This(); const IFrame = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
_src: []const u8 = "",
_executed: bool = false,
_content_window: ?*Window = null,
pub fn asElement(self: *IFrame) *Element { pub fn asElement(self: *IFrame) *Element {
return self._proto._proto; return self._proto._proto;
@@ -33,8 +39,27 @@ pub fn asNode(self: *IFrame) *Node {
return self.asElement().asNode(); return self.asElement().asNode();
} }
pub fn getContentWindow(_: *const IFrame, page: *Page) *Window { pub fn getContentWindow(self: *const IFrame) ?*Window {
return page.window; return self._content_window;
}
pub fn getContentDocument(self: *const IFrame) ?*Document {
const window = self._content_window orelse return null;
return window._document;
}
pub fn getSrc(self: *const IFrame, page: *Page) ![:0]const u8 {
if (self._src.len == 0) return "";
return try URL.resolve(page.call_arena, page.base(), self._src, .{});
}
pub fn setSrc(self: *IFrame, src: []const u8, page: *Page) !void {
const element = self.asElement();
try element.setAttributeSafe(comptime .wrap("src"), .wrap(src), page);
self._src = element.getAttributeSafe(comptime .wrap("src")) orelse unreachable;
if (element.asNode().isConnected()) {
try page.iframeAddedCallback(self);
}
} }
pub const JsApi = struct { pub const JsApi = struct {
@@ -46,5 +71,15 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
}; };
pub const src = bridge.accessor(IFrame.getSrc, IFrame.setSrc, .{});
pub const contentWindow = bridge.accessor(IFrame.getContentWindow, null, .{}); pub const contentWindow = bridge.accessor(IFrame.getContentWindow, null, .{});
pub const contentDocument = bridge.accessor(IFrame.getContentDocument, null, .{});
};
pub const Build = struct {
pub fn complete(node: *Node, _: *Page) !void {
const self = node.as(IFrame);
const element = self.asElement();
self._src = element.getAttributeSafe(comptime .wrap("src")) orelse "";
}
}; };

View File

@@ -431,6 +431,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
try notification.register(.page_created, self, onPageCreated); try notification.register(.page_created, self, onPageCreated);
try notification.register(.page_navigate, self, onPageNavigate); try notification.register(.page_navigate, self, onPageNavigate);
try notification.register(.page_navigated, self, onPageNavigated); try notification.register(.page_navigated, self, onPageNavigated);
try notification.register(.page_frame_created, self, onPageFrameCreated);
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
@@ -587,7 +588,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void { pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena();
return @import("domains/page.zig").pageNavigate(self, msg); return @import("domains/page.zig").pageNavigate(self, msg);
} }
@@ -597,6 +597,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg); return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg);
} }
pub fn onPageFrameCreated(ctx: *anyopaque, msg: *const Notification.PageFrameCreated) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageFrameCreated(self, msg);
}
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void { pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageNetworkIdle(self, msg); return @import("domains/page.zig").pageNetworkIdle(self, msg);
@@ -609,19 +614,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void { pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena();
try @import("domains/network.zig").httpRequestStart(self, msg); try @import("domains/network.zig").httpRequestStart(self, msg);
} }
pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void { pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena();
try @import("domains/fetch.zig").requestIntercept(self, msg); try @import("domains/fetch.zig").requestIntercept(self, msg);
} }
pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void { pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpRequestFail(self, msg); return @import("domains/network.zig").httpRequestFail(self, msg);
} }
@@ -633,7 +635,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void { pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpRequestDone(self, msg); return @import("domains/network.zig").httpRequestDone(self, msg);
} }
@@ -763,7 +764,7 @@ const IsolatedWorld = struct {
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world. // Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context { pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context {
if (self.context == null) { if (self.context == null) {
self.context = try self.browser.env.createContext(page, false); self.context = try self.browser.env.createContext(page);
} else { } else {
log.warn(.cdp, "not implemented", .{ log.warn(.cdp, "not implemented", .{
.feature = "createContext: Not implemented second isolated context creation", .feature = "createContext: Not implemented second isolated context creation",

View File

@@ -250,15 +250,17 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
else => unreachable, else => unreachable,
}, },
.address_bar => null, .address_bar => null,
.initialFrameNavigation => "initialFrameNavigation",
}; };
if (reason_) |reason| { if (reason_) |reason| {
try cdp.sendEvent("Page.frameScheduledNavigation", .{ if (event.opts.reason != .initialFrameNavigation) {
.frameId = frame_id, try cdp.sendEvent("Page.frameScheduledNavigation", .{
.delay = 0, .frameId = frame_id,
.reason = reason, .delay = 0,
.url = event.url, .reason = reason,
}, .{ .session_id = session_id }); .url = event.url,
}, .{ .session_id = session_id });
}
try cdp.sendEvent("Page.frameRequestedNavigation", .{ try cdp.sendEvent("Page.frameRequestedNavigation", .{
.frameId = frame_id, .frameId = frame_id,
.reason = reason, .reason = reason,
@@ -300,6 +302,27 @@ pub fn pageCreated(bc: anytype, page: *Page) !void {
bc.captured_responses = .empty; bc.captured_responses = .empty;
} }
pub fn pageFrameCreated(bc: anytype, event: *const Notification.PageFrameCreated) !void {
const session_id = bc.session_id orelse return;
const cdp = bc.cdp;
const frame_id = &id.toFrameId(event.page_id);
try cdp.sendEvent("Page.frameAttached", .{ .params = .{
.frameId = frame_id,
.parentFrameId = &id.toFrameId(event.parent_id),
} }, .{ .session_id = session_id });
if (bc.page_life_cycle_events) {
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
.name = "init",
.frameId = frame_id,
.loaderId = &id.toLoaderId(event.page_id),
.timestamp = event.timestamp,
}, .{ .session_id = session_id });
}
}
pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void { pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void {
// detachTarget could be called, in which case, we still have a page doing // detachTarget could be called, in which case, we still have a page doing
// things, but no session. // things, but no session.
@@ -345,6 +368,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
else => unreachable, else => unreachable,
}, },
.address_bar => null, .address_bar => null,
.initialFrameNavigation => "initialFrameNavigation",
}; };
if (reason_ != null) { if (reason_ != null) {

View File

@@ -211,7 +211,7 @@ pub const Connection = struct {
const easy = self.easy; const easy = self.easy;
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPPOST, @as(c_long, 1))); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPPOST, @as(c_long, 1)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(body.len)))); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(body.len))));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, body.ptr)); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_COPYPOSTFIELDS, body.ptr));
} }
// These are headers that may not be send to the users for inteception. // These are headers that may not be send to the users for inteception.

View File

@@ -126,7 +126,7 @@ fn run(
const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0); const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0);
try page.navigate(url, .{}); try page.navigate(url, .{});
_ = page.wait(2000); _ = session.wait(2000);
var ls: lp.js.Local.Scope = undefined; var ls: lp.js.Local.Scope = undefined;
page.js.localScope(&ls); page.js.localScope(&ls);

View File

@@ -414,6 +414,15 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
try_catch.init(&ls.local); try_catch.init(&ls.local);
defer try_catch.deinit(); defer try_catch.deinit();
// by default, on load, testing.js will call testing.assertOk(). This makes our
// tests work well in a browser. But, for our test runner, we disable that
// and call it explicitly. This gives us better error messages.
ls.local.eval("window._lightpanda_skip_auto_assert = true;", "auto_assert") catch |err| {
const caught = try_catch.caughtOrError(arena_allocator, err);
std.debug.print("disable auto assert failure\nError: {f}\n", .{caught});
return err;
};
try page.navigate(url, .{}); try page.navigate(url, .{});
_ = test_session.wait(2000); _ = test_session.wait(2000);