From 9ae23bd61a88b218bdffe3a8012b8aa5b6bac8a3 Mon Sep 17 00:00:00 2001
From: lanyue-llk <270302213+lanyue-llk@users.noreply.github.com>
Date: Wed, 30 Sep 2026 12:01:01 +0800
Subject: [PATCH] fix(window): preserve native named-context identity
---
.../classic/extracted/windows_and_popups.rs | 452 ++++++++++++++++++
.../src/domains/target/tests/test_support.rs | 11 +-
.../tests/tests_background_staging/runtime.rs | 22 +
.../target/tests/tests_target_creation.rs | 149 +++++-
moli-renderer-v8/src/context_bootstrap.rs | 2 +-
.../src/context_bootstrap/runtime_state.rs | 84 +++-
.../src/context_bootstrap/window_receiver.rs | 14 +-
.../window_runtime/dialogs.rs | 56 ++-
.../src/native_bridge/bindings.rs | 9 +
.../child_frame_runtime/window.rs | 4 +-
.../src/native_bridge/context_host/mod.rs | 2 +-
.../src/native_bridge/context_host/popups.rs | 93 ++--
.../context_host/popups/renderer_owned.rs | 21 +-
.../context_host/security_policy.rs | 5 +
.../context_host/window_execution_context.rs | 4 +-
.../operation_receiver.rs | 2 +-
.../element/activation/targets.rs | 64 ++-
moli-renderer-v8/src/native_bridge/mod.rs | 2 +-
.../browser_api/events_selection_storage.rs | 4 +-
.../window_security_and_domain.rs | 76 ++-
.../misc/extracted/popup_window.rs | 271 +++++++++++
.../misc/extracted/window_surface.rs | 108 +++++
.../navigation/element_navigation.rs | 40 ++
23 files changed, 1352 insertions(+), 143 deletions(-)
diff --git a/moli-protocol-server/src/protocol_server/tests/classic/extracted/windows_and_popups.rs b/moli-protocol-server/src/protocol_server/tests/classic/extracted/windows_and_popups.rs
index c4dc0fe97ac..6cdc4135796 100644
--- a/moli-protocol-server/src/protocol_server/tests/classic/extracted/windows_and_popups.rs
+++ b/moli-protocol-server/src/protocol_server/tests/classic/extracted/windows_and_popups.rs
@@ -730,6 +730,387 @@ async fn webdriver_classic_window_open_self_click_waits_for_current_url() {
)
.await;
}
+
+#[tokio::test]
+async fn webdriver_classic_keeps_all_live_popup_proxy_aliases() {
+ let app = build_router(test_state());
+ let session = classic_request_json(app.clone(), Method::POST, "/session").await;
+ let session_id = session["value"]["sessionId"]
+ .as_str()
+ .expect("classic session id");
+ let window_path = format!("/session/{session_id}/window");
+ let execute_path = format!("/session/{session_id}/execute/sync");
+ let original = classic_request_json(app.clone(), Method::GET, &window_path).await;
+ let original = original["value"].as_str().unwrap().to_owned();
+
+ let created = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "window.__oldReport = open('about:blank#report', 'report'); const helper = open('about:blank#helper', 'helper'); return [window.__oldReport, helper];",
+ "args": []
+ }),
+ )
+ .await;
+ let report = created["value"][0][CLASSIC_WINDOW_REFERENCE_KEY]
+ .as_str()
+ .expect("report handle")
+ .to_owned();
+ let helper = created["value"][1][CLASSIC_WINDOW_REFERENCE_KEY]
+ .as_str()
+ .expect("helper handle")
+ .to_owned();
+
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &window_path,
+ json!({ "handle": helper }),
+ )
+ .await,
+ json!({ "value": null })
+ );
+ let second_alias = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "window.__newReport = open('about:blank#next', 'report'); return window.__newReport;",
+ "args": []
+ }),
+ )
+ .await;
+ assert_eq!(
+ second_alias["value"][CLASSIC_WINDOW_REFERENCE_KEY],
+ json!(report)
+ );
+
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &window_path,
+ json!({ "handle": original }),
+ )
+ .await,
+ json!({ "value": null })
+ );
+ let old_alias = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({ "script": "return window.__oldReport;", "args": [] }),
+ )
+ .await;
+ assert_eq!(
+ old_alias["value"][CLASSIC_WINDOW_REFERENCE_KEY],
+ json!(report)
+ );
+
+ let handles = classic_request_json(
+ app.clone(),
+ Method::GET,
+ &format!("/session/{session_id}/window/handles"),
+ )
+ .await;
+ assert_eq!(
+ handles["value"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .filter(|handle| **handle == json!(report))
+ .count(),
+ 1
+ );
+
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &window_path,
+ json!({ "handle": report }),
+ )
+ .await,
+ json!({ "value": null })
+ );
+ let remaining = classic_request_json(app.clone(), Method::DELETE, &window_path).await;
+ assert_eq!(remaining["value"].as_array().map(Vec::len), Some(2));
+
+ for (owner, proxy) in [(&original, "__oldReport"), (&helper, "__newReport")] {
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &window_path,
+ json!({ "handle": owner }),
+ )
+ .await,
+ json!({ "value": null })
+ );
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": format!("return window.{proxy}.closed;"),
+ "args": []
+ }),
+ )
+ .await,
+ json!({ "value": true }),
+ "every live alias must observe the shared target close"
+ );
+ }
+
+ let _ = classic_request_json(app, Method::DELETE, &format!("/session/{session_id}")).await;
+}
+
+#[tokio::test]
+async fn webdriver_classic_noopener_reuses_a_related_named_popup() {
+ let app = build_router(test_state());
+ let session = classic_request_json(app.clone(), Method::POST, "/session").await;
+ let session_id = session["value"]["sessionId"]
+ .as_str()
+ .expect("classic session id");
+ let window_path = format!("/session/{session_id}/window");
+ let handles_path = format!("/session/{session_id}/window/handles");
+ let execute_path = format!("/session/{session_id}/execute/sync");
+
+ let created = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "window.__report = open('about:blank#first', 'report'); return window.__report;",
+ "args": []
+ }),
+ )
+ .await;
+ let report = created["value"][CLASSIC_WINDOW_REFERENCE_KEY]
+ .as_str()
+ .expect("report handle")
+ .to_owned();
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "return window.__report.opener === window;",
+ "args": []
+ }),
+ )
+ .await,
+ json!({ "value": true }),
+ "the original popup proxy must retain its opener"
+ );
+ let before = classic_request_json(app.clone(), Method::GET, &handles_path).await;
+
+ let reused = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "return open('about:blank#second', 'report', 'noopener') === null;",
+ "args": []
+ }),
+ )
+ .await;
+ assert_eq!(reused, json!({ "value": true }));
+ assert_eq!(
+ classic_request_json(app.clone(), Method::GET, &handles_path).await,
+ before,
+ "noopener must not create a new target when a related named target exists"
+ );
+
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &window_path,
+ json!({ "handle": report }),
+ )
+ .await,
+ json!({ "value": null })
+ );
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "return location.href;",
+ "args": []
+ }),
+ )
+ .await,
+ json!({ "value": "about:blank#second" }),
+ "the selected target must receive the noopener navigation"
+ );
+
+ let original = before["value"]
+ .as_array()
+ .and_then(|handles| {
+ handles.iter().find_map(|handle| {
+ let handle = handle.as_str()?;
+ (handle != report).then(|| handle.to_owned())
+ })
+ })
+ .expect("original window handle");
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &window_path,
+ json!({ "handle": original }),
+ )
+ .await,
+ json!({ "value": null })
+ );
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "return window.__report.opener === window;",
+ "args": []
+ }),
+ )
+ .await,
+ json!({ "value": true }),
+ "reusing with noopener must not clear the existing proxy's opener"
+ );
+
+ let _ = classic_request_json(app, Method::DELETE, &format!("/session/{session_id}")).await;
+}
+
+#[tokio::test]
+async fn webdriver_classic_close_retires_popup_aliases_before_named_reopen() {
+ let app = build_router(test_state());
+ let session = classic_request_json(app.clone(), Method::POST, "/session").await;
+ let session_id = session["value"]["sessionId"]
+ .as_str()
+ .expect("classic session id");
+ let window_path = format!("/session/{session_id}/window");
+ let execute_path = format!("/session/{session_id}/execute/sync");
+ let original = classic_request_json(app.clone(), Method::GET, &window_path).await;
+ let original = original["value"].as_str().unwrap().to_owned();
+
+ let created = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "window.__oldReport = open('about:blank#one', 'report'); return window.__oldReport;",
+ "args": []
+ }),
+ )
+ .await;
+ let old_report = created["value"][CLASSIC_WINDOW_REFERENCE_KEY]
+ .as_str()
+ .expect("old report handle")
+ .to_owned();
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &window_path,
+ json!({ "handle": old_report }),
+ )
+ .await,
+ json!({ "value": null })
+ );
+ let remaining = classic_request_json(app.clone(), Method::DELETE, &window_path).await;
+ assert_eq!(remaining, json!({ "value": [original.clone()] }));
+
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &window_path,
+ json!({ "handle": original }),
+ )
+ .await,
+ json!({ "value": null })
+ );
+ assert_eq!(
+ classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "return window.__oldReport.closed;",
+ "args": []
+ }),
+ )
+ .await,
+ json!({ "value": true }),
+ "a proxy must observe that its protocol target was closed"
+ );
+ let replacement = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "window.__newReport = open('about:blank#two', 'report'); return window.__newReport;",
+ "args": []
+ }),
+ )
+ .await;
+ let new_report = replacement["value"][CLASSIC_WINDOW_REFERENCE_KEY]
+ .as_str()
+ .expect("replacement report handle")
+ .to_owned();
+ assert_ne!(new_report, old_report);
+
+ let reused = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "window.__newReport.marker = 19; window.__newReport.document.body.textContent = 'kept'; for (let i = 0; i < 20; ++i) { if (open('', 'report') !== window.__newReport) return [false, i]; } return [true, window.__newReport.marker, window.__newReport.document.body.textContent, window.__newReport.closed];",
+ "args": []
+ }),
+ )
+ .await;
+ assert_eq!(reused, json!({ "value": [true, 19, "kept", false] }));
+ let handles = classic_request_json(
+ app.clone(),
+ Method::GET,
+ &format!("/session/{session_id}/window/handles"),
+ )
+ .await;
+ assert_eq!(handles["value"].as_array().map(Vec::len), Some(2));
+
+ let _ = classic_request_json(app, Method::DELETE, &format!("/session/{session_id}")).await;
+}
+
+#[tokio::test]
+async fn webdriver_classic_empty_parent_ignores_replaced_public_parent_property() {
+ let app = build_router(test_state());
+ let session = classic_request_json(app.clone(), Method::POST, "/session").await;
+ let session_id = session["value"]["sessionId"]
+ .as_str()
+ .expect("classic session id");
+ let result = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &format!("/session/{session_id}/execute/sync"),
+ json!({
+ "script": "const fake = {}; let reads = 0; Object.defineProperty(window, 'parent', { configurable: true, get() { ++reads; return fake; } }); const selected = open('', '_parent'); return [selected === window, selected === fake, reads];",
+ "args": []
+ }),
+ )
+ .await;
+ assert_eq!(result, json!({ "value": [true, false, 0] }));
+
+ let _ = classic_request_json(app, Method::DELETE, &format!("/session/{session_id}")).await;
+}
+
#[tokio::test]
async fn webdriver_classic_named_popup_reuse_navigates_existing_window() {
let app = build_router(test_state());
@@ -2581,3 +2962,74 @@ async fn webdriver_classic_new_window_user_prompt_behavior_matches_chromium_wpt(
.await;
}
}
+#[tokio::test]
+async fn webdriver_classic_named_popup_does_not_reuse_an_independent_tab() {
+ let app = build_router(test_state());
+ let session = classic_request_json(app.clone(), Method::POST, "/session").await;
+ let session_id = session["value"]["sessionId"]
+ .as_str()
+ .expect("classic session id");
+ let window_path = format!("/session/{session_id}/window");
+ let handles_path = format!("/session/{session_id}/window/handles");
+ let execute_path = format!("/session/{session_id}/execute/sync");
+
+ let original = classic_request_json(app.clone(), Method::GET, &window_path).await;
+ let original = original["value"]
+ .as_str()
+ .expect("original window handle")
+ .to_owned();
+ let named = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "window.name = 'independent-report'; return window.name;",
+ "args": []
+ }),
+ )
+ .await;
+ assert_eq!(named, json!({ "value": "independent-report" }));
+
+ let independent = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &format!("/session/{session_id}/window/new"),
+ json!({ "type": "tab" }),
+ )
+ .await;
+ let independent = independent["value"]["handle"]
+ .as_str()
+ .expect("independent tab handle")
+ .to_owned();
+ let switched = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &window_path,
+ json!({ "handle": independent }),
+ )
+ .await;
+ assert_eq!(switched, json!({ "value": null }));
+
+ let opened = classic_request_json_with_body(
+ app.clone(),
+ Method::POST,
+ &execute_path,
+ json!({
+ "script": "const popup = window.open('about:blank#independent-popup', 'independent-report'); return popup !== null;",
+ "args": []
+ }),
+ )
+ .await;
+ assert_eq!(opened, json!({ "value": true }));
+ let handles = classic_request_json(app.clone(), Method::GET, &handles_path).await;
+ let handles = handles["value"].as_array().expect("window handles");
+ assert_eq!(
+ handles.len(),
+ 3,
+ "an unrelated same-name tab must not be selected as the popup target: {handles:?}"
+ );
+ assert!(handles.contains(&json!(original)));
+ assert!(handles.contains(&json!(independent)));
+
+ let _ = classic_request_json(app, Method::DELETE, &format!("/session/{session_id}")).await;
+}
diff --git a/moli-protocol/src/domains/target/tests/test_support.rs b/moli-protocol/src/domains/target/tests/test_support.rs
index e04947b5ee4..826e3db8469 100644
--- a/moli-protocol/src/domains/target/tests/test_support.rs
+++ b/moli-protocol/src/domains/target/tests/test_support.rs
@@ -207,6 +207,15 @@ pub(super) async fn load_bc_with_titled_page_async(
bc_id: &str,
target_id: &str,
html: &str,
+) {
+ load_bc_with_page_url_async(ctx, bc_id, target_id, &format!("data:text/html,{html}")).await;
+}
+
+pub(super) async fn load_bc_with_page_url_async(
+ ctx: &mut TestContext,
+ bc_id: &str,
+ target_id: &str,
+ url: &str,
) {
// Most tests that use this helper assert Target-domain discovery output
// after manually seeding a loaded target. Real CDP clients only receive
@@ -219,7 +228,7 @@ pub(super) async fn load_bc_with_titled_page_async(
ctx.conn.insert_browser_context(bc);
let page = ctx
.conn
- .load_page_via_runtime_async(&format!("data:text/html,{html}"))
+ .load_page_via_runtime_async(url)
.await
.expect("page should load");
let renderer_page = crate::conn::RendererPageResidenceIdentity::from_page(&page);
diff --git a/moli-protocol/src/domains/target/tests/tests_background_staging/runtime.rs b/moli-protocol/src/domains/target/tests/tests_background_staging/runtime.rs
index b90445054c1..c84c36bbb3d 100644
--- a/moli-protocol/src/domains/target/tests/tests_background_staging/runtime.rs
+++ b/moli-protocol/src/domains/target/tests/tests_background_staging/runtime.rs
@@ -575,6 +575,28 @@ async fn same_context_named_popup_reuse_navigates_and_activates_loaded_owner() {
.await;
let response = take_response_by_id(&mut ctx, 1041949447);
assert_eq!(response["result"]["result"]["value"], json!("named target"));
+
+ ctx.process_async(json!({
+ "id": 1041949448,
+ "method": "Runtime.evaluate",
+ "sessionId": owner.session_id,
+ "params": {
+ "expression": "window.name"
+ }
+ }))
+ .await;
+ let response = take_response_by_id(&mut ctx, 1041949448);
+ assert_eq!(
+ response["result"]["result"]["value"],
+ json!("reportWindow"),
+ "a reused named target must retain its browsing-context name across Document replacement"
+ );
+ let browser_context = ctx.conn.browser_context.as_ref().expect("browser context");
+ assert_eq!(
+ browser_context.target_id_for_window_name("reportWindow"),
+ Some(owner.target_id.as_str()),
+ "the popup proxy name and the live target engine must share one browsing-context owner"
+ );
})
.await;
}
diff --git a/moli-protocol/src/domains/target/tests/tests_target_creation.rs b/moli-protocol/src/domains/target/tests/tests_target_creation.rs
index 66dcda99f27..f48216a1dba 100644
--- a/moli-protocol/src/domains/target/tests/tests_target_creation.rs
+++ b/moli-protocol/src/domains/target/tests/tests_target_creation.rs
@@ -1895,6 +1895,23 @@ async fn popup_initial_navigation_replaces_internal_blank_history_entry() {
#[tokio::test(flavor = "multi_thread")]
async fn window_open_named_target_reuses_existing_popup_target() {
+ // Rename access requires a same-origin WindowProxy. Separate HTTP paths
+ // retain full document navigations; data: documents have opaque origins.
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let address = listener.local_addr().unwrap();
+ let server = tokio::spawn(async move {
+ axum::serve(
+ listener,
+ Router::new()
+ .fallback(|| async { ([(CONTENT_TYPE, "text/html")], "named popup") }),
+ )
+ .await
+ .unwrap();
+ });
+ let opener_url = format!("http://{address}/opener");
+ let first_url = format!("http://{address}/first-popup");
+ let second_url = format!("http://{address}/second-popup");
+ let renamed_url = format!("http://{address}/renamed-popup");
let mut ctx = TestContext::new();
// Target creation/named-target selection is synchronous with
// `window.open()`, but fetching the selected target's URL is not. Mirror
@@ -1903,11 +1920,11 @@ async fn window_open_named_target_reuses_existing_popup_target() {
ctx.enable_background_navigation_scheduler_for_test();
tokio::task::LocalSet::new()
.run_until(async {
- load_bc_with_titled_page_async(
+ load_bc_with_page_url_async(
&mut ctx,
"BID-popup-name",
"TID-opener-name",
- "popup opener",
+ &opener_url,
)
.await;
ctx.process_async(json!({
@@ -1935,7 +1952,7 @@ async fn window_open_named_target_reuses_existing_popup_target() {
"method": "Runtime.evaluate",
"sessionId": opener_session_id,
"params": {
- "expression": "window.open('data:text/html,first-popup', 'reportWindow') !== null"
+ "expression": format!("window.__namedPopup = window.open({}, 'reportWindow'); window.__namedPopup !== null", json!(first_url))
}
}))
.await;
@@ -1951,7 +1968,7 @@ async fn window_open_named_target_reuses_existing_popup_target() {
.to_owned();
assert_eq!(
created["params"]["targetInfo"]["url"],
- json!("data:text/html,first-popup")
+ json!(first_url)
);
assert!(
first_sent
@@ -1959,13 +1976,12 @@ async fn window_open_named_target_reuses_existing_popup_target() {
.any(|message| message["method"] == json!("Page.windowOpen")),
"first named window.open should emit Page.windowOpen: {first_sent:?}"
);
-
ctx.process_async(json!({
"id": 15,
"method": "Runtime.evaluate",
"sessionId": opener_session_id,
"params": {
- "expression": "window.open('data:text/html,second-popup', 'reportWindow') !== null"
+ "expression": format!("window.open({}, 'reportWindow') !== null", json!(second_url))
}
}))
.await;
@@ -1994,7 +2010,7 @@ async fn window_open_named_target_reuses_existing_popup_target() {
&& message["params"]["targetInfo"]["targetId"]
== json!(target_id)
&& message["params"]["targetInfo"]["url"]
- == json!("data:text/html,second-popup")
+ == json!(second_url)
},
)
.await;
@@ -2004,7 +2020,7 @@ async fn window_open_named_target_reuses_existing_popup_target() {
);
assert_eq!(
changed["params"]["targetInfo"]["url"],
- json!("data:text/html,second-popup")
+ json!(second_url)
);
ctx.wait_until_scheduler_state("reused named popup remains selected", |conn| {
conn.browser_context_by_id("BID-popup-name")
@@ -2012,7 +2028,7 @@ async fn window_open_named_target_reuses_existing_popup_target() {
browser_context.active_target_id() == Some(target_id.as_str())
&& loaded_page_for_target(browser_context, &target_id).is_some_and(
|page| {
- page.final_url().as_str() == "data:text/html,second-popup"
+ page.final_url().as_str() == second_url
},
)
})
@@ -2022,12 +2038,123 @@ async fn window_open_named_target_reuses_existing_popup_target() {
assert_eq!(browser_context.active_target_id(), Some(target_id.as_str()));
assert_eq!(
browser_context.target_url(),
- "data:text/html,second-popup"
+ second_url
+ );
+ ctx.process_async(json!({
+ "id": 141,
+ "method": "Target.attachToTarget",
+ "params": { "targetId": target_id }
+ }))
+ .await;
+ let popup_session_id = take_response_by_id(&mut ctx, 141)["result"]["sessionId"]
+ .as_str()
+ .expect("popup session id")
+ .to_owned();
+ ctx.sent.clear();
+
+ ctx.process_async(json!({
+ "id": 16,
+ "method": "Runtime.evaluate",
+ "sessionId": opener_session_id,
+ "params": {
+ "expression": "window.__namedPopup.name = 'renamedWindow'; window.__namedPopup.name"
+ }
+ }))
+ .await;
+ assert_eq!(
+ take_response_by_id(&mut ctx, 16)["result"]["result"]["value"],
+ json!("renamedWindow")
+ );
+ ctx.process_async(json!({
+ "id": 161,
+ "method": "Runtime.evaluate",
+ "sessionId": popup_session_id,
+ "params": { "expression": "window.name" }
+ }))
+ .await;
+ assert_eq!(
+ take_response_by_id(&mut ctx, 161)["result"]["result"]["value"],
+ json!("renamedWindow"),
+ "a proxy rename must be immediately visible in the target"
+ );
+ ctx.process_async(json!({
+ "id": 162,
+ "method": "Runtime.evaluate",
+ "sessionId": opener_session_id,
+ "params": {
+ "expression": format!("window.open({}, 'renamedWindow') !== null", json!(renamed_url))
+ }
+ }))
+ .await;
+ let renamed_sent = ctx.take_all();
+ assert!(
+ !renamed_sent
+ .iter()
+ .any(|message| message["method"] == json!("Target.targetCreated")),
+ "opening a popup by its live renamed Window.name must reuse its bound target: {renamed_sent:?}"
+ );
+ ctx.wait_for_scheduler_message("renamed popup target navigation", |message| {
+ message["method"] == json!("Target.targetInfoChanged")
+ && message["params"]["targetInfo"]["targetId"] == json!(target_id)
+ && message["params"]["targetInfo"]["url"]
+ == json!(renamed_url)
+ })
+ .await;
+ let browser_context = ctx.conn.browser_context.as_ref().unwrap();
+ assert_eq!(
+ browser_context.target_id_for_window_name("renamedWindow"),
+ Some(target_id.as_str())
+ );
+ assert_eq!(
+ browser_context.target_id_for_window_name("reportWindow"),
+ None,
+ "renaming a live popup must invalidate its previous target name"
+ );
+
+ ctx.process_async(json!({
+ "id": 163,
+ "method": "Runtime.evaluate",
+ "sessionId": popup_session_id,
+ "params": { "expression": "window.name = 'targetRenamed'; window.name" }
+ }))
+ .await;
+ assert_eq!(
+ take_response_by_id(&mut ctx, 163)["result"]["result"]["value"],
+ json!("targetRenamed")
+ );
+ ctx.process_async(json!({
+ "id": 164,
+ "method": "Runtime.evaluate",
+ "sessionId": opener_session_id,
+ "params": { "expression": "window.__namedPopup.name" }
+ }))
+ .await;
+ assert_eq!(
+ take_response_by_id(&mut ctx, 164)["result"]["result"]["value"],
+ json!("targetRenamed"),
+ "a target rename must be immediately visible through its retained proxy"
+ );
+
+ ctx.process_async(json!({
+ "id": 17,
+ "method": "Runtime.evaluate",
+ "sessionId": opener_session_id,
+ "params": {
+ "expression": "window.open('about:blank', 'renamedWindow') !== null"
+ }
+ }))
+ .await;
+ let old_name_sent = ctx.take_all();
+ assert!(
+ old_name_sent
+ .iter()
+ .any(|message| message["method"] == json!("Target.targetCreated")),
+ "the target's previous name must no longer resolve to it: {old_name_sent:?}"
);
})
.await;
+ server.abort();
}
-
#[tokio::test(flavor = "multi_thread")]
async fn window_open_named_target_reused_in_same_command_emits_one_page_event() {
let mut ctx = TestContext::new();
diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs
index 19477d5d26b..31ae7d466d4 100644
--- a/moli-renderer-v8/src/context_bootstrap.rs
+++ b/moli-renderer-v8/src/context_bootstrap.rs
@@ -203,7 +203,7 @@ pub(crate) use window_accessors::{
mod window_events;
mod window_lazy_surface;
mod window_receiver;
-pub(crate) use window_receiver::is_window_receiver;
+pub(crate) use window_receiver::{NATIVE_WINDOW_BRAND_SLOT, is_window_receiver};
mod window_runtime;
pub(crate) use form_data_runtime::{
diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs
index 0573a0d1d0e..f03e49d423c 100644
--- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs
+++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs
@@ -871,26 +871,59 @@ fn window_surface_replaceable_setter<'s>(
define_replaceable_window_property(scope, receiver, name, args.get(0));
}
+fn window_name_receiver_owner<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ receiver: v8::Local<'s, v8::Object>,
+) -> Option<(*mut JsContextHost, native_bridge::OwnerDispatchScope)> {
+ if !super::is_window_receiver(scope, receiver) {
+ return None;
+ }
+ let host_ptr = crate::util::context_host_ptr_from_window_object(scope, receiver)
+ .or_else(|| {
+ receiver
+ .get_creation_context(scope)
+ .and_then(crate::util::context_host_ptr_from_context_slot)
+ })
+ .or_else(|| {
+ receiver
+ .strict_equals(scope.get_current_context().global(scope).into())
+ .then(|| context_host_ptr_from_global_bridge(scope))
+ .flatten()
+ })?;
+ let owner = super::navigation_window::runtime_window_dispatch_scope(scope, receiver)
+ .unwrap_or(native_bridge::OwnerDispatchScope::Top);
+ Some((host_ptr, owner))
+}
+
fn window_name_runtime_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let receiver = callback_this_object(scope, &args);
- if child_context_handle_from_owner(scope, receiver).is_none()
- && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
- {
- let host = unsafe { &*host_ptr };
- let name = if host.browsing_context_is_closed() {
- String::new()
- } else {
- host.browsing_context_name().get()
- };
- rv.set(v8::String::new(scope, &name).unwrap().into());
+ let Some((host_ptr, owner)) = window_name_receiver_owner(scope, receiver) else {
+ throw_type_error(scope, "Window.name getter called on incompatible receiver.");
return;
+ };
+ let host = unsafe { &*host_ptr };
+ let value = match owner {
+ native_bridge::OwnerDispatchScope::Top => {
+ let name = if host.browsing_context_is_closed() {
+ String::new()
+ } else {
+ host.browsing_context_name().get()
+ };
+ v8_string(scope, &name).map(v8::Local::::from)
+ }
+ native_bridge::OwnerDispatchScope::Child(_) => {
+ object_hidden_value(scope, receiver, WINDOW_NAME_SLOT)
+ }
+ native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => host
+ .lightweight_popup_window_name(popup_id)
+ .and_then(|name| v8_string(scope, &name))
+ .map(v8::Local::::from),
}
- let value = object_hidden_value(scope, receiver, WINDOW_NAME_SLOT)
- .unwrap_or_else(|| v8::String::empty(scope).into());
+ .unwrap_or_else(|| v8::String::empty(scope).into());
rv.set(value);
}
@@ -900,21 +933,28 @@ fn window_name_runtime_setter<'s>(
_rv: v8::ReturnValue<'s, v8::Value>,
) {
let receiver = callback_this_object(scope, &args);
- let next = args
- .get(0)
- .to_string(scope)
- .map(|value| value.to_rust_string_lossy(scope))
- .unwrap_or_default();
- if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
- let host = unsafe { &mut *host_ptr };
- if let Some(handle) = child_context_handle_from_owner(scope, receiver) {
- host.set_child_browsing_context_name(handle, next.clone());
- } else {
+ let Some((host_ptr, owner)) = window_name_receiver_owner(scope, receiver) else {
+ throw_type_error(scope, "Window.name setter called on incompatible receiver.");
+ return;
+ };
+ let Some(next) = args.get(0).to_string(scope) else {
+ return;
+ };
+ let next = next.to_rust_string_lossy(scope);
+ let host = unsafe { &mut *host_ptr };
+ match owner {
+ native_bridge::OwnerDispatchScope::Top => {
if host.browsing_context_is_closed() {
return;
}
host.browsing_context_name().set(next.clone());
}
+ native_bridge::OwnerDispatchScope::Child(handle) => {
+ host.set_child_browsing_context_name(handle, next.clone())
+ }
+ native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => {
+ host.set_lightweight_popup_window_name(popup_id, &next)
+ }
}
define_non_enumerable_string_property(scope, receiver, WINDOW_NAME_SLOT, &next);
}
diff --git a/moli-renderer-v8/src/context_bootstrap/window_receiver.rs b/moli-renderer-v8/src/context_bootstrap/window_receiver.rs
index b619477b1e2..2676e3d6b37 100644
--- a/moli-renderer-v8/src/context_bootstrap/window_receiver.rs
+++ b/moli-renderer-v8/src/context_bootstrap/window_receiver.rs
@@ -1,9 +1,11 @@
-use super::shared::WINDOW_NAVIGATOR_SLOT;
+use super::shared::{CHILD_BROWSING_CONTEXT_HANDLE_SLOT, WINDOW_NAVIGATOR_SLOT};
use crate::util::{
- callback_data_index_value, context_host_ptr_from_context_slot,
- context_host_ptr_from_window_object, get_private_value, throw_type_error,
+ callback_data_index_value, context_host_ptr_from_context_slot, get_private_value,
+ throw_type_error,
};
+pub(crate) const NATIVE_WINDOW_BRAND_SLOT: &str = "__moliNativeWindowBrand";
+
/// Recognizes a native Window receiver for WebIDL brand checks.
///
/// A retained, detached global proxy can lose both its creation context and
@@ -34,7 +36,11 @@ fn is_live_window_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
- if context_host_ptr_from_window_object(scope, receiver).is_some() {
+ if get_private_value(scope, receiver, NATIVE_WINDOW_BRAND_SLOT).is_some()
+ || crate::native_bridge::lightweight_popup_id_from_window(scope, receiver).is_some()
+ || crate::native_bridge::cross_origin_lightweight_popup_id(scope, receiver).is_some()
+ || get_private_value(scope, receiver, CHILD_BROWSING_CONTEXT_HANDLE_SLOT).is_some()
+ {
return true;
}
// The shared helper maps an `undefined` rollback marker to `None`.
diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs
index 9b08f02fbc7..e710c9aa40f 100644
--- a/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs
+++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs
@@ -161,15 +161,38 @@ pub(crate) fn window_open_callback<'s>(
rv.set(v8::null(scope).into());
return;
};
+ // Argument conversion and relative URL settings belong to the calling
+ // realm; native target selection belongs to the Window receiver.
+ let host = unsafe { &*host_ptr };
+ let entered_base_url = entered_window_api_base_url(scope, host);
+ let owner = crate::native_bridge::marked_window_dispatch_scope(scope, args.this())
+ .or_else(|| {
+ args.this()
+ .get_creation_context(scope)
+ .and_then(|context| {
+ host.window_execution_context_identity_for_access_check(context)
+ })
+ .map(|identity| identity.dispatch_scope())
+ })
+ .unwrap_or_else(|| host.entered_owner_dispatch_scope(scope));
+ let previous = owner.enter(scope);
+ window_open_for_receiver(scope, args, rv, parsed, host_ptr, entered_base_url);
+ owner.restore(scope, previous);
+}
+
+fn window_open_for_receiver<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ args: v8::FunctionCallbackArguments<'s>,
+ mut rv: v8::ReturnValue<'_, v8::Value>,
+ parsed: WindowOpenArgs,
+ host_ptr: *mut crate::native_bridge::JsContextHost,
+ entered_base_url: Url,
+) {
let entered_window = {
let host = unsafe { &*host_ptr };
window_open_entered_window(scope, host).unwrap_or_else(|| args.this())
};
let special_target = SpecialBrowsingContextTarget::parse(&parsed.target_name);
- let entered_base_url = {
- let host = unsafe { &*host_ptr };
- entered_window_api_base_url(scope, host)
- };
let url = if parsed.raw_url.is_empty() {
Url::parse("about:blank").expect("about:blank should parse")
} else {
@@ -198,6 +221,31 @@ pub(crate) fn window_open_callback<'s>(
}
let parsed_features = WindowOpenFeatures::parse(&parsed.features);
let suppress_opener = parsed_features.suppresses_opener();
+ let named_source = if special_target.is_none() && !parsed.target_name.is_empty() {
+ let host = unsafe { &*host_ptr };
+ match host.entered_owner_dispatch_scope(scope) {
+ OwnerDispatchScope::Top => host.browsing_context_name().get() == parsed.target_name,
+ OwnerDispatchScope::Child(handle) => {
+ host.child_browsing_context_handle_by_name(&parsed.target_name) == Some(handle)
+ }
+ OwnerDispatchScope::LightweightPopup(popup_id) => host
+ .lightweight_popup_window_name(popup_id)
+ .is_some_and(|name| name == parsed.target_name),
+ }
+ } else {
+ false
+ };
+ if named_source {
+ if parsed.raw_url.is_empty() {
+ rv.set(entered_window.into());
+ } else {
+ navigate_window_open_self(scope, entered_window, url.as_str(), &mut rv);
+ }
+ if suppress_opener {
+ rv.set_null();
+ }
+ return;
+ }
let mut creator_policy_container = {
let host = unsafe { &*host_ptr };
window_open_entered_policy_container(scope, host)
diff --git a/moli-renderer-v8/src/native_bridge/bindings.rs b/moli-renderer-v8/src/native_bridge/bindings.rs
index 4bae92947ff..4f32a318846 100644
--- a/moli-renderer-v8/src/native_bridge/bindings.rs
+++ b/moli-renderer-v8/src/native_bridge/bindings.rs
@@ -242,6 +242,15 @@ impl NativeBridgeBindings {
.new_instance(scope)
.unwrap_or_else(|| panic!("failed to instantiate `{prototype_name}` wrapper"));
if matches!(wrapper_kind, WrapperKind::Window) {
+ let key = v8::Private::for_api(
+ scope,
+ Some(crate::util::v8str(
+ scope,
+ crate::context_bootstrap::NATIVE_WINDOW_BRAND_SLOT,
+ )),
+ );
+ let brand = v8::Boolean::new(scope, true);
+ let _ = wrapper.set_private(scope, key, brand.into());
// Window brand checks can cross realm boundaries, so the small
// number of Window wrappers retain their object-local host marker.
let host_external = v8::External::new(scope, host_ptr as *mut c_void);
diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs
index ea72cde177a..163ee60f572 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs
@@ -1367,9 +1367,7 @@ impl JsContextHost {
(!is_cross_origin_window_proxy(scope, object)).then_some(object)
}
- pub(in crate::native_bridge::context_host) fn child_browsing_context_parent_top_for_realm_global<
- 's,
- >(
+ pub(in crate::native_bridge) fn child_browsing_context_parent_top_for_realm_global<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
handle: DomHandle,
diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs
index 4e304a95698..4f28d9330ed 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs
@@ -227,7 +227,7 @@ pub(crate) use window_execution_context::{
DetachedWindowFetchContext, WindowEnvironmentSettings, WindowExecutionContextAccessPolicy,
WindowExecutionContextBinding, WindowExecutionContextIdentity, WindowExecutionContextOwner,
WindowFetchContext, WindowOperationReceiver, WindowOperationReceiverCaptureError,
- WindowTaskTarget,
+ WindowTaskTarget, marked_window_dispatch_scope,
};
use window_execution_context::{
WindowExecutionContextRealmRecords, WindowExecutionContextRealmRegistration,
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 7d98f934cac..f3671857fbd 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs
@@ -643,7 +643,13 @@ impl JsContextHost {
true
}
- fn set_lightweight_popup_window_name(&mut self, popup_id: u64, next: &str) {
+ pub(crate) fn lightweight_popup_window_name(&self, popup_id: u64) -> Option {
+ self.lightweight_popup_record(popup_id)
+ .filter(|record| record.is_open())
+ .map(|record| record.name.get())
+ }
+
+ pub(crate) fn set_lightweight_popup_window_name(&mut self, popup_id: u64, next: &str) {
let Some(record) = self
.lightweight_popup_record(popup_id)
.filter(|record| record.is_open())
@@ -684,8 +690,7 @@ impl JsContextHost {
creator_policy_container: DocumentPolicyContainer,
update_existing_opener: bool,
) -> Option> {
- if opener.is_some()
- && let Some(name) = trackable_lightweight_popup_window_name(target_name)
+ if let Some(name) = trackable_lightweight_popup_window_name(target_name)
&& let Some(popup_id) =
self.lightweight_popup_browsing_contexts
.iter()
@@ -773,14 +778,15 @@ impl JsContextHost {
let initial_referrer = creator_policy_container.document_referrer.clone();
let opener_endpoint =
lightweight_popup_initiator_endpoint(scope, opener, opener_child_handle);
+ let creator_scope = opener_endpoint
+ .map(|endpoint| endpoint.dispatch_scope())
+ .unwrap_or_else(|| self.entered_owner_dispatch_scope(scope));
let creator_resource_authority = self
- .document_resource_loader_for_dispatch_scope(self.entered_owner_dispatch_scope(scope))
+ .document_resource_loader_for_dispatch_scope(creator_scope)
.expect("new popup Document requires its creator's exact resource authority")
.clone();
let storage_scope = self.lightweight_popup_storage_scope_for_initiated_navigation(
- scope,
- opener,
- opener_child_handle,
+ creator_scope,
&initial_url,
opener_sandbox_policy.is_some_and(|policy| policy.forces_opaque_origin),
);
@@ -887,9 +893,7 @@ impl JsContextHost {
initial_execution_context_owner,
)
} else if moli_url::is_about_blank(&initial_url)
- && let Some(inherited) = opener_endpoint.and_then(|endpoint| {
- self.window_access_origin_for_dispatch_scope(endpoint.dispatch_scope())
- })
+ && let Some(inherited) = self.window_access_origin_for_dispatch_scope(creator_scope)
{
inherited
} else if storage_scope.origin() == "null" {
@@ -1052,8 +1056,10 @@ impl JsContextHost {
&mut navigation_state.policy_container.sandbox,
opener_sandbox_policy,
);
- let initiator_endpoint =
- lightweight_popup_initiator_endpoint(scope, opener, opener_child_handle);
+ let creator_scope =
+ lightweight_popup_initiator_endpoint(scope, opener, opener_child_handle)
+ .map(|endpoint| endpoint.dispatch_scope())
+ .unwrap_or_else(|| self.entered_owner_dispatch_scope(scope));
let previous_url =
lightweight_popup_location_href(scope, window).unwrap_or_else(about_blank_url);
let target_url = parsed_url;
@@ -1083,9 +1089,7 @@ impl JsContextHost {
);
let queue_synthetic_load = if moli_url::is_about_blank(&target_url) {
let storage_scope = self.lightweight_popup_storage_scope_for_initiated_navigation(
- scope,
- opener,
- opener_child_handle,
+ creator_scope,
&target_url,
opener_sandbox_policy.is_some_and(|policy| policy.forces_opaque_origin),
);
@@ -1102,9 +1106,9 @@ impl JsContextHost {
LightweightPopupDocumentCommitOrigin::FromNavigationResponse(
"null".to_owned(),
)
- } else if let Some(inherited) = initiator_endpoint.and_then(|endpoint| {
- self.window_access_origin_for_dispatch_scope(endpoint.dispatch_scope())
- }) {
+ } else if let Some(inherited) =
+ self.window_access_origin_for_dispatch_scope(creator_scope)
+ {
LightweightPopupDocumentCommitOrigin::InheritExact(inherited)
} else {
LightweightPopupDocumentCommitOrigin::FromNavigationResponse(
@@ -1268,11 +1272,9 @@ impl JsContextHost {
target_store
}
- fn lightweight_popup_storage_scope_for_initiated_navigation<'s>(
+ fn lightweight_popup_storage_scope_for_initiated_navigation(
&mut self,
- scope: &mut v8::PinScope<'s, '_>,
- opener: Option>,
- opener_child_handle: Option,
+ creator: OwnerDispatchScope,
target_url: &Url,
sandbox_forces_opaque_origin: bool,
) -> LightweightPopupStorageScope {
@@ -1280,13 +1282,12 @@ impl JsContextHost {
return self.lightweight_popup_opaque_storage_scope(target_url);
}
if moli_url::is_about_blank(target_url)
- && let Some(opener_scope) =
- self.lightweight_popup_opener_storage_scope(scope, opener, opener_child_handle)
+ && let Some(opener_scope) = self.lightweight_popup_storage_scope_for_owner(creator)
{
if opener_scope.origin() == "null" {
return opener_scope;
}
- if opener_child_handle.is_none() {
+ if !matches!(creator, OwnerDispatchScope::Child(_)) {
return opener_scope;
}
let storage_key = web_storage_key_for_child_about_blank_popup(&opener_scope);
@@ -1346,22 +1347,27 @@ impl JsContextHost {
opener: Option>,
opener_child_handle: Option,
) -> Option {
- if let Some(opener) = opener
- && let Some(popup_id) = lightweight_popup_id_from_window(scope, opener)
- {
- return self
- .lightweight_popup_bound_web_storage_scope(popup_id)
- .map(LightweightPopupStorageScope::from_web_storage_scope);
- }
- if let Some(handle) = opener_child_handle {
- let top_origin = origin_ascii_serialization(self.document_url());
- return self
- .child_browsing_context_web_storage_scope(handle, &top_origin)
- .map(LightweightPopupStorageScope::from_web_storage_scope);
- }
- Some(LightweightPopupStorageScope::from_web_storage_scope(
- self.top_web_storage_scope(),
- ))
+ let owner = lightweight_popup_initiator_endpoint(scope, opener, opener_child_handle)
+ .map(|endpoint| endpoint.dispatch_scope())
+ .unwrap_or(OwnerDispatchScope::Top);
+ self.lightweight_popup_storage_scope_for_owner(owner)
+ }
+
+ fn lightweight_popup_storage_scope_for_owner(
+ &mut self,
+ owner: OwnerDispatchScope,
+ ) -> Option {
+ let storage_scope = match owner {
+ OwnerDispatchScope::Top => Some(self.top_web_storage_scope()),
+ OwnerDispatchScope::Child(handle) => {
+ let top_origin = origin_ascii_serialization(self.document_url());
+ self.child_browsing_context_web_storage_scope(handle, &top_origin)
+ }
+ OwnerDispatchScope::LightweightPopup(popup_id) => {
+ self.lightweight_popup_bound_web_storage_scope(popup_id)
+ }
+ };
+ storage_scope.map(LightweightPopupStorageScope::from_web_storage_scope)
}
pub(crate) fn active_lightweight_popup_base_url(
@@ -4447,10 +4453,9 @@ fn lightweight_popup_window_name_getter<'s>(
rv.set(v8::String::empty(scope).into());
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
&& let Some(popup_id) = lightweight_popup_id_from_window(scope, window)
- && let Some(record) = unsafe { &*host_ptr }.lightweight_popup_record(popup_id)
- && record.is_open()
+ && let Some(name) = unsafe { &*host_ptr }.lightweight_popup_window_name(popup_id)
{
- rv.set(v8::String::new(scope, &record.name.get()).unwrap().into());
+ rv.set(v8::String::new(scope, &name).unwrap().into());
}
}
diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs b/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs
index 5bdc4037989..0bbda1983af 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs
@@ -49,10 +49,12 @@ impl JsContextHost {
.sandbox
.with_response_content_security_policy(sandbox);
}
+ let creator_scope =
+ lightweight_popup_initiator_endpoint(scope, Some(creator), creator_child_handle)
+ .expect("popup navigation requires its creator Window")
+ .dispatch_scope();
let storage_scope = self.lightweight_popup_storage_scope_for_initiated_navigation(
- scope,
- Some(creator),
- creator_child_handle,
+ creator_scope,
&about_blank_url(),
policy.sandbox.forces_opaque_origin,
);
@@ -100,12 +102,11 @@ impl JsContextHost {
let environment = self
.page_script_environment()
.ok_or_else(|| anyhow::anyhow!("auxiliary creator Page is retired"))?;
- if exposes_opener
- && let Some(name) = trackable_lightweight_popup_window_name(target_name)
+ if let Some(name) = trackable_lightweight_popup_window_name(target_name)
&& let Some(existing) =
environment.named_related_window(&name, self.browsing_context_name())
{
- if update_existing_opener {
+ if update_existing_opener && exposes_opener {
existing.set_opener(Some(v8::Global::new(scope, creator)));
}
let (window, name) = existing
@@ -176,10 +177,12 @@ impl JsContextHost {
policy.sandbox = DocumentSandboxPolicy::default();
}
inherit_lightweight_popup_opener_sandbox(&mut policy.sandbox, sandbox);
+ let creator_scope =
+ lightweight_popup_initiator_endpoint(scope, Some(creator), creator_child_handle)
+ .expect("popup navigation requires its creator Window")
+ .dispatch_scope();
let storage_scope = self.lightweight_popup_storage_scope_for_initiated_navigation(
- scope,
- Some(creator),
- creator_child_handle,
+ creator_scope,
&initial_url,
policy.sandbox.forces_opaque_origin,
);
diff --git a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs
index 5b94a742b4d..41c038ccc7f 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs
@@ -524,6 +524,11 @@ impl JsContextHost {
if let Some(popup_id) = active_lightweight_popup_id(scope) {
return OwnerDispatchScope::LightweightPopup(popup_id);
}
+ if let Some(identity) =
+ self.window_execution_context_identity_for_access_check(scope.get_current_context())
+ {
+ return identity.dispatch_scope();
+ }
OwnerDispatchScope::Top
}
diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context.rs b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context.rs
index db36724c1e1..98850380735 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context.rs
@@ -21,7 +21,9 @@ mod settings;
pub(crate) use binding::WindowExecutionContextBinding;
pub(crate) use fetch::{DetachedWindowFetchContext, WindowFetchContext, WindowTaskTarget};
-pub(crate) use operation_receiver::{WindowOperationReceiver, WindowOperationReceiverCaptureError};
+pub(crate) use operation_receiver::{
+ WindowOperationReceiver, WindowOperationReceiverCaptureError, marked_window_dispatch_scope,
+};
pub(crate) use registry::{
WindowExecutionContextAccessPolicy, WindowExecutionContextIdentity, WindowExecutionContextOwner,
};
diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/operation_receiver.rs b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/operation_receiver.rs
index dc52daa5cb6..1d893c5f4c9 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/operation_receiver.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/operation_receiver.rs
@@ -119,7 +119,7 @@ impl WindowOperationReceiver {
}
}
-fn marked_window_dispatch_scope(
+pub(crate) fn marked_window_dispatch_scope(
scope: &mut v8::PinScope<'_, '_>,
receiver: v8::Local<'_, v8::Object>,
) -> Option {
diff --git a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs
index b6ecf3cc0e2..1bbc01947dd 100644
--- a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs
+++ b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs
@@ -476,23 +476,30 @@ fn browsing_context_dispatch_scope_for_node(
.map(crate::native_bridge::OwnerDispatchScope::Child)
}
-fn navigate_special_target_from_window<'s>(
+fn navigate_special_target_for_dispatch_scope<'s>(
scope: &mut v8::PinScope<'s, '_>,
runtime_ptr: *mut JsContextHost,
- source_window: v8::Local<'s, v8::Object>,
+ dispatch_scope: crate::native_bridge::OwnerDispatchScope,
target: Option,
resolved_url: &str,
) -> Option> {
+ use crate::native_bridge::OwnerDispatchScope;
let global = scope.get_current_context().global(scope);
- let target_window = match target {
- None | Some(SpecialBrowsingContextTarget::Current) => source_window,
- Some(SpecialBrowsingContextTarget::Top) => source_window
- .get(scope, v8str(scope, "top").into())
- .and_then(|value| v8::Local::::try_from(value).ok())?,
- Some(SpecialBrowsingContextTarget::Parent) => source_window
- .get(scope, v8str(scope, "parent").into())
- .and_then(|value| v8::Local::::try_from(value).ok())?,
- Some(SpecialBrowsingContextTarget::Blank) => return None,
+ let target_window = match (dispatch_scope, target) {
+ (_, Some(SpecialBrowsingContextTarget::Blank)) => return None,
+ (
+ OwnerDispatchScope::Child(handle),
+ Some(SpecialBrowsingContextTarget::Parent | SpecialBrowsingContextTarget::Top),
+ ) => {
+ let (parent, top) = unsafe { &mut *runtime_ptr }
+ .child_browsing_context_parent_top_for_realm_global(scope, handle, global);
+ if target == Some(SpecialBrowsingContextTarget::Parent) {
+ parent
+ } else {
+ top
+ }
+ }
+ _ => browsing_context_window_for_dispatch_scope(scope, runtime_ptr, dispatch_scope)?,
};
let navigated = if resolved_url.is_empty() {
true
@@ -516,12 +523,10 @@ pub(crate) fn navigate_existing_browsing_context_target<'s>(
"a new-context target cannot use existing-context navigation"
);
let dispatch_scope = unsafe { &*runtime_ptr }.entered_owner_dispatch_scope(scope);
- let source_window =
- browsing_context_window_for_dispatch_scope(scope, runtime_ptr, dispatch_scope)?;
- navigate_special_target_from_window(
+ navigate_special_target_for_dispatch_scope(
scope,
runtime_ptr,
- source_window,
+ dispatch_scope,
Some(target),
resolved_url,
)
@@ -542,16 +547,11 @@ pub(super) fn navigate_hyperlink_source_browsing_context(
crate::native_bridge::OwnerDispatchScope::Top => false,
crate::native_bridge::OwnerDispatchScope::Child(handle) => unsafe { &mut *runtime_ptr }
.navigate_child_browsing_context_to_url(scope, handle, resolved_url),
- crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => {
- let Some(source_window) =
- unsafe { &*runtime_ptr }.lightweight_popup_window(scope, popup_id)
- else {
- return false;
- };
- navigate_special_target_from_window(
+ crate::native_bridge::OwnerDispatchScope::LightweightPopup(_) => {
+ navigate_special_target_for_dispatch_scope(
scope,
runtime_ptr,
- source_window,
+ dispatch_scope,
Some(SpecialBrowsingContextTarget::Current),
resolved_url,
)
@@ -586,15 +586,10 @@ pub(crate) fn navigate_target_browsing_context<'s>(
}
None => {
let dispatch_scope = unsafe { &*runtime_ptr }.entered_owner_dispatch_scope(scope);
- let Some(source_window) =
- browsing_context_window_for_dispatch_scope(scope, runtime_ptr, dispatch_scope)
- else {
- return false;
- };
- navigate_special_target_from_window(
+ navigate_special_target_for_dispatch_scope(
scope,
runtime_ptr,
- source_window,
+ dispatch_scope,
None,
resolved_url,
)
@@ -680,15 +675,10 @@ pub(in crate::native_bridge) fn navigate_hyperlink_target_browsing_context<'s>(
else {
return false;
};
- let Some(source_window) =
- browsing_context_window_for_dispatch_scope(scope, runtime_ptr, dispatch_scope)
- else {
- return false;
- };
- navigate_special_target_from_window(
+ navigate_special_target_for_dispatch_scope(
scope,
runtime_ptr,
- source_window,
+ dispatch_scope,
special_target,
resolved_url,
)
diff --git a/moli-renderer-v8/src/native_bridge/mod.rs b/moli-renderer-v8/src/native_bridge/mod.rs
index 7e677c128d6..211ffe61574 100644
--- a/moli-renderer-v8/src/native_bridge/mod.rs
+++ b/moli-renderer-v8/src/native_bridge/mod.rs
@@ -8,7 +8,7 @@ mod context_host;
mod current_input;
pub(crate) use context_host::{
JsContextHost, JsContextHostPageTaskCapabilities, PendingScrollObservableEffects,
- PostParseAutofocusAdmission, ServiceWorkerWindowOwner,
+ PostParseAutofocusAdmission, ServiceWorkerWindowOwner, marked_window_dispatch_scope,
};
pub(crate) use current_input::{
CurrentInputEvent, CurrentInputEventScope, InputNavigationPolicy, navigation_policy_from_event,
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs
index 74eaa1dcbf0..bbf750d2b63 100644
--- a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs
@@ -314,12 +314,14 @@ async fn spawn_about_blank_popup_storage_child_server() -> (String, JoinHandle
"#;
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/window_security_and_domain.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/window_security_and_domain.rs
index 5049388dcfb..058de7fbcdb 100644
--- a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/window_security_and_domain.rs
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/window_security_and_domain.rs
@@ -1,5 +1,64 @@
use super::*;
+#[tokio::test]
+async fn cross_origin_window_open_special_targets_preserve_allowed_proxy_surface() {
+ for nested in [false, true] {
+ let leaf = r#""#
+ .to_owned();
+ let mut bodies = Vec::new();
+ if nested {
+ bodies.push(
+ r#""#
+ .to_owned(),
+ );
+ }
+ bodies.push(leaf);
+ let server = StaticHttpServer::spawn_with_bodies(bodies).await;
+ let loader = static_http_loader([]);
+ let mut vm = new_storage_page_task_executor_test_vm_with_loader(
+ "https://different-origin.test/parent",
+ &loader,
+ );
+ vm.eval(&format!(
+ r#"
+globalThis.results = [];
+addEventListener('message', event => results.push(event.data));
+const frame = document.createElement('iframe');
+frame.src = {:?};
+(document.body || document.documentElement).append(frame);
+"#,
+ server.base_url().join("/child").unwrap().as_str()
+ ))
+ .unwrap();
+ advance_page_task_executor_until_eval_equals(
+ &mut vm,
+ &loader,
+ "String(results.length)",
+ "2",
+ "special target messages",
+ )
+ .await;
+ assert_eq!(
+ vm.eval("JSON.stringify(results.sort((a, b) => a.target.localeCompare(b.target)))")
+ .unwrap(),
+ if nested {
+ r#"[{"target":"_parent","denied":false,"same":true,"isTop":false},{"target":"_top","denied":true,"same":false,"isTop":true}]"#
+ } else {
+ r#"[{"target":"_parent","denied":true,"same":true,"isTop":true},{"target":"_top","denied":true,"same":true,"isTop":true}]"#
+ }
+ );
+ }
+}
+
#[tokio::test]
async fn same_origin_nested_children_share_window_security_token_without_top_access() {
let (root_url, server) = spawn_nested_same_origin_window_access_server().await;
@@ -1537,8 +1596,21 @@ async fn third_party_about_blank_popup_does_not_reuse_opener_or_first_party_stor
assert_eq!(
vm.eval("globalThis.__aboutBlankPopupStorageMessage || 'missing'")
.expect("about:blank popup message should evaluate"),
- r#"{"popupBefore":null,"popupAfter":"popup-first-party","childAfter":"child-partition","opener":true}"#
- );
+ r#"{"popupBefore":null,"popupAfter":"popup-first-party","childAfter":"child-partition","opener":true,"isolatedReturn":true}"#
+ );
+ {
+ let host = vm._context_host.borrow();
+ let ordinary = host
+ .lightweight_popup_initial_empty_document_storage_key(1)
+ .expect("ordinary popup storage identity");
+ let isolated = host
+ .lightweight_popup_initial_empty_document_storage_key(2)
+ .expect("noopener popup storage identity");
+ assert_eq!(
+ ordinary, isolated,
+ "creator storage identity must survive noopener"
+ );
+ }
assert_eq!(
vm.eval("localStorage.getItem('popup-scope')")
.expect("top localStorage should evaluate"),
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/popup_window.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/popup_window.rs
index af6f17cb37b..65167cdf01b 100644
--- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/popup_window.rs
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/popup_window.rs
@@ -658,6 +658,277 @@ fn window_open_named_lightweight_popup_reuses_without_recloning_session_storage(
r#"{"sameWindow":true,"popupName":"sessionStorageTestWindow","initial":null,"openerFoo":"BAR","popupFoo":null}"#
);
}
+
+#[test]
+fn window_open_does_not_reuse_named_noopener_popup_from_an_isolated_group() {
+ let mut vm = new_storage_test_vm("https://example.com/");
+
+ assert_eq!(
+ vm.eval("open('about:blank#isolated', 'report', 'noopener') === null")
+ .expect("isolated named popup should evaluate"),
+ "true"
+ );
+ let first = vm.take_pending_popup_activations();
+ assert_eq!(first.len(), 1);
+ let first_popup_id = first[0].popup_id().expect("lightweight popup id");
+
+ assert_eq!(
+ vm.eval(
+ "(() => { const popup = open('about:blank#related', 'report'); return [popup.name, popup.location.href].join('|'); })()"
+ )
+ .expect("related named popup should evaluate"),
+ "report|about:blank#related"
+ );
+ let second = vm.take_pending_popup_activations();
+ assert_eq!(second.len(), 1);
+ assert_ne!(
+ second[0].popup_id(),
+ Some(first_popup_id),
+ "an isolated noopener popup is not a reusable named target for its creator"
+ );
+}
+
+#[test]
+fn window_open_noopener_reuses_a_related_named_popup_but_returns_null() {
+ let mut vm = new_storage_test_vm("https://example.com/");
+
+ let first = vm
+ .eval("globalThis.popup = open('about:blank#related', 'report'); popup.location.href")
+ .expect("related named popup should evaluate");
+ assert_eq!(first, "about:blank#related");
+ let first_activation = vm.take_pending_popup_activations();
+ let first_popup_id = first_activation[0].popup_id().expect("first popup id");
+
+ assert_eq!(
+ vm.eval("open('about:blank#isolated', 'report', 'noopener') === null")
+ .expect("isolated named popup should evaluate"),
+ "true"
+ );
+ let second_activation = vm.take_pending_popup_activations();
+ assert_eq!(second_activation.len(), 1);
+ assert_eq!(second_activation[0].popup_id(), Some(first_popup_id));
+ assert_eq!(
+ vm.eval("JSON.stringify([popup.location.href, popup.opener === window])")
+ .expect("reused popup state should evaluate"),
+ r#"["about:blank#isolated",true]"#
+ );
+}
+
+#[test]
+fn named_popup_group_survives_intermediate_opener_close() {
+ let mut vm = new_storage_test_vm("https://example.com/");
+
+ assert_eq!(
+ vm.eval(
+ "(() => { const parent = open('', 'parent'); const child = parent.open('', 'child'); parent.close(); return open('', 'child') === child; })()"
+ )
+ .expect("popup group should survive opener close"),
+ "true"
+ );
+}
+
+#[test]
+fn window_open_empty_url_selects_named_source_without_navigation() {
+ let mut vm = new_storage_test_vm("https://example.com/source");
+ assert_eq!(
+ vm.eval(
+ "window.name = 'self'; globalThis.marker = 7; const selected = open('', 'self'); JSON.stringify([selected === window, location.href, marker]);"
+ )
+ .expect("empty URL named-source selection should evaluate"),
+ r#"[true,"https://example.com/source",7]"#
+ );
+ assert!(vm.take_pending_location_navigation_with_seed().is_none());
+}
+
+#[test]
+fn window_open_empty_parent_uses_internal_identity_without_running_public_lookup() {
+ let mut vm = new_storage_test_vm("https://example.com/source");
+ assert_eq!(
+ vm.eval(
+ r#"
+const fake = { fake: true };
+let parentReads = 0;
+Object.defineProperty(window, "parent", {
+ configurable: true,
+ get() { ++parentReads; return fake; }
+});
+globalThis.marker = 9;
+const selected = open("", "_parent");
+JSON.stringify([selected === window, selected === fake, location.href, marker, parentReads]);
+"#,
+ )
+ .expect("empty URL parent selection should evaluate"),
+ r#"[true,false,"https://example.com/source",9,0]"#
+ );
+ assert!(vm.take_pending_location_navigation_with_seed().is_none());
+}
+
+#[test]
+fn borrowed_popup_open_parent_and_top_use_the_popup_receiver_identity() {
+ let mut vm = new_storage_test_vm("https://example.com/source");
+ assert_eq!(
+ vm.eval(
+ r#"
+const popup = open("about:blank#popup", "report");
+const selectedParent = popup.open("", "_parent");
+const selectedTop = popup.open("", "_top");
+JSON.stringify([
+ selectedParent === popup,
+ selectedTop === popup,
+ selectedParent === window,
+ selectedTop === window,
+ popup.location.href
+]);
+"#,
+ )
+ .expect("borrowed popup special-target selection should evaluate"),
+ r#"[true,true,false,false,"about:blank#popup"]"#
+ );
+ assert!(vm.take_pending_location_navigation_with_seed().is_none());
+}
+
+#[tokio::test]
+async fn child_window_open_empty_parent_and_top_use_internal_browsing_context_identity() {
+ let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
+ let mut vm =
+ new_storage_page_task_executor_test_vm_with_loader("https://example.com/source", &loader);
+ vm.eval(
+ r#"
+globalThis.frame = document.createElement("iframe");
+frame.srcdoc = "child
";
+(document.body || document.documentElement || document).appendChild(frame);
+"#,
+ )
+ .expect("child frame setup should evaluate");
+ vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128)
+ .await
+ .expect("child frame should commit");
+
+ assert_eq!(
+ vm.eval(
+ r#"
+frame.contentWindow.__expectedParent = window;
+frame.contentWindow.eval(`
+ const realParent = window.__expectedParent;
+ const realTop = window.__expectedParent;
+ const fake = {};
+ let publicParentReads = 0;
+ let internalParentReads = 0;
+ let internalTopReads = 0;
+ Object.defineProperty(window, "parent", {
+ configurable: true,
+ get() { ++publicParentReads; return fake; }
+ });
+ Object.defineProperty(window, "__moliWindowParent", {
+ configurable: true,
+ get() { ++internalParentReads; return fake; }
+ });
+ Object.defineProperty(window, "__moliWindowTop", {
+ configurable: true,
+ get() { ++internalTopReads; return fake; }
+ });
+ const selectedParent = open("", "_parent");
+ const selectedTop = open("", "_top");
+ JSON.stringify([
+ selectedParent === realParent,
+ selectedTop === realTop,
+ selectedParent === fake,
+ selectedTop === fake,
+ publicParentReads,
+ internalParentReads,
+ internalTopReads,
+ location.href
+ ]);
+`)
+"#,
+ )
+ .expect("child special-target selection should evaluate"),
+ r#"[true,true,false,false,0,0,0,"about:srcdoc"]"#
+ );
+ assert!(vm.take_pending_location_navigation_with_seed().is_none());
+}
+
+#[test]
+fn window_open_empty_url_reuses_named_popup_without_navigation() {
+ let mut vm = new_storage_test_vm("https://example.com/source");
+ assert_eq!(
+ vm.eval(
+ "globalThis.popup = open('about:blank#kept', 'report'); const selected = open('', 'report'); JSON.stringify([selected === popup, popup.location.href]);"
+ )
+ .expect("empty URL named-popup selection should evaluate"),
+ r#"[true,"about:blank#kept"]"#
+ );
+ let activations = vm.take_pending_popup_activations();
+ assert_eq!(activations.len(), 2);
+ assert_eq!(activations[0].popup_id(), activations[1].popup_id());
+}
+
+#[test]
+fn window_open_prefers_the_same_named_source_over_a_related_popup() {
+ let mut vm = new_storage_test_vm("https://example.com/source");
+
+ assert_eq!(
+ vm.eval(
+ r#"
+(() => {
+ const popup = open("about:blank#popup", "shared");
+ window.name = "shared";
+ const selected = open("about:blank#self", "shared");
+ return JSON.stringify({
+ selectedSelf: selected === window,
+ popupHref: popup.location.href,
+ popupName: popup.name
+ });
+})()
+"#,
+ )
+ .expect("same-named source selection should evaluate"),
+ r#"{"selectedSelf":true,"popupHref":"about:blank#popup","popupName":"shared"}"#
+ );
+ let activations = vm.take_pending_popup_activations();
+ assert_eq!(
+ activations.len(),
+ 1,
+ "the second open must navigate the source instead of reopening the popup"
+ );
+ let navigation = vm
+ .take_pending_location_navigation_with_seed()
+ .expect("same-named source navigation");
+ assert_eq!(navigation.url.as_str(), "about:blank#self");
+}
+
+#[test]
+fn window_open_noopener_prefers_the_same_named_source_and_returns_null() {
+ let mut vm = new_storage_test_vm("https://example.com/source");
+
+ assert_eq!(
+ vm.eval(
+ r#"
+const popup = open("about:blank#popup", "shared");
+window.name = "shared";
+open("about:blank#self", "shared", "noopener") === null;
+"#,
+ )
+ .expect("noopener same-named source selection should evaluate"),
+ "true"
+ );
+ let activations = vm.take_pending_popup_activations();
+ assert_eq!(
+ activations.len(),
+ 1,
+ "the noopener call must navigate the source instead of reopening the popup"
+ );
+ assert_eq!(
+ vm.eval("popup.location.href")
+ .expect("existing popup URL should evaluate"),
+ "about:blank#popup"
+ );
+ let navigation = vm
+ .take_pending_location_navigation_with_seed()
+ .expect("noopener same-named source navigation");
+ assert_eq!(navigation.url.as_str(), "about:blank#self");
+}
+
#[tokio::test]
async fn window_open_named_lightweight_popup_reuse_pushes_history_and_back_traverses() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/window_surface.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/window_surface.rs
index 5acb350809d..10931ee775d 100644
--- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/window_surface.rs
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/window_surface.rs
@@ -833,3 +833,111 @@ fn window_dialog_and_open_arguments_use_webidl_conversion() {
"undefined|TypeError|false|RangeError|null|TypeError|[object Window]|TypeError|TypeError|RangeError"
);
}
+#[test]
+fn window_name_accessors_reject_non_window_receivers_without_mutating_the_window() {
+ let mut vm = new_storage_test_vm("https://example.com/");
+
+ let result = vm
+ .eval(
+ r#"
+ (() => {
+ const descriptor = Object.getOwnPropertyDescriptor(window, "name");
+ const outcome = callback => {
+ try { callback(); return "ok"; }
+ catch (error) { return error.name; }
+ };
+ const before = window.name;
+ return JSON.stringify({
+ setter: outcome(() => descriptor.set.call({}, "forged")),
+ getter: outcome(() => descriptor.get.call({})),
+ before,
+ after: window.name
+ });
+ })()
+ "#,
+ )
+ .expect("window.name illegal receiver probe should evaluate");
+
+ assert_eq!(
+ result,
+ r#"{"setter":"TypeError","getter":"TypeError","before":"","after":""}"#
+ );
+}
+
+#[test]
+fn window_name_accessors_follow_the_receiver_realm_and_preserve_state_on_conversion_errors() {
+ let mut vm = new_storage_test_vm("https://example.com/");
+
+ let result = vm
+ .eval(
+ r#"
+ (() => {
+ const frame = document.createElement("iframe");
+ (document.body || document.documentElement || document).appendChild(frame);
+ const child = frame.contentWindow;
+ const parentDescriptor = Object.getOwnPropertyDescriptor(window, "name");
+ const childDescriptor = Object.getOwnPropertyDescriptor(child, "name");
+
+ parentDescriptor.set.call(child, "child-from-parent-realm");
+ childDescriptor.set.call(window, "parent-from-child-realm");
+ const borrowedParent = childDescriptor.get.call(window);
+ const borrowedChild = parentDescriptor.get.call(child);
+
+ const popup = window.open("about:blank", "popup-owner");
+ const borrowedPopupBefore = parentDescriptor.get.call(popup);
+ parentDescriptor.set.call(popup, "renamed-popup-owner");
+ const borrowedPopupAfter = parentDescriptor.get.call(popup);
+ const openerAfterPopupRename = window.name;
+
+ const iterator = document.createNodeIterator(document);
+ let invalidReceiverGetter;
+ let invalidReceiverSetter;
+ let invalidReceiverConverted = false;
+ try {
+ parentDescriptor.get.call(iterator);
+ } catch (error) {
+ invalidReceiverGetter = error.name;
+ }
+ try {
+ parentDescriptor.set.call(iterator, {
+ toString() {
+ invalidReceiverConverted = true;
+ return "forged";
+ }
+ });
+ } catch (error) {
+ invalidReceiverSetter = error.name;
+ }
+
+ let conversionError;
+ try {
+ childDescriptor.set.call(window, {
+ toString() { throw new RangeError("window-name-conversion"); }
+ });
+ } catch (error) {
+ conversionError = `${error.name}:${error.message}`;
+ }
+
+ return JSON.stringify({
+ borrowedParent,
+ borrowedChild,
+ borrowedPopupBefore,
+ borrowedPopupAfter,
+ openerAfterPopupRename,
+ invalidReceiverGetter,
+ invalidReceiverSetter,
+ invalidReceiverConverted,
+ parentAfterError: window.name,
+ childAfterError: child.name,
+ conversionError
+ });
+ })()
+ "#,
+ )
+ .expect("borrowed Window.name accessors should preserve their receiver realm");
+
+ assert_eq!(
+ result,
+ r#"{"borrowedParent":"parent-from-child-realm","borrowedChild":"child-from-parent-realm","borrowedPopupBefore":"popup-owner","borrowedPopupAfter":"renamed-popup-owner","openerAfterPopupRename":"parent-from-child-realm","invalidReceiverGetter":"TypeError","invalidReceiverSetter":"TypeError","invalidReceiverConverted":false,"parentAfterError":"parent-from-child-realm","childAfterError":"child-from-parent-realm","conversionError":"RangeError:window-name-conversion"}"#
+ );
+}
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 1cf3e780679..4feffb0a72f 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
@@ -1,5 +1,45 @@
use super::*;
+#[tokio::test]
+async fn noopener_named_hyperlink_reuses_an_existing_iframe() {
+ let loader = static_http_loader([]);
+ let mut vm = new_storage_page_task_executor_test_vm_with_loader(
+ "https://example.com/page.html",
+ &loader,
+ );
+ vm.eval(
+ r#"
+const body = document.body;
+const frame = document.createElement('iframe');
+frame.name = 'report';
+body.append(frame);
+const link = document.createElement('a');
+link.target = 'report';
+link.rel = 'noopener';
+link.href = 'about:blank#isolated';
+body.append(link);
+link.click();
+"#,
+ )
+ .expect("noopener named hyperlink should evaluate");
+
+ advance_page_task_executor_until_eval_equals(
+ &mut vm,
+ &loader,
+ "frame.contentWindow.location.href",
+ "about:blank#isolated",
+ "named iframe navigation",
+ )
+ .await;
+
+ assert_eq!(
+ vm.eval("frame.contentWindow.location.href")
+ .expect("iframe URL should evaluate"),
+ "about:blank#isolated"
+ );
+ assert!(vm.take_pending_popup_activations().is_empty());
+}
+
#[tokio::test]
async fn named_element_navigation_prefers_its_source_frame_over_duplicate_names() {
for action in ["anchor", "submit", "requestSubmit"] {