diff --git a/src/browser/Session.zig b/src/browser/Session.zig index f388b0fb..1b585f96 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -489,7 +489,8 @@ fn processRootQueuedNavigation(self: *Session) !void { const is_synthetic = qn.is_about_blank or std.mem.startsWith(u8, qn.url, "blob:"); if (is_synthetic) { - return self.replaceRootImmediate(current_frame._frame_id, qn); + defer self.arena_pool.release(qn.arena); + return self.replaceRootImmediate(current_frame._frame_id, qn.url, qn.opts); } // The qn arena is consumed here regardless of success — frame.navigate @@ -500,18 +501,18 @@ fn processRootQueuedNavigation(self: *Session) !void { return self.initiateRootNavigation(current_frame._frame_id, qn.url, qn.opts); } -// Legacy immediate-swap path: tear down the active page and create a new one -// in its place before issuing the navigation. Used for synthetic navigations -// (about:blank, blob:) where there is no in-flight HTTP and therefore no -// "pending" window to span. -fn replaceRootImmediate(self: *Session, frame_id: u32, qn: *QueuedNavigation) !void { - defer self.arena_pool.release(qn.arena); - +// Immediate-swap path for synthetic navigations (about:blank, blob:): there is +// no in-flight HTTP and therefore no "pending" window to span — and no +// frameHeaderDoneCallback to commit a pending Page. Tear down the active page +// and create a new one in its place, then navigate it. Reached from both the +// queued-navigation path (processRootQueuedNavigation) and the CDP entry point +// (initiateRootNavigation); each caller owns any arena tied to `url`/`opts`. +fn replaceRootImmediate(self: *Session, frame_id: u32, url: [:0]const u8, opts: Frame.NavigateOpts) !void { self.tearDownActivePage(); const new_frame = try self.installNewActivePage(frame_id); - new_frame.navigate(qn.url, qn.opts) catch |err| { - log.err(.browser, "queued navigation error", .{ .err = err }); + new_frame.navigate(url, opts) catch |err| { + log.err(.browser, "synthetic navigation error", .{ .err = err, .url = url }); return err; }; } @@ -524,6 +525,14 @@ fn replaceRootImmediate(self: *Session, frame_id: u32, qn: *QueuedNavigation) !v pub fn initiateRootNavigation(self: *Session, frame_id: u32, url: [:0]const u8, opts: Frame.NavigateOpts) !void { self.discardPendingPage(); + // Synthetic navigations (about:blank, blob:) have no HTTP round-trip and + // therefore no frameHeaderDoneCallback to commit a pending Page. Swap the + // active Page immediately instead of allocating a pending one that would + // never be promoted, leaving the previous document in place (issue #2363). + if (std.mem.eql(u8, "about:blank", url) or std.mem.startsWith(u8, url, "blob:")) { + return self.replaceRootImmediate(frame_id, url, opts); + } + const page = try self.allocatePage(frame_id); errdefer self.destroyPage(page); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index bb0274e1..f84e496f 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -1309,6 +1309,42 @@ test "cdp.frame: navigate inherits original fragment across redirect" { } } +test "cdp.frame: navigate to about:blank replaces a non-blank document" { + // Regression test for #2363. Page.navigate("about:blank") issued against a + // tab that already holds a real document must replace the active document + // with a fresh about:blank page — not leave the previous page in place. + // A synthetic (no-HTTP) navigation has no response-headers callback to + // commit a pending Page, so it must swap the active Page immediately. + var ctx = try testing.context(); + defer ctx.deinit(); + + var bc = try ctx.loadBrowserContext(.{ .id = "BID-AB", .url = "hi.html", .target_id = "TID-AB-0000000".* }); + + // Precondition: the tab is on a non-blank document. + { + const frame = bc.session.currentFrame() orelse unreachable; + try testing.expect(std.mem.endsWith(u8, frame.url, "/hi.html")); + } + + try ctx.processMessage(.{ .id = 70, .method = "Page.navigate", .params = .{ .url = "about:blank" } }); + { + var runner = try bc.session.runner(.{}); + try runner.wait(.{ .ms = 2000 }); + } + + // The active frame must now point at the replaced about:blank document. + const frame = bc.session.currentFrame() orelse unreachable; + try testing.expectEqualSlices(u8, "about:blank", frame.url); + + // ...and the active page's JS context must agree — the exact symptom in the + // bug report was window.location.href staying on the previous URL. + var ls: js.Local.Scope = undefined; + frame.js.localScope(&ls); + defer ls.deinit(); + const v = try ls.local.exec("window.location.href === 'about:blank'", null); + try testing.expect(v.toBool()); +} + test "cdp.frame: anchor click sends Referer matching the originating page" { // HTML Living Standard "navigate" algorithm + Fetch §4.5 "request's referrer": // when a navigation is initiated by a hyperlink click (or form submit, or