From 574264aaff2a422725e6e8830172bb7ec52034e6 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Fri, 3 Apr 2026 16:18:49 -0700 Subject: [PATCH 1/3] fix: update page URL and location on pushState/replaceState history.pushState() and replaceState() updated the navigation entry but did not update page.url or reinitialize window.location. This caused location.pathname to return the old value after pushState, breaking SPA routing detection in automation scripts. Both methods now set page.url and re-init the Location object after updating the navigation history. Fixes #2081 Ref #2043 --- src/browser/webapi/History.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index 12336f2c..a5016795 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -20,6 +20,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Location = @import("Location.zig"); const PopStateEvent = @import("event/PopStateEvent.zig"); const History = @This(); @@ -55,6 +56,10 @@ pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8 const json = state.toJson(arena) catch return error.DataClone; _ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true); + + // Update page URL and location so that location.pathname reflects the pushed state + page.url = url; + page.window._location = try Location.init(url, page); } pub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { @@ -63,6 +68,10 @@ pub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const const json = state.toJson(arena) catch return error.DataClone; _ = try page._session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true); + + // Update page URL and location so that location.pathname reflects the replaced state + page.url = url; + page.window._location = try Location.init(url, page); } fn goInner(delta: i32, page: *Page) !void { From 08cd9ca7990a0d8f6ff685975bcc8d4dac4ad3f4 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Fri, 10 Apr 2026 07:26:01 -0700 Subject: [PATCH 2/3] properly resolve URL before setting Location in History --- src/browser/webapi/History.zig | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index a5016795..727c96b2 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -22,6 +22,7 @@ const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const Location = @import("Location.zig"); const PopStateEvent = @import("event/PopStateEvent.zig"); +const URL = @import("URL.zig"); const History = @This(); @@ -52,24 +53,28 @@ pub fn setScrollRestoration(self: *History, str: []const u8) void { pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { const arena = page._session.arena; - const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url); + const url = if (_url) |u| + try @import("../URL.zig").resolve(arena, page.url, u, .{ .always_dupe = true }) + else + try arena.dupeZ(u8, page.url); const json = state.toJson(arena) catch return error.DataClone; _ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true); - // Update page URL and location so that location.pathname reflects the pushed state page.url = url; - page.window._location = try Location.init(url, page); + page.window._location._url = try URL.init(url, null, page); } pub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { const arena = page._session.arena; - const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url); + const url = if (_url) |u| + try @import("../URL.zig").resolve(arena, page.url, u, .{ .always_dupe = true }) + else + try arena.dupeZ(u8, page.url); const json = state.toJson(arena) catch return error.DataClone; _ = try page._session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true); - // Update page URL and location so that location.pathname reflects the replaced state page.url = url; page.window._location = try Location.init(url, page); } From d47e24ced03d9f387787d4a06d6822a89d86d8a9 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Fri, 10 Apr 2026 07:38:21 -0700 Subject: [PATCH 3/3] add test for History URL updating --- src/browser/tests/history_url_update.html | 19 +++++++++++++++++++ src/browser/webapi/History.zig | 1 + 2 files changed, 20 insertions(+) create mode 100644 src/browser/tests/history_url_update.html diff --git a/src/browser/tests/history_url_update.html b/src/browser/tests/history_url_update.html new file mode 100644 index 00000000..8aa6d8ca --- /dev/null +++ b/src/browser/tests/history_url_update.html @@ -0,0 +1,19 @@ + + + + diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index 727c96b2..ccbb4f43 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -138,4 +138,5 @@ pub const JsApi = struct { const testing = @import("../../testing.zig"); test "WebApi: History" { try testing.htmlRunner("history.html", .{}); + try testing.htmlRunner("history_url_update.html", .{}); }