diff --git a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs index 4f6dfd3413..28155e8b05 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs @@ -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, ¤t_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, + ¤t_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>, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/element_navigation.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/element_navigation.rs index 87f1c4952e..23f1ff6453 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/element_navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/element_navigation.rs @@ -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::(&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::(&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!(