Initial support for frames

Missing:

- [ ] Navigation support within frames (in fact, as-is, any navigation done
      inside a frame, will almost certainly break things
- [ ] Correct CDP support. I don't know how frames are supposed to be exposed
      to CDP. Normal navigate events? Distinct CDP frame_ids?
- [ ] Cross-origin restrictions. The interaction between frames is supposed to
      change depending on whether or not they're on the same origin
- [ ] Potentially handling src-less frames incorrectly. Might not really matter

Adds basic frame support. Initially explored adding a BrowsingContext and
embedding it in Page, with the goal of also having it embedded in a to-be
created Frame. But it turns out that 98% of Page _was_ BrowsingContext and
introducing a BrowsingContext as the primary interaction unit broke pretty much
_every_ single WebAPI. So Page was expanded:

- Added `_parent: ?*Page`, which is `null` for "root" page.
- Added `frame: ?*IFrame`, which is `null` for the "root" page. This is the
  HTMLIFrameElement for frame-pages.
- Added a _type: enum{root, frame}, which is currently only used to improve
  the logs
- Added a frames: std.ArrayList(*Page). This is a list of frames for the page.
  Note that a "frame-page" can itself haven nested frames.

Besides the above, there were 3 "big" changes.

1 - Adding frames (dynamically, parsed) has to create a new page, start
    navigation, track it (in the frames list). Part of this was just
    piggybacking off of code that handles <script>

2 - The page "load" event blocks on the frame "load" event. This cascades.
    when a page triggers it's load, it can do:
```zig
      if (self._parent) |p| {
        p.iframeLoaded(self);
      }
```
   Pages need to keep track of how many iframes they're waiting to load. When
   all iframes (and all scripts) are loaded, it can then triggers its own load
   event.

3 - Our JS execution expects 1 primary entered context (the pages). But we now
    have multiple page contexts, and we need to be in the correct one based
    on where javascript is being executed. There is no more an default entered
    context. Creating a Local.Scope enters the context, and ls.deinit() exits
    the context.
This commit is contained in:
Karl Seguin
2026-02-16 17:43:38 +08:00
parent 938cd5e136
commit 081979be3b
17 changed files with 437 additions and 114 deletions

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

@@ -216,16 +216,30 @@ _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,
_req_id: u32 = 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 = null,
_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", .{});
} }
@@ -246,6 +260,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,
@@ -253,28 +268,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) {
@@ -290,8 +318,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;
@@ -311,7 +343,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 });
} }
} }
} }
@@ -380,7 +412,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;
@@ -405,6 +437,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
@@ -501,7 +534,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;
}; };
} }
@@ -536,6 +569,7 @@ 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(); self._session.browser.http_client.abort();
@@ -584,7 +618,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 });
}; };
} }
@@ -597,6 +631,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
@@ -616,7 +682,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) {
@@ -670,6 +736,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 {
@@ -687,6 +766,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,
}); });
} }
@@ -707,7 +787,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) {
@@ -751,18 +831,19 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
} }
fn pageDoneCallback(ctx: *anyopaque) !void { fn pageDoneCallback(ctx: *anyopaque) !void {
var self: *Page = @ptrCast(@alignCast(ctx));
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.page, "navigate done", .{}); log.debug(.page, "navigate done", .{ .type = self._type });
} }
var self: *Page = @ptrCast(@alignCast(ctx));
self.clearTransferArena(); self.clearTransferArena();
//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" });
@@ -831,7 +912,7 @@ 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 }); log.err(.page, "navigate failed", .{ .err = err, .type = self._type });
var self: *Page = @ptrCast(@alignCast(ctx)); var self: *Page = @ptrCast(@alignCast(ctx));
self._parse_state = .{ .err = err }; self._parse_state = .{ .err = err };
@@ -839,7 +920,7 @@ fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
// 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;
}; };
} }
@@ -871,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;
@@ -879,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;
@@ -1102,10 +1187,73 @@ 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 page_frame = try self.arena.create(Page);
try Page.init(page_frame, self._session, self);
self._pending_loads += 1;
page_frame.iframe = iframe;
iframe._content_window = page_frame.window;
page_frame.navigate(src, .{}) 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;
@@ -1115,7 +1263,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 });
}; };
} }
@@ -1206,6 +1354,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,
}); });
} }
@@ -1216,18 +1365,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);
} }
@@ -1247,7 +1384,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 });
}; };
} }
} }
@@ -1342,7 +1479,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 });
}; };
} }
@@ -1358,7 +1495,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 });
}; };
} }
} }
@@ -1376,7 +1513,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;
} }
@@ -1385,7 +1522,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 });
}; };
} }
} }
@@ -1403,7 +1540,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;
}; };
@@ -1416,14 +1553,14 @@ 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 });
}; };
} }
} }
@@ -1478,7 +1615,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);
@@ -2147,7 +2284,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)
@@ -2178,7 +2314,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;
}; };
@@ -2236,7 +2372,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;
}; };
} }
@@ -2646,7 +2782,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);
@@ -2655,7 +2791,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 });
}; };
} }
@@ -2672,7 +2808,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);
@@ -2681,7 +2817,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 });
}; };
} }
@@ -2698,11 +2834,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 });
}; };
} }
@@ -2742,7 +2878,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 });
}; };
} }
} }
@@ -2789,7 +2925,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 });
}; };
} }
} }
@@ -2816,7 +2952,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 });
}; };
} }
} }
@@ -2867,7 +3003,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;
}; };
} }
@@ -3015,6 +3161,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", .{
@@ -3049,12 +3196,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;
} }
@@ -3091,6 +3238,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);
@@ -3258,6 +3406,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

@@ -105,13 +105,12 @@ pub fn deinit(self: *Session) void {
pub fn createPage(self: *Session) !*Page { pub fn createPage(self: *Session) !*Page {
lp.assert(self.page == null, "Session.createPage - page not null", .{}); lp.assert(self.page == null, "Session.createPage - page not null", .{});
self.page = @as(Page, undefined);
const page = &self.page.?;
const id = self.page_id_gen +% 1; const id = self.page_id_gen +% 1;
self.page_id_gen = id; self.page_id_gen = id;
try Page.init(page, id, self); self.page = @as(Page, undefined);
const page = &self.page.?;
try Page.init(page, id, self, null);
// 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 +149,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;
} }

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' 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

@@ -1326,6 +1326,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

@@ -679,7 +679,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;
const frames = page.frames.items;
if (idx >= frames.len) {
return null; 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

@@ -342,6 +342,7 @@ pub fn click(self: *HtmlElement, page: *Page) !void {
try page._event_manager.dispatch(self.asEventTarget(), event); try page._event_manager.dispatch(self.asEventTarget(), event);
} }
<<<<<<< HEAD
// TODO: Per spec, hidden is a tristate: true | false | "until-found". // TODO: Per spec, hidden is a tristate: true | false | "until-found".
// We only support boolean for now; "until-found" would need bridge union support. // We only support boolean for now; "until-found" would need bridge union support.
pub fn getHidden(self: *HtmlElement) bool { pub fn getHidden(self: *HtmlElement) bool {
@@ -373,13 +374,14 @@ 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

@@ -763,7 +763,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

@@ -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);