diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs
index d271cf6c5d..2ee1e65580 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs
@@ -172,13 +172,28 @@ fn bind_frame_element<'s>(
handle: crate::document_runtime::DomHandle,
) {
let host_ptr = host as *mut JsContextHost;
- if let Some(frame_element) = host
- .native_bridge_mut()
- .wrap_handle(scope, host_ptr, handle)
- .map(Into::into)
- {
- set_private_value(scope, global, "__moliWindowFrameElement", frame_element);
- }
+ let Some(owner_context) =
+ crate::native_bridge::node::node_owner_document_relevant_context(scope, host_ptr, handle)
+ else {
+ return;
+ };
+ let frame_element = {
+ let owner_scope = &mut v8::ContextScope::new(scope, owner_context);
+ let Some(frame_element) =
+ host.native_bridge_mut()
+ .wrap_handle(owner_scope, host_ptr, handle)
+ else {
+ return;
+ };
+ v8::Global::new(owner_scope, frame_element)
+ };
+ let frame_element = v8::Local::new(scope, &frame_element);
+ set_private_value(
+ scope,
+ global,
+ "__moliWindowFrameElement",
+ frame_element.into(),
+ );
}
fn bind_navigation<'s>(
diff --git a/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs b/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs
index 7f84666dfc..b647cb6cd6 100644
--- a/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs
+++ b/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs
@@ -1890,14 +1890,22 @@ fn anchor_click_default_action(
None,
);
}
- let declared_target_name =
- element_attribute(runtime, handle, "target").filter(|value| !value.is_empty());
+ let effective_target_name = element_attribute(runtime, handle, "target")
+ .filter(|value| !value.is_empty())
+ .or_else(|| {
+ let document = runtime.dom_host().owner_document_handle(handle)?;
+ runtime
+ .dom_host()
+ .document_base_target_for_handle(document)
+ .filter(|target| !target.is_empty())
+ .map(str::to_owned)
+ });
let (target_name, popup_disposition) = match navigation_policy {
HyperlinkNavigationPolicy::Auxiliary(disposition) => {
(Some("_blank".to_owned()), disposition)
}
HyperlinkNavigationPolicy::Current => {
- (declared_target_name, RendererPopupDisposition::Foreground)
+ (effective_target_name, RendererPopupDisposition::Foreground)
}
HyperlinkNavigationPolicy::Download => {
unreachable!("download hyperlink policy returned before target selection")
diff --git a/moli-renderer-v8/src/native_bridge/node.rs b/moli-renderer-v8/src/native_bridge/node.rs
index 396c93df74..3432e300d8 100644
--- a/moli-renderer-v8/src/native_bridge/node.rs
+++ b/moli-renderer-v8/src/native_bridge/node.rs
@@ -1346,6 +1346,9 @@ pub(super) fn node_owner_document_relevant_context<'s>(
let document_handle = unsafe { &*runtime_ptr }
.dom_host()
.owner_document_handle(handle)?;
+ if document_handle == unsafe { &*runtime_ptr }.document_handle() {
+ return unsafe { &*runtime_ptr }.page_default_context(scope);
+ }
if let Some(child_handle) =
unsafe { &*runtime_ptr }.child_browsing_context_host_for_document_handle(document_handle)
&& let Some(context) =
diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs
index 54993a9add..c2f170fa36 100644
--- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs
+++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs
@@ -675,6 +675,111 @@ fn child_webassembly_constructors_use_newtarget_child_realm_default_prototype()
r#"{"namespaceIsSeparate":true,"constructorFunctionPrototype":true,"moduleDirect":true,"moduleBound":true,"moduleProxy":true,"moduleProxyBound":true,"memoryProxy":true,"compileErrorDirect":true,"moduleTopRealm":true,"moduleIntrinsicSurvivesNamespaceReplacement":true}"#
);
}
+#[tokio::test(flavor = "current_thread")]
+async fn base_target_navigation_exposes_replacement_document_before_iframe_load() {
+ const HOST: &str = "anchor-base-target.test";
+
+ let server = StaticHttpServer::spawn(3).await;
+ let top_url = server.url_for_host(HOST, "/path/page.html");
+ let replacement_url = server.url_for_host(HOST, "/replacement.html");
+ let loader = static_http_loader([server.resolve_entry(HOST)]);
+ let mut vm = new_storage_page_task_executor_test_vm_with_loader(top_url.as_str(), &loader);
+
+ vm.eval(
+ r#"
+(() => {
+ globalThis.__targetLoadCount = 0;
+ globalThis.__targetLoadAccess = "pending";
+ globalThis.__frameLoadCount = 0;
+ globalThis.__baseTargetOwnerRealm = "pending";
+
+ const root = document.documentElement || document.appendChild(document.createElement('html'));
+ const body = document.body || root.appendChild(document.createElement('body'));
+ document.addEventListener('load', () => { __frameLoadCount += 1; }, true);
+ body.innerHTML = `
+
+
+ `;
+})()
+"#,
+ )
+ .expect("base-target network child setup should evaluate");
+ advance_page_task_executor_until_eval_equals(
+ &mut vm,
+ &loader,
+ "String(__frameLoadCount)",
+ "2",
+ "initial parser-created base-target child documents should load",
+ )
+ .await;
+
+ vm.eval(
+ r#"
+(() => {
+ const source = document.getElementById('base-target-source');
+ const target = document.getElementById('base-target-target');
+ __baseTargetOwnerRealm = [
+ source instanceof HTMLIFrameElement,
+ target instanceof HTMLIFrameElement,
+ target instanceof target.contentWindow.HTMLIFrameElement,
+ target.contentWindow.frameElement === target
+ ].join('|');
+
+ const doc = source.contentDocument;
+ const firstBase = doc.createElement('base');
+ firstBase.target = 'targetFrame';
+ const secondBase = doc.createElement('base');
+ secondBase.target = '_self';
+ doc.head.append(firstBase, secondBase);
+
+ const link = doc.createElement('a');
+ link.href = '/replacement.html';
+ link.setAttribute('target', '');
+ doc.body.appendChild(link);
+
+ target.addEventListener('load', () => {
+ __targetLoadCount += 1;
+ try {
+ __targetLoadAccess = target.contentDocument.location.href;
+ } catch (error) {
+ __targetLoadAccess = `${error && error.name}:${error && error.message}`;
+ }
+ }, true);
+ link.click();
+})()
+"#,
+ )
+ .expect("base-target replacement navigation should start");
+ advance_page_task_executor_until_eval_equals(
+ &mut vm,
+ &loader,
+ "String(__targetLoadCount)",
+ "1",
+ "base-target replacement document should load",
+ )
+ .await;
+
+ assert_eq!(
+ vm.eval("__baseTargetOwnerRealm")
+ .expect("base-target frame owner realm result should evaluate"),
+ "true|true|false|true",
+ "parser-created frame elements and window.frameElement must use the owner Document realm"
+ );
+ assert_eq!(
+ vm.eval("__targetLoadAccess")
+ .expect("target load callback access result should evaluate"),
+ replacement_url.as_str(),
+ "the replacement Document must be same-origin accessible during the iframe load callback"
+ );
+
+ let mut targets = server.finish_targets().await;
+ targets.sort();
+ assert_eq!(
+ targets,
+ ["/replacement.html", "/source.html", "/target.html"]
+ );
+}
+
#[test]
fn targeted_anchor_click_reports_same_document_hash_change_for_child_window() {
let mut vm = new_storage_test_vm("https://targeted-child-hash.test/page.html");