diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 17c49d0e..e082e19f 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -614,6 +614,8 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo .cdp_id = opts.cdp_id, .reason = opts.reason, .method = opts.method, + .body = if (opts.body) |b| try self.arena.dupe(u8, b) else null, + .header = if (opts.header) |h| try self.arena.dupeZ(u8, h) else null, }; var headers = try http_client.newHeaders(); @@ -3443,6 +3445,10 @@ pub const NavigatedOpts = struct { cdp_id: ?i64 = null, reason: NavigateReason = .address_bar, method: HttpClient.Method = .GET, + // Retained on the frame's arena so Page.reload can replay the prior + // navigation's HTTP method — matches Chrome's F5 behavior on POST pages. + body: ?[]const u8 = null, + header: ?[:0]const u8 = null, }; const NavigationType = enum { diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index b0aae79e..85751f35 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -333,8 +333,20 @@ fn doReload(cmd: *CDP.Command) !void { const session = bc.session; var frame = session.currentFrame() orelse return error.FrameNotLoaded; - // Dupe URL before replacePage() frees the old frame's arena. + // Capture URL plus the prior navigation's method/body/header before + // replacePage() frees the old frame's arena. Replaying the same HTTP + // method on reload matches Chrome's F5 behavior — POST navigations + // re-submit, GET navigations re-fetch. const reload_url = try cmd.arena.dupeZ(u8, frame.url); + const prev_nav = frame._navigated_options; + const prev_body: ?[]const u8 = if (prev_nav) |p| + if (p.body) |b| try cmd.arena.dupe(u8, b) else null + else + null; + const prev_header: ?[:0]const u8 = if (prev_nav) |p| + if (p.header) |h| try cmd.arena.dupeZ(u8, h) else null + else + null; if (frame._load_state != .waiting) { // Reset isolated world identities to disable V8 weak callbacks before @@ -351,6 +363,9 @@ fn doReload(cmd: *CDP.Command) !void { .cdp_id = cmd.input.id, .kind = .reload, .force = if (params) |p| p.ignoreCache orelse false else false, + .method = if (prev_nav) |p| p.method else .GET, + .body = prev_body, + .header = prev_header, }); } @@ -1003,6 +1018,61 @@ test "cdp.frame: reload" { } } +test "cdp.frame: reload replays POST navigation" { + var ctx = try testing.context(); + defer ctx.deinit(); + + // Manually wire up the browser context: loadBrowserContext only does GET + // navigations, but we need the first navigation to be POST. + const cdp_inst = ctx.cdp(); + _ = try cdp_inst.createBrowserContext(); + var bc = &cdp_inst.browser_context.?; + bc.id = "BID-A6"; + bc.session_id = "SID-X"; + bc.target_id = "TID-A6-0000000".*; + + // First navigation: POST a form-style payload to /echo_method. + { + const f = try bc.session.createPage(); + try f.navigate("http://127.0.0.1:9582/echo_method", .{ + .method = .POST, + .body = "key=value", + .header = "Content-Type: application/x-www-form-urlencoded", + }); + var runner = try bc.session.runner(.{}); + try runner.wait(.{ .ms = 2000 }); + } + + // Sanity: the body confirms a POST round-tripped. + { + const f = bc.session.currentFrame() orelse unreachable; + var ls: js.Local.Scope = undefined; + f.js.localScope(&ls); + defer ls.deinit(); + const v = try ls.local.exec("document.body.innerText.includes('method=POST')", null); + try testing.expect(v.toBool()); + } + + // Trigger a CDP reload. With the fix in place, doReload captures the + // prior POST method/body/header and replays them. Without it (regression + // guard), the second request would silently fall back to GET. + try ctx.processMessage(.{ .id = 50, .method = "Page.reload" }); + + { + var runner = try bc.session.runner(.{}); + try runner.wait(.{ .ms = 2000 }); + } + + { + const f = bc.session.currentFrame() orelse unreachable; + var ls: js.Local.Scope = undefined; + f.js.localScope(&ls); + defer ls.deinit(); + const v = try ls.local.exec("document.body.innerText.includes('method=POST')", null); + try testing.expect(v.toBool()); + } +} + test "cdp.frame: addScriptToEvaluateOnNewDocument" { var ctx = try testing.context(); defer ctx.deinit(); diff --git a/src/testing.zig b/src/testing.zig index e2500f81..eddd11b3 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -636,6 +636,19 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { }); } + if (std.mem.eql(u8, path, "/echo_method")) { + // Echo the request method back as HTML so tests can assert on what + // method the navigation used. Used by the Page.reload-replays-POST test. + const method_name = @tagName(req.head.method); + var html_buf: [128]u8 = undefined; + const html = try std.fmt.bufPrint(&html_buf, "method={s}", .{method_name}); + return req.respond(html, .{ + .extra_headers = &.{ + .{ .name = "Content-Type", .value = "text/html; charset=utf-8" }, + }, + }); + } + if (std.mem.startsWith(u8, path, "/src/browser/tests/")) { // strip off leading / so that it's relative to CWD return TestHTTPServer.sendFile(req, path[1..]);