diff --git a/moli-protocol/src/domains/page/tests/runtime.rs b/moli-protocol/src/domains/page/tests/runtime.rs index ab651a209..ca2cad22c 100644 --- a/moli-protocol/src/domains/page/tests/runtime.rs +++ b/moli-protocol/src/domains/page/tests/runtime.rs @@ -1040,6 +1040,15 @@ async fn runtime_add_binding_default_world_replays_into_runtime_materialized_chi ) .await; + // V8 replays bindings into future contexts while Runtime is enabled. + ctx.process_async(json!({ + "id": 420, + "method": "Runtime.enable", + "sessionId": "SID-1", + })) + .await; + ctx.expect_result(420, json!({}), Some("SID-1")); + ctx.process_async(json!({ "id": 421, "method": "Runtime.addBinding", @@ -1384,3 +1393,93 @@ async fn runtime_evaluate_child_history_back_emits_child_frame_navigation_and_li ) .await; } + +#[tokio::test(flavor = "multi_thread")] +async fn attached_runtime_binding_reports_child_calls_without_primary_registration() { + let mut ctx = TestContext::new(); + load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); + ctx.enable_page_events_for_test(Some("SID-1")); + ctx.process_async(json!({ + "id": 48_000, + "method": "Target.attachToTarget", + "params": {"targetId": "TID-1", "flatten": true} + })) + .await; + let attached_session_id = take_response_by_id(&mut ctx, 48_000)["result"]["sessionId"] + .as_str() + .expect("second target session") + .to_owned(); + assert_ne!(attached_session_id, "SID-1"); + ctx.process_async(json!({ + "id": 48_001, + "method": "Runtime.enable", + "sessionId": attached_session_id, + })) + .await; + ctx.expect_result(48_001, json!({}), Some(&attached_session_id)); + ctx.process_async(json!({ + "id": 48_002, + "method": "Runtime.addBinding", + "sessionId": attached_session_id, + "params": {"name": "attachedChildBinding"} + })) + .await; + ctx.expect_result(48_002, json!({}), Some(&attached_session_id)); + ctx.process_async(json!({ + "id": 48_003, + "method": "Page.navigate", + "sessionId": "SID-1", + "params": {"url": "data:text/html,"} + })) + .await; + let _ = take_response_by_id(&mut ctx, 48_003); + let frame_id = child_frame_id_for_single_iframe(&mut ctx, 48_004).await; + wait_until_frame_stopped_loading(&mut ctx, &frame_id).await; + let child_context_id = wait_for_child_default_execution_context_id( + &mut ctx, + &frame_id, + "attached child binding context", + ) + .await; + for (offset, scoped) in [(0_u64, false), (3, true)] { + if scoped { + ctx.process_async(json!({ + "id": 48_005 + offset, + "method": "Runtime.addBinding", + "sessionId": attached_session_id, + "params": {"name": "attachedChildBinding", "executionContextId": child_context_id} + })) + .await; + ctx.expect_result(48_005 + offset, json!({}), Some(&attached_session_id)); + } + ctx.sent.clear(); + ctx.process_async(json!({ + "id": 48_006 + offset, + "method": "Runtime.evaluate", + "sessionId": "SID-1", + "params": {"expression": "frames[0].attachedChildBinding('child-call'); 'called'"} + })) + .await; + assert_eq!( + take_response_by_id(&mut ctx, 48_006 + offset)["result"]["result"]["value"], + json!("called") + ); + let events = ctx + .sent + .iter() + .filter(|message| message["method"] == "Runtime.bindingCalled") + .collect::>(); + assert_eq!( + events.len(), + 1, + "one notification for the registering session (scoped={scoped}): {events:?}" + ); + assert_eq!(events[0]["sessionId"], json!(attached_session_id)); + assert_eq!(events[0]["params"]["name"], "attachedChildBinding"); + assert_eq!(events[0]["params"]["payload"], "child-call"); + assert_eq!( + events[0]["params"]["executionContextId"], + json!(child_context_id) + ); + } +} diff --git a/moli-protocol/src/domains/runtime/dispatcher.rs b/moli-protocol/src/domains/runtime/dispatcher.rs index 4cbf0a5b6..b9b8b2741 100644 --- a/moli-protocol/src/domains/runtime/dispatcher.rs +++ b/moli-protocol/src/domains/runtime/dispatcher.rs @@ -8654,46 +8654,24 @@ fn complete_pending_runtime_binding_context_lookup_command( mut task: RuntimeBindingCommandTask, completed_lookup: Result, ) -> RuntimeCommandTaskStep { - let is_child_default_context = match completed_lookup { - Ok(completed_lookup) => { - match conn.complete_child_default_execution_context_lookup(completed_lookup) { - Ok(is_child_default_context) => is_child_default_context, - Err(message) => { - return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan( - completed.command_id, - message, - )); - } - } - } - Err(message) => { - return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan( - completed.command_id, - message, - )); - } - }; - - if !is_child_default_context { - if matches!(task.action, RuntimeBindingCommand::Add) - || matches!(task.action, RuntimeBindingCommand::Remove) - && runtime_remove_binding_should_skip_live_page_update(conn, &completed.owner_scope) - { - task.skip_live_page_update_after_inspector_success = true; - } - return start_pending_runtime_binding_inspector_phase(conn, &completed, task); + if let Err(message) = completed_lookup + .and_then(|lookup| conn.complete_child_default_execution_context_lookup(lookup)) + { + return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan( + completed.command_id, + message, + )); } - task.command_response = Some(RuntimeBindingCommandResponse::empty_success()); - match start_pending_runtime_binding_page_phase( - conn, - completed.command_id, - task.clone(), - completed.owner_scope.clone(), - ) { - Some(pending) => RuntimeCommandTaskStep::Pending(Box::new(pending)), - None => complete_runtime_binding_after_live_update(conn, completed, task), + // Child default realms are registered V8 Inspector contexts too. Installing + // a native callback here would bypass the registering Inspector session. + if matches!(task.action, RuntimeBindingCommand::Add) + || matches!(task.action, RuntimeBindingCommand::Remove) + && runtime_remove_binding_should_skip_live_page_update(conn, &completed.owner_scope) + { + task.skip_live_page_update_after_inspector_success = true; } + start_pending_runtime_binding_inspector_phase(conn, &completed, task) } async fn complete_pending_runtime_binding_inspector_command( diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs index 343d0cef7..8f6ad81d1 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs @@ -461,10 +461,15 @@ impl JsContextHost { self.stored_document_start_scripts.clone() } - pub(crate) fn stored_default_runtime_binding_names(&self) -> Vec { + pub(crate) fn stored_default_native_runtime_binding_names(&self) -> Vec { self.stored_runtime_bindings .iter() - .filter(|binding| binding.execution_context_name.is_none()) + // Inspector installs session-owned bindings when each V8 context + // is reported. Replacing them with a native callback would lose + // the registering sessions and their context filters. + .filter(|binding| { + binding.devtools_session.is_none() && binding.execution_context_name.is_none() + }) .map(|binding| binding.name.clone()) .collect() } diff --git a/moli-renderer-v8/src/native_bridge/context_host/runtime_bindings.rs b/moli-renderer-v8/src/native_bridge/context_host/runtime_bindings.rs index cf57b720d..57a1ea4b4 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/runtime_bindings.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/runtime_bindings.rs @@ -42,6 +42,10 @@ impl JsContextHost { handle: DomHandle, window: v8::Local<'_, v8::Object>, ) { + let binding_names = self.stored_default_native_runtime_binding_names(); + if binding_names.is_empty() { + return; + } let execution_context_id = self.child_default_execution_context_id(handle).unwrap_or(0); let Some(document_owner) = self.current_child_document_task_owner(handle) else { return; @@ -54,12 +58,6 @@ impl JsContextHost { if !self.register_runtime_binding_execution_context(execution_context, document_owner) { return; } - let binding_names = self - .stored_runtime_bindings - .iter() - .filter(|binding| binding.execution_context_name.is_none()) - .map(|binding| binding.name.clone()) - .collect::>(); for binding_name in binding_names { let Some(key) = v8_string(scope, &binding_name) else { continue; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/child_realm_materialization_completion.rs b/moli-renderer-v8/src/runtime/page_vm/tests/child_realm_materialization_completion.rs index afcd69140..9acdcea3c 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/child_realm_materialization_completion.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/child_realm_materialization_completion.rs @@ -213,9 +213,18 @@ async fn detached_session_binding_is_not_installed_in_a_later_child_realm() { binding(session_b, "liveChildBinding"), ]; page_vm.set_stored_runtime_bindings(&bindings); - page_vm - .vm_mut() - .ensure_runtime_inspector_session(Some("session-a")); + for (session_id, name) in [ + ("session-a", "detachedChildBinding"), + ("session-b", "liveChildBinding"), + ] { + page_vm + .vm_mut() + .dispatch_inspector_protocol_message_for_session( + Some(session_id), + r#"{"id":1,"method":"Runtime.enable"}"#, + )?; + page_vm.add_runtime_binding(Some(session_id), name, None, None)?; + } assert!(page_vm.detach_runtime_inspector_session(Some("session-a"))); queue_child_realm_materialization(&mut page_vm, "post-detach-binding-child")?; diff --git a/moli-renderer-v8/src/script_vm/child_frame_realm_materialization.rs b/moli-renderer-v8/src/script_vm/child_frame_realm_materialization.rs index a994b2fee..61f8add25 100644 --- a/moli-renderer-v8/src/script_vm/child_frame_realm_materialization.rs +++ b/moli-renderer-v8/src/script_vm/child_frame_realm_materialization.rs @@ -327,7 +327,7 @@ impl ScriptVm { let binding_names = self ._context_host .borrow() - .stored_default_runtime_binding_names(); + .stored_default_native_runtime_binding_names(); for name in binding_names { if let Err(error) = self.install_runtime_binding_in_child_default_context(execution_context_id, &name)