diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index eb0e3b8e9..aee5bcfe7 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -283,6 +283,11 @@ _pending_loads: u32, _parent_notified: bool = false, +// whether our parent's load event is waiting for us to load. For a root page, +// this is meaningless. For an iframe, it's false when loading=lazy. We'll still +// load the iframe, but we won't delay the parent +_delays_parent_load: bool = true, + _type: enum { root, frame }, // only used for logs right now _req_id: u32 = 0, _navigated_options: ?NavigatedOpts = null, @@ -1012,7 +1017,7 @@ pub fn scriptsCompletedLoading(self: *Frame) void { self.pendingLoadCompleted(); } -pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame) void { +pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame, delays_load: bool) void { // When parsing HTML, fire any load event for an iframe on the next tick. const parsing_html = switch (self._parse_state) { .html => true, @@ -1022,7 +1027,9 @@ pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame) void { self.queueElementEvent(iframe._proto, .load) catch |err| { log.err(.frame, "iframe queue load", .{ .err = err, .url = iframe._src }); }; - self.pendingLoadCompleted(); + if (delays_load) { + self.pendingLoadCompleted(); + } return; } @@ -1040,7 +1047,9 @@ pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame) void { }; } - self.pendingLoadCompleted(); + if (delays_load) { + self.pendingLoadCompleted(); + } } fn pendingLoadCompleted(self: *Frame) void { @@ -1125,7 +1134,7 @@ fn notifyParentLoadComplete(self: *Frame) void { } self._parent_notified = true; - parent.iframeCompletedLoading(self.iframe.?); + parent.iframeCompletedLoading(self.iframe.?, self._delays_parent_load); } fn frameHeaderDoneCallback(response: HttpClient.Response) !HttpClient.HeaderResult { @@ -1677,7 +1686,11 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void { try Frame.init(new_frame, frame_id, self._page, .{ .parent = self }); errdefer new_frame.deinit(); - self._pending_loads += 1; + const delays_load = iframe.isLazyLoading() == false; + new_frame._delays_parent_load = delays_load; + if (delays_load) { + self._pending_loads += 1; + } new_frame.iframe = iframe; iframe._window = new_frame.window; errdefer iframe._window = null; @@ -1728,7 +1741,9 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void { _ = self.child_frames.swapRemove(idx); } log.warn(.frame, "iframe navigate failure", .{ .url = url, .err = err }); - self._pending_loads -= 1; + if (delays_load) { + self._pending_loads -= 1; + } iframe._window = null; return error.IFrameLoadError; }; diff --git a/src/browser/Runner.zig b/src/browser/Runner.zig index 6665430d5..eac23a500 100644 --- a/src/browser/Runner.zig +++ b/src/browser/Runner.zig @@ -492,3 +492,27 @@ test "Runner: networkidle notifies child frames" { try testing.expectEqual(true, child._notified_network_idle == .done); } } + +test "Runner: lazy iframe does not delay the load event" { + const page = try testing.pageTest("runner/iframe_lazy.html", .{ .wait_until_done = false }); + defer page.close(); + + var runner = page.session.runner(.{}); + try runner.waitForFrame(page.frame_id, 2000, .{ .until = .load }); + + const frame = page.frame().?; + try testing.expectEqual(true, frame._load_state == .complete); + try testing.expectEqual(2, frame.child_frames.items.len); + + // The lazy child's delayed response is still in flight: the parent's + // load event fired without waiting for it. + const lazy_child = frame.child_frames.items[0]; + try testing.expectEqual(false, lazy_child._delays_parent_load); + try testing.expectEqual(false, lazy_child._load_state == .complete); + + // It still loads to completion and notifies the parent (which fires the + // iframe element's load event, but doesn't touch _pending_loads). + try runner.waitForFrame(page.frame_id, 3000, .{ .until = .done }); + try testing.expectEqual(true, lazy_child._load_state == .complete); + try testing.expectEqual(true, lazy_child._parent_notified); +} diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 3d6cc242c..65ca3e2ef 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -655,9 +655,14 @@ fn _processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) errdefer iframe._window = null; - const parent_notified = frame._parent_notified; - if (parent_notified) { - // we already notified the parent that we had loaded + // Should this frame's navigation delay the parent's load event? If it was + // already holding it, then that just carries over. If it wasn't delaying + // it (maybe because it completed already), then it should start, UNLESS + // the iframe is lazy loaded. + const had_hold = !frame._parent_notified and frame._delays_parent_load; + const delays_load = had_hold or !iframe.isLazyLoading(); + const new_hold = delays_load and !had_hold; + if (new_hold) { parent._pending_loads += 1; } @@ -682,13 +687,14 @@ fn _processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) try Frame.init(frame, frame_id, page, .{ .parent = parent, .reuse_window = reuse_window }); errdefer { - if (parent_notified) { + if (new_hold) { parent._pending_loads -= 1; } frame.deinit(); } frame.iframe = iframe; + frame._delays_parent_load = delays_load; iframe._window = frame.window; frame.navigate(qn.url, qn.opts) catch |err| { diff --git a/src/browser/tests/runner/iframe_lazy.html b/src/browser/tests/runner/iframe_lazy.html new file mode 100644 index 000000000..373beffb6 --- /dev/null +++ b/src/browser/tests/runner/iframe_lazy.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/browser/webapi/element/html/IFrame.zig b/src/browser/webapi/element/html/IFrame.zig index 69802598f..a5163c8e4 100644 --- a/src/browser/webapi/element/html/IFrame.zig +++ b/src/browser/webapi/element/html/IFrame.zig @@ -16,6 +16,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); + const js = @import("../../../js/js.zig"); const Frame = @import("../../../Frame.zig"); const Window = @import("../../Window.zig"); @@ -47,6 +49,12 @@ pub fn getContentDocument(self: *const IFrame) ?*Document { return window._document; } +// loading=lazy iframes are still but don't delay the page's "load" event +pub fn isLazyLoading(self: *IFrame) bool { + const loading = self.asElement().getAttributeSafe(comptime .wrap("loading")) orelse return false; + return std.ascii.eqlIgnoreCase(loading, "lazy"); +} + pub fn getSrc(self: *IFrame, frame: *Frame) ![]const u8 { if (self._src.len == 0) return ""; return self.asNode().resolveURLReflect(self._src, frame, .{}); diff --git a/src/testing.zig b/src/testing.zig index 14bf707f9..47bab7db7 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -859,6 +859,13 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { } if (std.mem.startsWith(u8, path, "/src/browser/tests/")) { + if (std.mem.indexOf(u8, path, "delay_ms=")) |pos| { + const digits_start = pos + "delay_ms=".len; + var end = digits_start; + while (end < path.len and std.ascii.isDigit(path[end])) : (end += 1) {} + const delay_ms = std.fmt.parseInt(u64, path[digits_start..end], 10) catch 0; + std.Thread.sleep(delay_ms * std.time.ns_per_ms); + } // strip off leading / so that it's relative to CWD return TestHTTPServer.sendFile(req, path[1..]); }