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..]);
}