diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 929cd07c..927d2f05 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -96,6 +96,12 @@ frame: Frame, // to the original page like this. popups: std.ArrayList(*Frame) = .empty, +// Popups that have called window.close() but whose teardown is deferred to +// Page.deinit. We can't deinit synchronously from window.close() because +// that's invoked from JS still running on top of the Frame's V8 context (or +// from a script eval whose parser still holds the Frame). +queued_close: std.ArrayList(*Frame) = .empty, + // Initialize a Page and its root Frame. pub fn init(self: *Page, session: *Session, frame_id: u32) !void { const frame_arena = try session.arena_pool.acquire(.large, "Page.frame_arena"); @@ -115,6 +121,11 @@ pub fn init(self: *Page, session: *Session, frame_id: u32) !void { // Tear down the Page and its root Frame. Equivalent to the old // Session.removePage + Session.resetFrameResources. pub fn deinit(self: *Page, abort_http: bool) void { + for (self.queued_close.items) |popup| { + popup.deinit(abort_http); + } + self.queued_close = .empty; + for (self.popups.items) |popup| { popup.deinit(abort_http); } diff --git a/src/browser/tests/window/open.html b/src/browser/tests/window/open.html index 4bc0b72d..864339ac 100644 --- a/src/browser/tests/window/open.html +++ b/src/browser/tests/window/open.html @@ -107,3 +107,27 @@ testing.expectEqual(null, window.opener); } + + diff --git a/src/browser/tests/window/support/popup_self_close.html b/src/browser/tests/window/support/popup_self_close.html new file mode 100644 index 00000000..6b39da4a --- /dev/null +++ b/src/browser/tests/window/support/popup_self_close.html @@ -0,0 +1,12 @@ + + diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 0945dc7f..570c5a3e 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -568,7 +568,14 @@ pub fn close(self: *Window) void { } } - frame.deinit(true); + // We can't tear the Frame down here — close() is invoked from JS still + // running on top of this Frame's V8 context, often deep inside a script + // eval whose parser is still holding the Frame. Destroying the context + // now leaves dangling pointers in the unwinding script eval (load event + // dispatch, runMacrotasks, etc.). Defer to Page.deinit instead. + page.queued_close.append(page.frame_arena, frame) catch |err| { + log.err(.frame, "queue popup close", .{ .err = err }); + }; } pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, frame: *Frame) !void {