mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge pull request #1565 from lightpanda-io/frames
Initial support for frames
This commit is contained in:
@@ -73,6 +73,7 @@ const EventListeners = struct {
|
||||
page_navigated: List = .{},
|
||||
page_network_idle: List = .{},
|
||||
page_network_almost_idle: List = .{},
|
||||
page_frame_created: List = .{},
|
||||
http_request_fail: List = .{},
|
||||
http_request_start: List = .{},
|
||||
http_request_intercept: List = .{},
|
||||
@@ -89,6 +90,7 @@ const Events = union(enum) {
|
||||
page_navigated: *const PageNavigated,
|
||||
page_network_idle: *const PageNetworkIdle,
|
||||
page_network_almost_idle: *const PageNetworkAlmostIdle,
|
||||
page_frame_created: *const PageFrameCreated,
|
||||
http_request_fail: *const RequestFail,
|
||||
http_request_start: *const RequestStart,
|
||||
http_request_intercept: *const RequestIntercept,
|
||||
@@ -129,6 +131,12 @@ pub const PageNetworkAlmostIdle = struct {
|
||||
timestamp: u64,
|
||||
};
|
||||
|
||||
pub const PageFrameCreated = struct {
|
||||
page_id: u32,
|
||||
parent_id: u32,
|
||||
timestamp: u64,
|
||||
};
|
||||
|
||||
pub const RequestStart = struct {
|
||||
transfer: *Transfer,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
// Look up the inline handler for this target
|
||||
const element = switch (target._type) {
|
||||
.node => |n| n.is(Element) orelse return null,
|
||||
const html_element = switch (target._type) {
|
||||
.node => |n| n.is(Element.Html) orelse 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 {
|
||||
|
||||
@@ -186,7 +186,7 @@ _notified_network_almost_idle: IdleNotification = .init,
|
||||
|
||||
// A navigation event that happens from a script gets scheduled to run on the
|
||||
// next tick.
|
||||
_queued_navigation: ?QueuedNavigation = null,
|
||||
_queued_navigation: ?*QueuedNavigation = null,
|
||||
|
||||
// The URL of the current page
|
||||
url: [:0]const u8 = "about:blank",
|
||||
@@ -221,16 +221,29 @@ _arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
|
||||
count: usize,
|
||||
}) else void) = if (IS_DEBUG) .empty else {},
|
||||
|
||||
parent: ?*Page,
|
||||
window: *Window,
|
||||
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
|
||||
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,
|
||||
_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) {
|
||||
log.debug(.page, "page.init", .{});
|
||||
}
|
||||
@@ -251,6 +264,7 @@ pub fn init(self: *Page, id: u32, session: *Session) !void {
|
||||
self.* = .{
|
||||
.id = id,
|
||||
.js = undefined,
|
||||
.parent = parent,
|
||||
.arena = page_arena,
|
||||
.document = document,
|
||||
.window = undefined,
|
||||
@@ -258,28 +272,41 @@ pub fn init(self: *Page, id: u32, session: *Session) !void {
|
||||
.call_arena = call_arena,
|
||||
._session = session,
|
||||
._factory = factory,
|
||||
._pending_loads = 1, // always 1 for the ScriptManager
|
||||
._type = if (parent == null) .root else .frame,
|
||||
._script_manager = undefined,
|
||||
._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{
|
||||
._page = self,
|
||||
._proto = undefined,
|
||||
._document = self.document,
|
||||
._location = &default_location,
|
||||
._performance = Performance.init(),
|
||||
._screen = try factory.eventTarget(Screen{
|
||||
._proto = undefined,
|
||||
._orientation = null,
|
||||
}),
|
||||
._visual_viewport = try factory.eventTarget(VisualViewport{
|
||||
._proto = undefined,
|
||||
}),
|
||||
._screen = screen,
|
||||
._visual_viewport = visual_viewport,
|
||||
});
|
||||
|
||||
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
|
||||
errdefer self._script_manager.deinit();
|
||||
|
||||
self.js = try browser.env.createContext(self, true);
|
||||
self.js = try browser.env.createContext(self);
|
||||
errdefer self.js.deinit();
|
||||
|
||||
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 {
|
||||
for (self.frames.items) |frame| {
|
||||
frame.deinit();
|
||||
}
|
||||
|
||||
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.
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (self._queued_navigation) |qn| {
|
||||
self.arena_pool.release(qn.arena);
|
||||
}
|
||||
|
||||
const session = self._session;
|
||||
session.browser.env.destroyContext(self.js);
|
||||
|
||||
@@ -316,7 +351,7 @@ pub fn deinit(self: *Page) void {
|
||||
var it = self._arena_pool_leak_track.valueIterator();
|
||||
while (it.next()) |value_ptr| {
|
||||
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) {
|
||||
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
|
||||
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;
|
||||
}
|
||||
found.count = 0;
|
||||
@@ -410,6 +445,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
.reason = opts.reason,
|
||||
.body = opts.body != null,
|
||||
.req_id = req_id,
|
||||
.type = self._type,
|
||||
});
|
||||
|
||||
// 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,
|
||||
.error_callback = pageErrorCallback,
|
||||
}) 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;
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
// which holds this event's node.
|
||||
// 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 {
|
||||
if (self.canScheduleNavigation(priority) == false) {
|
||||
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(
|
||||
session.transfer_arena,
|
||||
arena,
|
||||
self.base(),
|
||||
request_url,
|
||||
.{ .always_dupe = true },
|
||||
);
|
||||
|
||||
const session = self._session;
|
||||
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.window._location = try Location.init(self.url, self);
|
||||
self.document._location = self.window._location;
|
||||
@@ -541,15 +581,23 @@ pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOp
|
||||
.url = resolved_url,
|
||||
.reason = opts.reason,
|
||||
.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,
|
||||
.arena = arena,
|
||||
.url = resolved_url,
|
||||
.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
|
||||
@@ -589,7 +637,7 @@ pub fn documentIsLoaded(self: *Page) void {
|
||||
self._load_state = .load;
|
||||
self.document._ready_state = .interactive;
|
||||
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 {
|
||||
if (self._load_state == .complete) {
|
||||
// Ideally, documentIsComplete would only be called once, but with
|
||||
@@ -621,7 +701,7 @@ pub fn documentIsComplete(self: *Page) void {
|
||||
|
||||
self._load_state = .complete;
|
||||
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) {
|
||||
@@ -675,6 +755,19 @@ fn _documentIsComplete(self: *Page) !void {
|
||||
ls.toLocal(self.window._on_pageshow),
|
||||
.{ .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 {
|
||||
@@ -692,6 +785,7 @@ fn pageHeaderDoneCallback(transfer: *Http.Transfer) !bool {
|
||||
.url = self.url,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
.type = self._type,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -712,7 +806,7 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
} orelse .unknown;
|
||||
|
||||
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) {
|
||||
@@ -756,18 +850,17 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
}
|
||||
|
||||
fn pageDoneCallback(ctx: *anyopaque) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "navigate done", .{});
|
||||
}
|
||||
|
||||
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.
|
||||
try self._session.navigation.commitNavigation(self);
|
||||
|
||||
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" });
|
||||
@@ -836,36 +929,19 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
||||
}
|
||||
|
||||
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
log.err(.page, "navigate failed", .{ .err = err });
|
||||
|
||||
var self: *Page = @ptrCast(@alignCast(ctx));
|
||||
|
||||
log.err(.page, "navigate failed", .{ .err = err, .type = self._type });
|
||||
self._parse_state = .{ .err = err };
|
||||
|
||||
// In case of error, we want to complete the page with a custom HTML
|
||||
// containing the error.
|
||||
pageDoneCallback(ctx) catch |e| {
|
||||
log.err(.browser, "pageErrorCallback", .{ .err = e });
|
||||
log.err(.browser, "pageErrorCallback", .{ .err = e, .type = self._type });
|
||||
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 {
|
||||
return self._wait(wait_ms) catch |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 expand the switch (err) to have more customized logs for
|
||||
// specific messages.
|
||||
log.err(.browser, "page wait", .{ .err = err });
|
||||
log.err(.browser, "page wait", .{ .err = err, .type = self._type });
|
||||
},
|
||||
}
|
||||
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 {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self._type == .root);
|
||||
}
|
||||
|
||||
var timer = try std.time.Timer.start();
|
||||
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", .{
|
||||
.err = err,
|
||||
.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 {
|
||||
self.version += 1;
|
||||
|
||||
@@ -1120,7 +1272,7 @@ pub fn domChanged(self: *Page) void {
|
||||
|
||||
self._intersection_check_scheduled = true;
|
||||
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", .{
|
||||
.element = element,
|
||||
.listener_type = listener_type,
|
||||
.type = self._type,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1221,18 +1374,6 @@ pub fn setAttrListener(
|
||||
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 {
|
||||
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| {
|
||||
if (observer.interested(entry)) {
|
||||
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.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;
|
||||
const observer = self._intersection_observers.items[i];
|
||||
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) {
|
||||
log.err(.page, "page.MutationLimit", .{});
|
||||
log.err(.page, "page.MutationLimit", .{ .type = self._type });
|
||||
self._mutation_delivery_depth = 0;
|
||||
return;
|
||||
}
|
||||
@@ -1390,7 +1531,7 @@ pub fn deliverMutations(self: *Page) void {
|
||||
while (it) |node| : (it = node.next) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
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 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;
|
||||
};
|
||||
|
||||
@@ -1421,19 +1562,19 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
|
||||
|
||||
for (slots) |slot| {
|
||||
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;
|
||||
};
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
const target = slot.asNode().asEventTarget();
|
||||
_ = 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", .{});
|
||||
self._session.notification.dispatch(.page_network_idle, &.{
|
||||
.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", .{});
|
||||
self._session.notification.dispatch(.page_network_almost_idle, &.{
|
||||
.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
|
||||
pub fn nodeComplete(self: *Page, node: *Node) !void {
|
||||
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 self.nodeIsReady(true, node);
|
||||
@@ -2152,7 +2293,6 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
const tag_name = try String.init(self.arena, name, .{});
|
||||
|
||||
// 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;
|
||||
_ = 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;
|
||||
};
|
||||
|
||||
@@ -2241,7 +2381,7 @@ fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespac
|
||||
const node = element.asNode();
|
||||
if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) {
|
||||
@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;
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
_ = 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);
|
||||
@@ -2660,7 +2800,7 @@ pub fn attributeChange(self: *Page, element: *Element, name: String, value: Stri
|
||||
while (it) |node| : (it = node.next) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
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 {
|
||||
_ = 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);
|
||||
@@ -2686,7 +2826,7 @@ pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value:
|
||||
while (it) |node| : (it = node.next) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
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 {
|
||||
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;
|
||||
};
|
||||
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
|
||||
if (findMatchingSlot(shadow_root.asNode(), slot_name)) |slot| {
|
||||
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) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
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) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
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| {
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -2982,6 +3132,7 @@ pub const NavigateReason = enum {
|
||||
script,
|
||||
history,
|
||||
navigation,
|
||||
initialFrameNavigation,
|
||||
};
|
||||
|
||||
pub const NavigateOpts = struct {
|
||||
@@ -3006,7 +3157,8 @@ const NavigationPriority = enum {
|
||||
anchor,
|
||||
};
|
||||
|
||||
const QueuedNavigation = struct {
|
||||
pub const QueuedNavigation = struct {
|
||||
arena: Allocator,
|
||||
url: [:0]const u8,
|
||||
opts: NavigateOpts,
|
||||
priority: NavigationPriority,
|
||||
@@ -3020,6 +3172,7 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
||||
.node = target,
|
||||
.x = x,
|
||||
.y = y,
|
||||
.type = self._type,
|
||||
});
|
||||
}
|
||||
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
|
||||
const target_val = anchor.getTarget();
|
||||
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;
|
||||
}
|
||||
|
||||
if (try element.hasAttribute(comptime .wrap("download"), self)) {
|
||||
log.warn(.browser, "a.download", .{});
|
||||
log.warn(.browser, "a.download", .{ .type = self._type });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3096,6 +3249,7 @@ pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
||||
.url = self.url,
|
||||
.node = element,
|
||||
.key = keyboard_event._key,
|
||||
.type = self._type,
|
||||
});
|
||||
}
|
||||
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
|
||||
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"));
|
||||
|
||||
var buf = std.Io.Writer.Allocating.init(transfer_arena);
|
||||
var buf = std.Io.Writer.Allocating.init(arena);
|
||||
try form_data.write(encoding, &buf.writer);
|
||||
|
||||
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
|
||||
opts.header = "Content-Type: application/x-www-form-urlencoded";
|
||||
} 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.
|
||||
@@ -3259,6 +3414,10 @@ test "WebApi: Page" {
|
||||
try testing.htmlRunner("page", .{});
|
||||
}
|
||||
|
||||
test "WebApi: Frames" {
|
||||
try testing.htmlRunner("frames", .{});
|
||||
}
|
||||
|
||||
test "WebApi: Integration" {
|
||||
try testing.htmlRunner("integration", .{});
|
||||
}
|
||||
|
||||
@@ -83,6 +83,10 @@ imported_modules: std.StringHashMapUnmanaged(ImportedModule),
|
||||
// importmap contains resolved urls.
|
||||
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 {
|
||||
return .{
|
||||
.page = page,
|
||||
@@ -96,6 +100,7 @@ pub fn init(allocator: Allocator, http_client: *Http.Client, page: *Page) Script
|
||||
.client = http_client,
|
||||
.static_scripts_done = false,
|
||||
.buffer_pool = BufferPool.init(allocator, 5),
|
||||
.page_notified_of_completion = false,
|
||||
.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.documentIsLoaded();
|
||||
|
||||
if (self.async_scripts.first == null) {
|
||||
// Looks like all async scripts are done too!
|
||||
// Page makes this safe to call multiple times.
|
||||
page.documentIsComplete();
|
||||
if (self.async_scripts.first == null and self.page_notified_of_completion == false) {
|
||||
self.page_notified_of_completion = true;
|
||||
page.scriptsCompletedLoading();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const log = @import("../log.zig");
|
||||
|
||||
@@ -31,7 +32,7 @@ const Browser = @import("Browser.zig");
|
||||
const Notification = @import("../Notification.zig");
|
||||
|
||||
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.
|
||||
// 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.
|
||||
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,
|
||||
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();
|
||||
errdefer browser.arena_pool.release(arena);
|
||||
|
||||
const transfer_arena = try browser.arena_pool.acquire();
|
||||
errdefer browser.arena_pool.release(transfer_arena);
|
||||
|
||||
self.* = .{
|
||||
.page = null,
|
||||
.arena = arena,
|
||||
@@ -83,7 +71,6 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
|
||||
.storage_shed = .{},
|
||||
.browser = browser,
|
||||
.notification = notification,
|
||||
.transfer_arena = transfer_arena,
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||
};
|
||||
}
|
||||
@@ -96,7 +83,6 @@ pub fn deinit(self: *Session) void {
|
||||
|
||||
self.cookie_jar.deinit();
|
||||
self.storage_shed.deinit(browser.app.allocator);
|
||||
browser.arena_pool.release(self.transfer_arena);
|
||||
browser.arena_pool.release(self.arena);
|
||||
}
|
||||
|
||||
@@ -107,11 +93,7 @@ pub fn createPage(self: *Session) !*Page {
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
|
||||
const id = self.page_id_gen +% 1;
|
||||
self.page_id_gen = id;
|
||||
|
||||
try Page.init(page, id, self);
|
||||
try Page.init(page, self.nextPageId(), self, null);
|
||||
|
||||
// Creates a new NavigationEventTarget for this page.
|
||||
try self.navigation.onNewPage(page);
|
||||
@@ -150,13 +132,14 @@ pub fn replacePage(self: *Session) !*Page {
|
||||
|
||||
var current = self.page.?;
|
||||
const page_id = current.id;
|
||||
const parent = current.parent;
|
||||
current.deinit();
|
||||
|
||||
self.browser.env.memoryPressureNotification(.moderate);
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, page_id, self);
|
||||
try Page.init(page, page_id, self, parent);
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -176,54 +159,208 @@ pub fn findPage(self: *Session, id: u32) ?*Page {
|
||||
}
|
||||
|
||||
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||
var page = &(self.page orelse return .no_page);
|
||||
while (true) {
|
||||
if (self.page) |*page| {
|
||||
switch (page.wait(wait_ms)) {
|
||||
.done => {
|
||||
if (page._queued_navigation == null) {
|
||||
return .done;
|
||||
}
|
||||
self.processScheduledNavigation() catch return .done;
|
||||
},
|
||||
else => |result| return result,
|
||||
const wait_result = self._wait(page, wait_ms) catch |err| {
|
||||
switch (err) {
|
||||
error.JsError => {}, // already logged (with hopefully more context)
|
||||
else => log.err(.browser, "session wait", .{
|
||||
.err = err,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
return .no_page;
|
||||
return .done;
|
||||
};
|
||||
|
||||
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 {
|
||||
defer self.browser.arena_pool.reset(self.transfer_arena, 4 * 1024);
|
||||
const url, const opts, const page_id = blk: {
|
||||
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;
|
||||
fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
||||
var timer = try std.time.Timer.start();
|
||||
var ms_remaining = wait_ms;
|
||||
|
||||
// This was already aborted on the page, but it would be pretty
|
||||
// bad if old requests went to the new page, so let's make double sure
|
||||
self.browser.http_client.abort();
|
||||
const browser = self.browser;
|
||||
var http_client = browser.http_client;
|
||||
|
||||
// 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();
|
||||
|
||||
break :blk .{ url, opts, page.id };
|
||||
break :blk .{ page_id, parent };
|
||||
};
|
||||
|
||||
const page = self.createPage() catch |err| {
|
||||
log.err(.browser, "queued navigation page error", .{
|
||||
.err = err,
|
||||
.url = url,
|
||||
});
|
||||
return err;
|
||||
};
|
||||
page.id = page_id;
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, page_id, self, parent);
|
||||
|
||||
page.navigate(url, opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = url });
|
||||
// Creates a new NavigationEventTarget for this page.
|
||||
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 page;
|
||||
}
|
||||
|
||||
pub fn nextPageId(self: *Session) u32 {
|
||||
const id = self.page_id_gen +% 1;
|
||||
self.page_id_gen = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -47,9 +47,6 @@ isolate: js.Isolate,
|
||||
// from this, and we can free it when the context is done.
|
||||
handle: v8.Global,
|
||||
|
||||
// True if the context is auto-entered,
|
||||
entered: bool,
|
||||
|
||||
cpu_profiler: ?*v8.CpuProfiler = null,
|
||||
|
||||
heap_profiler: ?*v8.HeapProfiler = null,
|
||||
@@ -247,11 +244,6 @@ pub fn deinit(self: *Context) void {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -333,11 +325,14 @@ pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
|
||||
const isolate = self.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
|
||||
ls.local = .{
|
||||
.ctx = self,
|
||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle)),
|
||||
.isolate = isolate,
|
||||
.handle = local_v8_context,
|
||||
.call_arena = self.call_arena,
|
||||
};
|
||||
}
|
||||
@@ -364,7 +359,6 @@ pub fn stringToPersistedFunction(self: *Context, str: []const u8) !js.Function.G
|
||||
extra = "(e)";
|
||||
}
|
||||
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);
|
||||
if (!js_val.isFunction()) {
|
||||
return error.StringFunctionError;
|
||||
|
||||
@@ -175,8 +175,23 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
.data = null,
|
||||
.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);
|
||||
|
||||
private_symbols = PrivateSymbols.init(isolate_handle);
|
||||
}
|
||||
|
||||
@@ -225,7 +240,7 @@ pub fn deinit(self: *Env) void {
|
||||
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();
|
||||
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;
|
||||
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;
|
||||
self.context_id = context_id + 1;
|
||||
|
||||
@@ -279,7 +287,6 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
|
||||
.env = self,
|
||||
.page = page,
|
||||
.id = context_id,
|
||||
.entered = enter,
|
||||
.isolate = isolate,
|
||||
.arena = context_arena,
|
||||
.handle = context_global,
|
||||
|
||||
@@ -1333,6 +1333,7 @@ pub const Scope = struct {
|
||||
handle_scope: js.HandleScope,
|
||||
|
||||
pub fn deinit(self: *Scope) void {
|
||||
v8.v8__Context__Exit(self.local.handle);
|
||||
self.handle_scope.deinit();
|
||||
}
|
||||
|
||||
|
||||
75
src/browser/tests/frames/frames.html
Normal file
75
src/browser/tests/frames/frames.html
Normal 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>
|
||||
6
src/browser/tests/frames/support/sub1.html
Normal file
6
src/browser/tests/frames/support/sub1.html
Normal 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>
|
||||
7
src/browser/tests/frames/support/sub2.html
Normal file
7
src/browser/tests/frames/support/sub2.html
Normal 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>
|
||||
@@ -220,4 +220,8 @@
|
||||
return val;
|
||||
});
|
||||
}
|
||||
|
||||
if (window._lightpanda_skip_auto_assert !== true) {
|
||||
window.addEventListener('load', testing.assertOk);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -713,7 +713,7 @@ pub fn normalize(self: *Node, page: *Page) !void {
|
||||
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;
|
||||
switch (self._type) {
|
||||
.cdata => |cd| {
|
||||
|
||||
@@ -52,6 +52,7 @@ const Allocator = std.mem.Allocator;
|
||||
const Window = @This();
|
||||
|
||||
_proto: *EventTarget,
|
||||
_page: *Page,
|
||||
_document: *Document,
|
||||
_css: CSS = .init,
|
||||
_crypto: Crypto = .init,
|
||||
@@ -96,6 +97,21 @@ pub fn getWindow(self: *Window) *Window {
|
||||
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 {
|
||||
return self._document;
|
||||
}
|
||||
@@ -388,23 +404,31 @@ pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
||||
return decoded;
|
||||
}
|
||||
|
||||
pub fn getFrame(_: *Window, _: usize) !?*Window {
|
||||
// TODO return the iframe's window.
|
||||
return null;
|
||||
pub fn getFrame(self: *Window, idx: usize) !?*Window {
|
||||
const page = self._page;
|
||||
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 {
|
||||
const TreeWalker = @import("TreeWalker.zig");
|
||||
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;
|
||||
return @intCast(self._page.frames.items.len);
|
||||
}
|
||||
|
||||
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 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 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 screen = bridge.accessor(Window.getScreen, null, .{});
|
||||
pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{});
|
||||
|
||||
@@ -373,13 +373,13 @@ pub fn setTabIndex(self: *HtmlElement, value: i32, page: *Page) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("tabindex"), .wrap(str), page);
|
||||
}
|
||||
|
||||
fn getAttributeFunction(
|
||||
pub fn getAttributeFunction(
|
||||
self: *HtmlElement,
|
||||
listener_type: GlobalEventHandler,
|
||||
page: *Page,
|
||||
) !?js.Function.Global {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,21 @@
|
||||
// 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/>.
|
||||
|
||||
const log = @import("../../../../log.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Page = @import("../../../Page.zig");
|
||||
const Window = @import("../../Window.zig");
|
||||
const Document = @import("../../Document.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const URL = @import("../../URL.zig");
|
||||
|
||||
const IFrame = @This();
|
||||
_proto: *HtmlElement,
|
||||
_src: []const u8 = "",
|
||||
_executed: bool = false,
|
||||
_content_window: ?*Window = null,
|
||||
|
||||
pub fn asElement(self: *IFrame) *Element {
|
||||
return self._proto._proto;
|
||||
@@ -33,8 +39,27 @@ pub fn asNode(self: *IFrame) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getContentWindow(_: *const IFrame, page: *Page) *Window {
|
||||
return page.window;
|
||||
pub fn getContentWindow(self: *const IFrame) ?*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 {
|
||||
@@ -46,5 +71,15 @@ pub const JsApi = struct {
|
||||
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 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 "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -431,6 +431,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
try notification.register(.page_created, self, onPageCreated);
|
||||
try notification.register(.page_navigate, self, onPageNavigate);
|
||||
try notification.register(.page_navigated, self, onPageNavigated);
|
||||
try notification.register(.page_frame_created, self, onPageFrameCreated);
|
||||
}
|
||||
|
||||
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 {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
defer self.resetNotificationArena();
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
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 {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
defer self.resetNotificationArena();
|
||||
try @import("domains/network.zig").httpRequestStart(self, msg);
|
||||
}
|
||||
|
||||
pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
defer self.resetNotificationArena();
|
||||
try @import("domains/fetch.zig").requestIntercept(self, msg);
|
||||
}
|
||||
|
||||
pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
defer self.resetNotificationArena();
|
||||
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 {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
defer self.resetNotificationArena();
|
||||
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.
|
||||
pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context {
|
||||
if (self.context == null) {
|
||||
self.context = try self.browser.env.createContext(page, false);
|
||||
self.context = try self.browser.env.createContext(page);
|
||||
} else {
|
||||
log.warn(.cdp, "not implemented", .{
|
||||
.feature = "createContext: Not implemented second isolated context creation",
|
||||
|
||||
@@ -250,15 +250,17 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
|
||||
else => unreachable,
|
||||
},
|
||||
.address_bar => null,
|
||||
.initialFrameNavigation => "initialFrameNavigation",
|
||||
};
|
||||
if (reason_) |reason| {
|
||||
try cdp.sendEvent("Page.frameScheduledNavigation", .{
|
||||
.frameId = frame_id,
|
||||
.delay = 0,
|
||||
.reason = reason,
|
||||
.url = event.url,
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
if (event.opts.reason != .initialFrameNavigation) {
|
||||
try cdp.sendEvent("Page.frameScheduledNavigation", .{
|
||||
.frameId = frame_id,
|
||||
.delay = 0,
|
||||
.reason = reason,
|
||||
.url = event.url,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
try cdp.sendEvent("Page.frameRequestedNavigation", .{
|
||||
.frameId = frame_id,
|
||||
.reason = reason,
|
||||
@@ -300,6 +302,27 @@ pub fn pageCreated(bc: anytype, page: *Page) !void {
|
||||
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 {
|
||||
// detachTarget could be called, in which case, we still have a page doing
|
||||
// things, but no session.
|
||||
@@ -345,6 +368,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
||||
else => unreachable,
|
||||
},
|
||||
.address_bar => null,
|
||||
.initialFrameNavigation => "initialFrameNavigation",
|
||||
};
|
||||
|
||||
if (reason_ != null) {
|
||||
|
||||
@@ -211,7 +211,7 @@ pub const Connection = struct {
|
||||
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_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.
|
||||
|
||||
@@ -126,7 +126,7 @@ fn run(
|
||||
const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0);
|
||||
try page.navigate(url, .{});
|
||||
|
||||
_ = page.wait(2000);
|
||||
_ = session.wait(2000);
|
||||
|
||||
var ls: lp.js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
|
||||
@@ -414,6 +414,15 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
|
||||
try_catch.init(&ls.local);
|
||||
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, .{});
|
||||
_ = test_session.wait(2000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user