diff --git a/moli-core/src/runtime/navigation_engine.rs b/moli-core/src/runtime/navigation_engine.rs index 09020f4b56..84c34036e8 100644 --- a/moli-core/src/runtime/navigation_engine.rs +++ b/moli-core/src/runtime/navigation_engine.rs @@ -455,9 +455,9 @@ pub struct NavigationRuntimeConfig { subframe_loading_enabled: bool, layout_configuration: LayoutConfiguration, debugger_enabled: bool, - /// CLI `--document-start-script` sources. `moli fetch` reads them straight from - /// `BrowserConfig`; `moli serve` only sees this runtime config, so they must travel - /// here or every protocol-server page silently skips them. + /// Startup sources transported from BrowserConfig to protocol connections. + /// The connection owns them before any engine is materialized; they are + /// BrowserContext initialization policy, not NavigationEngine state. document_start_scripts: Vec, } @@ -745,8 +745,7 @@ impl NavigationEngine { subframe_loading_enabled, layout_configuration, debugger_enabled, - // Not engine state: the protocol server seeds these into each browser - // context's default document-start registry when it binds the config. + // Protocol connections capture startup scripts before creating engines. document_start_scripts: _, } = config; let resource_runtime = browser_context_access @@ -821,8 +820,7 @@ impl NavigationEngine { subframe_loading_enabled, layout_configuration, debugger_enabled, - // Not engine state: the protocol server seeds these into each browser - // context's default document-start registry when it binds the config. + // Protocol connections capture startup scripts before creating engines. document_start_scripts: _, } = config; renderer_owner_source diff --git a/moli-protocol-server/src/protocol_server/cdp.rs b/moli-protocol-server/src/protocol_server/cdp.rs index 4c882c4cb9..70313042f3 100644 --- a/moli-protocol-server/src/protocol_server/cdp.rs +++ b/moli-protocol-server/src/protocol_server/cdp.rs @@ -22,7 +22,7 @@ pub(super) async fn json_version(State(state): State) -> impl IntoResp "Browser": version::PRODUCT, "Moli-Version": version::MOLI_VERSION, "Protocol-Version": version::PROTOCOL_VERSION, - "User-Agent": state.fetch_config.user_agent(), + "User-Agent": state.navigation_runtime_config.fetch_config().user_agent(), "V8-Version": version::js_version(), "WebKit-Version": version::WEBKIT_VERSION, "webSocketDebuggerUrl": state.browser_ws_url, diff --git a/moli-protocol-server/src/protocol_server/mod.rs b/moli-protocol-server/src/protocol_server/mod.rs index 3db121fd65..0e3161120f 100644 --- a/moli-protocol-server/src/protocol_server/mod.rs +++ b/moli-protocol-server/src/protocol_server/mod.rs @@ -14,7 +14,7 @@ use axum::{ }; use moli_cookie_jar::StoredCookie; use moli_core::{ - LayoutConfiguration, LayoutPolicy, OptionalResourceFetchMask, + LayoutPolicy, OptionalResourceFetchMask, runtime::{NavigationRuntimeConfig, storage_partition::StoragePartitionState}, }; use moli_fetch::FetchConfig; @@ -557,8 +557,8 @@ struct AppState { addr: SocketAddr, control: BrowserInstanceControl, remote_allow_origins: Vec, - debugger_enabled: bool, browser_id: Uuid, + navigation_runtime_config: NavigationRuntimeConfig, browser_ws_url: String, bidi_ws_url: String, bidi_session_registry: SharedBidiSessionRegistry, @@ -566,10 +566,6 @@ struct AppState { cdp_agent_host_directory: SharedCdpAgentHostDirectory, cdp_owner_registry: SharedCdpOwnerRegistry, storage_partition: Arc, - fetch_config: FetchConfig, - optional_resource_fetch_mask: OptionalResourceFetchMask, - subframe_loading_enabled: bool, - layout_configuration: LayoutConfiguration, } impl AppState { @@ -617,17 +613,13 @@ impl AppState { remote_allow_origins: Vec::new(), browser_id, browser_ws_url: format!("ws://{addr}/devtools/browser/{browser_id}"), - debugger_enabled: navigation_runtime_config.debugger_enabled(), bidi_ws_url: format!("ws://{addr}/session"), bidi_session_registry: SharedBidiSessionRegistry::default(), classic_session_registry: SharedClassicSessionRegistry::default(), cdp_agent_host_directory, cdp_owner_registry, storage_partition, - fetch_config: navigation_runtime_config.fetch_config().clone(), - optional_resource_fetch_mask: navigation_runtime_config.optional_resource_fetch_mask(), - subframe_loading_enabled: navigation_runtime_config.subframe_loading_enabled(), - layout_configuration: navigation_runtime_config.layout_configuration(), + navigation_runtime_config, }) } diff --git a/moli-protocol-server/src/protocol_server/tests/mod.rs b/moli-protocol-server/src/protocol_server/tests/mod.rs index 8b4c2685c7..02b8fb6ae1 100644 --- a/moli-protocol-server/src/protocol_server/tests/mod.rs +++ b/moli-protocol-server/src/protocol_server/tests/mod.rs @@ -1345,4 +1345,5 @@ mod p6_output_handoff; mod page_endpoint; mod request_guard; mod screenshot; +mod startup_scripts; mod websocket; diff --git a/moli-protocol-server/src/protocol_server/tests/startup_scripts.rs b/moli-protocol-server/src/protocol_server/tests/startup_scripts.rs new file mode 100644 index 0000000000..db8b8dd025 --- /dev/null +++ b/moli-protocol-server/src/protocol_server/tests/startup_scripts.rs @@ -0,0 +1,292 @@ +use super::*; + +type TestSocket = + tokio_tungstenite::WebSocketStream>; + +const PROBE: &str = r#"JSON.stringify([window, frames[0]].map(w => [ + w.__startupRuns, w.__pageStartupRuns, w.__scriptOrder +]))"#; + +fn startup_config() -> NavigationRuntimeConfig { + protocol_server_test_runtime_config( + protocol_server_test_fetch_config(FetchConfig::default()), + OptionalResourceFetchMask::NONE, + ) + .with_document_start_scripts(vec![ + "globalThis.__startupRuns = (globalThis.__startupRuns || 0) + 1; globalThis.__scriptOrder = ['startup-a'];".to_owned(), + "globalThis.__scriptOrder.push('startup-b');".to_owned(), + ]) +} + +fn fixture() -> (String, DedicatedFixtureServer) { + let app = Router::new() + .route( + "/page", + get(|| async { + axum::response::Html(concat!( + "" + )) + }), + ) + .route( + "/child", + get(|| async { + axum::response::Html(concat!( + "child" + )) + }), + ); + let (addr, server) = spawn_dedicated_fixture_server(app, "startup-scripts"); + (format!("http://{addr}/page"), server) +} + +fn expected_probe(dynamic: Option<&str>) -> serde_json::Value { + let mut order = vec!["startup-a", "startup-b"]; + order.extend(dynamic); + order.push("page"); + json!([[1, 1, order], [1, 1, order]]) +} + +async fn http_json( + addr: std::net::SocketAddr, + method: &str, + path: &str, + body: serde_json::Value, +) -> serde_json::Value { + timeout(Duration::from_secs(15), async { + let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap(); + let body = body.to_string(); + stream.write_all(format!( + "{method} {path} HTTP/1.0\r\nHost: {addr}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}", body.len() + ).as_bytes()).await.unwrap(); + let mut bytes = Vec::new(); + stream.read_to_end(&mut bytes).await.unwrap(); + let response = String::from_utf8(bytes).unwrap(); + let (_, body) = response.split_once("\r\n\r\n").unwrap(); + serde_json::from_str(body).unwrap_or_else(|error| panic!("{method} {path}: {error}; {response}")) + }).await.expect("protocol HTTP response") +} + +async fn bidi_command( + socket: &mut TestSocket, + id: u64, + method: &str, + params: serde_json::Value, +) -> serde_json::Value { + let messages = send_cdp_command(socket, id, method, None, params).await; + let response = messages + .into_iter() + .find(|message| message["id"] == id) + .unwrap(); + assert_eq!(response["type"], "success", "{method}: {response}"); + response["result"].clone() +} + +#[tokio::test] +async fn startup_scripts_cdp_navigations_and_late_browser_contexts() { + let (url, _fixture) = fixture(); + let (addr, server) = spawn_test_protocol_server_with_runtime_config(startup_config()).await; + let version = http_json(addr, "GET", "/json/version", json!({})).await; + let (mut socket, _) = connect_async(version["webSocketDebuggerUrl"].as_str().unwrap()) + .await + .unwrap(); + let session = cdp_create_default_session_and_navigate(&mut socket, &url).await; + let probe = cdp_runtime_evaluate_string(&mut socket, &session, 10, PROBE).await; + assert_eq!( + serde_json::from_str::(&probe).unwrap(), + expected_probe(None) + ); + + // Creating these contexts happens after the standalone engine has materialized. + for base in [20, 40] { + let context = cdp_create_browser_context(&mut socket, base).await; + let target = cdp_create_attached_target(&mut socket, base + 1, &context).await; + let registered = send_cdp_command( + &mut socket, + base + 5, + "Page.addScriptToEvaluateOnNewDocument", + Some(&target.session_id), + json!({"source": "globalThis.__scriptOrder.push('target');"}), + ) + .await; + let identifier = registered + .iter() + .find(|message| message["id"] == base + 5) + .unwrap()["result"]["identifier"] + .as_str() + .unwrap() + .to_owned(); + for navigation in 0..2 { + if navigation == 1 { + let removed = send_cdp_command( + &mut socket, + base + 6, + "Page.removeScriptToEvaluateOnNewDocument", + Some(&target.session_id), + json!({"identifier": identifier}), + ) + .await; + assert!( + removed + .iter() + .any(|message| message["id"] == base + 6 && message["result"] == json!({})) + ); + } + cdp_navigate_and_wait_for_load( + &mut socket, + base + 3, + &target.session_id, + &format!("{url}?n={navigation}"), + ) + .await; + let probe = + cdp_runtime_evaluate_string(&mut socket, &target.session_id, base + 4, PROBE).await; + assert_eq!( + serde_json::from_str::(&probe).unwrap(), + expected_probe((navigation == 0).then_some("target")) + ); + } + } + socket.close(None).await.unwrap(); + abort_test_cdp_server(server).await; +} + +#[tokio::test] +async fn startup_scripts_bidi_navigations_and_preload_removal_are_isolated() { + let (url, _fixture) = fixture(); + let (addr, server) = spawn_test_protocol_server_with_runtime_config(startup_config()).await; + let (mut socket, _) = connect_async(format!("ws://{addr}/session")).await.unwrap(); + bidi_command(&mut socket, 1, "session.new", json!({})).await; + let tree = bidi_command(&mut socket, 2, "browsingContext.getTree", json!({})).await; + let first = tree["contexts"][0]["context"].as_str().unwrap().to_owned(); + + let messages = send_cdp_command( + &mut socket, + 3, + "script.removePreloadScript", + None, + json!({"script": "1"}), + ) + .await; + let response = messages.iter().find(|message| message["id"] == 3).unwrap(); + assert_eq!( + response["type"], "error", + "CLI scripts have no protocol ID: {response}" + ); + assert_eq!(response["error"], "no such script"); + let user = bidi_command(&mut socket, 4, "browser.createUserContext", json!({})).await; + let second = bidi_command( + &mut socket, + 5, + "browsingContext.create", + json!({ + "type": "tab", "userContext": user["userContext"] + }), + ) + .await; + let contexts = [first, second["context"].as_str().unwrap().to_owned()]; + let added = bidi_command( + &mut socket, + 6, + "script.addPreloadScript", + json!({ + "functionDeclaration": "() => { globalThis.__scriptOrder.push('dynamic'); }", + "userContexts": ["default", user["userContext"]] + }), + ) + .await; + + for dynamic in [Some("dynamic"), None] { + if dynamic.is_none() { + bidi_command( + &mut socket, + 7, + "script.removePreloadScript", + json!({"script": added["script"]}), + ) + .await; + } + for context in &contexts { + for navigation in 0..2 { + bidi_command(&mut socket, 8, "browsingContext.navigate", json!({ + "context": context, "url": format!("{url}?n={navigation}"), "wait": "complete" + })).await; + let result = bidi_command( + &mut socket, + 9, + "script.evaluate", + json!({ + "target": {"context": context}, "expression": PROBE, "awaitPromise": false + }), + ) + .await; + assert_eq!(result["type"], "success", "{result}"); + let probe: serde_json::Value = + serde_json::from_str(result["result"]["value"].as_str().unwrap()).unwrap(); + assert_eq!( + probe, + expected_probe(dynamic), + "context={context}, dynamic={dynamic:?}" + ); + } + } + } + socket.close(None).await.unwrap(); + abort_test_cdp_server(server).await; +} + +#[tokio::test] +async fn startup_scripts_classic_navigations_and_new_windows() { + let (url, _fixture) = fixture(); + let (addr, server) = spawn_test_protocol_server_with_runtime_config(startup_config()).await; + let session = http_json(addr, "POST", "/session", json!({})).await; + let session = session["value"]["sessionId"].as_str().unwrap(); + for window in 0..2 { + if window == 1 { + let new = http_json( + addr, + "POST", + &format!("/session/{session}/window/new"), + json!({"type": "tab"}), + ) + .await; + let switched = http_json( + addr, + "POST", + &format!("/session/{session}/window"), + json!({"handle": new["value"]["handle"]}), + ) + .await; + assert_eq!(switched, json!({"value": null})); + } + for navigation in 0..2 { + let navigated = http_json( + addr, + "POST", + &format!("/session/{session}/url"), + json!({"url": format!("{url}?n={navigation}")}), + ) + .await; + assert_eq!(navigated, json!({"value": null})); + let result = http_json( + addr, + "POST", + &format!("/session/{session}/execute/sync"), + json!({"script": format!("return {PROBE};"), "args": []}), + ) + .await; + let probe: serde_json::Value = + serde_json::from_str(result["value"].as_str().unwrap()).unwrap(); + assert_eq!(probe, expected_probe(None)); + } + } + let deleted = http_json(addr, "DELETE", &format!("/session/{session}"), json!({})).await; + assert_eq!(deleted, json!({"value": null})); + abort_test_cdp_server(server).await; +} diff --git a/moli-protocol-server/src/protocol_server/webdriver_bidi.rs b/moli-protocol-server/src/protocol_server/webdriver_bidi.rs index 83c998fbc4..f5082f5ca7 100644 --- a/moli-protocol-server/src/protocol_server/webdriver_bidi.rs +++ b/moli-protocol-server/src/protocol_server/webdriver_bidi.rs @@ -64,13 +64,7 @@ pub(super) async fn ws_bidi_session_upgrade_handler( let web_socket_url = state.bidi_ws_url; let session_registry = state.bidi_session_registry; let control = state.control; - let navigation_runtime_config = NavigationRuntimeConfig::new_with_layout_configuration( - state.fetch_config, - state.optional_resource_fetch_mask, - state.subframe_loading_enabled, - state.layout_configuration, - ) - .with_debugger_enabled(state.debugger_enabled); + let navigation_runtime_config = state.navigation_runtime_config; ws.on_upgrade(move |socket| { handle_bidi_session_socket( socket, diff --git a/moli-protocol-server/src/protocol_server/webdriver_classic.rs b/moli-protocol-server/src/protocol_server/webdriver_classic.rs index 70fd3af0d0..1dc34668c9 100644 --- a/moli-protocol-server/src/protocol_server/webdriver_classic.rs +++ b/moli-protocol-server/src/protocol_server/webdriver_classic.rs @@ -1668,13 +1668,7 @@ pub(super) async fn webdriver_classic_new_session( let initial_storage_partition = state.initial_storage_partition(); let runtime = ClassicSessionRuntimeHandle::spawn( initial_storage_partition, - moli_core::runtime::NavigationRuntimeConfig::new_with_layout_configuration( - state.fetch_config.clone(), - state.optional_resource_fetch_mask, - state.subframe_loading_enabled, - state.layout_configuration, - ) - .with_debugger_enabled(state.debugger_enabled), + state.navigation_runtime_config.clone(), instance_connection, ); let create_context = ClassicDevToolsCommandContext::new(session.session_id.as_str()); @@ -5019,7 +5013,7 @@ fn webdriver_classic_capabilities( "browserName": "moli", "browserVersion": version::PRODUCT, "platformName": std::env::consts::OS, - "acceptInsecureCerts": !state.fetch_config.tls_verify_host(), + "acceptInsecureCerts": !state.navigation_runtime_config.fetch_config().tls_verify_host(), "pageLoadStrategy": page_load_strategy.as_str(), "unhandledPromptBehavior": unhandled_prompt_behavior.returned_capability(), "setWindowRect": false, diff --git a/moli-protocol/src/conn.rs b/moli-protocol/src/conn.rs index 818012a009..b221a6f0dd 100644 --- a/moli-protocol/src/conn.rs +++ b/moli-protocol/src/conn.rs @@ -1197,19 +1197,6 @@ struct StandaloneNavigationEngineSlot { } impl StandaloneNavigationEngineSlot { - fn materialized(engine: NavigationEngine) -> Self { - let runtime_config = engine.runtime_config(); - let slot = Self { - engine: OnceCell::new(), - runtime_config, - renderer_publication_sender: None, - }; - slot.engine - .set(engine) - .expect("fresh standalone navigation engine slot must be empty"); - slot - } - fn deferred(runtime_config: NavigationRuntimeConfig) -> Self { Self { engine: OnceCell::new(), @@ -1348,6 +1335,9 @@ pub struct CdpConnection { base_http_no_proxy: Option, base_tls_verify_host: bool, initial_storage_partition: CdpInitialStoragePartitionOwner, + // Startup policy survives engine materialization and replacement. Protocol + // preload registries never own these scripts or assign them identifiers. + startup_document_start_scripts: Vec, pub(crate) global_io_streams: HashMap, pub(crate) tracing_state: crate::domains::tracing::TracingState, download_registry: SharedDownloadRegistry, @@ -1598,9 +1588,9 @@ impl CdpConnection { CdpInitialStoragePartitionOwner::from_initial_storage_partition( initial_storage_partition, ); - Self::new_with_initial_storage_partition_owner_and_engine( + Self::new_with_initial_storage_partition_owner_and_deferred_runtime( initial_storage_partition, - StandaloneNavigationEngineSlot::deferred(navigation_runtime_config), + navigation_runtime_config, ) } @@ -1608,18 +1598,33 @@ impl CdpConnection { initial_storage_partition: CdpInitialStoragePartitionOwner, navigation_runtime_config: NavigationRuntimeConfig, ) -> Self { - Self::new_with_initial_storage_partition_owner_and_engine( + let conn = Self::new_with_initial_storage_partition_owner_and_deferred_runtime( initial_storage_partition, - StandaloneNavigationEngineSlot::materialized( - NavigationEngine::new_with_runtime_config(navigation_runtime_config), - ), - ) + navigation_runtime_config, + ); + conn.standalone_navigation_engine.ensure(); + conn } - fn new_with_initial_storage_partition_owner_and_engine( + fn new_with_initial_storage_partition_owner_and_deferred_runtime( initial_storage_partition: CdpInitialStoragePartitionOwner, - standalone_navigation_engine: StandaloneNavigationEngineSlot, + navigation_runtime_config: NavigationRuntimeConfig, ) -> Self { + let startup_document_start_scripts = navigation_runtime_config + .document_start_scripts() + .iter() + .map(|source| DocumentStartScript { + registry_key: None, + devtools_session: None, + source: source.clone(), + world_name: None, + has_bidi_channel_argument: false, + bidi_channel_handoffs: Vec::new(), + }) + .collect(); + let standalone_navigation_engine = StandaloneNavigationEngineSlot::deferred( + navigation_runtime_config.with_document_start_scripts(Vec::new()), + ); let fetch_config = standalone_navigation_engine.fetch_config(); let base_browser_identity = fetch_config.browser_identity().clone(); let base_http_proxy = fetch_config.http_proxy().map(str::to_owned); @@ -1669,6 +1674,7 @@ impl CdpConnection { base_http_no_proxy, base_tls_verify_host, initial_storage_partition, + startup_document_start_scripts, global_io_streams: HashMap::new(), tracing_state: crate::domains::tracing::TracingState::default(), download_registry: SharedDownloadRegistry::default(), @@ -3186,7 +3192,7 @@ impl CdpConnection { .runtime_config() .debugger_enabled(), ); - self.apply_global_browser_context_state(&mut browser_context); + self.initialize_browser_context_state(&mut browser_context); browser_context } @@ -3202,7 +3208,9 @@ impl CdpConnection { self.insert_browser_context(self.new_browser_context(browser_context_id)); } - fn apply_global_browser_context_state(&self, browser_context: &mut BrowserContext) { + fn initialize_browser_context_state(&self, browser_context: &mut BrowserContext) { + browser_context.startup_document_start_scripts = + self.startup_document_start_scripts.clone(); browser_context.global_cache_disabled = self.global_cache_disabled; browser_context.global_extra_headers = self.global_extra_headers.clone(); browser_context.global_network_conditions = self.global_network_conditions; @@ -3221,7 +3229,7 @@ impl CdpConnection { .runtime_config() .debugger_enabled(), )?; - self.apply_global_browser_context_state(&mut browser_context); + self.initialize_browser_context_state(&mut browser_context); Ok(browser_context) } diff --git a/moli-protocol/src/conn/page_state/surfaces.rs b/moli-protocol/src/conn/page_state/surfaces.rs index 70685edb5a..dbc29f44ec 100644 --- a/moli-protocol/src/conn/page_state/surfaces.rs +++ b/moli-protocol/src/conn/page_state/surfaces.rs @@ -250,8 +250,7 @@ impl BrowserContext { } pub fn document_start_script_descriptors(&self) -> Vec { - let mut scripts = Vec::new(); - scripts.extend(self.default_document_start_script_descriptors()); + let mut scripts = self.default_document_start_script_descriptors(); let target_id = self.active_target_id(); scripts.extend( self.active_page_target() @@ -266,12 +265,18 @@ impl BrowserContext { } pub(crate) fn default_document_start_script_descriptors(&self) -> Vec { - self.default_document_start_scripts + self.startup_document_start_scripts .iter() - .map(|(identifier, script)| { - script - .with_registry_key(Self::default_document_start_script_registry_key(identifier)) - }) + .cloned() + .chain( + self.default_document_start_scripts + .iter() + .map(|(identifier, script)| { + script.with_registry_key(Self::default_document_start_script_registry_key( + identifier, + )) + }), + ) .collect() } diff --git a/moli-protocol/src/conn/state/browser_context.rs b/moli-protocol/src/conn/state/browser_context.rs index e7c8a4f2f0..a33889ad45 100644 --- a/moli-protocol/src/conn/state/browser_context.rs +++ b/moli-protocol/src/conn/state/browser_context.rs @@ -79,6 +79,8 @@ pub struct BrowserContext { pub proxy_socks_version: Option, pub(crate) default_emulated_device_metrics: Option, pub(crate) next_default_document_start_script_id: u32, + // Installed at context creation, outside the mutable protocol preload registry. + pub(crate) startup_document_start_scripts: Vec, pub(crate) default_document_start_scripts: Vec<(String, DocumentStartScript)>, pub(crate) storage_quota_overrides: HashMap, pub(crate) http_cache_root: Option, @@ -546,6 +548,7 @@ impl BrowserContext { proxy_socks_version: None, default_emulated_device_metrics: None, next_default_document_start_script_id: 0, + startup_document_start_scripts: Vec::new(), default_document_start_scripts: Vec::new(), storage_quota_overrides: HashMap::new(), http_cache_root, @@ -588,23 +591,7 @@ impl BrowserContext { config: NavigationRuntimeConfig, renderer_output_transport_sender: Option, ) { - let first_bind = self.page_navigation_runtime_config.is_none(); self.page_navigation_runtime_config = Some(config.clone()); - if first_bind { - // Configured `--document-start-script`s behave like default preloads: every - // page of every browser context runs them, matching `moli fetch`. Seed once - // per context so a re-bind cannot register duplicates. - for source in config.document_start_scripts() { - self.record_default_document_start_script(&DocumentStartScript { - registry_key: None, - devtools_session: None, - source: source.clone(), - world_name: None, - has_bidi_channel_argument: false, - bidi_channel_handoffs: Vec::new(), - }); - } - } self.renderer_output_transport_sender = renderer_output_transport_sender; if self.renderer_output_transport_sender.is_some() { self.renderer_runtime() @@ -990,6 +977,7 @@ impl BrowserContext { "targetCanAccessOpenerCount": self.target_can_access_opener.len(), "targetWindowNameCount": self.named_auxiliary_targets.len(), "defaultDocumentStartScriptCount": self.default_document_start_scripts.len(), + "startupDocumentStartScriptCount": self.startup_document_start_scripts.len(), "domRemoteObjectNodeCacheCount": active_target .map_or(0, |target| target.dom_remote_object_node_cache.len()), "sharedWorkerTargetCount": self.shared_worker_targets.len(), diff --git a/moli-protocol/src/conn/tests/mod.rs b/moli-protocol/src/conn/tests/mod.rs index 738656637d..d10a64b83b 100644 --- a/moli-protocol/src/conn/tests/mod.rs +++ b/moli-protocol/src/conn/tests/mod.rs @@ -48,6 +48,7 @@ mod navigation_commit; mod navigation_error; mod resource_runtime; mod site_data; +mod startup_scripts; #[test] fn protocol_page_access_reports_a_typed_missing_document() { diff --git a/moli-protocol/src/conn/tests/startup_scripts.rs b/moli-protocol/src/conn/tests/startup_scripts.rs new file mode 100644 index 0000000000..68eee28da7 --- /dev/null +++ b/moli-protocol/src/conn/tests/startup_scripts.rs @@ -0,0 +1,76 @@ +use super::*; + +const STARTUP_SOURCE: &str = "globalThis.__startupCount = (globalThis.__startupCount || 0) + 1"; + +fn startup_config() -> NavigationRuntimeConfig { + NavigationRuntimeConfig::default().with_document_start_scripts(vec![STARTUP_SOURCE.to_owned()]) +} + +fn assert_startup_scripts(context: &BrowserContext) { + let scripts = context.default_document_start_script_descriptors(); + assert_eq!(scripts.len(), 1, "one startup script per context"); + assert_eq!(scripts[0].source, STARTUP_SOURCE); + assert!(scripts[0].registry_key.is_none()); + assert!(context.default_document_start_scripts.is_empty()); +} + +#[test] +fn startup_scripts_survive_materialized_connection_initialization() { + let mut conn = CdpConnection::new_with_initial_storage_partition_and_runtime_config( + crate::CdpInitialStoragePartition::memory(), + startup_config(), + ); + conn.insert_browser_context(conn.new_browser_context("default".to_owned())); + assert_startup_scripts(conn.browser_context.as_ref().unwrap()); + + let context = conn + .new_ephemeral_browser_context("ephemeral".to_owned()) + .unwrap(); + assert_startup_scripts(&context); +} + +#[test] +fn startup_scripts_survive_deferred_engine_materialization_reset_and_rebind() { + let mut conn = CdpConnection::new_with_deferred_navigation_runtime( + crate::CdpInitialStoragePartition::memory(), + startup_config(), + ); + assert!(!conn.standalone_navigation_engine.is_materialized()); + assert_startup_scripts(&conn.new_browser_context("before-engine".to_owned())); + + conn.active_navigation_engine(); + assert!(conn.standalone_navigation_engine.is_materialized()); + assert!(conn.release_idle_navigation_engine_memory_if_idle().reset); + conn.insert_browser_context(conn.new_browser_context("after-reset".to_owned())); + + let config = conn.standalone_navigation_engine.runtime_config(); + let context = conn.browser_context.as_mut().unwrap(); + context.bind_page_navigation_engines(config.clone(), None); + context.bind_page_navigation_engines(config, None); + assert_startup_scripts(context); +} + +#[test] +fn startup_scripts_do_not_share_protocol_preload_identifiers() { + let mut conn = CdpConnection::new_with_deferred_navigation_runtime( + crate::CdpInitialStoragePartition::memory(), + startup_config(), + ); + conn.insert_browser_context(conn.new_browser_context("default".to_owned())); + let context = conn.browser_context.as_mut().unwrap(); + assert!(context.remove_default_document_start_script("1").is_none()); + let script = context + .default_document_start_script_descriptors() + .remove(0); + let identifier = context.record_default_document_start_script(&script); + assert_eq!( + identifier, "1", + "startup scripts must not consume protocol IDs" + ); + assert!( + context + .remove_default_document_start_script(&identifier) + .is_some() + ); + assert_startup_scripts(context); +}