From 1fc5a24e9887d5cd83303c24f32c9fb3d01f2338 Mon Sep 17 00:00:00 2001 From: Andrey Mnatsakanov Date: Sun, 26 Jul 2026 12:32:27 +0200 Subject: [PATCH] fix(js): keep the current URL when pushState/replaceState omit url resolveOrFallback(undefined) returned undefined, so applyVirtual set __virtualUrl to null and location reverted to the document URL. Per the HTML spec a missing url keeps the current document URL; return __currentUrl() so the new history entry captures the current location instead of clearing it. Fixes #496 --- crates/obscura-js/js/bootstrap.js | 5 ++++- crates/obscura-js/src/runtime.rs | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 3ef371a..e3073d8 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -5613,7 +5613,10 @@ globalThis.atob = globalThis.atob || ((s) => { const c="ABCDEFGHIJKLMNOPQRSTUVWX const stack = [{state: null, url: undefined}]; // initial entry; url=undefined means "use document URL" let idx = 0; const resolveOrFallback = (url) => { - if (url === null || url === undefined) return undefined; + // A missing url (pushState/replaceState called with < 3 args) keeps the + // current document URL per the HTML spec — capture it so the entry does not + // reset location back to the original document URL. + if (url === null || url === undefined) return __currentUrl(); try { return new URL(String(url), __currentUrl()).href; } catch (e) { return String(url); } }; const applyVirtual = () => { diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index fdfeb4d..5b4d27e 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -1317,6 +1317,28 @@ mod tests { rt } + #[test] + fn replace_state_without_url_preserves_current_location() { + let mut rt = setup_runtime(""); + let path = rt + .evaluate( + "(function(){history.pushState({}, '', '/dashboard'); history.replaceState({scroll:1}); return location.pathname;})()", + ) + .unwrap(); + assert_eq!(path, serde_json::json!("/dashboard")); + } + + #[test] + fn push_state_without_url_preserves_current_location() { + let mut rt = setup_runtime(""); + let path = rt + .evaluate( + "(function(){history.pushState({}, '', '/a'); history.pushState({b:1}); return location.pathname;})()", + ) + .unwrap(); + assert_eq!(path, serde_json::json!("/a")); + } + #[test] fn test_document_title() { let mut rt = setup_runtime("Test");