mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -216,16 +216,30 @@ _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,
|
||||
|
||||
_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,
|
||||
|
||||
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", .{});
|
||||
}
|
||||
@@ -246,6 +260,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,
|
||||
@@ -253,28 +268,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) {
|
||||
@@ -290,8 +318,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;
|
||||
@@ -311,7 +343,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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,7 +412,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;
|
||||
@@ -405,6 +437,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
|
||||
@@ -501,7 +534,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;
|
||||
};
|
||||
}
|
||||
@@ -536,6 +569,7 @@ 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();
|
||||
@@ -584,7 +618,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 });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
if (self._load_state == .complete) {
|
||||
// Ideally, documentIsComplete would only be called once, but with
|
||||
@@ -616,7 +682,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) {
|
||||
@@ -670,6 +736,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 {
|
||||
@@ -687,6 +766,7 @@ fn pageHeaderDoneCallback(transfer: *Http.Transfer) !bool {
|
||||
.url = self.url,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
.type = self._type,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -707,7 +787,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) {
|
||||
@@ -751,18 +831,19 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
}
|
||||
|
||||
fn pageDoneCallback(ctx: *anyopaque) !void {
|
||||
var self: *Page = @ptrCast(@alignCast(ctx));
|
||||
|
||||
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();
|
||||
|
||||
//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" });
|
||||
@@ -831,7 +912,7 @@ fn pageDoneCallback(ctx: *anyopaque) !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));
|
||||
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
|
||||
// containing the error.
|
||||
pageDoneCallback(ctx) catch |e| {
|
||||
log.err(.browser, "pageErrorCallback", .{ .err = e });
|
||||
log.err(.browser, "pageErrorCallback", .{ .err = e, .type = self._type });
|
||||
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 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;
|
||||
@@ -879,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;
|
||||
|
||||
@@ -1102,10 +1187,73 @@ 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 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 {
|
||||
self.version += 1;
|
||||
|
||||
@@ -1115,7 +1263,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 });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1206,6 +1354,7 @@ pub fn setAttrListener(
|
||||
log.debug(.event, "Page.setAttrListener", .{
|
||||
.element = element,
|
||||
.listener_type = listener_type,
|
||||
.type = self._type,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1216,18 +1365,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);
|
||||
}
|
||||
@@ -1247,7 +1384,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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1342,7 +1479,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 });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1358,7 +1495,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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1376,7 +1513,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;
|
||||
}
|
||||
@@ -1385,7 +1522,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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1403,7 +1540,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;
|
||||
};
|
||||
|
||||
@@ -1416,14 +1553,14 @@ 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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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);
|
||||
@@ -2147,7 +2284,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)
|
||||
@@ -2178,7 +2314,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;
|
||||
};
|
||||
|
||||
@@ -2236,7 +2372,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;
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
_ = 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);
|
||||
@@ -2655,7 +2791,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 });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
_ = 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);
|
||||
@@ -2681,7 +2817,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 });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2698,11 +2834,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 });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2742,7 +2878,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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2789,7 +2925,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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2816,7 +2952,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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2867,7 +3003,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;
|
||||
};
|
||||
}
|
||||
@@ -3015,6 +3161,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", .{
|
||||
@@ -3049,12 +3196,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;
|
||||
}
|
||||
|
||||
@@ -3091,6 +3238,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);
|
||||
@@ -3258,6 +3406,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,13 +105,12 @@ pub fn deinit(self: *Session) void {
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
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;
|
||||
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.
|
||||
try self.navigation.onNewPage(page);
|
||||
@@ -150,13 +149,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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' 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,
|
||||
|
||||
@@ -1326,6 +1326,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);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -679,7 +679,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, .{});
|
||||
|
||||
@@ -342,6 +342,7 @@ pub fn click(self: *HtmlElement, page: *Page) !void {
|
||||
try page._event_manager.dispatch(self.asEventTarget(), event);
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
// TODO: Per spec, hidden is a tristate: true | false | "until-found".
|
||||
// We only support boolean for now; "until-found" would need bridge union support.
|
||||
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);
|
||||
}
|
||||
|
||||
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 "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -763,7 +763,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",
|
||||
|
||||
@@ -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