diff --git a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs index a56cae227a..e78ff9c29c 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs @@ -24,6 +24,7 @@ use super::navigation_result::{ }; use super::navigation_serialize::sync_child_navigation_entry_seed_from_owner; use super::navigation_window::{ + child_browsing_context_handle_for_runtime_owner, runtime_window_is_global, window_location_for_holder, window_navigation_for_holder, }; use super::*; @@ -107,6 +108,7 @@ fn mutate_history_object<'s>( let Some(owner) = require_fully_active_history_owner(scope, history) else { return; }; + let kind = effective_history_mutation_kind(scope, owner, kind); let Some(state) = structured_clone_value_for_storage(scope, parsed.state) else { return; }; @@ -116,9 +118,9 @@ fn mutate_history_object<'s>( }; let current_href = location_href_slot(scope, location).unwrap_or_else(|| "about:blank".to_owned()); - let current_url = match history_same_origin_reference_url(scope, owner, ¤t_href) { - Ok(url) => url, - Err(_) => { + let current_url = match history_document_url(scope, owner, ¤t_href) { + Some(url) => url, + None => { throw_history_security_error( scope, "Failed to execute 'pushState' or 'replaceState' on 'History': The current URL is invalid.", @@ -126,13 +128,9 @@ fn mutate_history_object<'s>( return; } }; - let resolve_base_href = if history_url_inherits_origin(scope, owner, ¤t_href) { - current_url.as_str() - } else { - ¤t_href - }; + let api_base_url = history_api_base_url(scope, owner, ¤t_url); let url = match parsed.url { - Some(target) if !target.is_empty() => resolve_history_state_url(resolve_base_href, &target), + Some(target) if !target.is_empty() => resolve_history_state_url(&api_base_url, &target), _ => Some(current_url.clone()), }; let Some(url) = url else { @@ -142,7 +140,7 @@ fn mutate_history_object<'s>( ); return; }; - if !moli_url::same_origin(&url, ¤t_url) { + if !document_can_have_url_rewritten(¤t_url, &url) { throw_history_security_error( scope, "Failed to execute 'pushState' or 'replaceState' on 'History': A history state object with URL of a different origin cannot be created in a document with origin.", @@ -305,18 +303,16 @@ fn mutate_history_object<'s>( } else if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) - && let Some(document_handle) = - unsafe { &*host_ptr }.lightweight_popup_document_handle(popup_id) { - let _ = unsafe { &mut *host_ptr }.set_dom_document_url_for_handle(document_handle, url); + let _ = unsafe { &mut *host_ptr }.set_lightweight_popup_same_document_url(popup_id, url); } } -fn resolve_history_state_url(base_href: &str, target: &str) -> Option { +fn resolve_history_state_url(base_url: &url::Url, target: &str) -> Option { if let Ok(absolute) = url::Url::parse(target) { return Some(absolute); } - url::Url::parse(base_href).ok()?.join(target).ok() + base_url.join(target).ok() } fn parse_history_mutation_args<'s>( @@ -344,28 +340,155 @@ fn parse_history_mutation_args<'s>( } } -fn history_same_origin_reference_url<'s>( +fn history_api_base_url<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, - current_href: &str, -) -> Result { - if history_url_inherits_origin(scope, owner, current_href) - && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) - { - return Ok(unsafe { &mut *host_ptr }.host_document().url().clone()); + current_url: &url::Url, +) -> url::Url { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return current_url.clone(); + }; + let host = unsafe { &*host_ptr }; + if runtime_window_is_global(scope, owner) { + return host + .dom_host() + .document_base_url() + .unwrap_or_else(|| current_url.clone()); } - url::Url::parse(current_href) + if let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) { + return host + .child_browsing_context_base_url(handle) + .unwrap_or_else(|| current_url.clone()); + } + if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) { + return host + .lightweight_popup_request_base_url(scope, popup_id) + .unwrap_or_else(|| current_url.clone()); + } + current_url.clone() } -fn history_url_inherits_origin<'s>( +fn history_document_url<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, current_href: &str, -) -> bool { - !runtime_window_is_global(scope, owner) - && matches!(current_href, "about:blank" | "about:srcdoc") +) -> Option { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return url::Url::parse(current_href).ok(); + }; + let host = unsafe { &*host_ptr }; + if runtime_window_is_global(scope, owner) { + return Some(host.document_url().clone()); + } + if let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) { + return host.child_browsing_context_current_url(handle); + } + if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) { + if host.lightweight_popup_current_document_is_initial_empty(popup_id) { + return url::Url::parse("about:blank").ok(); + } + return host.lightweight_popup_document_url(popup_id); + } + url::Url::parse(current_href).ok() +} + +fn effective_history_mutation_kind<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + kind: HistoryMutationKind, +) -> HistoryMutationKind { + if !matches!(kind, HistoryMutationKind::Push) { + return kind; + } + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return kind; + }; + let host = unsafe { &*host_ptr }; + let is_initial_empty = child_browsing_context_handle_for_runtime_owner(scope, owner) + .is_some_and(|handle| host.child_browsing_context_is_on_initial_about_blank_entry(handle)) + || crate::native_bridge::lightweight_popup_id_from_window(scope, owner).is_some_and( + |popup_id| host.lightweight_popup_current_document_is_initial_empty(popup_id), + ); + if is_initial_empty { + HistoryMutationKind::Replace + } else { + kind + } +} + +fn document_can_have_url_rewritten(document_url: &url::Url, target_url: &url::Url) -> bool { + if document_url.scheme() != target_url.scheme() + || document_url.username() != target_url.username() + || document_url.password() != target_url.password() + || document_url.host() != target_url.host() + || document_url.port() != target_url.port() + { + return false; + } + if matches!(target_url.scheme(), "http" | "https") { + return true; + } + if target_url.scheme() == "file" { + return document_url.path() == target_url.path(); + } + document_url.path() == target_url.path() && document_url.query() == target_url.query() } fn throw_history_security_error(scope: &mut v8::PinScope<'_, '_>, message: &str) { crate::context_bootstrap::throw_dom_exception_value(scope, message, "SecurityError"); } + +#[cfg(test)] +mod tests { + use super::document_can_have_url_rewritten; + use url::Url; + + fn url(value: &str) -> Url { + Url::parse(value).expect("test URL should parse") + } + + #[test] + fn history_url_rewrite_rules_match_url_component_boundaries() { + let cases = [ + ( + "https://example.test/home", + "https://example.test/shop", + true, + ), + ( + "https://example.test/home", + "https://user:pass@example.test/home", + false, + ), + ("file:///path/to/x", "file:///path/to/x?search", true), + ("file:///path/to/x", "file:///path/to/y", false), + ("about:blank", "about:blank#hash", true), + ("about:blank", "about:blank?search", false), + ("about:blank", "about:srcdoc", false), + ("data:text/html,body", "data:text/html,body#hash", true), + ("data:text/html,body", "data:text/html,body?search", false), + ( + "blob:https://example.test/00000000-0000-0000-0000-000000000001", + "blob:https://example.test/00000000-0000-0000-0000-000000000001#hash", + true, + ), + ( + "blob:https://example.test/00000000-0000-0000-0000-000000000001", + "blob:https://example.test/00000000-0000-0000-0000-000000000001?search", + false, + ), + ( + "blob:https://example.test/00000000-0000-0000-0000-000000000001", + "blob:https://example.test/00000000-0000-0000-0000-000000000002", + false, + ), + ]; + for (document, target, expected) in cases { + assert_eq!( + document_can_have_url_rewritten(&url(document), &url(target)), + expected, + "rewrite result for {document} -> {target}" + ); + } + } +} diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs index 592d934bca..842c918748 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs @@ -217,7 +217,8 @@ fn entry_origin_url<'s>( } fn child_navigation_entry_url_inherits_origin(url: &url::Url) -> bool { - url.scheme() == "about" && matches!(url.as_str(), "about:blank" | "about:srcdoc") + url.scheme() == "about" + && (url.path().eq_ignore_ascii_case("blank") || url.path().eq_ignore_ascii_case("srcdoc")) } pub(super) fn set_history_length_from_visible_entries<'s>( @@ -278,3 +279,26 @@ fn history_length_floor_from_visible_entries<'s>( .max(0.0); visible_length.max(top_length) } + +#[cfg(test)] +mod tests { + use super::child_navigation_entry_url_inherits_origin; + use url::Url; + + #[test] + fn about_document_history_urls_inherit_origin_with_fragments() { + for raw_url in [ + "about:blank", + "about:blank#history", + "about:srcdoc", + "about:srcdoc#history", + ] { + assert!(child_navigation_entry_url_inherits_origin( + &Url::parse(raw_url).expect("about URL should parse") + )); + } + assert!(!child_navigation_entry_url_inherits_origin( + &Url::parse("about:other#history").expect("about URL should parse") + )); + } +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs index aec6b8c553..b68126a3d2 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs @@ -728,8 +728,9 @@ impl ChildBrowsingContextEntry { self.navigation_entry_seed = entry_seed.clone(); if same_document_update { self.committed_navigation_entry_seed = entry_seed; - self.pending_attribute_bootstrap_commit = false; - self.pending_live_navigation_reflects_window_state = false; + if !self.pending_attribute_bootstrap_commit { + self.pending_live_navigation_reflects_window_state = false; + } } same_document_update } diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index bef01d32e9..58ffc78224 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -386,6 +386,7 @@ pub(super) struct LightweightPopupDocumentState { struct LightweightPopupDocumentRecord { owner: LightweightPopupDocumentOwner, local_window_id: LightweightPopupLocalWindowId, + is_initial_empty_document: bool, url: Url, access_origin: super::window_security_tokens::WindowAccessOrigin, state: LightweightPopupDocumentState, @@ -685,15 +686,25 @@ impl JsContextHost { true } - fn set_lightweight_popup_same_document_url(&mut self, popup_id: u64, url: Url) -> bool { - let Some(record) = self.lightweight_popup_record_mut(popup_id) else { - return false; + pub(crate) fn set_lightweight_popup_same_document_url( + &mut self, + popup_id: u64, + url: Url, + ) -> bool { + let document_handle = { + let Some(record) = self.lightweight_popup_record_mut(popup_id) else { + return false; + }; + let LightweightPopupLifecycle::Open(open) = &mut record.lifecycle else { + return false; + }; + record.location_url = url.clone(); + open.document.url = url.clone(); + open.document.handle }; - let LightweightPopupLifecycle::Open(open) = &mut record.lifecycle else { - return false; - }; - record.location_url = url.clone(); - open.document.url = url; + if let Some(document_handle) = document_handle { + let _ = self.set_dom_document_url_for_handle(document_handle, url); + } true } @@ -933,6 +944,7 @@ impl JsContextHost { document: LightweightPopupDocumentRecord { owner: initial_document_owner, local_window_id: initial_local_window_id, + is_initial_empty_document: true, url: initial_url.clone(), access_origin: initial_origin, state: initial_document_state.clone(), @@ -1081,7 +1093,6 @@ impl JsContextHost { window, &target_url, crate::context_bootstrap::LocationNavigationKind::Assign, - Some(&previous_url), ); let queue_synthetic_load = if moli_url::is_about_blank(&target_url) { let storage_scope = self.lightweight_popup_storage_scope_for_initiated_navigation( @@ -1228,6 +1239,14 @@ impl JsContextHost { .map(|document| document.url.clone()) } + pub(crate) fn lightweight_popup_current_document_is_initial_empty( + &self, + popup_id: u64, + ) -> bool { + self.lightweight_popup_document_record(popup_id) + .is_some_and(|document| document.is_initial_empty_document) + } + pub(crate) fn lightweight_popup_session_storage_store( &self, popup_id: u64, @@ -1495,9 +1514,7 @@ impl JsContextHost { window: v8::Local<'s, v8::Object>, target_url: &Url, kind: crate::context_bootstrap::LocationNavigationKind, - current_url: Option<&Url>, ) { - let effective_kind = lightweight_popup_effective_navigation_kind(current_url, kind); let base_url = self .lightweight_popup_base_url(scope, popup_id) .unwrap_or_else(|| target_url.clone()); @@ -1512,7 +1529,7 @@ impl JsContextHost { &base_url, &document_referrer, ); - apply_local_window_location_navigation(scope, window, target_url, effective_kind); + apply_local_window_location_navigation(scope, window, target_url, kind); sync_window_location_history_navigation_runtime_surface(scope, window); } @@ -1566,6 +1583,10 @@ impl JsContextHost { let Some(window) = self.lightweight_popup_window(scope, popup_id) else { return false; }; + let kind = lightweight_popup_effective_navigation_kind( + self.lightweight_popup_current_document_is_initial_empty(popup_id), + kind, + ); let current_url = lightweight_popup_location_href(scope, window) .or_else(|| self.lightweight_popup_location_url(popup_id)); if !matches!( @@ -1696,7 +1717,6 @@ impl JsContextHost { window, &target_url, kind, - current_url.as_ref(), ); self.install_lightweight_popup_empty_document(scope, popup_id, window, target_url); self.queue_lightweight_popup_load_event(navigation_task); @@ -1709,7 +1729,6 @@ impl JsContextHost { window, &target_url, kind, - current_url.as_ref(), ); if self .start_lightweight_popup_document_load( @@ -1951,6 +1970,7 @@ impl JsContextHost { LightweightPopupDocumentRecord { owner: commit.owner, local_window_id: current_local_window_id, + is_initial_empty_document: false, url: commit.location_url.clone(), access_origin, state: commit.state, @@ -4926,13 +4946,13 @@ fn set_lightweight_popup_persisted_script_global<'s>( } fn lightweight_popup_effective_navigation_kind( - current_url: Option<&Url>, + current_document_is_initial_empty: bool, kind: crate::context_bootstrap::LocationNavigationKind, ) -> crate::context_bootstrap::LocationNavigationKind { if matches!( kind, crate::context_bootstrap::LocationNavigationKind::Assign - ) && current_url.is_some_and(moli_url::is_about_blank) + ) && current_document_is_initial_empty { return crate::context_bootstrap::LocationNavigationKind::Replace; } diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/hash_change_delivery.rs b/moli-renderer-v8/src/runtime/page_vm/tests/hash_change_delivery.rs index a4d60059d5..6d0e2cfeb5 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/hash_change_delivery.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/hash_change_delivery.rs @@ -398,10 +398,17 @@ async fn hashchange_discards_a_retired_child_local_window() { r#" const frame = document.createElement("iframe"); frame.id = "stale-hashchange-recipient"; +frame.srcdoc = "

child

"; document.body.appendChild(frame); "created" "#, )?; + run_expected_child_frame_task_source_after_realm_prerequisite_for_wait( + &mut page_vm, + ChildFrameSemanticTurnKind::NavigationCommit, + "stale hashchange child srcdoc commit", + ) + .await; materialize_child_realm_through_page_turn_for_test( &mut page_vm, "stale-hashchange-recipient", @@ -411,7 +418,7 @@ document.body.appendChild(frame); globalThis.__staleHashChanges = 0; const staleFrame = document.getElementById("stale-hashchange-recipient"); staleFrame.contentWindow.addEventListener("hashchange", () => parent.__staleHashChanges++); -staleFrame.contentWindow.history.replaceState(null, "", "/child-hashchange"); +staleFrame.contentWindow.history.replaceState(null, "", "about:srcdoc#child-hashchange"); staleFrame.contentWindow.location.hash = "#queued"; staleFrame.remove(); "retired" @@ -447,7 +454,7 @@ async fn hashchange_discards_a_retired_lightweight_popup_local_window() { r##" globalThis.__popupHashChanges = []; globalThis.__hashPopup = open("about:blank", "hashchange-owner-popup"); -__hashPopup.history.replaceState(null, "", "/popup-hashchange"); +__hashPopup.history.replaceState(null, "", "about:blank#popup-hashchange"); __hashPopup.addEventListener("hashchange", () => __popupHashChanges.push("retired")); __hashPopup.location.hash = "#queued-before-replacement"; open("about:blank", "hashchange-owner-popup"); @@ -479,7 +486,7 @@ open("about:blank", "hashchange-owner-popup"); page_vm.vm_mut().eval( r##" -__hashPopup.history.replaceState(null, "", "/replacement-popup-hashchange"); +__hashPopup.history.replaceState(null, "", "about:blank#replacement-popup-hashchange"); __hashPopup.addEventListener("hashchange", () => __popupHashChanges.push("current")); __hashPopup.location.hash = "#queued-after-replacement"; "queued-current" diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs b/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs index f3f4653968..2cf40d4dd8 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs @@ -313,10 +313,17 @@ async fn history_traversal_discards_a_retired_child_local_window() { r##" const frame = document.createElement("iframe"); frame.id = "history-stale-child"; +frame.srcdoc = "

child

"; document.body.appendChild(frame); "created" "##, )?; + run_expected_child_frame_task_source_after_realm_prerequisite_for_wait( + &mut page_vm, + ChildFrameSemanticTurnKind::NavigationCommit, + "history stale-child srcdoc commit", + ) + .await; materialize_child_realm_through_page_turn_for_test(&mut page_vm, "history-stale-child")?; page_vm.vm_mut().eval( r##" @@ -325,7 +332,7 @@ const child = document.getElementById("history-stale-child").contentWindow; child.addEventListener("popstate", () => { parent.__retiredChildHistoryEvents += 1; }); -child.history.pushState(null, "", "#queued"); +child.history.pushState(null, "", "about:srcdoc#queued"); child.history.back(); document.getElementById("history-stale-child").remove(); "retired" diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index cb50d538d1..3473a49a63 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -26279,6 +26279,39 @@ JSON.stringify({ ); } +#[test] +fn window_open_named_reuse_before_first_commit_pushes_history() { + let mut vm = new_storage_test_vm("https://example.com/base/page.html"); + + assert_eq!( + vm.eval( + r#" +(() => { + const firstUrl = URL.createObjectURL( + new Blob(['

first

'], { type: 'text/html' }) + ); + const secondUrl = URL.createObjectURL( + new Blob(['

second

'], { type: 'text/html' }) + ); + const popup = open(firstUrl, 'pendingNamedPopup'); + const reopened = open(secondUrl, 'pendingNamedPopup'); + const result = [ + popup === reopened, + reopened.location.href === secondUrl, + reopened.history.length, + ].join('|'); + popup.close(); + URL.revokeObjectURL(firstUrl); + URL.revokeObjectURL(secondUrl); + return result; +})() +"#, + ) + .expect("pending named popup reuse should evaluate"), + "true|true|2" + ); +} + #[tokio::test] async fn lightweight_popup_cross_document_navigation_clears_old_onload_handler() { let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); @@ -27580,6 +27613,32 @@ async fn window_open_204_popup_ignores_navigation_and_preserves_initial_empty_hi ); } +#[test] +fn window_open_initial_empty_history_push_is_a_replacement() { + let mut vm = new_storage_test_vm("https://initial-popup-history.test/page.html"); + + assert_eq!( + vm.eval( + r#" +(() => { + const popup = open(); + popup.history.pushState({ step: 1 }, '', 'about:blank#pushed'); + popup.history.replaceState({ step: 2 }, '', 'about:blank#replaced'); + const result = [ + popup.location.href, + popup.history.state.step, + popup.history.length, + ].join('|'); + popup.close(); + return result; +})() +"#, + ) + .expect("initial empty popup history mutation should evaluate"), + "about:blank#replaced|2|1" + ); +} + #[tokio::test] async fn window_open_without_url_replaces_initial_empty_history_on_first_navigation() { let (origin, server) = spawn_lightweight_popup_relative_navigation_server().await; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs index 98304d0027..1070d953b3 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs @@ -4192,7 +4192,7 @@ async fn reset_navigation_history_updates_all_live_window_realms() { .clone(); vm.eval_in_child_default_context( child_context_id, - r##"history.pushState({ realm: "child-default" }, "", "#child-default")"##, + r##"history.pushState({ realm: "child-default" }, "", "about:srcdoc#child-default")"##, ) .expect("child default history setup should evaluate"); @@ -4209,7 +4209,7 @@ async fn reset_navigation_history_updates_all_live_window_realms() { .expect("top isolated history setup should evaluate"); vm.eval_in_isolated_context( child_isolated_context_id, - r##"history.pushState({ realm: "child-isolated" }, "", "#child-isolated")"##, + r##"history.pushState({ realm: "child-isolated" }, "", "about:srcdoc#child-isolated")"##, ) .expect("child isolated history setup should evaluate"); @@ -4347,7 +4347,11 @@ async fn reset_navigation_history_preserves_child_entry_created_by_top_dispose_l child_context_id, r##" (() => { - history.pushState({ realm: "child-before-reset" }, "", "#child-before-reset"); + history.pushState( + { realm: "child-before-reset" }, + "", + "about:srcdoc#child-before-reset" + ); globalThis.__lmChildCurrentBeforeReset = navigation.currentEntry; globalThis.__lmChildEntriesBeforeReset = navigation.entries(); globalThis.__lmChildDisposed = []; @@ -4364,7 +4368,7 @@ navigation.entries()[0].addEventListener("dispose", () => { document.querySelector("iframe").contentWindow.history.pushState( { realm: "child-during-top-dispose" }, "", - "#child-during-top-dispose" + "about:srcdoc#child-during-top-dispose" ); }); "##, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs index 87523954c5..acac8ff631 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs @@ -6709,7 +6709,7 @@ async fn element_matches_delegates_loaded_child_document_elements() { r##" (() => { const doc = document.querySelector("iframe").contentDocument; - doc.defaultView.history.replaceState(null, "", "#target"); + doc.defaultView.history.replaceState(null, "", "about:srcdoc#target"); const code = doc.getElementById("code"); return [ code.ownerDocument === doc, @@ -11000,9 +11000,9 @@ const frame = document.createElement('iframe'); frame.name = 'target'; const root = document.body || document.documentElement || document; root.appendChild(frame); -frame.contentWindow.history.pushState(null, '', '/child.html'); +frame.contentWindow.history.replaceState(null, '', 'about:blank#child'); const link = document.createElement('a'); -link.href = '/child.html#next'; +link.href = 'about:blank#next'; link.target = 'target'; root.appendChild(link); let seen = []; @@ -11020,10 +11020,7 @@ seen.join('|') ) .expect("targeted same-document anchor click should dispatch child navigate"); - assert_eq!( - result, - "true,true,https://targeted-child-hash.test/child.html#next,-1" - ); + assert_eq!(result, "true,true,about:blank#next,-1"); } #[test] fn anchor_click_to_identical_url_dispatches_replace_navigate_event() { @@ -11933,7 +11930,7 @@ globalThis.__firstJointLengthFrame = first; (() => { const child = __firstJointLengthFrame.contentWindow; const before = history.length; - child.history.pushState(null, '', '#first'); + child.history.pushState(null, '', 'about:srcdoc#first'); return [before, history.length, child.history.length].join('|'); })() "#, @@ -11960,7 +11957,7 @@ globalThis.__secondJointLengthFrame = second; (() => { const child = __secondJointLengthFrame.contentWindow; const before = history.length; - child.history.pushState(null, '', '#second'); + child.history.pushState(null, '', 'about:srcdoc#second'); return [before, history.length, child.history.length].join('|'); })() "#, @@ -11970,6 +11967,65 @@ globalThis.__secondJointLengthFrame = second; ); } +#[test] +fn initial_empty_child_history_can_rewrite_about_blank_fragment() { + let mut vm = new_storage_test_vm("https://initial-empty-history.test/page.html"); + + assert_eq!( + vm.eval( + r#" +(() => { + const frame = document.createElement('iframe'); + (document.body || document.documentElement || document).appendChild(frame); + const child = frame.contentWindow; + const before = history.length; + child.history.pushState({ step: 1 }, '', 'about:blank#pushed'); + child.history.replaceState({ step: 2 }, '', 'about:blank#replaced'); + return [ + child.location.href, + child.history.state.step, + history.length, + before, + ].join('|'); +})() +"#, + ) + .expect("initial empty child history mutation should evaluate"), + "about:blank#replaced|2|1|1" + ); +} + +#[test] +fn initial_empty_child_history_mutation_preserves_pending_srcdoc_navigation() { + let mut vm = new_storage_test_vm("https://pending-child-history.test/page.html"); + + assert_eq!( + vm.eval( + r#" +(() => { + const frame = document.createElement('iframe'); + frame.srcdoc = '

child

'; + (document.body || document.documentElement || document).appendChild(frame); + frame.contentWindow.history.pushState(null, '', 'about:blank#before-commit'); + globalThis.__pendingHistoryFrame = frame; + return frame.contentWindow.location.href; +})() +"#, + ) + .expect("pending child history mutation should evaluate"), + "about:blank#before-commit" + ); + + vm.drain_pending_child_frame_work_for_test(); + assert_eq!( + vm.eval( + "[__pendingHistoryFrame.contentWindow.location.href, Boolean(__pendingHistoryFrame.contentDocument.getElementById('committed'))].join('|')" + ) + .expect("pending srcdoc navigation should still commit"), + "about:srcdoc|true" + ); +} + #[tokio::test] async fn top_history_back_routes_to_child_joint_history_entry() { let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); @@ -12004,7 +12060,11 @@ async fn top_history_back_routes_to_child_joint_history_entry() { r#" (() => { const frame = document.querySelector('iframe'); - frame.contentWindow.history.pushState({ child: true }, '', '#child'); + frame.contentWindow.history.pushState( + { child: true }, + '', + 'about:srcdoc#child' + ); return [ location.href, history.length, @@ -12018,7 +12078,7 @@ async fn top_history_back_routes_to_child_joint_history_entry() { assert_eq!( setup, - "https://joint-child-back.test/page.html|2|2|https://joint-child-back.test/page.html#child" + "https://joint-child-back.test/page.html|2|2|about:srcdoc#child" ); let _ = vm .run_one_oldest_ready_page_task_executor_turn(&loader) @@ -12084,7 +12144,11 @@ async fn top_history_back_ignores_removed_child_joint_history_entry() { r#" (() => { const frame = document.querySelector('iframe'); - frame.contentWindow.history.pushState({ child: true }, '', '#child'); + frame.contentWindow.history.pushState( + { child: true }, + '', + 'about:srcdoc#child' + ); return [ location.href, navigation.entries().length, @@ -12100,7 +12164,7 @@ async fn top_history_back_ignores_removed_child_joint_history_entry() { assert_eq!( setup, - "https://removed-child-joint-back.test/page.html|1|0|2|2|https://removed-child-joint-back.test/page.html#child" + "https://removed-child-joint-back.test/page.html|1|0|2|2|about:srcdoc#child" ); vm.eval("document.querySelector('iframe').remove(); history.back(); 'queued'") @@ -12208,7 +12272,7 @@ async fn detached_child_navigation_error_exposes_committed_entry_during_dispatch (() => { globalThis.__lmDetachedChildNavigateErrorLog = []; const child = document.querySelector('iframe').contentWindow; - child.history.pushState({ child: true }, "", "#one"); + child.history.pushState({ child: true }, "", "about:srcdoc#one"); return [ child.navigation.entries().length, child.navigation.currentEntry.index, @@ -12218,10 +12282,7 @@ async fn detached_child_navigation_error_exposes_committed_entry_during_dispatch "##, ) .expect("child initial same-document navigation should evaluate"); - assert_eq!( - initial, - "2|1|https://child-detach-navigation-error.test/page.html#one" - ); + assert_eq!(initial, "2|1|about:srcdoc#one"); let setup = vm .eval( diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs index e22d2d37e0..40f96fca54 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs @@ -9823,7 +9823,7 @@ fn child_frame_target_selector_invalidation_uses_child_document_world() { `); childDocument.close(); - childWindow.history.replaceState(null, '', '#old'); + childWindow.history.replaceState(null, '', 'about:blank#old'); globalThis.__childTargetFrame = frame; globalThis.__childTargetOldStyle = childWindow.getComputedStyle(childDocument.getElementById('old')); @@ -9855,7 +9855,7 @@ fn child_frame_target_selector_invalidation_uses_child_document_world() { r#" (() => { const childWindow = globalThis.__childTargetFrame.contentWindow; - childWindow.history.replaceState(null, '', '#new'); + childWindow.history.replaceState(null, '', 'about:blank#new'); const result = [ globalThis.__childTargetOldStyle.color, globalThis.__childTargetNewStyle.color @@ -9923,7 +9923,7 @@ fn popup_target_selector_invalidation_uses_popup_document_world() { newTarget.id = 'popup-new-target'; newTarget.className = 'probe'; popupBody.append(oldTarget, newTarget); - popup.history.replaceState(null, '', '#popup-old-target'); + popup.history.replaceState(null, '', 'about:blank#popup-old-target'); globalThis.__popupTargetOldStyle = popup.getComputedStyle(oldTarget); globalThis.__popupTargetNewStyle = popup.getComputedStyle(newTarget); @@ -9952,7 +9952,7 @@ fn popup_target_selector_invalidation_uses_popup_document_world() { .eval( r#" (() => { - __popupTargetWindow.history.replaceState(null, '', '#popup-new-target'); + __popupTargetWindow.history.replaceState(null, '', 'about:blank#popup-new-target'); const result = [ globalThis.__popupTargetOldStyle.color, globalThis.__popupTargetNewStyle.color diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs index a885977e25..7b1f468247 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs @@ -9614,7 +9614,7 @@ fn dom_parser_uses_its_constructor_realm_associated_document() { const frame = document.createElement('iframe'); (document.body || document.documentElement || document).appendChild(frame); const child = frame.contentWindow; - child.history.replaceState(null, '', '/child-v1.html'); + child.history.replaceState(null, '', 'about:blank#child-v1'); const topParser = new DOMParser(); const childParser = new child.DOMParser(); @@ -9635,7 +9635,7 @@ fn dom_parser_uses_its_constructor_realm_associated_document() { 'text/html' ).URL; const beforeSameDocumentUpdate = parseUrl(childParser); - child.history.replaceState(null, '', '/child-v2.html'); + child.history.replaceState(null, '', 'about:blank#child-v2'); let invalidReceiver; try { @@ -9664,7 +9664,7 @@ fn dom_parser_uses_its_constructor_realm_associated_document() { assert_eq!( result, - r#"{"top":"https://dom-parser-realm.test/top.html","child":"https://dom-parser-realm.test/child-v2.html","childWithTopNewTarget":"https://dom-parser-realm.test/child-v2.html","topWithChildNewTarget":"https://dom-parser-realm.test/top.html","beforeSameDocumentUpdate":"https://dom-parser-realm.test/child-v1.html","invalidReceiver":"TypeError"}"# + r#"{"top":"https://dom-parser-realm.test/top.html","child":"about:blank#child-v2","childWithTopNewTarget":"about:blank#child-v2","topWithChildNewTarget":"https://dom-parser-realm.test/top.html","beforeSameDocumentUpdate":"about:blank#child-v1","invalidReceiver":"TypeError"}"# ); } @@ -9679,7 +9679,7 @@ fn dom_parser_non_object_new_target_prototype_uses_new_target_realm_default() { const frame = document.createElement('iframe'); (document.body || document.documentElement || document).appendChild(frame); const child = frame.contentWindow; - child.history.replaceState(null, '', '/child.html'); + child.history.replaceState(null, '', 'about:blank#child'); const TopBad = new Function(); TopBad.prototype = 7;