fix(renderer): complete intercepted top-level hyperlink navigation

This commit is contained in:
ldm0
2026-10-08 21:01:53 +08:00
committed by Donough Liu
parent 4a615ac638
commit faefbd8aa4
2 changed files with 140 additions and 45 deletions
@@ -546,51 +546,12 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>(
}
cancel_pending_same_document_navigation_finishes(scope, navigation);
if outcome.intercepted {
let effective_href = outcome
.redirected_url
.as_deref()
.unwrap_or(resolved.as_str())
.to_owned();
let effective_kind = outcome
.redirected_history
.as_deref()
.map(|history| match history {
"replace" => LocationNavigationKind::Replace,
_ => LocationNavigationKind::Assign,
})
.unwrap_or(kind);
let transition_resolver = navigation_current_entry(scope, owner).and_then(|from| {
install_navigation_transition(
scope,
navigation,
from,
outcome.destination,
match effective_kind {
LocationNavigationKind::Assign => "push",
LocationNavigationKind::Replace => "replace",
LocationNavigationKind::Reload => "reload",
},
)
});
sync_location_object(scope, location, &effective_href);
update_navigation_current_entry_for_same_document(
scope,
owner,
&effective_href,
effective_kind,
);
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
&& let Ok(url) = url::Url::parse(&effective_href)
{
let host = unsafe { &mut *host_ptr };
host.set_document_url(url.clone());
host.record_same_document_navigation(&url, "fragment");
}
settle_location_intercepted_same_document_navigation(
commit_intercepted_location_navigation(
scope,
navigation,
outcome,
transition_resolver,
resolved.as_str(),
kind,
&current_href,
);
return;
@@ -847,6 +808,8 @@ fn window_for_child_cross_document_location_navigation<'s>(
unsafe { &mut *host_ptr }.existing_child_browsing_context_window_wrapper(scope, handle)
}
/// Returns whether the caller should queue a cross-document navigation.
/// Intercepted navigations commit and settle here without redispatching the event.
pub(crate) fn dispatch_top_level_navigation_event_with_source_element<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
@@ -889,7 +852,23 @@ pub(crate) fn dispatch_top_level_navigation_event_with_source_element<'s>(
return false;
}
cancel_pending_same_document_navigation_finishes(scope, navigation);
!outcome.intercepted
if outcome.intercepted {
let kind = match navigation_type {
"replace" => LocationNavigationKind::Replace,
"reload" => LocationNavigationKind::Reload,
_ => LocationNavigationKind::Assign,
};
commit_intercepted_location_navigation(
scope,
navigation,
outcome,
href,
kind,
&current_href,
);
return false;
}
true
}
pub(crate) fn dispatch_top_level_form_navigation_event<'s>(
@@ -957,6 +936,66 @@ fn finish_location_navigation_canceled<'s>(
finish_navigation_error_events(scope, navigation, error, href);
}
fn commit_intercepted_location_navigation<'s>(
scope: &mut v8::PinScope<'s, '_>,
navigation: v8::Local<'s, v8::Object>,
outcome: NavigationDispatchOutcome<'s>,
href: &str,
kind: LocationNavigationKind,
current_href: &str,
) {
let context = navigation
.get_creation_context(scope)
.unwrap_or_else(|| scope.get_current_context());
let scope = &mut v8::ContextScope::new(scope, context);
let owner = runtime_window_owner(scope, navigation);
let effective_href = outcome.redirected_url.as_deref().unwrap_or(href).to_owned();
let effective_kind = outcome
.redirected_history
.as_deref()
.map(|history| match history {
"replace" => LocationNavigationKind::Replace,
_ => LocationNavigationKind::Assign,
})
.unwrap_or(kind);
let transition_resolver = navigation_current_entry(scope, owner).and_then(|from| {
install_navigation_transition(
scope,
navigation,
from,
outcome.destination,
match effective_kind {
LocationNavigationKind::Assign => "push",
LocationNavigationKind::Replace => "replace",
LocationNavigationKind::Reload => "reload",
},
)
});
if let Some(location) = window_location_for_holder(scope, owner) {
sync_location_object(scope, location, &effective_href);
}
update_navigation_current_entry_for_same_document(
scope,
owner,
&effective_href,
effective_kind,
);
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
&& let Ok(url) = url::Url::parse(&effective_href)
{
let host = unsafe { &mut *host_ptr };
host.set_document_url(url.clone());
host.record_same_document_navigation(&url, "fragment");
}
settle_location_intercepted_same_document_navigation(
scope,
navigation,
outcome,
transition_resolver,
current_href,
);
}
fn settle_location_intercepted_same_document_navigation<'s>(
scope: &mut v8::PinScope<'s, '_>,
navigation: v8::Local<'s, v8::Object>,
@@ -94,7 +94,8 @@ fn child_navigation_to_top_fires_once_and_honors_cancel_or_intercept() {
.eval(&format!(
r#"
(() => {{
const seen = [];
const seen = globalThis.__lmTargetNavigationLog = [];
const from = navigation.currentEntry;
let source = document;
for (let i = 0; i < {depth}; ++i) {{
const frame = source.createElement('iframe');
@@ -110,6 +111,7 @@ fn child_navigation_to_top_fires_once_and_honors_cancel_or_intercept() {
anchor.href = 'https://anchor-navigation-target.test/destination';
anchor.target = {target:?};
source.body.appendChild(anchor);
const initialHistoryLength = history.length;
navigation.onnavigate = event => {{
seen.push([
'top',
@@ -122,9 +124,23 @@ fn child_navigation_to_top_fires_once_and_honors_cancel_or_intercept() {
]);
event.signal.onabort = () => seen.push('top-abort');
if ({action:?} === 'cancel') event.preventDefault();
if ({action:?} === 'intercept') event.intercept({{ handler() {{}} }});
if ({action:?} === 'intercept') event.intercept({{
handler() {{
seen.push([
'handler',
location.href,
navigation.currentEntry.url,
navigation.transition instanceof NavigationTransition,
navigation.transition.from === from,
navigation.transition.navigationType,
history.length === initialHistoryLength + 1
]);
return Promise.resolve().then(() => seen.push('handler-complete'));
}}
}});
}};
navigation.onnavigateerror = () => seen.push('top-error');
navigation.onnavigatesuccess = () => seen.push('top-success');
const realm = {use_child_realm} ? source.defaultView : window;
if ({api:?} === 'click') realm.HTMLElement.prototype.click.call(anchor);
else realm.open.call(source.defaultView, anchor.href, anchor.target);
@@ -148,12 +164,52 @@ fn child_navigation_to_top_fires_once_and_honors_cancel_or_intercept() {
serde_json::json!("top-abort"),
serde_json::json!("top-error"),
]);
} else if action == "intercept" {
expected.push(serde_json::json!([
"handler",
expected_url,
expected_url,
true,
true,
"push",
true
]));
}
assert_eq!(
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
serde_json::json!(expected),
"{description}"
);
if action == "intercept" {
expected.extend([
serde_json::json!("handler-complete"),
serde_json::json!("top-success"),
]);
}
if action != "allow" {
let settled = vm
.eval(
"JSON.stringify({ log: globalThis.__lmTargetNavigationLog, \
href: location.href, entryUrl: navigation.currentEntry.url, \
transition: navigation.transition })",
)
.expect(&description);
let committed_url = if action == "intercept" {
expected_url
} else {
"https://anchor-navigation-target.test/source"
};
assert_eq!(
serde_json::from_str::<serde_json::Value>(&settled).unwrap(),
serde_json::json!({
"log": expected,
"href": committed_url,
"entryUrl": committed_url,
"transition": null
}),
"{description}"
);
}
let pending = vm.take_pending_location_navigation_with_seed();
if action == "allow" {
assert_eq!(