From de4cc98f472039b1f00dcd2bdf82bb49df602edc Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 3 Nov 2026 05:52:29 +0800 Subject: [PATCH] fix(serve): preserve startup scripts across protocol lifecycles Capture startup scripts before creating navigation engines and install them when each BrowserContext is created. Keep CLI scripts outside mutable protocol preload registries so engine resets and protocol removals preserve startup policy. Pass complete runtime configuration through BiDi and Classic entry points. Add six regression tests covering protocol navigation, iframes, late contexts, new windows, preload removal and engine reset. Validation: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (22,197 passed, 20 skipped). --- moli-core/src/runtime/navigation_engine.rs | 12 +- .../src/protocol_server/cdp.rs | 2 +- .../src/protocol_server/mod.rs | 14 +- .../src/protocol_server/tests/mod.rs | 1 + .../protocol_server/tests/startup_scripts.rs | 292 ++++++++++++++++++ .../src/protocol_server/webdriver_bidi.rs | 8 +- .../src/protocol_server/webdriver_classic.rs | 10 +- moli-protocol/src/conn.rs | 58 ++-- moli-protocol/src/conn/page_state/surfaces.rs | 19 +- .../src/conn/state/browser_context.rs | 20 +- moli-protocol/src/conn/tests/mod.rs | 1 + .../src/conn/tests/startup_scripts.rs | 76 +++++ 12 files changed, 431 insertions(+), 82 deletions(-) create mode 100644 moli-protocol-server/src/protocol_server/tests/startup_scripts.rs create mode 100644 moli-protocol/src/conn/tests/startup_scripts.rs 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); +}