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("