diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 1b7342d6f..7f7588d81 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -894,6 +894,20 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: Allocator, request_url }; const session = target._session; + + // Re-navigating to the exact current URL is only a reload when the URL + // has no fragment. With a fragment it's a fragment navigation per the + // HTML "navigate" steps (url equals the document's URL excluding + // fragments and url's fragment is non-null): no reload, and since the + // fragment didn't change, no hashchange and no new history entry either. + if (!opts.force and + std.mem.eql(u8, target.url, resolved_url) and + std.mem.indexOfScalar(u8, resolved_url, '#') != null) + { + session.releaseArena(arena); + return; + } + // Short-circuit only true fragment-only navigations (same path/query, different // fragment). Identical URLs fall through and trigger a real reload. const is_fragment_navigation = !std.mem.eql(u8, target.url, resolved_url) and URL.eqlDocument(target.url, resolved_url); diff --git a/src/browser/tests/window/location.html b/src/browser/tests/window/location.html index 55e721d8d..17e38a748 100644 --- a/src/browser/tests/window/location.html +++ b/src/browser/tests/window/location.html @@ -68,3 +68,24 @@ location.hash = ""; testing.expectEqual("", location.hash); + + diff --git a/src/browser/webapi/Location.zig b/src/browser/webapi/Location.zig index ceed9f807..85a118365 100644 --- a/src/browser/webapi/Location.zig +++ b/src/browser/webapi/Location.zig @@ -101,21 +101,30 @@ pub fn setSearch(_: *const Location, search: []const u8, frame: *Frame) !void { } pub fn setHash(_: *const Location, hash: []const u8, frame: *Frame) !void { - const normalized_hash = blk: { - if (hash.len == 0) { - const old_url = frame.url; + const old_url = frame.url; + const base_end = std.mem.indexOfScalar(u8, old_url, '#') orelse old_url.len; + // Includes the leading '#'; empty when the URL has no fragment. + const old_fragment = old_url[base_end..]; - break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index| - old_url[0..index] - else - old_url; - } else if (hash[0] == '#') - break :blk hash - else - break :blk try std.fmt.allocPrint(frame.call_arena, "#{s}", .{hash}); + const normalized_hash: []const u8 = blk: { + if (hash.len == 0) { + break :blk ""; + } else if (hash[0] == '#') { + break :blk hash; + } + break :blk try std.fmt.allocPrint(frame.call_arena, "#{s}", .{hash}); }; - return frame.scheduleNavigation(normalized_hash, .{ + // Per the Location hash setter, when the fragment doesn't change no + // navigation happens at all — in particular `location.hash = ""` on a + // fragment-less URL must not turn into a same-URL reload. + if (std.mem.eql(u8, old_fragment, normalized_hash)) { + return; + } + + const target_url = if (normalized_hash.len == 0) old_url[0..base_end] else normalized_hash; + + return frame.scheduleNavigation(target_url, .{ .reason = .script, .kind = .{ .replace = null }, }, .{ .script = frame });