mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(navigation): keep hash setters within the current document
Clearing Location.hash produced a URL without a target fragment and entered the full-URL navigation path, interrupting load/harness completion and replacing srcdoc documents. Preserve explicit hash-setter intent in the shared Location route and compare encoded fragments before taking any navigation. Keep the main branch's full href/assign no-fragment behavior and retired-facade checks. Cover top-level hash removal, canonical hash reassignment, and srcdoc Document identity, origin and NavigateEvent metadata. Fixes:536fa76aee(PR #1701) Validate this independent patch on maince0781f2ad: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo nextest run --no-fail-fast Summary [ 493.678s] 20436 tests run: 20436 passed (9 slow), 18 skipped Original-commit:9446beaf8c
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use super::location_runtime::{
|
||||
is_same_document_fragment_navigation, location_href_slot, resolve_location_navigation_target,
|
||||
sync_location_object,
|
||||
sync_location_object, urls_equal_ignoring_fragment,
|
||||
};
|
||||
use super::navigation_activation::install_navigation_transition;
|
||||
use super::navigation_callbacks::cancel_active_intercepted_same_document_navigation;
|
||||
@@ -48,6 +48,13 @@ pub(crate) enum LocationNavigationKind {
|
||||
Reload,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct LocationNavigationOptions {
|
||||
dispatch_child_navigate_event_for_all_kinds: bool,
|
||||
force_exact_same_document_navigation: bool,
|
||||
hash_setter: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_cross_window_location_target(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
raw: &str,
|
||||
@@ -84,7 +91,30 @@ pub(crate) fn navigate_location_object<'s>(
|
||||
raw_target: Option<String>,
|
||||
) {
|
||||
navigate_location_object_with_source_element_and_child_navigate_event(
|
||||
scope, location, kind, raw_target, None, false, false,
|
||||
scope,
|
||||
location,
|
||||
kind,
|
||||
raw_target,
|
||||
None,
|
||||
LocationNavigationOptions::default(),
|
||||
);
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn navigate_location_hash<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
location: v8::Local<'s, v8::Object>,
|
||||
target: String,
|
||||
) {
|
||||
navigate_location_object_with_source_element_and_child_navigate_event(
|
||||
scope,
|
||||
location,
|
||||
LocationNavigationKind::Assign,
|
||||
Some(target),
|
||||
None,
|
||||
LocationNavigationOptions {
|
||||
hash_setter: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,8 +161,10 @@ pub(crate) fn navigate_top_level_same_document_from_browser(
|
||||
kind,
|
||||
Some(target),
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
LocationNavigationOptions {
|
||||
force_exact_same_document_navigation: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
true
|
||||
}
|
||||
@@ -186,8 +218,10 @@ pub(crate) fn navigate_top_level_meta_refresh(
|
||||
kind,
|
||||
Some(target.to_string()),
|
||||
None,
|
||||
false,
|
||||
same_document_fragment,
|
||||
LocationNavigationOptions {
|
||||
force_exact_same_document_navigation: same_document_fragment,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
context_host_ptr_from_global_bridge(scope)
|
||||
.is_some_and(|host_ptr| unsafe { &*host_ptr }.has_pending_location_navigation())
|
||||
@@ -208,8 +242,7 @@ pub(crate) fn navigate_location_object_with_source_element<'s>(
|
||||
kind,
|
||||
raw_target,
|
||||
source_element,
|
||||
false,
|
||||
false,
|
||||
LocationNavigationOptions::default(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,7 +253,15 @@ pub(crate) fn navigate_location_object_with_child_navigate_event<'s>(
|
||||
raw_target: Option<String>,
|
||||
) {
|
||||
navigate_location_object_with_source_element_and_child_navigate_event(
|
||||
scope, location, kind, raw_target, None, true, false,
|
||||
scope,
|
||||
location,
|
||||
kind,
|
||||
raw_target,
|
||||
None,
|
||||
LocationNavigationOptions {
|
||||
dispatch_child_navigate_event_for_all_kinds: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -230,9 +271,13 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>(
|
||||
kind: LocationNavigationKind,
|
||||
raw_target: Option<String>,
|
||||
source_element: Option<v8::Local<'s, v8::Object>>,
|
||||
dispatch_child_navigate_event_for_all_kinds: bool,
|
||||
force_exact_same_document_navigation: bool,
|
||||
options: LocationNavigationOptions,
|
||||
) {
|
||||
let LocationNavigationOptions {
|
||||
dispatch_child_navigate_event_for_all_kinds,
|
||||
force_exact_same_document_navigation,
|
||||
hash_setter,
|
||||
} = options;
|
||||
let owner = runtime_window_owner(scope, location);
|
||||
if let Some(context) = location.get_creation_context(scope)
|
||||
&& let Some(host_ptr) = crate::util::context_host_ptr_from_context_slot(context)
|
||||
@@ -300,7 +345,7 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>(
|
||||
if !matches!(kind, LocationNavigationKind::Reload)
|
||||
&& exact_same_href
|
||||
&& source_element.is_none()
|
||||
&& raw_target_is_fragment_only
|
||||
&& (raw_target_is_fragment_only || hash_setter)
|
||||
&& !force_exact_same_document_navigation
|
||||
{
|
||||
return;
|
||||
@@ -337,9 +382,18 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>(
|
||||
return;
|
||||
}
|
||||
|
||||
// A hash setter can remove the fragment entirely. Full URL assignments
|
||||
// without a target fragment still take the cross-document route.
|
||||
let same_document_fragment = if hash_setter {
|
||||
current_url
|
||||
.as_ref()
|
||||
.is_some_and(|current| urls_equal_ignoring_fragment(current, &resolved))
|
||||
} else {
|
||||
is_same_document_fragment_navigation(current_url.as_ref(), &resolved)
|
||||
};
|
||||
if !matches!(kind, LocationNavigationKind::Reload)
|
||||
&& (!exact_same_href || force_exact_same_document_navigation)
|
||||
&& is_same_document_fragment_navigation(current_url.as_ref(), &resolved)
|
||||
&& same_document_fragment
|
||||
{
|
||||
let opaque_origin = navigation_document_has_opaque_origin(scope, owner);
|
||||
if !opaque_origin
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::location_navigation::{
|
||||
LocationNavigationKind, navigate_location_object,
|
||||
LocationNavigationKind, navigate_location_hash, navigate_location_object,
|
||||
navigate_location_object_with_child_navigate_event,
|
||||
};
|
||||
use super::navigation_callbacks::{document_location_getter, document_location_setter};
|
||||
|
||||
@@ -395,7 +395,7 @@ fn location_writable_attribute_setter_callback<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
navigate_location_object(scope, holder, LocationNavigationKind::Assign, Some(target));
|
||||
navigate_location_hash(scope, holder, target);
|
||||
}
|
||||
LocationAttribute::Search => {
|
||||
let query = value.strip_prefix('?').unwrap_or(&value).to_owned();
|
||||
|
||||
@@ -394,6 +394,121 @@ fn cross_document_pending_navigation_slot_is_not_script_writable() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn location_hash_clear_stays_in_the_current_document() {
|
||||
let mut vm = new_storage_test_vm("https://location-hash-clear.test/index.html#start");
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const before = document;
|
||||
const events = [];
|
||||
navigation.onnavigate = event => events.push([
|
||||
event.destination.sameDocument, event.hashChange
|
||||
]);
|
||||
location.hash = '';
|
||||
return JSON.stringify({
|
||||
href: location.href,
|
||||
hash: location.hash,
|
||||
sameDocument: document === before,
|
||||
events
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("clearing a hash should navigate within the existing Document");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
|
||||
serde_json::json!({
|
||||
"href": "https://location-hash-clear.test/index.html",
|
||||
"hash": "",
|
||||
"sameDocument": true,
|
||||
"events": [[true, true]]
|
||||
})
|
||||
);
|
||||
assert!(vm.take_pending_location_navigation_with_seed().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn location_hash_reassignment_compares_the_encoded_fragment() {
|
||||
let mut vm = new_storage_test_vm("https://location-same-hash.test/index.html#te%3Cst");
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
let events = 0;
|
||||
navigation.onnavigate = () => ++events;
|
||||
location.hash = 'te<st';
|
||||
return JSON.stringify([location.href, events]);
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("reassigning the same encoded hash should be a no-op");
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"["https://location-same-hash.test/index.html#te%3Cst",0]"#
|
||||
);
|
||||
assert!(vm.take_pending_location_navigation_with_seed().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clearing_a_srcdoc_hash_preserves_the_child_document_and_origin() {
|
||||
let mut vm = new_storage_test_vm("https://srcdoc-hash-clear.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const root = document.documentElement ||
|
||||
document.appendChild(document.createElement('html'));
|
||||
const frame = document.createElement('iframe');
|
||||
frame.id = 'hash-clear-srcdoc';
|
||||
frame.srcdoc = '<p id="start">child</p>';
|
||||
root.appendChild(frame);
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("srcdoc child should be created");
|
||||
run_child_navigation_commit_and_host_load_for_test(&mut vm, "srcdoc hash clear").await;
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const hashChild = document.getElementById('hash-clear-srcdoc').contentWindow;
|
||||
const initialHref = hashChild.location.href;
|
||||
const before = hashChild.document;
|
||||
const events = [];
|
||||
hashChild.navigation.onnavigate = event => events.push([
|
||||
event.destination.sameDocument, event.hashChange
|
||||
]);
|
||||
hashChild.location.hash = 'start';
|
||||
hashChild.location.hash = '';
|
||||
return JSON.stringify({
|
||||
initialHref,
|
||||
href: hashChild.location.href,
|
||||
sameDocument: hashChild.document === before,
|
||||
sameOrigin: hashChild.origin === window.origin,
|
||||
events
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("clearing a srcdoc hash must keep the original child accessible");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
|
||||
serde_json::json!({
|
||||
"initialHref": "about:srcdoc",
|
||||
"href": "about:srcdoc",
|
||||
"sameDocument": true,
|
||||
"sameOrigin": true,
|
||||
"events": [[true, true], [true, true]]
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
!vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::NavigationCommit)
|
||||
.await,
|
||||
"hash-only changes must not queue a child document replacement"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn location_hash_empty_fragment_serializes_empty_and_clears_target() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
|
||||
Reference in New Issue
Block a user