diff --git a/Cargo.lock b/Cargo.lock index 1b5a16f9ec..9656ca4776 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2543,6 +2543,7 @@ version = "0.1.0" dependencies = [ "anyhow", "http", + "indexmap", "moli-browser-profile", "moli-cookie-jar", "moli-crypto", @@ -2553,6 +2554,8 @@ dependencies = [ "moli-parser", "moli-renderer-v8", "moli-selector", + "moli-shared-worker", + "moli-site", "moli-storage-key", "moli-storage-service", "moli-test-support", diff --git a/moli-cookie-jar/src/jar.rs b/moli-cookie-jar/src/jar.rs index 7c48bd952e..d897f1eca1 100644 --- a/moli-cookie-jar/src/jar.rs +++ b/moli-cookie-jar/src/jar.rs @@ -769,8 +769,8 @@ fn insert_context<'a>(url: &'a Url, source: CookieSource) -> InsertContext<'a> { match source { #[cfg(any(test, feature = "test-support"))] CookieSource::Http => InsertContext::http(url), - // CDP is a privileged browser-side API. Treat it closer to Chromium's manager-side set - // path than to `document.cookie`, so fork-side HttpOnly guards do not reject it. - CookieSource::Cdp => InsertContext::cdp(url), + // Browser management is privileged. Treat it closer to Chromium's manager-side set path + // than to `document.cookie`, so fork-side HttpOnly guards do not reject it. + CookieSource::Management => InsertContext::cdp(url), } } diff --git a/moli-cookie-jar/src/model/network_context.rs b/moli-cookie-jar/src/model/network_context.rs index f8e1576351..70e9d5a929 100644 --- a/moli-cookie-jar/src/model/network_context.rs +++ b/moli-cookie-jar/src/model/network_context.rs @@ -19,7 +19,7 @@ use super::stored_cookie::StoredCookiePartitionKey; pub enum CookieSource { #[cfg(any(test, feature = "test-support"))] Http, - Cdp, + Management, } /// SameSite request context computed at the network boundary. diff --git a/moli-cookie-jar/src/tests.rs b/moli-cookie-jar/src/tests.rs index 38d5df9c04..e0bffaca0f 100644 --- a/moli-cookie-jar/src/tests.rs +++ b/moli-cookie-jar/src/tests.rs @@ -1592,7 +1592,7 @@ fn cdp_upsert_updates_network_visible_cookie_header() { creation_index: 999, last_access_index: 999, }, - CookieSource::Cdp, + CookieSource::Management, ); assert_eq!(store.cookie_header(&url), Some("sid=cdp".to_owned())); @@ -1622,7 +1622,7 @@ fn cdp_upsert_host_only_cookie_preserves_explicit_domain_without_request_hint() creation_index: 999, last_access_index: 999, }, - CookieSource::Cdp, + CookieSource::Management, ); assert_eq!( @@ -4072,7 +4072,7 @@ fn cdp_upsert_applies_prefix_and_same_site_guards() { creation_index: 999, last_access_index: 999, }, - CookieSource::Cdp, + CookieSource::Management, ); store.upsert( StoredCookie { @@ -4092,7 +4092,7 @@ fn cdp_upsert_applies_prefix_and_same_site_guards() { creation_index: 999, last_access_index: 999, }, - CookieSource::Cdp, + CookieSource::Management, ); store.upsert( StoredCookie { @@ -4112,7 +4112,7 @@ fn cdp_upsert_applies_prefix_and_same_site_guards() { creation_index: 999, last_access_index: 999, }, - CookieSource::Cdp, + CookieSource::Management, ); assert_eq!( @@ -4166,7 +4166,7 @@ fn cdp_upsert_accepts_partitioned_cookie() { creation_index: 999, last_access_index: 999, }, - CookieSource::Cdp, + CookieSource::Management, ); assert_eq!( diff --git a/moli-core/Cargo.toml b/moli-core/Cargo.toml index 645622ad68..0103414f3e 100644 --- a/moli-core/Cargo.toml +++ b/moli-core/Cargo.toml @@ -16,6 +16,7 @@ wpt-extensions = ["moli-renderer-v8/wpt-extensions"] [dependencies] anyhow = "1.0.100" http = "1" +indexmap = "2.6" moli-crypto = { path = "../moli-crypto" } moli-header-field = { path = "../moli-header-field" } moli-browser-profile = { path = "../moli-browser-profile" } @@ -24,6 +25,8 @@ moli-parser = { path = "../moli-parser" } moli-page-types = { path = "../moli-page-types" } moli-renderer-v8 = { path = "../moli-renderer-v8" } moli-selector = { path = "../moli-selector" } +moli-shared-worker = { path = "../moli-shared-worker" } +moli-site = { path = "../moli-site" } moli-storage-key = { path = "../moli-storage-key" } moli-storage-service = { path = "../moli-storage-service" } moli-url = { path = "../moli-url" } diff --git a/moli-core/src/browser/browser_context.rs b/moli-core/src/browser/browser_context.rs new file mode 100644 index 0000000000..da91037481 --- /dev/null +++ b/moli-core/src/browser/browser_context.rs @@ -0,0 +1,795 @@ +use std::path::PathBuf; + +use crate::{ + browser::{ + BrowserContextId, DocumentHandle, DownloadManager, DownloadPolicy, PermissionOverrides, + WebContentsHandle, WebContentsId, + }, + network::{SharedWebStorageStore, new_shared_web_storage_store}, + runtime::{ + NavigationEngine, NavigationPageStorageHandles, NavigationResourceStorageHandles, + NavigationRuntimeConfig, RendererBrowserContextRuntime, RendererBrowserContextRuntimeOwner, + RendererBrowserContextRuntimeOwnerAccess, storage_partition::StoragePartitionState, + }, + storage::{ + SharedIndexedDbManager, SharedStorageBucketStore, WeakIndexedDbManager, + new_shared_storage_bucket_store_with_indexed_db_manager, + }, +}; +use indexmap::IndexMap; +use moli_browser_profile::BrowserIdentityProfile; +use moli_cookie_jar::{ + CookieSource, SharedBrowserCookieStore, StoredCookie, new_shared_browser_cookie_store, +}; + +use super::web_contents::{ClosingWebContents, DocumentHost, WebContents}; +use crate::browser::{ + EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, EmulatedNetworkConditions, +}; + +mod dialogs; +mod document_commands; +mod document_policy; +mod document_queries; +mod fetch; +mod input; +mod navigation; +mod permissions; +mod residence; +mod resource_commands; +mod resource_runtime; +mod settings; +mod storage_partition; +mod workers; +pub use document_commands::*; +pub use document_policy::*; +pub use fetch::*; +pub use input::*; +pub use navigation::DocumentNavigationMetadata; +pub use permissions::{CompletedContextPermissionUpdate, PendingContextPermissionUpdate}; +pub use resource_commands::*; +use storage_partition::StoragePartition; +pub use storage_partition::{OriginStorageUsage, SiteDataClearOptions, StoragePartitionKind}; + +#[derive(Clone)] +pub struct BrowserContextStoragePartitionHandles { + pub cookie_store: SharedBrowserCookieStore, + pub web_storage_store: SharedWebStorageStore, + pub indexed_db_manager: SharedIndexedDbManager, + pub storage_bucket_store: SharedStorageBucketStore, +} + +#[derive(Clone)] +pub struct BrowserContextResourceStorageHandles { + pub cookie_store: SharedBrowserCookieStore, + pub web_storage_store: SharedWebStorageStore, + pub session_storage_store: SharedWebStorageStore, +} + +#[derive(Clone)] +pub struct BrowserContextPageStorageHandles { + pub cookie_store: SharedBrowserCookieStore, + pub web_storage_store: SharedWebStorageStore, + pub session_storage_store: SharedWebStorageStore, + pub indexed_db_manager: Option, + pub storage_bucket_store: Option, +} + +impl BrowserContextStoragePartitionHandles { + fn from_stores( + cookie_store: SharedBrowserCookieStore, + web_storage_store: SharedWebStorageStore, + indexed_db_manager: SharedIndexedDbManager, + storage_bucket_store: SharedStorageBucketStore, + ) -> Self { + Self { + cookie_store, + web_storage_store, + indexed_db_manager, + storage_bucket_store, + } + } + + pub fn memory() -> Self { + Self::with_initial_cookies(Vec::new()) + } + + pub fn with_initial_cookies(initial_cookies: impl IntoIterator) -> Self { + let cookie_store = new_shared_browser_cookie_store(); + seed_initial_cookies(&cookie_store, initial_cookies); + let indexed_db_manager = crate::storage::new_indexed_db_manager(None) + .expect("in-memory IndexedDB manager should initialize"); + let storage_bucket_store = + new_shared_storage_bucket_store_with_indexed_db_manager(&indexed_db_manager); + Self::from_stores( + cookie_store, + new_shared_web_storage_store(), + indexed_db_manager, + storage_bucket_store, + ) + } + + fn from_initial_storage_partition( + cookie_store: SharedBrowserCookieStore, + local_storage_store: SharedWebStorageStore, + indexed_db_manager: SharedIndexedDbManager, + storage_bucket_store: SharedStorageBucketStore, + ) -> Self { + Self::from_stores( + cookie_store, + local_storage_store, + indexed_db_manager, + storage_bucket_store, + ) + } + + pub fn from_storage_partition( + initial_cookies: impl IntoIterator, + storage_partition: &StoragePartitionState, + ) -> Self { + let cookie_store = new_shared_browser_cookie_store(); + seed_initial_cookies(&cookie_store, initial_cookies); + let shared_storage = storage_partition.shared_storage_handles(); + Self::from_initial_storage_partition( + cookie_store, + shared_storage.web_storage_store(), + shared_storage.indexed_db_manager(), + shared_storage.storage_bucket_store(), + ) + } + + pub fn resource_storage_handles( + &self, + session_storage_store: SharedWebStorageStore, + ) -> BrowserContextResourceStorageHandles { + BrowserContextResourceStorageHandles { + cookie_store: self.cookie_store.clone(), + web_storage_store: self.web_storage_store.clone(), + session_storage_store, + } + } + + pub fn page_storage_handles( + &self, + session_storage_store: SharedWebStorageStore, + ) -> BrowserContextPageStorageHandles { + BrowserContextPageStorageHandles { + cookie_store: self.cookie_store.clone(), + web_storage_store: self.web_storage_store.clone(), + session_storage_store, + indexed_db_manager: Some(crate::storage::downgrade_indexed_db_manager( + &self.indexed_db_manager, + )), + storage_bucket_store: Some(self.storage_bucket_store.clone()), + } + } +} + +impl BrowserContextResourceStorageHandles { + pub fn into_navigation_storage(self) -> NavigationResourceStorageHandles { + NavigationResourceStorageHandles::new( + self.cookie_store, + self.web_storage_store, + self.session_storage_store, + ) + } +} + +impl BrowserContextPageStorageHandles { + pub fn into_navigation_storage(self) -> NavigationPageStorageHandles { + NavigationPageStorageHandles::new( + self.cookie_store, + self.web_storage_store, + self.session_storage_store, + self.indexed_db_manager, + self.storage_bucket_store, + ) + } +} + +fn seed_initial_cookies( + cookie_store: &SharedBrowserCookieStore, + initial_cookies: impl IntoIterator, +) { + let mut store = cookie_store.lock(); + for cookie in initial_cookies { + let _ = store.upsert_with_request_url_report(cookie, None, CookieSource::Management); + } +} + +#[cfg(any(test, feature = "test-support"))] +#[doc(hidden)] +pub fn seed_initial_cookies_for_test( + cookie_store: &SharedBrowserCookieStore, + initial_cookies: impl IntoIterator, +) { + seed_initial_cookies(cookie_store, initial_cookies); +} + +/// Physical context ownership held only by the Browser owner sequence. +/// No projection identity, output transport or session state belongs here. +pub struct BrowserContext { + id: BrowserContextId, + page_navigation_runtime_config: Option, + network_policy: ContextNetworkPolicy, + emulation_defaults: ContextEmulationDefaults, + environment_owner: crate::ProcessEnvironmentOwner, + browser_identity_override: Option, + permission_overrides: PermissionOverrides, + download_policy: Option, + downloads: DownloadManager, + // The Browser collection and its only selector have the same lifetime. + // Keep insertion order when choosing a replacement foreground page. + web_contents: IndexMap, + selected_web_contents: Option, + // Drop Documents/engines before the runtime root and its storage handles. + renderer_output_transport_sender: Option, + renderer_runtime_owner: Option, + storage_partition: StoragePartition, +} + +impl BrowserContext { + pub fn id(&self) -> BrowserContextId { + self.id + } + + pub(crate) fn web_contents(&self, handle: WebContentsHandle) -> Result<&WebContents, String> { + if handle.context() != self.id { + return Err("WebContents belongs to a different BrowserContext".into()); + } + self.web_contents + .get(&handle.id()) + .ok_or_else(|| "WebContents unavailable".into()) + } + + pub(crate) fn web_contents_mut( + &mut self, + handle: WebContentsHandle, + ) -> Result<&mut WebContents, String> { + if handle.context() != self.id { + return Err("WebContents belongs to a different BrowserContext".into()); + } + self.web_contents + .get_mut(&handle.id()) + .ok_or_else(|| "WebContents unavailable".into()) + } + + pub(crate) fn document(&self, handle: DocumentHandle) -> Result<&DocumentHost, String> { + if handle.web_contents().context() != self.id { + return Err("Document belongs to a different BrowserContext".into()); + } + let document = self + .web_contents + .get(&handle.web_contents().id()) + .and_then(|contents| contents.main_frame.current_document.as_ref()) + .ok_or("NoDocumentLoaded")?; + if document.id != handle.id() { + return Err("Document changed".into()); + } + Ok(document) + } + + pub(crate) fn document_mut( + &mut self, + handle: DocumentHandle, + ) -> Result<&mut DocumentHost, String> { + if handle.web_contents().context() != self.id { + return Err("Document belongs to a different BrowserContext".into()); + } + let document = self + .web_contents + .get_mut(&handle.web_contents().id()) + .and_then(|contents| contents.main_frame.current_document.as_mut()) + .ok_or("NoDocumentLoaded")?; + if document.id != handle.id() { + return Err("Document changed".into()); + } + Ok(document) + } + + pub fn inherited_document_policy( + &self, + fetch_config: moli_fetch::FetchConfig, + defaults: &crate::browser::PermissionDefaults, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + global_geolocation: Option<&EmulatedGeolocationOverrideState>, + ) -> super::web_contents::InheritedDocumentPolicy { + let mut policy = + self.inherited_resource_policy(fetch_config, global_headers, global_network_conditions); + policy.permissions = self.permission_overrides.snapshot(defaults); + policy.emulation.geolocation = policy + .emulation + .geolocation + .or_else(|| global_geolocation.cloned()); + policy + } + + pub fn inherited_resource_policy( + &self, + mut fetch_config: moli_fetch::FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> super::web_contents::InheritedDocumentPolicy { + if let Some(identity) = &self.browser_identity_override { + fetch_config.set_browser_identity(identity.clone()); + } + if let Some(proxy) = &self.network_policy.http_proxy { + fetch_config.set_http_proxy(Some(proxy.clone())); + } + if let Some(no_proxy) = &self.network_policy.http_no_proxy { + fetch_config.set_http_no_proxy(Some(no_proxy.clone())); + } + if let Some(verify) = self.network_policy.tls_verify_host { + fetch_config.set_tls_verify_host(verify); + } + let mut emulation = self.emulation_defaults.clone(); + emulation.network_conditions = emulation.network_conditions.or(global_network_conditions); + super::web_contents::InheritedDocumentPolicy { + selected_web_contents: self.selected_web_contents, + navigator_queries: Default::default(), + fetch_config, + extra_headers: super::web_contents::merge_extra_header_layers(&[ + global_headers, + &self.network_policy.extra_headers, + ]), + emulation, + permissions: Vec::new(), + storage: self.storage_partition.handles.clone(), + } + } + + pub fn new( + handles: BrowserContextStoragePartitionHandles, + kind: StoragePartitionKind, + http_cache_root: Option, + http_cache_max_bytes: Option, + ) -> Self { + Self { + id: BrowserContextId::allocate(), + page_navigation_runtime_config: None, + network_policy: ContextNetworkPolicy::default(), + emulation_defaults: ContextEmulationDefaults::default(), + environment_owner: Default::default(), + browser_identity_override: None, + permission_overrides: PermissionOverrides::default(), + download_policy: None, + downloads: DownloadManager::default(), + web_contents: IndexMap::new(), + selected_web_contents: None, + renderer_output_transport_sender: None, + renderer_runtime_owner: Some(RendererBrowserContextRuntime::new()), + storage_partition: StoragePartition::new( + handles, + kind, + http_cache_root, + http_cache_max_bytes, + ), + } + } + + pub fn selected_web_contents_id(&self) -> Option { + self.selected_web_contents + } + + pub fn select_web_contents(&mut self, id: WebContentsId) -> bool { + if !self.web_contents.contains_key(&id) { + return false; + } + self.selected_web_contents = Some(id); + true + } + + pub fn close_web_contents( + &mut self, + handle: WebContentsHandle, + ) -> Result { + self.web_contents(handle)?; + let id = handle.id(); + let removed = self + .web_contents + .shift_remove(&id) + .expect("validated WebContents must remain resident until close"); + if self.selected_web_contents == Some(id) { + self.selected_web_contents = None; + } + for contents in self.web_contents.values_mut() { + if contents + .window + .opener + .is_some_and(|opener| opener.web_contents_id == id) + { + contents.window.opener = None; + } + } + Ok(removed.begin_close()) + } + + pub fn close_all_web_contents(&mut self) -> Vec { + self.selected_web_contents = None; + std::mem::take(&mut self.web_contents) + .into_values() + .map(WebContents::begin_close) + .collect() + } + + fn new_page_navigation_engine(&self, config: NavigationRuntimeConfig) -> NavigationEngine { + let engine = NavigationEngine::new_with_runtime_config_and_browser_context_access( + config, + self.renderer_runtime_owner_access(), + ) + .expect("live BrowserContext owner must accept a page engine"); + if let Some(sender) = self.renderer_output_transport_sender.clone() { + engine.set_renderer_output_transport_sender(sender); + } + engine + } + + fn renderer_runtime(&self) -> RendererBrowserContextRuntime { + self.renderer_runtime_owner + .as_ref() + .expect("BrowserContext renderer owner was already taken for teardown") + .handle() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn renderer_runtime_id_for_test(&self) -> crate::RendererBrowserContextRuntimeId { + self.renderer_runtime().id() + } + + fn renderer_runtime_owner_access(&self) -> RendererBrowserContextRuntimeOwnerAccess { + self.renderer_runtime_owner + .as_ref() + .expect("BrowserContext renderer owner was already taken for teardown") + .owner_access() + } + + pub(crate) fn shutdown(mut self) { + let mut runtime = self + .renderer_runtime_owner + .take() + .expect("BrowserContext renderer owner must exist until teardown"); + runtime.terminate_renderer_producers_for_owner_shutdown(); + drop(self); + runtime.shutdown_network_and_join(); + } +} + +/// Context-scoped request defaults, with no frontend or session attribution. +/// A missing value inherits the process policy; an empty bypass list does not. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ContextNetworkPolicy { + pub http_proxy: Option, + pub http_no_proxy: Option, + pub tls_verify_host: Option, + pub extra_headers: moli_fetch::RequestHeaders, +} + +/// Installed context defaults; inherited process values are not copied here. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct ContextEmulationDefaults { + pub network_conditions: Option, + pub geolocation: Option, + pub device_metrics: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn permission_rule(setting: &str) -> crate::page::PermissionOverrideRegistration { + crate::page::PermissionOverrideRegistration { + permission: json!({"name": "geolocation"}), + setting: setting.into(), + origin: None, + embedded_origin: None, + } + } + + fn install_test_document( + context: &mut BrowserContext, + page: crate::page::Page, + ) -> WebContentsHandle { + let (handle, _) = context + .register_web_contents(WebContents::default()) + .expect("test WebContents"); + context + .replace_document_for_test( + handle, + Some(DocumentHost::new( + crate::browser::DocumentId::allocate(), + page, + )), + ) + .expect("test Document"); + handle + } + + #[tokio::test] + async fn permission_refresh_visits_all_documents_without_protocol_projection() { + let browser = crate::runtime::Browser::new(crate::runtime::BrowserConfig::default()) + .expect("test browser"); + let mut defaults = crate::browser::PermissionDefaults::default(); + defaults.set(permission_rule("denied")); + let mut context = BrowserContext::new( + BrowserContextStoragePartitionHandles::memory(), + StoragePartitionKind::Ephemeral, + None, + None, + ); + let mut handles = Vec::new(); + for title in ["first", "second"] { + let page = browser + .fetch(&format!("data:text/html,{title}")) + .await + .expect("test Page"); + handles.push(install_test_document(&mut context, page)); + } + context.set_permission_override(&mut defaults, permission_rule("granted")); + + assert_eq!(context.permission_override_count(), 1); + for expected in ["granted", "denied"] { + let pending = context + .start_permission_update(&defaults) + .expect("permission update") + .expect("loaded Documents"); + assert_eq!(pending.page_count_for_test(), 2); + let completed = pending.wait().await; + context + .finish_permission_update(completed) + .expect("permission completion"); + for (handle, title) in handles.iter().copied().zip(["first", "second"]) { + let document = context + .document_handle_for_web_contents(handle) + .expect("test WebContents") + .expect("test Document"); + assert_eq!(context.document_title(document).unwrap(), title); + let result = context + .evaluate_document_expression_for_test( + document, + "navigator.permissions.query({name:'geolocation'}).then(status => status.state)", + true, + ) + .await + .unwrap(); + assert_eq!(result, json!({"type": "string", "value": expected})); + } + context.clear_permission_overrides(); + } + assert_eq!(defaults.snapshot(), vec![permission_rule("denied")]); + } + + #[tokio::test] + async fn permission_completion_cannot_retarget_replacement_web_contents() { + let browser = crate::runtime::Browser::new(crate::runtime::BrowserConfig::default()) + .expect("test browser"); + let mut context = BrowserContext::new( + BrowserContextStoragePartitionHandles::memory(), + StoragePartitionKind::Ephemeral, + None, + None, + ); + let first = browser + .fetch("data:text/html,first") + .await + .unwrap(); + let old_handle = install_test_document(&mut context, first); + let mut defaults = crate::browser::PermissionDefaults::default(); + context.set_permission_override(&mut defaults, permission_rule("granted")); + let completed = context + .start_permission_update(&defaults) + .unwrap() + .unwrap() + .wait() + .await; + + context + .close_web_contents(old_handle) + .unwrap() + .close_async() + .await; + let replacement = browser + .fetch("data:text/html,replacement") + .await + .unwrap(); + let replacement = install_test_document(&mut context, replacement); + assert_ne!(replacement, old_handle); + assert_eq!( + context.finish_permission_update(completed), + Err("NoDocumentLoaded".into()) + ); + let document = context + .document_handle_for_web_contents(replacement) + .unwrap() + .unwrap(); + assert_eq!(context.document_title(document).unwrap(), "replacement"); + } + + #[tokio::test] + async fn resource_maintenance_never_adopts_a_peers_transport_or_policy() { + let mut context = BrowserContext::new( + BrowserContextStoragePartitionHandles::memory(), + StoragePartitionKind::Ephemeral, + None, + None, + ); + let mut ids = Vec::new(); + for (agent, verify) in [("Native/first", false), ("Native/peer", true)] { + let mut contents = WebContents::default(); + contents.install_navigation_engine( + context.new_page_navigation_engine(NavigationRuntimeConfig::default()), + ); + contents.browser_identity_override = Some(BrowserIdentityProfile::new(agent, "en")); + contents.tls_verify_host_override = Some(verify); + ids.push(contents.id()); + context.web_contents.insert(contents.id(), contents); + } + let inherited = context.inherited_resource_policy( + moli_fetch::FetchConfig::default(), + &Default::default(), + None, + ); + let first = context + .web_contents + .get_mut(&ids[0]) + .unwrap() + .ensure_resource_request_client(&inherited) + .unwrap(); + let peer = context + .web_contents + .get_mut(&ids[1]) + .unwrap() + .ensure_resource_request_client(&inherited) + .unwrap(); + assert!(!first.shares_resource_runtime_with(&peer)); + assert!(std::sync::Arc::ptr_eq( + &first.cookie_store(), + &peer.cookie_store() + )); + let contents = context.web_contents.get_mut(&ids[0]).unwrap(); + contents.invalidate_resource_runtime(); + assert!( + contents + .start_resource_runtime_rebuild(&inherited) + .unwrap() + .is_none() + ); + let rebuilt = contents.ensure_resource_request_client(&inherited).unwrap(); + assert!(!rebuilt.shares_resource_runtime_with(&peer)); + assert!(rebuilt.shares_page_network_policy_with(&first)); + assert!( + rebuilt + .browser_resource_runtime() + .matches_fetch_config(contents.navigation_fetch_config().unwrap()) + ); + assert!( + !contents + .navigation_fetch_config() + .unwrap() + .tls_verify_host() + ); + assert_eq!( + rebuilt + .browser_resource_runtime() + .browser_identity() + .user_agent(), + "Native/first" + ); + let contents = context.web_contents.get(&ids[1]).unwrap(); + assert!( + contents + .navigation_fetch_config() + .unwrap() + .tls_verify_host() + ); + assert_eq!( + peer.browser_resource_runtime() + .browser_identity() + .user_agent(), + "Native/peer" + ); + assert!( + peer.browser_resource_runtime() + .matches_fetch_config(contents.navigation_fetch_config().unwrap()) + ); + } + + #[tokio::test] + async fn document_policy_capture_needs_no_projection_or_current_document() { + let mut context = BrowserContext::new( + BrowserContextStoragePartitionHandles::memory(), + StoragePartitionKind::Ephemeral, + None, + None, + ); + context.network_policy.extra_headers = vec![("X-Policy".into(), "context".into())].into(); + context.network_policy.tls_verify_host = Some(false); + context + .set_default_locale_override(Some("fr-FR".into())) + .unwrap(); + let mut contents = WebContents::default(); + contents.install_navigation_engine( + context.new_page_navigation_engine(NavigationRuntimeConfig::default()), + ); + contents.network_request_policy.extra_headers = + vec![("X-Policy".into(), "page".into())].into(); + contents.emulation_policy.script_execution_disabled = true; + contents.emulation_policy.max_touch_points = 1; + assert!( + contents + .start_fetch_interception_update(true, None) + .unwrap() + .is_none() + ); + let id = contents.id(); + context.web_contents.insert(id, contents); + let geolocation = EmulatedGeolocationOverrideState::Position( + crate::browser::EmulatedGeolocationOverride { + latitude: 48.85837, + longitude: 2.294481, + accuracy: 7.0, + altitude: None, + altitude_accuracy: None, + heading: None, + speed: None, + }, + ); + let inherited = context.inherited_document_policy( + moli_fetch::FetchConfig::default(), + &crate::browser::PermissionDefaults::default(), + &vec![ + ("X-Global".into(), "global".into()), + ("X-Policy".into(), "global".into()), + ] + .into(), + Some(EmulatedNetworkConditions::offline()), + Some(&geolocation), + ); + let contents = context.web_contents.get_mut(&id).unwrap(); + let policy = contents + .capture_document_policy(inherited, &url::Url::parse("about:blank").unwrap()) + .unwrap(); + assert_eq!( + policy.extra_http_headers.to_byte_strings(), + [ + ("X-Global".into(), "global".into()), + ("X-Policy".into(), "page".into()) + ] + ); + assert_eq!(context.environment_owner.locale().as_deref(), Some("fr-FR")); + assert!(policy.network_offline); + assert_eq!(policy.navigator_overrides.online, Some(false)); + assert_eq!(policy.navigator_overrides.max_touch_points, 1); + assert_eq!( + policy + .navigator_overrides + .geolocation + .as_ref() + .map(|position| position.latitude), + Some(48.85837), + ); + assert!(policy.script_execution_disabled); + assert!(policy.fetch_subresource_interception_enabled); + assert!( + !contents + .navigation_engine_for_test() + .unwrap() + .fetch_config() + .tls_verify_host() + ); + assert!(contents.main_frame.current_document.is_none()); + assert!( + contents + .start_fetch_interception_update(false, None) + .unwrap() + .is_none() + ); + assert_eq!(contents.fetch_subresource_interception(), (false, None)); + assert!( + policy.fetch_subresource_interception_enabled, + "capture is a value, not a live registration view" + ); + } +} diff --git a/moli-core/src/browser/browser_context/dialogs.rs b/moli-core/src/browser/browser_context/dialogs.rs new file mode 100644 index 0000000000..7234cdc0f2 --- /dev/null +++ b/moli-core/src/browser/browser_context/dialogs.rs @@ -0,0 +1,98 @@ +use crate::{ + browser::{ + DocumentHandle, WebContentsHandle, + web_contents::{ + JavaScriptDialogClosed, JavaScriptDialogError, JavaScriptDialogKey, + JavaScriptDialogSnapshot, + }, + }, + page::RendererPendingJavaScriptDialog, +}; + +use super::BrowserContext; + +impl BrowserContext { + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn clear_web_contents_javascript_dialogs_for_test( + &mut self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.web_contents_mut(handle)?.javascript_dialogs.clear(); + Ok(()) + } + + pub fn web_contents_has_pending_javascript_dialog( + &self, + handle: WebContentsHandle, + ) -> Result { + Ok(!self.web_contents(handle)?.javascript_dialogs.is_empty()) + } + + pub fn install_document_javascript_dialog( + &mut self, + document: DocumentHandle, + dialog: RendererPendingJavaScriptDialog, + ) -> Result { + if let Err(error) = self.ensure_document_current(document) { + let _ = dialog.finish(false, String::new()); + return Err(error); + } + Ok(self + .web_contents_mut(document.web_contents())? + .javascript_dialogs + .install(document.id(), dialog)) + } + + pub fn document_javascript_dialog_snapshot( + &self, + document: DocumentHandle, + key: JavaScriptDialogKey, + ) -> Option { + self.ensure_document_current(document).ok()?; + self.web_contents(document.web_contents()) + .ok()? + .javascript_dialogs + .snapshot(key) + } + + pub fn set_document_javascript_dialog_prompt_text( + &mut self, + document: DocumentHandle, + key: JavaScriptDialogKey, + prompt_text: String, + ) -> Result<(), JavaScriptDialogError> { + self.ensure_document_current(document) + .map_err(|_| JavaScriptDialogError::NotFound)?; + self.web_contents_mut(document.web_contents()) + .map_err(|_| JavaScriptDialogError::NotFound)? + .javascript_dialogs + .set_prompt_text(key, prompt_text) + } + + pub fn finish_document_javascript_dialog( + &mut self, + document: DocumentHandle, + key: JavaScriptDialogKey, + accepted: bool, + prompt_text: Option, + ) -> Option { + self.ensure_document_current(document).ok()?; + self.web_contents_mut(document.web_contents()) + .ok()? + .javascript_dialogs + .finish(key, accepted, prompt_text) + } + + pub fn dismiss_document_javascript_dialog( + &mut self, + document: DocumentHandle, + key: JavaScriptDialogKey, + ) { + if self.ensure_document_current(document).is_ok() + && let Ok(contents) = self.web_contents_mut(document.web_contents()) + { + contents.javascript_dialogs.dismiss(key); + } + } +} diff --git a/moli-core/src/browser/browser_context/document_commands.rs b/moli-core/src/browser/browser_context/document_commands.rs new file mode 100644 index 0000000000..c3db0e1094 --- /dev/null +++ b/moli-core/src/browser/browser_context/document_commands.rs @@ -0,0 +1,704 @@ +use super::BrowserContext; +use crate::browser::{DocumentHandle, WebContentsHandle}; +use crate::page::{ + CompletedPageCommand, PendingPageCommand, RendererCaptureScreencastFrameReply, + RendererCaptureScreencastFrameRequest, RendererCaptureScreenshotReply, + RendererCaptureScreenshotRequest, RendererCommandTurnOutput, RendererPageDiagnosticsSnapshot, + RendererResourceTextSearchOutcome, RendererSetDocumentContentResult, SubresourceNetworkRecord, +}; + +struct PendingDocumentCommand { + document: DocumentHandle, + pending: PendingPageCommand, +} + +struct CompletedDocumentCommand { + document: DocumentHandle, + completed: Result, +} + +impl PendingDocumentCommand { + async fn wait(self) -> CompletedDocumentCommand { + CompletedDocumentCommand { + document: self.document, + completed: self.pending.wait().await.map_err(|error| error.to_string()), + } + } +} + +impl CompletedDocumentCommand { + fn renderer_output_predecessor(&self) -> Option { + self.completed + .as_ref() + .ok() + .and_then(CompletedPageCommand::renderer_output_predecessor) + } + + fn into_parts(self) -> (DocumentHandle, Result) { + (self.document, self.completed) + } +} + +macro_rules! define_document_command { + ($pending:ident, $completed:ident) => { + pub struct $pending(PendingDocumentCommand); + + pub struct $completed(CompletedDocumentCommand); + + impl $pending { + pub fn new(document: DocumentHandle, pending: PendingPageCommand) -> Self { + Self(PendingDocumentCommand { document, pending }) + } + + pub async fn wait(self) -> $completed { + $completed(self.0.wait().await) + } + } + + impl $completed { + pub fn document(&self) -> DocumentHandle { + self.0.document + } + + pub fn into_parts(self) -> (DocumentHandle, Result) { + self.0.into_parts() + } + } + }; + ($pending:ident, $completed:ident, renderer_output_predecessor) => { + define_document_command!($pending, $completed); + + impl $completed { + pub fn renderer_output_predecessor(&self) -> Option { + self.0.renderer_output_predecessor() + } + } + }; +} + +define_document_command!( + PendingSetDocumentContent, + CompletedSetDocumentContent, + renderer_output_predecessor +); +define_document_command!( + PendingCaptureDocumentSnapshot, + CompletedCaptureDocumentSnapshot, + renderer_output_predecessor +); +define_document_command!( + PendingCaptureDocumentImage, + CompletedCaptureDocumentImage, + renderer_output_predecessor +); +define_document_command!( + PendingCaptureDocumentScreencastFrame, + CompletedCaptureDocumentScreencastFrame +); +define_document_command!( + PendingDocumentResourceTextSearch, + CompletedDocumentResourceTextSearch, + renderer_output_predecessor +); +define_document_command!( + PendingDocumentCspBypassUpdate, + CompletedDocumentCspBypassUpdate, + renderer_output_predecessor +); +define_document_command!( + PendingTopLevelSameDocumentNavigation, + CompletedTopLevelSameDocumentNavigation, + renderer_output_predecessor +); +define_document_command!( + PendingTopLevelHistoryTraversal, + CompletedTopLevelHistoryTraversal, + renderer_output_predecessor +); +define_document_command!( + PendingNavigationHistoryReset, + CompletedNavigationHistoryReset, + renderer_output_predecessor +); +define_document_command!( + PendingChildFrameNavigation, + CompletedChildFrameNavigation, + renderer_output_predecessor +); +define_document_command!( + PendingDocumentStorageKeySnapshot, + CompletedDocumentStorageKeySnapshot +); +define_document_command!( + PendingChildFrameTreeSnapshot, + CompletedChildFrameTreeSnapshot +); +define_document_command!( + PendingDocumentCookieOwnerSnapshot, + CompletedDocumentCookieOwnerSnapshot +); +define_document_command!(PendingDocumentBlobRead, CompletedDocumentBlobRead); +define_document_command!( + PendingNetworkResourceLoadPreparation, + CompletedNetworkResourceLoadPreparation +); +define_document_command!( + PendingAppManifestLoadPreparation, + CompletedAppManifestLoadPreparation, + renderer_output_predecessor +); +define_document_command!( + PendingAppManifestPublication, + CompletedAppManifestPublication, + renderer_output_predecessor +); +define_document_command!(PendingDocumentInputCommand, CompletedDocumentInputCommand); +define_document_command!( + PendingDocumentAutofillTrigger, + CompletedDocumentAutofillTrigger +); +define_document_command!( + PendingDocumentLifecycleStop, + CompletedDocumentLifecycleStop, + renderer_output_predecessor +); +define_document_command!( + PendingDocumentDiagnosticsSnapshot, + CompletedDocumentDiagnosticsSnapshot, + renderer_output_predecessor +); + +#[cfg(any(test, feature = "test-support"))] +impl CompletedDocumentDiagnosticsSnapshot { + pub fn page_state_for_test( + &self, + ) -> Option<&std::sync::Arc> { + self.0 + .completed + .as_ref() + .ok() + .map(CompletedPageCommand::page_state) + } + + pub fn into_page_completion_for_test(self) -> Result { + self.0.completed + } +} +define_document_command!( + PendingChildFrameLifecycleWork, + CompletedChildFrameLifecycleWork +); +define_document_command!( + PendingDocumentResourceRuntimeUpdate, + CompletedDocumentResourceRuntimeUpdate +); + +impl CompletedTopLevelHistoryTraversal { + pub fn renderer_accepted(&self) -> Option { + self.0 + .completed + .as_ref() + .ok() + .and_then(CompletedPageCommand::bool_reply_value) + } +} + +pub struct DocumentSnapshot { + pub url: String, + pub html: String, +} + +impl BrowserContext { + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn apply_document_cookie_facade_overrides_for_test( + &mut self, + document: DocumentHandle, + overrides: Option, + ) -> Result<(), String> { + let page = &mut self.document_mut(document)?.page; + match overrides { + Some(overrides) if overrides != Default::default() => page + .apply_document_cookie_facade_overrides_async(&overrides) + .await + .map_err(|error| error.to_string()), + Some(_) | None => page + .clear_document_cookie_facade_overrides_async() + .await + .map_err(|error| error.to_string()), + } + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn document_runtime_heap_usage_for_test( + &mut self, + document: DocumentHandle, + ) -> Result { + let completion = self + .document(document)? + .page + .start_runtime_heap_usage() + .map_err(|error| error.to_string())? + .wait() + .await + .map_err(|error| error.to_string())?; + self.document_mut(document)? + .page + .finish_runtime_heap_usage(completion) + .map_err(|error| error.to_string()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_idle_override_for_test( + &self, + document: DocumentHandle, + ) -> Result, String> { + Ok(self.document(document)?.page.idle_override()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn evaluate_document_expression_for_test( + &mut self, + document: DocumentHandle, + expression: &str, + await_promise: bool, + ) -> Result { + self.document_mut(document)? + .page + .evaluate_runtime_expression_without_navigation_follow_with_await_async( + expression, + await_promise, + ) + .await + .map_err(|error| format!("runtime evaluation failed: {error}")) + } + + pub fn document_handle_for_web_contents( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + let document = self + .web_contents(handle)? + .main_frame + .current_document + .as_ref() + .map(|document| document.id); + Ok(document.map(|document| DocumentHandle::new(handle, document))) + } + + pub fn start_set_document_content( + &self, + document: DocumentHandle, + frame_id: String, + html: String, + ) -> Result { + let pending = self + .document(document)? + .page + .start_set_document_content(frame_id, html) + .map_err(|error| error.to_string())?; + Ok(PendingSetDocumentContent::new(document, pending)) + } + + pub fn finish_set_document_content( + &mut self, + completed: CompletedSetDocumentContent, + ) -> Result<(RendererSetDocumentContentResult, RendererCommandTurnOutput), String> { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_set_document_content_command_turn(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_top_level_same_document_navigation( + &self, + document: DocumentHandle, + url: String, + ) -> Result { + let pending = self + .document(document)? + .page + .start_top_level_same_document_navigation(url) + .map_err(|error| error.to_string())?; + Ok(PendingTopLevelSameDocumentNavigation::new( + document, pending, + )) + } + + pub fn finish_top_level_same_document_navigation( + &mut self, + completed: CompletedTopLevelSameDocumentNavigation, + ) -> Result<(bool, RendererCommandTurnOutput), String> { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_top_level_same_document_navigation_command_turn(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_capture_document_screencast_frame( + &self, + document: DocumentHandle, + request: RendererCaptureScreencastFrameRequest, + ) -> Result { + let pending = self + .document(document)? + .page + .start_capture_screencast_frame(request) + .map_err(|error| error.to_string())?; + Ok(PendingCaptureDocumentScreencastFrame::new( + document, pending, + )) + } + + pub fn finish_capture_document_screencast_frame( + &mut self, + completed: CompletedCaptureDocumentScreencastFrame, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_capture_screencast_frame(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_capture_document_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + let pending = self + .document(document)? + .page + .start_serialize_html() + .map_err(|error| error.to_string())?; + Ok(PendingCaptureDocumentSnapshot::new(document, pending)) + } + + pub fn finish_capture_document_snapshot( + &mut self, + completed: CompletedCaptureDocumentSnapshot, + ) -> Result { + let (document, completion) = completed.into_parts(); + let document = self.document_mut(document)?; + let html = document + .page + .finish_serialize_html(completion?) + .map_err(|error| error.to_string())?; + Ok(DocumentSnapshot { + url: document.page.final_url().as_str().to_owned(), + html, + }) + } + + pub fn start_capture_document_image( + &self, + document: DocumentHandle, + request: RendererCaptureScreenshotRequest, + ) -> Result { + let pending = self + .document(document)? + .page + .start_capture_screenshot_with_request(request) + .map_err(|error| error.to_string())?; + Ok(PendingCaptureDocumentImage::new(document, pending)) + } + + pub fn finish_capture_document_image( + &mut self, + completed: CompletedCaptureDocumentImage, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_capture_screenshot(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_top_level_history_traversal( + &self, + document: DocumentHandle, + delta: i64, + ) -> Result { + let pending = self + .document(document)? + .page + .start_top_level_history_traversal_by_delta(delta) + .map_err(|error| error.to_string())?; + Ok(PendingTopLevelHistoryTraversal::new(document, pending)) + } + + pub fn finish_top_level_history_traversal( + &mut self, + completed: CompletedTopLevelHistoryTraversal, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_top_level_history_traversal_by_delta(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_navigation_history_reset( + &self, + document: DocumentHandle, + ) -> Result { + self.document(document)?; + let pending = self + .web_contents + .get(&document.web_contents().id()) + .ok_or("NoDocumentLoaded")? + .start_reset_navigation_history()?; + Ok(PendingNavigationHistoryReset::new(document, pending)) + } + + pub fn finish_navigation_history_reset( + &mut self, + completed: CompletedNavigationHistoryReset, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document(document)?; + self.web_contents + .get_mut(&document.web_contents().id()) + .ok_or("NoDocumentLoaded")? + .finish_reset_navigation_history(completion?) + } + + pub fn start_child_frame_navigation( + &self, + document: DocumentHandle, + frame_id: String, + url: String, + ) -> Result { + let pending = self + .document(document)? + .page + .start_child_frame_navigation_to_url(&frame_id, &url) + .map_err(|error| error.to_string())?; + Ok(PendingChildFrameNavigation::new(document, pending)) + } + + pub fn finish_child_frame_navigation( + &mut self, + completed: CompletedChildFrameNavigation, + ) -> Result<(bool, RendererCommandTurnOutput), String> { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_child_frame_navigation_to_url_command_turn(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_child_frame_document_resource_text_search( + &self, + document: DocumentHandle, + frame_id: String, + url: String, + query: String, + case_sensitive: bool, + is_regex: bool, + ) -> Result { + let pending = self + .document(document)? + .page + .start_child_frame_resource_search_by_lines( + frame_id, + url, + query, + case_sensitive, + is_regex, + ) + .map_err(|error| error.to_string())?; + Ok(PendingDocumentResourceTextSearch::new(document, pending)) + } + + pub fn start_document_text_search( + &self, + document: DocumentHandle, + text: String, + query: String, + case_sensitive: bool, + is_regex: bool, + ) -> Result { + let pending = self + .document(document)? + .page + .start_text_search_by_lines(text, query, case_sensitive, is_regex) + .map_err(|error| error.to_string())?; + Ok(PendingDocumentResourceTextSearch::new(document, pending)) + } + + pub fn finish_document_resource_text_search( + &mut self, + completed: CompletedDocumentResourceTextSearch, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_resource_search_by_lines(completion?) + .map_err(|error| error.to_string()) + } + + pub fn set_document_javascript_dialog_handler_enabled( + &self, + document: DocumentHandle, + enabled: bool, + ) -> Result<(), String> { + let pending = self + .document(document)? + .page + .start_set_javascript_dialog_handler_enabled(enabled) + .map_err(|error| error.to_string())?; + drop(pending); + Ok(()) + } + + pub fn start_document_csp_bypass_update( + &self, + document: DocumentHandle, + bypass: bool, + ) -> Result { + let pending = self + .document(document)? + .page + .start_set_bypass_content_security_policy(bypass) + .map_err(|error| error.to_string())?; + Ok(PendingDocumentCspBypassUpdate::new(document, pending)) + } + + pub fn finish_document_csp_bypass_update( + &mut self, + completed: CompletedDocumentCspBypassUpdate, + ) -> Result<(), String> { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_set_bypass_content_security_policy(completion?) + .map_err(|error| error.to_string()) + } + + pub fn document_subresource_network_records( + &self, + document: DocumentHandle, + ) -> Result, String> { + Ok(self + .document(document)? + .page + .subresource_network_records() + .to_vec()) + } + + pub fn ensure_document_current(&self, document: DocumentHandle) -> Result<(), String> { + self.document(document).map(|_| ()) + } + + pub fn document_observable_output_snapshot( + &self, + document: DocumentHandle, + ) -> Result, String> { + Ok(self + .document(document)? + .page + .script_execution() + .observable_output_items() + .to_vec()) + } + + pub fn document_response_headers( + &self, + document: DocumentHandle, + ) -> Result<&[(String, String)], String> { + Ok(self.document(document)?.page.headers()) + } + + pub fn start_document_lifecycle_stop( + &self, + document: DocumentHandle, + ) -> Result { + let pending = self + .document(document)? + .page + .start_stop_document_lifecycle() + .map_err(|error| error.to_string())?; + Ok(PendingDocumentLifecycleStop::new(document, pending)) + } + + pub fn finish_document_lifecycle_stop( + &mut self, + completed: CompletedDocumentLifecycleStop, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_stop_document_lifecycle(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_document_diagnostics_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + let pending = self + .document(document)? + .page + .start_page_diagnostics_snapshot() + .map_err(|error| error.to_string())?; + Ok(PendingDocumentDiagnosticsSnapshot::new(document, pending)) + } + + pub fn finish_document_diagnostics_snapshot( + &mut self, + completed: CompletedDocumentDiagnosticsSnapshot, + ) -> Result { + let (document, completion) = completed.into_parts(); + let snapshot = self + .document_mut(document)? + .page + .finish_page_diagnostics_snapshot(completion?) + .map_err(|error| error.to_string())?; + Ok(snapshot) + } + + pub fn start_document_child_frame_lifecycle_work( + &mut self, + document: DocumentHandle, + timeout: std::time::Duration, + ) -> Result { + self.document(document)?; + let storage = self.storage_partition.handles.clone(); + let pending = self + .web_contents_mut(document.web_contents())? + .start_child_frame_lifecycle_work(&storage, timeout)?; + Ok(PendingChildFrameLifecycleWork::new(document, pending)) + } + + pub fn finish_document_child_frame_lifecycle_work( + &mut self, + completed: CompletedChildFrameLifecycleWork, + ) -> Result<(bool, RendererCommandTurnOutput), String> { + let (document, completion) = completed.into_parts(); + self.document(document)?; + let completed = self + .web_contents_mut(document.web_contents())? + .complete_child_frame_lifecycle_work(completion?)?; + Ok(completed) + } + + pub fn crash_web_contents_renderer_from_io( + &self, + web_contents: WebContentsHandle, + ) -> Result<(), String> { + if let Some(document) = self + .web_contents(web_contents)? + .main_frame + .current_document + .as_ref() + { + document.page.crash_devtools_target_from_io(); + } + Ok(()) + } +} diff --git a/moli-core/src/browser/browser_context/document_policy.rs b/moli-core/src/browser/browser_context/document_policy.rs new file mode 100644 index 0000000000..6d89dd5188 --- /dev/null +++ b/moli-core/src/browser/browser_context/document_policy.rs @@ -0,0 +1,364 @@ +use super::BrowserContext; +use crate::browser::DocumentHandle; +use crate::page::{ + CompletedPageCommand, EmulatedIdleOverride, EmulatedMediaOverrides, PendingPageCommand, + ViewportSurface, +}; + +pub enum DocumentPolicyUpdate { + NetworkRequestPolicy { + extra_headers: moli_fetch::RequestHeaders, + bypass_service_worker: bool, + cache_disabled: bool, + blocked_url_patterns: Vec, + }, + ExtraHttpHeaders(moli_fetch::RequestHeaders), + BlockedUrls(Vec), + BypassServiceWorker(bool), + NetworkOffline(bool), + IdleOverride(Option), + NavigatorOverrides(moli_page_types::NavigatorOverrides), + EmulatedMedia(EmulatedMediaOverrides), + ViewportSurface(Option), + ScriptExecutionDisabled(bool), + DocumentActivity(moli_page_types::DocumentActivity), +} + +#[derive(Clone, Copy)] +enum DocumentPolicyUpdateKind { + SetNetworkRequestPolicy, + SetExtraHttpHeaders, + SetBlockedUrls, + SetBypassServiceWorker, + SetNetworkConditions, + SetIdleOverride, + SetNavigatorOverrides, + SetEmulatedMedia, + SetViewportSurface, + SetScriptExecutionDisabled, + SetDocumentActivity, +} + +pub struct PendingDocumentPolicyUpdate { + document: DocumentHandle, + kind: DocumentPolicyUpdateKind, + pending: PendingPageCommand, +} + +pub struct CompletedDocumentPolicyUpdate { + document: DocumentHandle, + kind: DocumentPolicyUpdateKind, + completed: Result, +} + +pub struct PendingDocumentPolicyBatch { + context: crate::browser::BrowserContextId, + document: DocumentHandle, + admission_error: Option, + updates: Vec, +} + +pub struct CompletedDocumentPolicyBatch { + context: crate::browser::BrowserContextId, + document: DocumentHandle, + admission_error: Option, + updates: Vec, +} + +pub struct DocumentRuntimePolicyReconciliation { + pub script_execution_disabled: bool, + pub emulated_media: EmulatedMediaOverrides, + pub network_offline: bool, + pub viewport_surface: Option, +} + +impl PendingDocumentPolicyUpdate { + pub async fn wait(self) -> CompletedDocumentPolicyUpdate { + CompletedDocumentPolicyUpdate { + document: self.document, + kind: self.kind, + completed: self.pending.wait().await.map_err(|error| error.to_string()), + } + } +} + +impl CompletedDocumentPolicyUpdate { + pub fn document(&self) -> DocumentHandle { + self.document + } +} + +impl PendingDocumentPolicyBatch { + pub async fn wait(self) -> CompletedDocumentPolicyBatch { + let mut updates = Vec::with_capacity(self.updates.len()); + for update in self.updates { + updates.push(update.wait().await); + } + CompletedDocumentPolicyBatch { + context: self.context, + document: self.document, + admission_error: self.admission_error, + updates, + } + } +} + +impl CompletedDocumentPolicyBatch { + pub fn context(&self) -> crate::browser::BrowserContextId { + self.context + } + + pub fn document(&self) -> DocumentHandle { + self.document + } +} + +impl BrowserContext { + pub fn start_document_runtime_policy_reconciliation( + &mut self, + document: DocumentHandle, + policy: DocumentRuntimePolicyReconciliation, + ) -> PendingDocumentPolicyBatch { + let updates = vec![ + DocumentPolicyUpdate::ScriptExecutionDisabled(policy.script_execution_disabled), + DocumentPolicyUpdate::EmulatedMedia(policy.emulated_media), + DocumentPolicyUpdate::NetworkOffline(policy.network_offline), + DocumentPolicyUpdate::ViewportSurface(policy.viewport_surface), + ]; + self.start_document_policy_batch(document, updates) + } + + pub fn start_document_policy_batch( + &mut self, + document: DocumentHandle, + updates: Vec, + ) -> PendingDocumentPolicyBatch { + let mut pending = Vec::with_capacity(updates.len()); + let mut admission_error = None; + for update in updates { + match self.start_document_policy_update(document, update) { + Ok(update) => pending.push(update), + Err(error) => { + admission_error.get_or_insert(error); + } + } + } + PendingDocumentPolicyBatch { + context: document.web_contents().context(), + document, + admission_error, + updates: pending, + } + } + + pub fn start_document_policy_batch_with_surface( + &mut self, + document: DocumentHandle, + updates: Vec, + foreground: bool, + navigator_queries: moli_page_types::NavigatorQueryOverrides, + global_network_conditions: Option, + global_geolocation: Option<&crate::browser::EmulatedGeolocationOverrideState>, + ) -> PendingDocumentPolicyBatch { + let mut batch = self.start_document_policy_batch(document, updates); + match self.start_document_page_surface_update( + document, + foreground, + navigator_queries, + global_network_conditions, + global_geolocation, + ) { + Ok(surface) => { + batch.updates.extend(surface.updates); + if let Some(error) = surface.admission_error { + batch.admission_error.get_or_insert(error); + } + } + Err(error) => { + batch.admission_error.get_or_insert(error); + } + } + batch + } + + pub fn start_document_policy_update( + &mut self, + document: DocumentHandle, + update: DocumentPolicyUpdate, + ) -> Result { + let page = &mut self.document_mut(document)?.page; + let (kind, pending) = match update { + DocumentPolicyUpdate::NetworkRequestPolicy { + extra_headers, + bypass_service_worker, + cache_disabled, + blocked_url_patterns, + } => ( + DocumentPolicyUpdateKind::SetNetworkRequestPolicy, + page.start_set_network_request_policy( + &extra_headers, + bypass_service_worker, + cache_disabled, + &blocked_url_patterns, + ), + ), + DocumentPolicyUpdate::ExtraHttpHeaders(headers) => ( + DocumentPolicyUpdateKind::SetExtraHttpHeaders, + page.start_set_extra_http_headers(&headers), + ), + DocumentPolicyUpdate::BlockedUrls(patterns) => ( + DocumentPolicyUpdateKind::SetBlockedUrls, + page.start_set_blocked_url_patterns(&patterns), + ), + DocumentPolicyUpdate::BypassServiceWorker(bypass) => ( + DocumentPolicyUpdateKind::SetBypassServiceWorker, + page.start_set_bypass_service_worker(bypass), + ), + DocumentPolicyUpdate::NetworkOffline(offline) => ( + DocumentPolicyUpdateKind::SetNetworkConditions, + page.start_set_network_offline(offline), + ), + DocumentPolicyUpdate::IdleOverride(override_) => ( + DocumentPolicyUpdateKind::SetIdleOverride, + page.start_set_idle_override(override_), + ), + DocumentPolicyUpdate::NavigatorOverrides(overrides) => ( + DocumentPolicyUpdateKind::SetNavigatorOverrides, + page.start_set_navigator_overrides(&overrides), + ), + DocumentPolicyUpdate::EmulatedMedia(overrides) => ( + DocumentPolicyUpdateKind::SetEmulatedMedia, + page.start_set_emulated_media(&overrides), + ), + DocumentPolicyUpdate::ViewportSurface(surface) => ( + DocumentPolicyUpdateKind::SetViewportSurface, + page.start_set_viewport_surface(surface), + ), + DocumentPolicyUpdate::ScriptExecutionDisabled(disabled) => ( + DocumentPolicyUpdateKind::SetScriptExecutionDisabled, + page.start_set_script_execution_disabled(disabled), + ), + DocumentPolicyUpdate::DocumentActivity(activity) => ( + DocumentPolicyUpdateKind::SetDocumentActivity, + page.start_set_document_activity(activity), + ), + }; + Ok(PendingDocumentPolicyUpdate { + document, + kind, + pending: pending.map_err(|error| error.to_string())?, + }) + } + + pub fn finish_document_policy_update( + &mut self, + completed: CompletedDocumentPolicyUpdate, + ) -> Result<(), String> { + let completion = completed.completed?; + let page = match self.document_mut(completed.document) { + Ok(document) => &mut document.page, + Err(error) => { + completion + .into_unit_page_command_turn() + .map(drop) + .map_err(|unexpected| { + format!( + "retired Document policy command returned an unexpected reply: {unexpected}" + ) + })?; + return Err(error); + } + }; + match completed.kind { + DocumentPolicyUpdateKind::SetNetworkRequestPolicy => { + page.finish_set_network_request_policy(completion) + } + DocumentPolicyUpdateKind::SetExtraHttpHeaders => { + page.finish_set_extra_http_headers(completion) + } + DocumentPolicyUpdateKind::SetBlockedUrls => { + page.finish_set_blocked_url_patterns(completion) + } + DocumentPolicyUpdateKind::SetBypassServiceWorker => { + page.finish_set_bypass_service_worker(completion) + } + DocumentPolicyUpdateKind::SetNetworkConditions => { + page.finish_set_network_offline(completion) + } + DocumentPolicyUpdateKind::SetIdleOverride => page.finish_set_idle_override(completion), + DocumentPolicyUpdateKind::SetNavigatorOverrides => { + page.finish_set_navigator_overrides(completion) + } + DocumentPolicyUpdateKind::SetEmulatedMedia => { + page.finish_set_emulated_media(completion) + } + DocumentPolicyUpdateKind::SetViewportSurface => { + page.finish_set_viewport_surface(completion) + } + DocumentPolicyUpdateKind::SetScriptExecutionDisabled => { + page.finish_set_script_execution_disabled(completion) + } + DocumentPolicyUpdateKind::SetDocumentActivity => { + page.finish_set_document_activity(completion) + } + } + .map_err(|error| error.to_string()) + } + + pub fn finish_document_policy_batch( + &mut self, + completed: CompletedDocumentPolicyBatch, + ) -> Result<(), String> { + let mut first_error = completed.admission_error; + for update in completed.updates { + if let Err(error) = self.finish_document_policy_update(update) { + first_error.get_or_insert(error); + } + } + first_error.map_or(Ok(()), Err) + } + + pub fn start_document_page_surface_update( + &mut self, + document: DocumentHandle, + foreground: bool, + navigator_queries: moli_page_types::NavigatorQueryOverrides, + global_network_conditions: Option, + global_geolocation: Option<&crate::browser::EmulatedGeolocationOverrideState>, + ) -> Result { + let mut surface = self.page_surface_for_web_contents( + document.web_contents(), + foreground, + global_network_conditions, + global_geolocation, + )?; + surface.navigator_queries = navigator_queries; + Ok(self.start_document_policy_batch( + document, + vec![ + DocumentPolicyUpdate::NavigatorOverrides(surface.navigator_overrides()), + DocumentPolicyUpdate::DocumentActivity(surface.document_activity()), + ], + )) + } + + pub fn page_surface_for_web_contents( + &self, + handle: crate::browser::WebContentsHandle, + foreground: bool, + global_network_conditions: Option, + global_geolocation: Option<&crate::browser::EmulatedGeolocationOverrideState>, + ) -> Result { + Ok(self.web_contents(handle)?.page_surface( + foreground, + self.emulation_defaults + .network_conditions + .or(global_network_conditions), + self.emulation_defaults + .geolocation + .as_ref() + .or(global_geolocation), + self.emulation_defaults.device_metrics.as_ref(), + )) + } +} diff --git a/moli-core/src/browser/browser_context/document_queries.rs b/moli-core/src/browser/browser_context/document_queries.rs new file mode 100644 index 0000000000..28ae9e9c98 --- /dev/null +++ b/moli-core/src/browser/browser_context/document_queries.rs @@ -0,0 +1,108 @@ +use super::{ + BrowserContext, + document_commands::{ + CompletedChildFrameTreeSnapshot, CompletedDocumentCookieOwnerSnapshot, + CompletedDocumentStorageKeySnapshot, PendingChildFrameTreeSnapshot, + PendingDocumentCookieOwnerSnapshot, PendingDocumentStorageKeySnapshot, + }, +}; +use crate::browser::DocumentHandle; +use crate::page::{ChildFrameTreeSnapshot, DocumentCookieOwnerSnapshot}; + +impl BrowserContext { + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_response_status_for_test( + &self, + document: DocumentHandle, + ) -> Result { + Ok(self.document(document)?.page.status()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_script_execution_for_test( + &self, + document: DocumentHandle, + ) -> Result { + Ok(self.document(document)?.page.script_execution().clone()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_renderer_inspection_endpoint_for_test( + &self, + document: DocumentHandle, + ) -> Result { + Ok(self.document(document)?.page.renderer_inspection_endpoint()) + } + + pub fn start_document_storage_key_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + let pending = self + .document(document)? + .page + .start_document_storage_key_snapshot() + .map_err(|error| error.to_string())?; + Ok(PendingDocumentStorageKeySnapshot::new(document, pending)) + } + + pub fn finish_document_storage_key_snapshot( + &mut self, + completed: CompletedDocumentStorageKeySnapshot, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_document_storage_key_snapshot(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_child_frame_tree_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + let pending = self + .document(document)? + .page + .start_child_frame_tree_snapshot() + .map_err(|error| error.to_string())?; + Ok(PendingChildFrameTreeSnapshot::new(document, pending)) + } + + pub fn finish_child_frame_tree_snapshot( + &mut self, + completed: CompletedChildFrameTreeSnapshot, + ) -> Result, String> { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_child_frame_tree_snapshot(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_document_cookie_owner_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + let pending = self + .document(document)? + .page + .start_document_cookie_owner_snapshot() + .map_err(|error| error.to_string())?; + Ok(PendingDocumentCookieOwnerSnapshot::new(document, pending)) + } + + pub fn finish_document_cookie_owner_snapshot( + &mut self, + completed: CompletedDocumentCookieOwnerSnapshot, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_document_cookie_owner_snapshot(completion?) + .map_err(|error| error.to_string()) + } +} diff --git a/moli-core/src/browser/browser_context/fetch.rs b/moli-core/src/browser/browser_context/fetch.rs new file mode 100644 index 0000000000..8d61f58955 --- /dev/null +++ b/moli-core/src/browser/browser_context/fetch.rs @@ -0,0 +1,433 @@ +use super::BrowserContext; +use crate::{ + RendererOutputFence, + browser::{DocumentHandle, WebContentsHandle}, + page::{ + CompletedPageCommand, PendingPageCommand, PendingSubresourceContinueOutcome, + RendererSyntheticResponseBody, SubresourceAuthCredentials, SubresourceResourceType, + }, +}; +use url::Url; + +/// Closed Browser-native command family for a concrete renderer Document. +/// Frontend routing identities never enter this API. +pub enum DocumentFetchCommand { + ContinueRequest { + internal_id: u64, + url: Option, + method: Option, + body: Option>, + headers: Option, + intercept_response: bool, + handle_auth_requests: bool, + }, + ContinueAuth { + internal_id: u64, + auth: SubresourceAuthCredentials, + }, + CancelAuth { + internal_id: u64, + }, + FailAuth { + internal_id: u64, + error_text: String, + }, + FailRequest { + internal_id: u64, + error_text: String, + }, + FailResponse { + internal_id: u64, + error_text: String, + }, + FulfillRequest { + internal_id: u64, + response_code: u16, + response_headers: Vec<(String, String)>, + response_body: RendererSyntheticResponseBody, + }, + FulfillResponse { + internal_id: u64, + response_code: u16, + response_headers: Vec<(String, String)>, + response_body: RendererSyntheticResponseBody, + }, + ContinueResponse { + internal_id: u64, + response_code: Option, + response_headers: Option>, + }, + DispatchWebSocketText { + socket_id: u64, + data: String, + }, + DispatchWebSocketBinary { + socket_id: u64, + data: Vec, + }, + CloseWebSocket { + socket_id: u64, + code: Option, + reason: String, + }, +} + +#[derive(Clone, Copy)] +enum DocumentFetchCommandKind { + InterceptionUpdate { accept_stale: bool }, + ContinueRequest, + ContinueAuth, + CancelAuth, + FailAuth, + FailRequest, + FailResponse, + FulfillRequest, + FulfillResponse, + ContinueResponse, + DispatchWebSocketText, + DispatchWebSocketBinary, + CloseWebSocket, +} + +pub struct PendingDocumentFetchCommand { + document: DocumentHandle, + kind: DocumentFetchCommandKind, + pending: PendingPageCommand, +} + +pub struct CompletedDocumentFetchCommand { + document: DocumentHandle, + kind: DocumentFetchCommandKind, + completed: Result, +} + +pub enum DocumentFetchCommandOutcome { + Continued(PendingSubresourceContinueOutcome), + Complete, +} + +impl DocumentFetchCommandOutcome { + pub fn into_continue_outcome(self) -> Result { + match self { + Self::Continued(outcome) => Ok(outcome), + Self::Complete => Err("Fetch command did not produce a continue outcome".to_owned()), + } + } +} + +impl PendingDocumentFetchCommand { + fn new( + document: DocumentHandle, + kind: DocumentFetchCommandKind, + pending: PendingPageCommand, + ) -> Self { + Self { + document, + kind, + pending, + } + } + + pub async fn wait(self) -> CompletedDocumentFetchCommand { + CompletedDocumentFetchCommand { + document: self.document, + kind: self.kind, + completed: self.pending.wait().await.map_err(|error| error.to_string()), + } + } +} + +impl CompletedDocumentFetchCommand { + pub fn document(&self) -> DocumentHandle { + self.document + } + + pub fn renderer_output_predecessor(&self) -> Option { + self.completed + .as_ref() + .ok() + .and_then(CompletedPageCommand::renderer_output_predecessor) + } + + pub fn is_interception_update(&self) -> bool { + matches!( + self.kind, + DocumentFetchCommandKind::InterceptionUpdate { .. } + ) + } + + fn into_parts( + self, + ) -> ( + DocumentHandle, + DocumentFetchCommandKind, + Result, + ) { + (self.document, self.kind, self.completed) + } +} + +impl BrowserContext { + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn web_contents_fetch_interception_for_test( + &self, + web_contents: WebContentsHandle, + ) -> Result<(bool, Option), String> { + Ok(self + .web_contents(web_contents)? + .fetch_subresource_interception()) + } + + pub fn start_document_fetch_command( + &self, + document: DocumentHandle, + command: DocumentFetchCommand, + ) -> Result { + let page = &self.document(document)?.page; + let (kind, pending) = match command { + DocumentFetchCommand::ContinueRequest { + internal_id, + url, + method, + body, + headers, + intercept_response, + handle_auth_requests, + } => ( + DocumentFetchCommandKind::ContinueRequest, + page.start_continue_pending_subresource_fetch( + internal_id, + url, + method, + body, + headers, + intercept_response, + handle_auth_requests, + ), + ), + DocumentFetchCommand::ContinueAuth { internal_id, auth } => ( + DocumentFetchCommandKind::ContinueAuth, + page.start_continue_pending_subresource_auth(internal_id, auth), + ), + DocumentFetchCommand::CancelAuth { internal_id } => ( + DocumentFetchCommandKind::CancelAuth, + page.start_cancel_pending_subresource_auth(internal_id), + ), + DocumentFetchCommand::FailAuth { + internal_id, + error_text, + } => ( + DocumentFetchCommandKind::FailAuth, + page.start_fail_pending_subresource_auth(internal_id, error_text), + ), + DocumentFetchCommand::FailRequest { + internal_id, + error_text, + } => ( + DocumentFetchCommandKind::FailRequest, + page.start_fail_pending_subresource_fetch(internal_id, error_text), + ), + DocumentFetchCommand::FailResponse { + internal_id, + error_text, + } => ( + DocumentFetchCommandKind::FailResponse, + page.start_fail_pending_subresource_response(internal_id, error_text), + ), + DocumentFetchCommand::FulfillRequest { + internal_id, + response_code, + response_headers, + response_body, + } => ( + DocumentFetchCommandKind::FulfillRequest, + page.start_fulfill_pending_subresource_fetch( + internal_id, + response_code, + response_headers, + response_body, + ), + ), + DocumentFetchCommand::FulfillResponse { + internal_id, + response_code, + response_headers, + response_body, + } => ( + DocumentFetchCommandKind::FulfillResponse, + page.start_fulfill_pending_subresource_response( + internal_id, + response_code, + response_headers, + response_body, + ), + ), + DocumentFetchCommand::ContinueResponse { + internal_id, + response_code, + response_headers, + } => ( + DocumentFetchCommandKind::ContinueResponse, + page.start_continue_pending_subresource_response( + internal_id, + response_code, + response_headers, + ), + ), + DocumentFetchCommand::DispatchWebSocketText { socket_id, data } => ( + DocumentFetchCommandKind::DispatchWebSocketText, + page.start_receive_synthetic_websocket_text(socket_id, data), + ), + DocumentFetchCommand::DispatchWebSocketBinary { socket_id, data } => ( + DocumentFetchCommandKind::DispatchWebSocketBinary, + page.start_receive_synthetic_websocket_binary(socket_id, data), + ), + DocumentFetchCommand::CloseWebSocket { + socket_id, + code, + reason, + } => ( + DocumentFetchCommandKind::CloseWebSocket, + page.start_close_synthetic_websocket_from_server(socket_id, code, reason), + ), + }; + Ok(PendingDocumentFetchCommand::new( + document, + kind, + pending.map_err(|error| error.to_string())?, + )) + } + + pub fn start_web_contents_fetch_interception_update( + &mut self, + web_contents: WebContentsHandle, + enabled: bool, + resource_type: Option, + accept_stale_completion: bool, + ) -> Result, String> { + let contents = self.web_contents_mut(web_contents)?; + let document = contents + .main_frame + .current_document + .as_ref() + .map(|document| document.id); + let pending = contents.start_fetch_interception_update(enabled, resource_type)?; + Ok(match (document, pending) { + (Some(document), Some(pending)) => Some(PendingDocumentFetchCommand::new( + DocumentHandle::new(web_contents, document), + DocumentFetchCommandKind::InterceptionUpdate { + accept_stale: accept_stale_completion, + }, + pending, + )), + (None, None) => None, + _ => unreachable!("fetch interception update must follow current Document presence"), + }) + } + + pub fn install_web_contents_fetch_interception_policy( + &mut self, + web_contents: WebContentsHandle, + enabled: bool, + resource_type: Option, + ) -> Result<(), String> { + self.web_contents_mut(web_contents)? + .install_fetch_interception_policy(enabled, resource_type); + Ok(()) + } + + pub fn finish_document_fetch_command( + &mut self, + completed: CompletedDocumentFetchCommand, + ) -> Result { + let (document, kind, completed) = completed.into_parts(); + let completion = completed?; + if let DocumentFetchCommandKind::InterceptionUpdate { accept_stale } = kind { + return self.finish_document_fetch_interception_update( + document, + completion, + accept_stale, + ); + } + let page = &mut self.document_mut(document)?.page; + match kind { + DocumentFetchCommandKind::ContinueRequest => page + .finish_continue_pending_subresource_fetch(completion) + .map(DocumentFetchCommandOutcome::Continued), + DocumentFetchCommandKind::ContinueAuth => page + .finish_continue_pending_subresource_auth(completion) + .map(DocumentFetchCommandOutcome::Continued), + DocumentFetchCommandKind::CancelAuth => page + .finish_cancel_pending_subresource_auth(completion) + .map(|_| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::FailAuth => page + .finish_fail_pending_subresource_auth(completion) + .map(|_| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::FailRequest => page + .finish_fail_pending_subresource_fetch(completion) + .map(|_| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::FailResponse => page + .finish_fail_pending_subresource_response(completion) + .map(|_| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::FulfillRequest => page + .finish_fulfill_pending_subresource_fetch(completion) + .map(|()| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::FulfillResponse => page + .finish_fulfill_pending_subresource_response(completion) + .map(|()| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::ContinueResponse => page + .finish_continue_pending_subresource_response(completion) + .map(|()| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::DispatchWebSocketText => page + .finish_receive_synthetic_websocket_text(completion) + .map(|()| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::DispatchWebSocketBinary => page + .finish_receive_synthetic_websocket_binary(completion) + .map(|()| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::CloseWebSocket => page + .finish_close_synthetic_websocket_from_server(completion) + .map(|()| DocumentFetchCommandOutcome::Complete), + DocumentFetchCommandKind::InterceptionUpdate { .. } => unreachable!(), + } + .map_err(|error| error.to_string()) + } + + fn finish_document_fetch_interception_update( + &mut self, + document: DocumentHandle, + completion: CompletedPageCommand, + accept_stale: bool, + ) -> Result { + match self.document_mut(document) { + Ok(document) => document + .page + .finish_set_fetch_subresource_interception(completion) + .map(|()| DocumentFetchCommandOutcome::Complete) + .map_err(|error| error.to_string()), + Err(error) if error == "NoDocumentLoaded" || accept_stale => { + completion + .into_unit_page_command_turn() + .map(drop) + .map_err(|error| error.to_string())?; + Ok(DocumentFetchCommandOutcome::Complete) + } + Err(error) if error == "Document changed" => Err("Renderer Page changed".to_owned()), + Err(error) => Err(error), + } + } + + pub fn finish_unobserved_document_fetch_interception_update( + completed: CompletedDocumentFetchCommand, + ) -> Result { + let (_, kind, completed) = completed.into_parts(); + if !matches!(kind, DocumentFetchCommandKind::InterceptionUpdate { .. }) { + return Err("NoDocumentLoaded".to_owned()); + } + completed? + .into_unit_page_command_turn() + .map(drop) + .map_err(|error| error.to_string())?; + Ok(DocumentFetchCommandOutcome::Complete) + } +} diff --git a/moli-core/src/browser/browser_context/input.rs b/moli-core/src/browser/browser_context/input.rs new file mode 100644 index 0000000000..b64ef8fbcb --- /dev/null +++ b/moli-core/src/browser/browser_context/input.rs @@ -0,0 +1,174 @@ +use super::BrowserContext; +use super::document_commands::{ + CompletedDocumentAutofillTrigger, CompletedDocumentInputCommand, + PendingDocumentAutofillTrigger, PendingDocumentInputCommand, +}; +use crate::browser::{DocumentHandle, DocumentLifetimeObserver}; +use crate::page::{ + PageInputExt, RendererAutofillTriggerOutcome, RendererAutofillTriggerRequest, + RendererCommandTurnOutput, RendererDragData, RendererPointerEventProperties, + RendererTouchPoint, +}; + +/// Native input admitted to the current document, independent of inspector sessions. +pub enum PageInputCommand { + Key { + event_name: String, + key: String, + code: String, + text: String, + modifiers: u8, + auto_repeat: bool, + should_insert_text: bool, + }, + Mouse { + x: f64, + y: f64, + event_name: String, + button: i32, + buttons: Option, + click_count: i32, + delta_x: f64, + delta_y: f64, + pointer: RendererPointerEventProperties, + modifiers: u8, + }, + Touch { + points: Vec, + event_name: String, + activate: bool, + }, + Drag { + x: f64, + y: f64, + event_name: String, + data: RendererDragData, + modifiers: u8, + }, + InsertText(String), +} + +impl BrowserContext { + pub fn observe_document_lifetime( + &mut self, + document: DocumentHandle, + ) -> Result { + Ok(self.document_mut(document)?.lifetime.observe()) + } + + pub fn start_document_autofill_trigger( + &self, + document: DocumentHandle, + request: RendererAutofillTriggerRequest, + ) -> Result { + let pending = self + .document(document)? + .page + .start_autofill_trigger(request) + .map_err(|error| error.to_string())?; + Ok(PendingDocumentAutofillTrigger::new(document, pending)) + } + + pub fn finish_document_autofill_trigger( + &mut self, + completed: CompletedDocumentAutofillTrigger, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_autofill_trigger(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_document_input_command( + &self, + document: DocumentHandle, + command: PageInputCommand, + ) -> Result { + let page = &self.document(document)?.page; + let pending = match command { + PageInputCommand::Key { + event_name, + key, + code, + text, + modifiers, + auto_repeat, + should_insert_text, + } => page.start_dispatch_key_event_with_outcome( + &event_name, + &key, + &code, + &text, + modifiers, + auto_repeat, + should_insert_text, + ), + PageInputCommand::Mouse { + x, + y, + event_name, + button, + buttons, + click_count, + delta_x, + delta_y, + pointer, + modifiers, + } => page.start_dispatch_mouse_event_at_point_with_pointer_outcome( + x, + y, + &event_name, + button, + buttons, + click_count, + delta_x, + delta_y, + pointer, + modifiers, + ), + PageInputCommand::Touch { + points, + event_name, + activate, + } => page.start_dispatch_touch_event_at_points_with_outcome( + points, + &event_name, + activate, + ), + PageInputCommand::Drag { + x, + y, + event_name, + data, + modifiers, + } => page.start_dispatch_drag_event_at_point_with_outcome( + x, + y, + &event_name, + data, + modifiers, + ), + PageInputCommand::InsertText(text) => page.start_insert_text_into_active_control(&text), + } + .map_err(|error| error.to_string())?; + Ok(PendingDocumentInputCommand::new(document, pending)) + } + + pub fn finish_document_input_command( + &mut self, + completed: CompletedDocumentInputCommand, + ) -> Result { + let (document, completion) = completed.into_parts(); + let completion = completion?; + match self.document_mut(document) { + Ok(document) => Ok(document.page.finish_page_command_turn(completion)), + // InputInjector acknowledges an event consumed by the retired + // renderer, but its frozen Page state cannot enter a replacement. + Err(error) if matches!(error.as_str(), "Document changed" | "NoDocumentLoaded") => { + Ok(completion.into_output()) + } + Err(error) => Err(error), + } + } +} diff --git a/moli-core/src/browser/browser_context/navigation.rs b/moli-core/src/browser/browser_context/navigation.rs new file mode 100644 index 0000000000..777be2c196 --- /dev/null +++ b/moli-core/src/browser/browser_context/navigation.rs @@ -0,0 +1,853 @@ +use url::Url; + +use crate::{ + browser::{ + DocumentHandle, DocumentId, NavigationId, NavigationRequestLoadPolicy, + RendererPageResidenceIdentity, WebContentsHandle, + web_contents::{ + AdmittedDocumentMaterialization, AdmittedNavigationLoad, BuiltInitialDocument, + ClaimedNavigationRequest, CommittedDocumentNavigation, CommittedInitialDocument, + DocumentNavigationDestination, HistoryTraversalDestination, InitialDocumentAdmission, + InterceptedNavigationLoad, InterceptedNavigationResponse, NavigationInterceptionPermit, + NavigationRequestInterception, PageNavigationHistoryEntry, PausedDocumentTransfer, + PreparedDocumentNavigation, PreparedNavigationResponse, ResolvedHistoryTraversal, + RetiringDocument, SameDocumentNavigationCommitted, + }, + }, + page::{RendererDocumentLifecycleEvent, SameDocumentHistoryUpdate}, +}; + +use super::BrowserContext; + +#[derive(Clone, Debug)] +pub struct DocumentNavigationMetadata { + pub current_url: Url, + pub initiator_url: Option, + pub requested_url: Url, + pub redirected: bool, + pub redirect_count: usize, +} + +impl BrowserContext { + pub fn selected_document_handle(&self) -> Option { + let contents = WebContentsHandle::new(self.id, self.selected_web_contents?); + let document = self + .web_contents(contents) + .ok()? + .main_frame + .current_document + .as_ref()?; + Some(DocumentHandle::new(contents, document.id)) + } + + pub fn selected_document_navigation_metadata(&self) -> Option { + self.document_navigation_metadata(self.selected_document_handle()?) + .ok() + } + + pub fn document_navigation_metadata( + &self, + handle: DocumentHandle, + ) -> Result { + let page = &self.document(handle)?.page; + Ok(DocumentNavigationMetadata { + current_url: page.final_url().clone(), + initiator_url: page.navigation_initiator_url().cloned(), + requested_url: page.requested_url().clone(), + redirected: page.navigation_redirected(), + redirect_count: page.navigation_redirect_count(), + }) + } + + pub fn has_loaded_document(&self, handle: WebContentsHandle) -> bool { + self.web_contents(handle) + .is_ok_and(|contents| contents.main_frame.current_document.is_some()) + } + + pub fn initial_document_build_pending( + &self, + handle: WebContentsHandle, + ) -> Result { + Ok(self + .web_contents(handle)? + .navigation() + .initial_document_build() + .is_some_and(|build| build.completion.pending())) + } + + pub fn pending_document( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self.web_contents(handle)?.navigation().pending_document()) + } + + pub fn accepts_document_preparation( + &self, + handle: WebContentsHandle, + navigation: NavigationId, + renderer: RendererPageResidenceIdentity, + ) -> Result { + Ok(self + .web_contents(handle)? + .navigation() + .accepts_document_preparation(navigation, renderer)) + } + + pub fn document_renderer_matches( + &self, + handle: WebContentsHandle, + renderer: RendererPageResidenceIdentity, + ) -> bool { + self.document_handle(handle) + .ok() + .flatten() + .and_then(|document| self.document_renderer_residence(document).ok()) + == Some(renderer) + } + + pub fn retire_document( + &mut self, + handle: WebContentsHandle, + ) -> Result { + Ok(RetiringDocument::from_page( + self.web_contents_mut(handle)?.replace_document(None), + )) + } + + pub fn document_handle( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self + .web_contents(handle)? + .main_frame + .current_document + .as_ref() + .map(|document| DocumentHandle::new(handle, document.id))) + } + + pub fn start_initial_document( + &mut self, + handle: WebContentsHandle, + inherited: crate::browser::web_contents::InheritedDocumentPolicy, + ) -> Result { + self.web_contents_mut(handle)? + .start_initial_document_build(inherited) + } + + pub fn commit_initial_document( + &mut self, + built: BuiltInitialDocument, + ) -> Result> { + let Some(contents) = self.web_contents.get_mut(&built.key().web_contents()) else { + return Err(Box::new(built)); + }; + contents.commit_initial_document(built) + } + + pub fn start_document_materialization( + &mut self, + handle: WebContentsHandle, + navigation: NavigationId, + page: PreparedNavigationResponse, + destination: DocumentNavigationDestination, + inherited: crate::browser::web_contents::InheritedDocumentPolicy, + ) -> Result { + self.web_contents_mut(handle)? + .start_document_materialization(navigation, page, destination, inherited) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn start_loaded_document_navigation_for_test( + &self, + handle: WebContentsHandle, + navigation: NavigationId, + page: crate::page::Page, + destination: DocumentNavigationDestination, + artifacts: &crate::page::RendererPageCreationArtifacts, + permissions: Vec, + ) -> Result< + impl std::future::Future> + use<>, + &'static str, + > { + self.web_contents(handle) + .map_err(|_| "navigation WebContents unavailable")? + .start_loaded_document_navigation(navigation, page, destination, artifacts, permissions) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn capture_document_policy_for_test( + &mut self, + handle: WebContentsHandle, + inherited: crate::browser::web_contents::InheritedDocumentPolicy, + final_url: &Url, + ) -> Result { + self.web_contents_mut(handle)? + .capture_document_policy(inherited, final_url) + } + + pub fn start_navigation_load( + &mut self, + handle: WebContentsHandle, + navigation: NavigationId, + policy: NavigationRequestLoadPolicy, + inherited: crate::browser::web_contents::InheritedDocumentPolicy, + ) -> Result { + self.web_contents_mut(handle)? + .start_navigation_load(navigation, policy, inherited) + } + + pub fn commit_document_navigation( + &mut self, + prepared: PreparedDocumentNavigation, + ) -> Result { + self.web_contents + .get_mut(&prepared.web_contents_id()) + .ok_or("navigation WebContents unavailable")? + .commit_document_navigation(prepared) + .map_err(str::to_owned) + } + + pub fn web_contents_identity( + &self, + handle: WebContentsHandle, + ) -> Result< + ( + crate::browser::WebContentsId, + crate::browser::MainFrameSlotId, + ), + String, + > { + let contents = self.web_contents(handle)?; + Ok((contents.id(), contents.main_frame.id())) + } + + pub fn pause_navigation_request( + &mut self, + handle: WebContentsHandle, + navigation: NavigationId, + request: NavigationRequestInterception, + ) -> Result { + self.web_contents_mut(handle)? + .pause_navigation_request(navigation, request) + } + + pub fn take_navigation_request( + &mut self, + permit: NavigationInterceptionPermit, + ) -> Option { + self.web_contents + .get_mut(&permit.web_contents())? + .take_navigation_request(permit) + } + + pub fn start_claimed_navigation_request( + &mut self, + request: ClaimedNavigationRequest, + inherited: crate::browser::web_contents::InheritedDocumentPolicy, + ) -> Result { + self.web_contents + .get_mut(&request.permit().web_contents()) + .ok_or("navigation WebContents unavailable")? + .start_claimed_navigation_request(request, inherited) + } + + pub fn start_navigation_load_for_interception( + &mut self, + permit: NavigationInterceptionPermit, + policy: NavigationRequestLoadPolicy, + inherited: crate::browser::web_contents::InheritedDocumentPolicy, + ) -> Result { + self.web_contents + .get_mut(&permit.web_contents()) + .ok_or("navigation WebContents unavailable")? + .start_navigation_load_for_interception(permit, policy, inherited) + } + + pub fn pause_navigation_auth( + &mut self, + response: InterceptedNavigationResponse, + ) -> Result { + self.web_contents + .get_mut(&response.web_contents()) + .ok_or("navigation WebContents unavailable")? + .pause_navigation_auth(response) + } + + pub fn take_navigation_auth( + &mut self, + permit: NavigationInterceptionPermit, + ) -> Option> { + self.web_contents + .get_mut(&permit.web_contents())? + .take_navigation_auth(permit) + } + + pub fn pause_navigation_response( + &mut self, + handle: WebContentsHandle, + navigation: NavigationId, + transfer: PausedDocumentTransfer, + ) -> Result { + self.web_contents_mut(handle)? + .pause_navigation_response(navigation, transfer) + } + + pub fn take_navigation_response( + &mut self, + permit: NavigationInterceptionPermit, + ) -> Option { + self.web_contents + .get_mut(&permit.web_contents())? + .take_navigation_response(permit) + } + + pub fn restore_navigation_response( + &mut self, + permit: NavigationInterceptionPermit, + transfer: PausedDocumentTransfer, + ) -> Result<(), Box> { + let Some(contents) = self.web_contents.get_mut(&permit.web_contents()) else { + return Err(Box::new(transfer)); + }; + contents.restore_navigation_response(permit, transfer) + } + + pub fn resolve_history_traversal( + &self, + handle: WebContentsHandle, + destination: HistoryTraversalDestination, + ) -> Result { + self.web_contents(handle)? + .resolve_history_traversal(destination) + .map_err(str::to_owned) + } + + pub fn document_service_worker_client_id(&self, handle: DocumentHandle) -> Result { + Ok(self.document(handle)?.page.service_worker_client_id()) + } + + pub fn document_renderer_residence( + &self, + handle: DocumentHandle, + ) -> Result { + Ok(RendererPageResidenceIdentity::from_page( + &self.document(handle)?.page, + )) + } + + pub fn document_url(&self, handle: DocumentHandle) -> Result { + Ok(self.document(handle)?.page.final_url().clone()) + } + + pub fn document_title(&self, handle: DocumentHandle) -> Result { + Ok(self.document(handle)?.page.document_title()) + } + + pub fn commit_document_title( + &mut self, + handle: WebContentsHandle, + change: &crate::RendererDocumentTitleChanged, + ) -> Result, String> { + Ok(self.web_contents_mut(handle)?.commit_document_title(change)) + } + + pub fn document_lifecycle_snapshot( + &self, + handle: DocumentHandle, + ) -> Result, String> { + Ok(self.document(handle)?.lifecycle.snapshot()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn page_for_test(&self, handle: DocumentHandle) -> Option<&crate::page::Page> { + Some(&self.document(handle).ok()?.page) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn page_for_test_mut(&mut self, handle: DocumentHandle) -> Option<&mut crate::page::Page> { + Some(&mut self.document_mut(handle).ok()?.page) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_lifecycle_for_test( + &self, + handle: DocumentHandle, + ) -> Option<&crate::browser::DocumentLifecycle> { + Some(&self.document(handle).ok()?.lifecycle) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn install_document_lifecycle_for_test( + &mut self, + handle: DocumentHandle, + lifecycle: crate::browser::DocumentLifecycle, + ) -> Result<(), String> { + let snapshot = lifecycle.snapshot().expect("fixture lifecycle"); + let previous = self + .document(handle)? + .lifecycle + .snapshot() + .map(|snapshot| (snapshot.frame, snapshot.document, snapshot.epoch)); + self.document_mut(handle)?.lifecycle = lifecycle; + if previous != Some((snapshot.frame, snapshot.document, snapshot.epoch)) + || snapshot.terminated.is_some() + { + self.web_contents_mut(handle.web_contents())? + .javascript_dialogs + .clear(); + } + Ok(()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn apply_document_lifecycle_for_test( + &mut self, + handle: DocumentHandle, + event: RendererDocumentLifecycleEvent, + ) -> Result { + self.document(handle)?; + Ok(self + .web_contents_mut(handle.web_contents())? + .apply_document_lifecycle(event) + .is_some()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn replace_document_identity_for_test( + &mut self, + handle: DocumentHandle, + document: crate::browser::DocumentId, + ) -> Result<(), String> { + let host = self.document_mut(handle)?; + std::mem::take(&mut host.lifetime).supersede(); + host.id = document; + host.lifecycle = crate::browser::DocumentLifecycle::default(); + self.web_contents_mut(handle.web_contents())? + .javascript_dialogs + .clear(); + Ok(()) + } + + pub fn initial_document_url( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self + .web_contents(handle)? + .navigation() + .initial_empty_document_url_if_current() + .map(str::to_owned)) + } + + pub fn begin_initial_empty_document( + &mut self, + handle: WebContentsHandle, + initial_url: String, + creator: Option, + storage_key: Option, + ) -> Result<(), String> { + self.web_contents_mut(handle)?.begin_initial_empty_document( + initial_url, + creator, + storage_key, + ); + Ok(()) + } + + pub fn mark_initial_url_replaces_empty_document( + &mut self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .mark_next_navigation_history_replace_initial_empty_document(); + Ok(()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn mark_initial_empty_document_materialized_for_test( + &mut self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .mark_initial_empty_document_materialized(); + Ok(()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn mark_initial_empty_document_exited_for_test( + &mut self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .mark_initial_empty_document_exited(); + Ok(()) + } + + pub fn initial_document_storage_key( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self + .web_contents(handle)? + .navigation() + .initial_empty_document_storage_key_if_current() + .cloned()) + } + + pub fn is_on_initial_document( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self + .web_contents(handle)? + .navigation() + .is_on_initial_empty_document()) + } + + pub fn initial_document_has_pending_navigation( + &self, + handle: WebContentsHandle, + ) -> Result { + Ok(self + .web_contents(handle)? + .navigation() + .initial_empty_document_pending_cross_document_navigation()) + } + + pub fn navigation_history_snapshot( + &self, + handle: WebContentsHandle, + ) -> Result<(usize, Vec), String> { + Ok(self.web_contents(handle)?.navigation_history_snapshot()) + } + + pub fn navigation_history_entry_url( + &self, + handle: WebContentsHandle, + entry_id: i32, + ) -> Result, String> { + Ok(self + .web_contents(handle)? + .navigation_history_entry_url(entry_id)) + } + + pub fn mark_next_navigation_history_replace_current( + &mut self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .mark_next_navigation_history_replace_current(); + Ok(()) + } + + pub fn mark_next_navigation_history_traverse_to_entry( + &mut self, + handle: WebContentsHandle, + entry_id: i32, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .mark_next_navigation_history_traverse_to_entry(entry_id); + Ok(()) + } + + pub fn commit_same_document_navigation( + &mut self, + handle: WebContentsHandle, + document: DocumentId, + url: Url, + history_update: SameDocumentHistoryUpdate, + ) -> Result, String> { + Ok(self + .web_contents_mut(handle)? + .commit_same_document_navigation(document, url, history_update)) + } + + pub fn mark_renderer_crashed(&mut self, handle: WebContentsHandle) -> Result<(), String> { + self.web_contents_mut(handle)?.mark_renderer_crashed(); + Ok(()) + } + + pub fn start_document_navigation( + &mut self, + handle: WebContentsHandle, + ) -> Result { + Ok(self.web_contents_mut(handle)?.start_document_navigation()) + } + + pub fn accepts_pending_navigation( + &self, + handle: WebContentsHandle, + navigation: &NavigationId, + ) -> Result { + Ok(self + .web_contents(handle)? + .navigation() + .accepts_pending_document_navigation_event(navigation)) + } + + pub fn accepts_document_body_completion( + &self, + handle: WebContentsHandle, + navigation: &NavigationId, + ) -> Result { + Ok(self + .web_contents(handle)? + .navigation() + .accepts_document_body_completion_event(navigation)) + } + + pub fn has_pending_document_navigation( + &self, + handle: WebContentsHandle, + ) -> Result { + Ok(self + .web_contents(handle)? + .navigation() + .has_pending_document_navigation()) + } + + pub fn navigation_is_default(&self, handle: WebContentsHandle) -> Result { + Ok(self.web_contents(handle)?.navigation().is_default()) + } + + pub fn navigation_retains( + &self, + handle: WebContentsHandle, + navigation: NavigationId, + ) -> Result { + Ok(self + .web_contents(handle)? + .navigation() + .retains_navigation(navigation)) + } + + pub fn current_document_navigation( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self + .web_contents(handle)? + .navigation() + .current_document_navigation()) + } + + pub fn committed_document_navigation( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self + .web_contents(handle)? + .navigation() + .committed_document_navigation()) + } + + pub fn clear_pending_navigation_if_matches( + &mut self, + handle: WebContentsHandle, + navigation: &NavigationId, + ) -> Result { + Ok(self + .web_contents_mut(handle)? + .clear_pending_document_navigation_if_matches(navigation)) + } + + pub fn clear_document_navigation_state( + &mut self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .clear_document_navigation_state(); + Ok(()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn replace_document_for_test( + &mut self, + handle: WebContentsHandle, + document: Option, + ) -> Result, String> { + Ok(self.web_contents_mut(handle)?.replace_document(document)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn navigation_cancellation_handle_for_test( + &self, + handle: WebContentsHandle, + token: &NavigationId, + ) -> Option { + self.web_contents(handle) + .ok()? + .navigation() + .document_navigation_cancellation_handle(token) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn arm_background_navigation_completion_for_test( + &mut self, + handle: WebContentsHandle, + token: &NavigationId, + additional_cancellation: Option, + ) -> bool { + self.web_contents_mut(handle).is_ok_and(|contents| { + contents.arm_background_navigation_completion(token, additional_cancellation) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn settle_background_navigation_completion_for_test( + &mut self, + handle: WebContentsHandle, + token: &NavigationId, + ) -> bool { + self.web_contents_mut(handle) + .is_ok_and(|contents| contents.settle_background_navigation_completion(token)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn commit_pending_document_navigation_for_test( + &mut self, + handle: WebContentsHandle, + token: &NavigationId, + ) -> bool { + self.web_contents_mut(handle) + .is_ok_and(|contents| contents.commit_pending_document_navigation_if_matches(token)) + } + + pub fn has_inflight_background_navigation_for_web_contents( + &self, + handle: WebContentsHandle, + ) -> Result { + Ok(self + .web_contents(handle)? + .navigation() + .has_inflight_background_navigation()) + } + + pub fn accepts_any_pending_navigation_event(&self, token: &NavigationId) -> bool { + self.web_contents.values().any(|contents| { + contents + .navigation() + .accepts_pending_document_navigation_event(token) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_navigation_cancellation_handle_for_test( + &self, + token: &NavigationId, + ) -> Option { + self.web_contents.values().find_map(|contents| { + contents + .navigation() + .document_navigation_cancellation_handle(token) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn accepts_any_document_body_completion_for_test(&self, token: &NavigationId) -> bool { + self.web_contents.values().any(|contents| { + contents + .navigation() + .accepts_document_body_completion_event(token) + }) + } + + pub fn arm_background_navigation_completion( + &mut self, + token: &NavigationId, + additional_cancellation: Option, + ) -> bool { + let Some(contents) = self.web_contents.values_mut().find(|contents| { + contents + .navigation() + .accepts_pending_document_navigation_event(token) + }) else { + if let Some(cancellation) = additional_cancellation { + cancellation.cancel(); + } + return false; + }; + contents.arm_background_navigation_completion(token, additional_cancellation) + } + + pub fn settle_background_navigation_completion(&mut self, token: &NavigationId) -> bool { + self.web_contents + .values_mut() + .any(|contents| contents.settle_background_navigation_completion(token)) + } + + pub fn has_inflight_background_navigation(&self) -> bool { + self.web_contents + .values() + .any(|contents| contents.navigation().has_inflight_background_navigation()) + } + + pub fn apply_renderer_document_lifecycle( + &mut self, + renderer_page: RendererPageResidenceIdentity, + event: RendererDocumentLifecycleEvent, + ) -> Option { + self.web_contents + .values_mut() + .find(|contents| { + contents + .main_frame + .current_document + .as_ref() + .is_some_and(|document| { + RendererPageResidenceIdentity::from_page(&document.page) == renderer_page + }) + })? + .apply_document_lifecycle(event) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn has_paused_navigation_auth_for_test(&self, handle: WebContentsHandle) -> bool { + self.web_contents(handle) + .is_ok_and(|contents| contents.navigation().has_paused_auth_for_test()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn paused_navigation_response_for_test( + &self, + handle: WebContentsHandle, + ) -> Option<&PausedDocumentTransfer> { + self.web_contents(handle) + .ok()? + .navigation() + .paused_response_for_test() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn record_navigation_history_for_test( + &mut self, + handle: WebContentsHandle, + snapshot: (String, String), + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .record_navigation_history_for_test(snapshot); + Ok(()) + } +} diff --git a/moli-core/src/browser/browser_context/permissions.rs b/moli-core/src/browser/browser_context/permissions.rs new file mode 100644 index 0000000000..6c50260a4c --- /dev/null +++ b/moli-core/src/browser/browser_context/permissions.rs @@ -0,0 +1,117 @@ +use crate::{ + browser::{BrowserContextId, DocumentId, PermissionDefaults, WebContentsId}, + page::{CompletedPageCommand, PendingPageCommand}, +}; + +use super::BrowserContext; + +pub struct PendingContextPermissionUpdate { + context_id: BrowserContextId, + pages: Vec, +} + +struct PendingPermissionPageUpdate { + contents_id: WebContentsId, + document_id: DocumentId, + pending: PendingPageCommand, +} + +pub struct CompletedContextPermissionUpdate { + context_id: BrowserContextId, + pages: Vec, +} + +struct CompletedPermissionPageUpdate { + contents_id: WebContentsId, + document_id: DocumentId, + completed: Result, +} + +impl PendingContextPermissionUpdate { + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn page_count_for_test(&self) -> usize { + self.pages.len() + } + + pub async fn wait(self) -> CompletedContextPermissionUpdate { + let mut pages = Vec::with_capacity(self.pages.len()); + for page in self.pages { + pages.push(CompletedPermissionPageUpdate { + contents_id: page.contents_id, + document_id: page.document_id, + completed: page.pending.wait().await.map_err(|error| error.to_string()), + }); + } + CompletedContextPermissionUpdate { + context_id: self.context_id, + pages, + } + } +} + +impl CompletedContextPermissionUpdate { + pub fn context_id(&self) -> BrowserContextId { + self.context_id + } +} + +impl BrowserContext { + pub fn start_permission_update( + &self, + defaults: &PermissionDefaults, + ) -> Result, String> { + let mut pages = Vec::new(); + let overrides = self.permission_overrides.snapshot(defaults); + for (contents_id, contents) in &self.web_contents { + let Some(document) = &contents.main_frame.current_document else { + continue; + }; + pages.push(PendingPermissionPageUpdate { + contents_id: *contents_id, + document_id: document.id, + pending: document + .page + .start_set_permission_overrides(&overrides) + .map_err(|error| { + format!("failed to update page permission overrides: {error}") + })?, + }); + } + Ok( + (!pages.is_empty()).then_some(PendingContextPermissionUpdate { + context_id: self.id, + pages, + }), + ) + } + + pub fn finish_permission_update( + &mut self, + completed: CompletedContextPermissionUpdate, + ) -> Result<(), String> { + if completed.context_id != self.id { + return Err("Permission update belongs to a different BrowserContext".into()); + } + for page in completed.pages { + let completion = page.completed?; + let document = self + .web_contents + .get_mut(&page.contents_id) + .and_then(|contents| contents.main_frame.current_document.as_mut()) + .ok_or("NoDocumentLoaded")?; + if document.id == page.document_id { + document + .page + .finish_set_permission_overrides(completion) + .map_err(|error| error.to_string())?; + } else { + completion + .into_unit_page_command_turn() + .map(drop) + .map_err(|error| error.to_string())?; + } + } + Ok(()) + } +} diff --git a/moli-core/src/browser/browser_context/residence.rs b/moli-core/src/browser/browser_context/residence.rs new file mode 100644 index 0000000000..739fb634a5 --- /dev/null +++ b/moli-core/src/browser/browser_context/residence.rs @@ -0,0 +1,488 @@ +use crate::{ + browser::{ + MainFrameSlotId, WebContentsHandle, WebContentsId, + web_contents::{ + EmulationPolicy, EmulationPolicyChange, InitialDocumentSnapshot, NetworkRequestPolicy, + WebContents, WindowOpener, WindowSurface, WindowSurfaceState, + }, + }, + runtime::{NavigationEngine, NavigationRuntimeConfig}, +}; + +use super::BrowserContext; + +impl BrowserContext { + pub fn bind_page_navigation_engines( + &mut self, + config: NavigationRuntimeConfig, + renderer_output_transport_sender: Option, + ) { + self.page_navigation_runtime_config = Some(config.clone()); + self.renderer_output_transport_sender = renderer_output_transport_sender; + + let sender = self.renderer_output_transport_sender.clone(); + let runtime = self.renderer_runtime_owner_access(); + for contents in self.web_contents.values_mut() { + if contents.has_navigation_engine() { + continue; + } + let engine = NavigationEngine::new_with_runtime_config_and_browser_context_access( + config.clone(), + runtime.clone(), + ) + .expect("live BrowserContext owner must accept a page engine"); + if let Some(sender) = sender.clone() { + engine.set_renderer_output_transport_sender(sender); + } + contents.install_navigation_engine(engine); + } + } + + pub fn set_renderer_output_transport_sender( + &mut self, + sender: crate::RendererOutputTransportSender, + ) { + self.renderer_output_transport_sender = Some(sender.clone()); + for contents in self.web_contents.values() { + contents.set_renderer_output_transport_sender(sender.clone()); + } + } + + pub fn register_web_contents( + &mut self, + mut contents: WebContents, + ) -> Result<(WebContentsHandle, MainFrameSlotId), String> { + let id = contents.id(); + let main_frame = contents.main_frame.id(); + if self.web_contents.contains_key(&id) { + return Err("WebContents identity must be unique".into()); + } + if let Some(config) = self.page_navigation_runtime_config.clone() { + contents.install_navigation_engine(self.new_page_navigation_engine(config)); + } + self.web_contents.insert(id, contents); + Ok((WebContentsHandle::new(self.id, id), main_frame)) + } + + pub fn contains_web_contents(&self, handle: WebContentsHandle) -> bool { + self.web_contents(handle).is_ok() + } + + pub fn web_contents_count(&self) -> usize { + self.web_contents.len() + } + + pub fn selected_web_contents_handle(&self) -> Option { + Some(WebContentsHandle::new(self.id, self.selected_web_contents?)) + } + + pub fn web_contents_handle_for_window_id(&self, window_id: u64) -> Option { + let id = self + .web_contents + .keys() + .copied() + .find(|id| id.get() == window_id)?; + Some(WebContentsHandle::new(self.id, id)) + } + + pub fn web_contents_handle_for_window_name(&self, name: &str) -> Option { + let id = self + .web_contents + .values() + .find(|contents| contents.window.name.as_deref() == Some(name))? + .id(); + Some(WebContentsHandle::new(self.id, id)) + } + + pub fn web_contents_opener( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self + .web_contents(handle)? + .window + .opener + .map(|opener| (opener.web_contents_id, opener.can_access))) + } + + pub fn web_contents_window_name( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self.web_contents(handle)?.window.name.as_deref()) + } + + pub fn clone_session_storage_namespace( + &self, + web_contents: WebContentsId, + ) -> Option { + self.web_contents + .get(&web_contents) + .map(|contents| contents.session_storage.deep_clone()) + } + + pub fn web_contents_is_crashed(&self, handle: WebContentsHandle) -> Result { + Ok(self.web_contents(handle)?.crashed) + } + + pub fn performance_metric_snapshot( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self.web_contents(handle)?.performance_metric_snapshot()) + } + + pub fn observe_renderer_page_state( + &mut self, + handle: WebContentsHandle, + snapshot: &std::sync::Arc, + ) -> Result { + Ok(self + .web_contents_mut(handle)? + .observe_renderer_page_state(snapshot)) + } + + pub fn set_web_contents_crashed( + &mut self, + handle: WebContentsHandle, + crashed: bool, + ) -> Result<(), String> { + self.web_contents_mut(handle)?.crashed = crashed; + Ok(()) + } + + pub fn web_contents_initial_document_state( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self + .web_contents(handle)? + .navigation() + .initial_empty_document_state() + .map(Into::into)) + } + + pub fn web_contents_window_surface( + &self, + handle: WebContentsHandle, + ) -> Result { + Ok(self.web_contents(handle)?.window.surface) + } + + pub fn update_web_contents_window_surface( + &mut self, + handle: WebContentsHandle, + state: Option, + width: Option, + height: Option, + x: Option, + y: Option, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .window + .surface + .update(state, width, height, x, y); + Ok(()) + } + + pub fn set_web_contents_window_name( + &mut self, + handle: WebContentsHandle, + name: Option, + ) -> Result<(), String> { + self.web_contents(handle)?; + for contents in self.web_contents.values_mut() { + if contents.id() == handle.id() { + contents.window.name = name.clone(); + } else if name.is_some() && contents.window.name == name { + contents.window.name = None; + } + } + Ok(()) + } + + pub fn set_web_contents_opener( + &mut self, + handle: WebContentsHandle, + opener: Option, + can_access: bool, + ) -> Result<(), String> { + if let Some(opener) = opener { + self.web_contents(opener)?; + self.web_contents_mut(handle)?.window.opener = Some(WindowOpener { + web_contents_id: opener.id(), + can_access, + }); + } else { + self.web_contents_mut(handle)?.window.opener = None; + } + Ok(()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn set_web_contents_opener_for_test( + &mut self, + handle: WebContentsHandle, + opener: WebContentsId, + can_access: bool, + ) -> Result<(), String> { + self.web_contents_mut(handle)?.window.opener = Some(WindowOpener { + web_contents_id: opener, + can_access, + }); + Ok(()) + } + + pub fn set_web_contents_network_offline( + &mut self, + handle: WebContentsHandle, + offline: bool, + ) -> Result<(), String> { + self.web_contents_mut(handle)?.set_network_offline(offline); + Ok(()) + } + + pub fn web_contents_network_offline(&self, handle: WebContentsHandle) -> Result { + Ok(self.web_contents(handle)?.network_offline) + } + + pub fn web_contents_tls_verify_host_override( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self.web_contents(handle)?.tls_verify_host_override) + } + + pub fn set_web_contents_tls_verify_host_override( + &mut self, + handle: WebContentsHandle, + enabled: Option, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .set_tls_verify_host_override(enabled); + Ok(()) + } + + pub fn web_contents_bypass_content_security_policy( + &self, + handle: WebContentsHandle, + ) -> Result { + Ok(self.web_contents(handle)?.bypass_content_security_policy) + } + + pub fn set_web_contents_bypass_content_security_policy( + &mut self, + handle: WebContentsHandle, + bypass: bool, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .set_bypass_content_security_policy(bypass); + Ok(()) + } + + pub fn web_contents_browser_identity_override( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self.web_contents(handle)?.browser_identity_override.clone()) + } + + pub fn set_web_contents_browser_identity_override( + &mut self, + handle: WebContentsHandle, + identity: Option, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .set_browser_identity_override(identity); + Ok(()) + } + + pub fn web_contents_network_request_policy( + &self, + handle: WebContentsHandle, + ) -> Result { + Ok(self.web_contents(handle)?.network_request_policy.clone()) + } + + pub fn set_web_contents_network_request_policy( + &mut self, + handle: WebContentsHandle, + policy: NetworkRequestPolicy, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .set_network_request_policy(policy); + Ok(()) + } + + pub fn web_contents_locale_override( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self.web_contents(handle)?.environment_owner.locale()) + } + + pub fn set_web_contents_locale_override( + &mut self, + handle: WebContentsHandle, + locale: Option, + ) -> Result<(), String> { + self.web_contents(handle)? + .environment_owner + .set_locale(locale.as_deref()) + .map_err(str::to_owned) + } + + pub fn web_contents_timezone_override( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + Ok(self.web_contents(handle)?.environment_owner.timezone()) + } + + pub fn set_web_contents_timezone_override( + &mut self, + handle: WebContentsHandle, + timezone: Option, + ) -> Result<(), String> { + self.web_contents(handle)? + .environment_owner + .set_timezone(timezone.as_deref()) + .map_err(str::to_owned) + } + + pub fn web_contents_has_non_default_policy( + &self, + handle: WebContentsHandle, + ) -> Result { + let contents = self.web_contents(handle)?; + Ok(contents.network_offline + || contents.browser_identity_override.is_some() + || contents.tls_verify_host_override.is_some() + || contents.bypass_content_security_policy + || contents.environment_owner.locale().is_some() + || contents.environment_owner.timezone().is_some() + || contents.emulation_policy != EmulationPolicy::default() + || contents.network_request_policy != NetworkRequestPolicy::default()) + } + + pub fn web_contents_window_counts(&self) -> (usize, usize, usize) { + let opener = self + .web_contents + .values() + .filter(|contents| contents.window.opener.is_some()) + .count(); + let accessible_opener = self + .web_contents + .values() + .filter(|contents| { + contents + .window + .opener + .is_some_and(|opener| opener.can_access) + }) + .count(); + let named = self + .web_contents + .values() + .filter(|contents| contents.window.name.is_some()) + .count(); + (opener, accessible_opener, named) + } + + pub fn web_contents_emulation_policy( + &self, + handle: WebContentsHandle, + ) -> Result { + Ok(self.web_contents(handle)?.emulation_policy.clone()) + } + + pub fn apply_web_contents_emulation_policy_change( + &mut self, + handle: WebContentsHandle, + change: EmulationPolicyChange, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .emulation_policy + .apply(change); + Ok(()) + } + + pub fn apply_web_contents_emulation_policy_changes( + &mut self, + handle: WebContentsHandle, + changes: Vec, + ) -> Result<(), String> { + self.web_contents_mut(handle)? + .emulation_policy + .apply_changes(changes); + Ok(()) + } + + pub fn loaded_document_count(&self) -> usize { + self.web_contents + .values() + .filter(|contents| contents.main_frame.current_document.is_some()) + .count() + } + + pub fn has_pending_javascript_dialog(&self) -> bool { + self.web_contents + .values() + .any(|contents| !contents.javascript_dialogs.is_empty()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn navigation_engine_for_test( + &self, + handle: WebContentsHandle, + ) -> Option<&NavigationEngine> { + self.web_contents(handle).ok()?.navigation_engine_for_test() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn navigation_engine_for_test_mut( + &mut self, + handle: WebContentsHandle, + ) -> Option<&mut NavigationEngine> { + self.web_contents_mut(handle) + .ok()? + .navigation_engine_for_test_mut() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn take_document_host_for_test( + &mut self, + handle: WebContentsHandle, + ) -> Option { + self.web_contents_mut(handle) + .ok()? + .main_frame + .current_document + .take() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn web_contents_id_for_test(&self, handle: WebContentsHandle) -> Option { + Some(self.web_contents(handle).ok()?.id()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn web_contents_for_test(&self, handle: WebContentsHandle) -> Option<&WebContents> { + self.web_contents(handle).ok() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn web_contents_for_test_mut( + &mut self, + handle: WebContentsHandle, + ) -> Option<&mut WebContents> { + self.web_contents_mut(handle).ok() + } +} diff --git a/moli-core/src/browser/browser_context/resource_commands.rs b/moli-core/src/browser/browser_context/resource_commands.rs new file mode 100644 index 0000000000..29f1cf205b --- /dev/null +++ b/moli-core/src/browser/browser_context/resource_commands.rs @@ -0,0 +1,134 @@ +use super::{ + BrowserContext, + document_commands::{ + CompletedAppManifestLoadPreparation, CompletedAppManifestPublication, + CompletedDocumentBlobRead, CompletedNetworkResourceLoadPreparation, + PendingAppManifestLoadPreparation, PendingAppManifestPublication, PendingDocumentBlobRead, + PendingNetworkResourceLoadPreparation, + }, +}; +use crate::browser::DocumentHandle; +use crate::page::{ + RendererAppManifestLoadPreparation, RendererAppManifestLoadPublication, + RendererCommandTurnOutput, RendererNetworkResourceLoadPreparation, +}; +use std::sync::Arc; +use url::Url; + +pub struct BrowserAppManifestLoadPreparation { + pub document: DocumentHandle, + pub preparation: RendererAppManifestLoadPreparation, +} + +impl BrowserContext { + pub fn start_document_blob_read( + &self, + document: DocumentHandle, + uuid: String, + ) -> Result { + let pending = self + .document(document)? + .page + .start_blob_bytes_for_uuid(uuid) + .map_err(|error| error.to_string())?; + Ok(PendingDocumentBlobRead::new(document, pending)) + } + + pub fn finish_document_blob_read( + &mut self, + completed: CompletedDocumentBlobRead, + ) -> Result>, String> { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_blob_bytes_for_uuid(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_network_resource_load_preparation( + &self, + document: DocumentHandle, + frame_id: String, + url: Url, + disable_cache: bool, + include_credentials: bool, + ) -> Result { + let pending = self + .document(document)? + .page + .start_prepare_network_resource_load(frame_id, url, disable_cache, include_credentials) + .map_err(|error| error.to_string())?; + Ok(PendingNetworkResourceLoadPreparation::new( + document, pending, + )) + } + + pub fn finish_network_resource_load_preparation( + &mut self, + completed: CompletedNetworkResourceLoadPreparation, + ) -> Result { + let (document, completion) = completed.into_parts(); + let page = self.document_mut(document).map_err(|error| { + if error == "Document changed" { + "Document changed while preparing the network resource load".to_owned() + } else { + error + } + })?; + page.page + .finish_prepare_network_resource_load(completion?) + .map_err(|error| error.to_string()) + } + + pub fn start_app_manifest_load_preparation( + &self, + document: DocumentHandle, + ) -> Result { + let pending = self + .document(document)? + .page + .start_prepare_app_manifest_load() + .map_err(|error| error.to_string())?; + Ok(PendingAppManifestLoadPreparation::new(document, pending)) + } + + pub fn finish_app_manifest_load_preparation( + &mut self, + completed: CompletedAppManifestLoadPreparation, + ) -> Result { + let (document, completion) = completed.into_parts(); + let preparation = self + .document_mut(document)? + .page + .finish_prepare_app_manifest_load(completion?) + .map_err(|error| error.to_string())?; + Ok(BrowserAppManifestLoadPreparation { + document, + preparation, + }) + } + + pub fn start_app_manifest_publication( + &self, + document: DocumentHandle, + publication: RendererAppManifestLoadPublication, + ) -> Result { + let pending = self + .document(document)? + .page + .start_publish_app_manifest_load(publication) + .map_err(|error| error.to_string())?; + Ok(PendingAppManifestPublication::new(document, pending)) + } + + pub fn finish_app_manifest_publication( + &mut self, + completed: CompletedAppManifestPublication, + ) -> Result { + let (document, completion) = completed.into_parts(); + self.document_mut(document)? + .page + .finish_publish_app_manifest_load(completion?) + .map_err(|error| error.to_string()) + } +} diff --git a/moli-core/src/browser/browser_context/resource_runtime.rs b/moli-core/src/browser/browser_context/resource_runtime.rs new file mode 100644 index 0000000000..4cdea80052 --- /dev/null +++ b/moli-core/src/browser/browser_context/resource_runtime.rs @@ -0,0 +1,167 @@ +use super::{ + BrowserContext, CompletedDocumentResourceRuntimeUpdate, PendingDocumentResourceRuntimeUpdate, +}; +use crate::browser::web_contents::WebContents; +use crate::network::ResourceRequestClient; +use moli_fetch::FetchConfig; + +impl BrowserContext { + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn reset_selected_resource_runtime_for_test(&mut self) -> bool { + let Some(contents) = self + .selected_web_contents_id() + .and_then(|id| self.web_contents.get_mut(&id)) + else { + return false; + }; + if !contents.has_navigation_engine() { + return false; + } + contents.reset_resource_runtime_for_test().await; + true + } + + pub fn web_contents_has_navigation_engine( + &self, + handle: crate::browser::WebContentsHandle, + ) -> bool { + self.web_contents(handle) + .is_ok_and(WebContents::has_navigation_engine) + } + + pub fn web_contents_navigation_fetch_config( + &self, + handle: crate::browser::WebContentsHandle, + ) -> Option<&FetchConfig> { + self.web_contents(handle).ok()?.navigation_fetch_config() + } + + pub fn web_contents_navigation_layout_policy( + &self, + handle: crate::browser::WebContentsHandle, + ) -> Option { + self.web_contents(handle).ok()?.navigation_layout_policy() + } + + pub fn web_contents_navigation_renderer_owner_id( + &self, + handle: crate::browser::WebContentsHandle, + ) -> Option { + self.web_contents(handle) + .ok()? + .navigation_renderer_owner_id() + } + + pub fn web_contents_navigation_diagnostics( + &self, + handle: crate::browser::WebContentsHandle, + ) -> Option { + self.web_contents(handle).ok()?.navigation_diagnostics() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn web_contents_navigation_browser_context_runtime_id_for_test( + &self, + handle: crate::browser::WebContentsHandle, + ) -> Option { + Some( + self.web_contents(handle) + .ok()? + .navigation_engine_for_test()? + .browser_context_runtime() + .id(), + ) + } + + pub fn invalidate_selected_resource_runtime(&mut self) { + if let Some(contents) = self + .selected_web_contents_id() + .and_then(|id| self.web_contents.get_mut(&id)) + { + contents.invalidate_resource_runtime(); + } + } + + pub fn configure_selected_navigation_policy( + &mut self, + defaults: FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> Result<(), String> { + let inherited = + self.inherited_resource_policy(defaults, global_headers, global_network_conditions); + let Some(contents) = self + .selected_web_contents_id() + .and_then(|id| self.web_contents.get_mut(&id)) + else { + return Ok(()); + }; + contents + .configure_navigation_engine_policy(&inherited) + .map(drop) + } + + pub fn ensure_web_contents_resource_request_client( + &mut self, + web_contents: crate::browser::WebContentsHandle, + defaults: FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> Result { + let inherited = + self.inherited_resource_policy(defaults, global_headers, global_network_conditions); + self.web_contents_mut(web_contents)? + .ensure_resource_request_client(&inherited) + } + + pub fn start_web_contents_resource_runtime_rebuild( + &mut self, + web_contents: crate::browser::WebContentsHandle, + defaults: FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> Result, String> { + let document = self.document_handle_for_web_contents(web_contents)?; + let inherited = + self.inherited_resource_policy(defaults, global_headers, global_network_conditions); + let pending = self + .web_contents_mut(web_contents)? + .start_resource_runtime_rebuild(&inherited)?; + match (document, pending) { + (Some(document), Some(pending)) => Ok(Some(PendingDocumentResourceRuntimeUpdate::new( + document, pending, + ))), + (None, None) => Ok(None), + _ => Err("resource runtime Document changed during admission".to_owned()), + } + } + + pub fn finish_document_resource_runtime_update( + &mut self, + completed: CompletedDocumentResourceRuntimeUpdate, + ) -> Result<(), String> { + let (document, completion) = completed.into_parts(); + let completion = completion?; + match self.document(document) { + Ok(_) => self + .web_contents_mut(document.web_contents())? + .finish_resource_runtime_update(completion), + Err(error) if matches!(error.as_str(), "Document changed" | "NoDocumentLoaded") => { + match self.web_contents_mut(document.web_contents()) { + Ok(contents) => contents.finish_resource_runtime_update(completion), + Err(_) => WebContents::finish_unobserved_resource_runtime_update(completion), + } + } + Err(error) => Err(error), + } + } + + pub fn finish_unobserved_document_resource_runtime_update( + completed: CompletedDocumentResourceRuntimeUpdate, + ) -> Result<(), String> { + let (_, completion) = completed.into_parts(); + WebContents::finish_unobserved_resource_runtime_update(completion?) + } +} diff --git a/moli-core/src/browser/browser_context/settings.rs b/moli-core/src/browser/browser_context/settings.rs new file mode 100644 index 0000000000..1880c59e3f --- /dev/null +++ b/moli-core/src/browser/browser_context/settings.rs @@ -0,0 +1,449 @@ +use std::collections::HashSet; +#[cfg(any(test, feature = "test-support"))] +use std::path::Path; + +use moli_browser_profile::BrowserIdentityProfile; +#[cfg(any(test, feature = "test-support"))] +use moli_cookie_jar::{BrowserCookieStore, SharedBrowserCookieStore}; +use moli_cookie_jar::{ + CookieSource, NetworkCookieRequestContext, StoredCookie, StoredCookiePartitionKey, + StoredCookieQueryReport, StoredCookieSetReport, +}; + +use crate::{ + RendererBrowserContextRuntimeId, RendererOwnerLocalHostId, + browser::{ + BrowserContextPageStorageHandles, ContextEmulationDefaults, ContextNetworkPolicy, + DownloadAccessError, DownloadBody, DownloadObservation, DownloadPolicy, + EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, EmulatedNetworkConditions, + OriginStorageUsage, PermissionDefaults, SiteDataClearOptions, StoragePartitionKind, + WebContentsHandle, + }, + network::{ResourceRequestClient, new_shared_web_storage_store}, + page::PermissionOverrideRegistration, + runtime::RendererSharedWorkerRuntimeDiagnostics, +}; +#[cfg(any(test, feature = "test-support"))] +use crate::{ + browser::BrowserContextResourceStorageHandles, + network::SharedWebStorageStore, + storage::{SharedIndexedDbManager, SharedStorageBucketStore}, +}; +use moli_fetch::Request; +use url::Url; + +use super::BrowserContext; + +impl BrowserContext { + pub fn download_policy(&self) -> Option<&DownloadPolicy> { + self.download_policy.as_ref() + } + + pub fn set_download_policy(&mut self, policy: Option) { + self.download_policy = policy; + } + + pub fn start_download_request( + &mut self, + policy: &DownloadPolicy, + client: ResourceRequestClient, + request: Request, + suggested_filename: Option, + ) -> Result, String> { + self.downloads + .start_request(policy, client, request, suggested_filename) + } + + pub fn start_download_response( + &mut self, + web_contents: WebContentsHandle, + policy: &DownloadPolicy, + url: Url, + headers: Vec<(String, String)>, + body: DownloadBody, + ) -> Result, String> { + self.web_contents(web_contents)?; + self.downloads.start_response(policy, url, headers, body) + } + + pub fn cancel_download(&self, guid: &str) -> Option> { + self.downloads.cancel(guid) + } + + pub fn read_download_artifact( + &self, + guid: &str, + ) -> Option, String>>, DownloadAccessError>> { + self.downloads.read_artifact(guid) + } + + pub fn set_permission_override( + &mut self, + defaults: &mut PermissionDefaults, + registration: PermissionOverrideRegistration, + ) { + self.permission_overrides.set(defaults, registration); + } + + pub fn clear_permission_overrides(&mut self) { + self.permission_overrides.clear(); + } + + pub fn permission_override_count(&self) -> usize { + self.permission_overrides.override_count() + } + + pub fn permission_snapshot( + &self, + defaults: &PermissionDefaults, + ) -> Vec { + self.permission_overrides.snapshot(defaults) + } + + pub fn storage_partition_kind(&self) -> StoragePartitionKind { + self.storage_partition.kind + } + + pub fn storage_partition_kind_label(&self) -> &'static str { + self.storage_partition.kind_label() + } + + pub fn page_storage_handles( + &self, + web_contents: Option, + ) -> Result { + let session_storage_store = match web_contents { + Some(handle) => self.web_contents(handle)?.session_storage.store().clone(), + None => self + .selected_web_contents + .and_then(|id| self.web_contents.get(&id)) + .map(|contents| contents.session_storage.store().clone()) + .unwrap_or_else(new_shared_web_storage_store), + }; + Ok(self + .storage_partition + .handles + .page_storage_handles(session_storage_store)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn resource_storage_handles_for_test(&self) -> BrowserContextResourceStorageHandles { + let session_storage_store = self + .selected_web_contents + .and_then(|id| self.web_contents.get(&id)) + .map(|contents| contents.session_storage.store().clone()) + .unwrap_or_else(new_shared_web_storage_store); + self.storage_partition + .handles + .resource_storage_handles(session_storage_store) + } + + pub fn observe_request_cookie_access_report( + &self, + request_url: &Url, + request_context: NetworkCookieRequestContext, + ) -> Option { + let mut cookie_store = self.storage_partition.cookie_store().lock(); + let report = + cookie_store.observe_cookie_access_report_for_request(request_url, request_context); + (!report.included_cookies.is_empty() || !report.excluded_cookies.is_empty()) + .then_some(report) + } + + pub fn storage_quota_for_origin(&self, origin: &str) -> (f64, bool) { + self.storage_partition.storage_quota_for_origin(origin) + } + + pub fn set_storage_quota_override(&mut self, origin: String, quota: f64) { + self.storage_partition + .set_storage_quota_override(origin, quota); + } + + pub fn clear_storage_quota_override(&mut self, origin: &str) { + self.storage_partition.clear_storage_quota_override(origin); + } + + pub fn storage_usage_for_origin( + &self, + serialized_origin: &str, + ) -> Result { + self.storage_partition.usage_for_origin(serialized_origin) + } + + pub fn clear_site_data_for_origin( + &mut self, + origin: &Url, + options: SiteDataClearOptions, + ) -> Result<(), String> { + self.storage_partition + .clear_site_data_for_origin(origin, options) + } + + pub fn clear_site_data_for_storage_key( + &mut self, + storage_key: &moli_storage_key::MoliStorageKey, + options: SiteDataClearOptions, + ) -> Result<(), String> { + self.storage_partition + .clear_site_data_for_storage_key(storage_key, options) + } + + pub fn clear_http_cache(&self) -> Result<(), String> { + self.storage_partition.clear_http_cache() + } + + pub fn snapshot_cookies(&self) -> Vec { + self.storage_partition.cookie_store().lock().cookies() + } + + pub fn store_cookie( + &self, + cookie: StoredCookie, + request_url: Option<&Url>, + source: CookieSource, + ) -> StoredCookieSetReport { + self.storage_partition + .cookie_store() + .lock() + .upsert_with_request_url_report(cookie, request_url, source) + } + + pub fn delete_cookies_with_partition_key( + &mut self, + name: Option<&str>, + domain: Option<&str>, + path: Option<&str>, + url_host: Option<&str>, + partition_key: Option<&StoredCookiePartitionKey>, + ) { + self.storage_partition + .cookie_store() + .lock() + .delete_cookies_with_partition_key(name, domain, path, url_host, partition_key); + } + + pub fn browser_identity_override(&self) -> Option<&BrowserIdentityProfile> { + self.browser_identity_override.as_ref() + } + + pub fn set_browser_identity_override(&mut self, identity: Option) { + self.browser_identity_override = identity; + } + + pub fn emulation_defaults(&self) -> &ContextEmulationDefaults { + &self.emulation_defaults + } + + pub fn set_default_locale_override(&mut self, locale: Option) -> Result<(), String> { + self.environment_owner + .set_locale(locale.as_deref()) + .map_err(str::to_owned) + } + + pub fn set_default_timezone_override( + &mut self, + timezone: Option, + ) -> Result<(), String> { + self.environment_owner + .set_timezone(timezone.as_deref()) + .map_err(str::to_owned) + } + + pub fn default_locale_override(&self) -> Option { + self.environment_owner.locale() + } + + pub fn default_timezone_override(&self) -> Option { + self.environment_owner.timezone() + } + + pub fn set_default_network_conditions( + &mut self, + conditions: Option, + ) { + self.emulation_defaults.network_conditions = conditions; + } + + pub fn set_default_geolocation_override( + &mut self, + geolocation: Option, + ) { + self.emulation_defaults.geolocation = geolocation; + } + + pub fn set_default_device_metrics(&mut self, metrics: EmulatedDeviceMetrics) -> bool { + self.emulation_defaults + .device_metrics + .replace(metrics) + .is_some() + } + + pub fn network_policy(&self) -> &ContextNetworkPolicy { + &self.network_policy + } + + pub fn set_default_extra_headers(&mut self, headers: moli_fetch::RequestHeaders) { + self.network_policy.extra_headers = headers; + } + + pub fn set_network_policy(&mut self, policy: ContextNetworkPolicy) { + self.network_policy = policy; + } + + pub fn set_context_tls_verify_host_override(&mut self, enabled: bool) { + self.network_policy.tls_verify_host = Some(enabled); + } + + pub fn set_javascript_dialog_handler_enabled(&self, enabled: bool) { + self.renderer_runtime_owner + .as_ref() + .expect("BrowserContext renderer owner was already taken for teardown") + .handle() + .set_javascript_dialog_handler_enabled(enabled); + } + + pub fn routes_renderer_browser_context_runtime( + &self, + runtime_id: RendererBrowserContextRuntimeId, + ) -> bool { + self.renderer_runtime_owner + .as_ref() + .is_some_and(|owner| owner.handle().id() == runtime_id) + } + + pub fn renderer_memory_diagnostics(&self) -> serde_json::Value { + self.renderer_runtime_owner + .as_ref() + .expect("BrowserContext renderer owner was already taken for teardown") + .handle() + .moli_memory_diagnostics() + } + + pub fn shared_worker_runtime_diagnostics(&self) -> RendererSharedWorkerRuntimeDiagnostics { + self.renderer_runtime_owner + .as_ref() + .expect("BrowserContext renderer owner was already taken for teardown") + .handle() + .shared_worker_runtime_diagnostics_for_diagnostics() + } + + pub fn web_contents_for_renderer_owner( + &self, + owner_local_host_id: RendererOwnerLocalHostId, + ) -> Option { + let id = self.web_contents.iter().find_map(|(id, contents)| { + contents + .main_frame + .current_document + .as_ref() + .is_some_and(|document| { + document.page.renderer_owner_local_host_id() == owner_local_host_id + }) + .then_some(*id) + })?; + Some(WebContentsHandle::new(self.id, id)) + } + + pub fn loaded_document_renderer_owner_ids(&self) -> HashSet { + self.web_contents + .values() + .filter_map(|contents| contents.main_frame.current_document.as_ref()) + .map(|document| document.page.renderer_owner_local_host_id().as_u64()) + .collect() + } + + pub fn dedicated_worker_running_isolate_count(&self) -> usize { + self.web_contents + .values() + .filter_map(|contents| contents.main_frame.current_document.as_ref()) + .map(|document| { + document + .page + .dedicated_worker_running_worker_isolate_count_for_diagnostics() + }) + .sum() + } + + pub fn javascript_dialog_handler_enabled(&self) -> bool { + self.renderer_runtime_owner + .as_ref() + .expect("BrowserContext renderer owner was already taken for teardown") + .handle() + .javascript_dialog_handler_enabled() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn http_cache_configuration_for_test(&self) -> (Option<&Path>, Option) { + self.storage_partition.http_cache_configuration() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn cookie_store_for_test(&self) -> &SharedBrowserCookieStore { + self.storage_partition.cookie_store() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn web_storage_store_for_test(&self) -> &SharedWebStorageStore { + self.storage_partition.web_storage_store() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn selected_session_storage_store_for_test(&self) -> Option<&SharedWebStorageStore> { + Some( + self.web_contents + .get(&self.selected_web_contents?)? + .session_storage + .store(), + ) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn indexed_db_manager_for_test(&self) -> &SharedIndexedDbManager { + self.storage_partition.indexed_db_manager() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn storage_bucket_store_for_test(&self) -> &SharedStorageBucketStore { + self.storage_partition.storage_bucket_store() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn replace_storage_bucket_store_for_test( + &mut self, + storage_bucket_store: SharedStorageBucketStore, + ) { + self.storage_partition + .replace_storage_bucket_store(storage_bucket_store); + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn with_cookie_store_for_test(&self, f: impl FnOnce(&BrowserCookieStore) -> R) -> R { + let cookie_store = self.storage_partition.cookie_store().lock(); + f(&cookie_store) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn with_cookie_store_mut_for_test( + &self, + f: impl FnOnce(&mut BrowserCookieStore) -> R, + ) -> R { + let mut cookie_store = self.storage_partition.cookie_store().lock(); + f(&mut cookie_store) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn set_http_proxy_override_for_test(&mut self, proxy: Option) { + self.network_policy.http_proxy = proxy; + } +} diff --git a/moli-protocol/src/conn/state/browser_context/storage_partition.rs b/moli-core/src/browser/browser_context/storage_partition.rs similarity index 79% rename from moli-protocol/src/conn/state/browser_context/storage_partition.rs rename to moli-core/src/browser/browser_context/storage_partition.rs index 061285d1e5..15119a5dd0 100644 --- a/moli-protocol/src/conn/state/browser_context/storage_partition.rs +++ b/moli-core/src/browser/browser_context/storage_partition.rs @@ -1,23 +1,23 @@ use std::{collections::HashMap, path::PathBuf}; -use moli_cookie_jar::SharedBrowserCookieStore; -use moli_core::{ +use crate::{ network::SharedWebStorageStore, storage::{SharedIndexedDbManager, SharedStorageBucketStore, StorageBucketIdentity}, }; +use moli_cookie_jar::SharedBrowserCookieStore; use super::BrowserContextStoragePartitionHandles; #[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub(crate) struct OriginStorageUsage { - pub(crate) local_storage_usage: u64, - pub(crate) indexed_db_usage: u64, - pub(crate) storage_buckets_usage: u64, - pub(crate) total_usage: u64, +pub struct OriginStorageUsage { + pub local_storage_usage: u64, + pub indexed_db_usage: u64, + pub storage_buckets_usage: u64, + pub total_usage: u64, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub(crate) struct SiteDataClearOptions { +pub struct SiteDataClearOptions { pub cookies: bool, pub local_storage: bool, pub indexed_db: bool, @@ -26,22 +26,22 @@ pub(crate) struct SiteDataClearOptions { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum StoragePartitionKind { +pub enum StoragePartitionKind { ProfileBacked, Ephemeral, } /// The stores and their policy share one owner; only capability handles clone. -pub(super) struct StoragePartition { - pub(super) kind: StoragePartitionKind, - pub(super) handles: BrowserContextStoragePartitionHandles, +pub struct StoragePartition { + pub kind: StoragePartitionKind, + pub handles: BrowserContextStoragePartitionHandles, storage_quota_overrides: HashMap, http_cache_root: Option, http_cache_max_bytes: Option, } impl StoragePartition { - pub(super) fn new( + pub fn new( handles: BrowserContextStoragePartitionHandles, kind: StoragePartitionKind, http_cache_root: Option, @@ -56,15 +56,12 @@ impl StoragePartition { } } - pub(super) fn usage_for_origin( - &self, - serialized_origin: &str, - ) -> Result { + pub fn usage_for_origin(&self, serialized_origin: &str) -> Result { let local_storage_usage = { let store = self.web_storage_store().lock(); usize_to_u64_saturating(store.usage_bytes_for_origin_areas(serialized_origin)) }; - let indexed_db_usage = moli_core::storage::indexed_db_origins_with_prefix_usage_bytes( + let indexed_db_usage = crate::storage::indexed_db_origins_with_prefix_usage_bytes( self.indexed_db_manager(), &moli_storage_key::storage_key_prefix_for_origin(serialized_origin), )?; @@ -90,7 +87,7 @@ impl StoragePartition { }; let mut usage = cache_usage; for identity in bucket_identities { - let indexed_db_usage = moli_core::storage::indexed_db_origin_usage_bytes( + let indexed_db_usage = crate::storage::indexed_db_origin_usage_bytes( self.indexed_db_manager(), &identity.indexed_db_storage_key(), )?; @@ -110,13 +107,13 @@ impl StoragePartition { ) -> Result<(), String> { let bucket_store = self.storage_bucket_store(); for cleanup in cleanups { - moli_core::storage::complete_storage_bucket_deletion(bucket_store, &cleanup) + crate::storage::complete_storage_bucket_deletion(bucket_store, &cleanup) .map_err(|error| format!("FailedToCompleteStorageBucketDeletion: {error}"))?; } Ok(()) } - pub(super) fn clear_site_data_for_origin( + pub fn clear_site_data_for_origin( &mut self, origin: &url::Url, options: SiteDataClearOptions, @@ -137,7 +134,7 @@ impl StoragePartition { } if options.indexed_db { - moli_core::storage::clear_indexed_db_origins_with_prefix( + crate::storage::clear_indexed_db_origins_with_prefix( self.indexed_db_manager(), &moli_storage_key::storage_key_prefix_for_origin(&serialized_origin), )?; @@ -159,7 +156,7 @@ impl StoragePartition { Ok(()) } - pub(super) fn clear_site_data_for_storage_key( + pub fn clear_site_data_for_storage_key( &mut self, storage_key: &moli_storage_key::MoliStorageKey, options: SiteDataClearOptions, @@ -186,7 +183,7 @@ impl StoragePartition { } if options.indexed_db { - moli_core::storage::clear_indexed_db_origin( + crate::storage::clear_indexed_db_origin( self.indexed_db_manager(), &serialized_storage_key, )?; @@ -208,57 +205,54 @@ impl StoragePartition { Ok(()) } - pub(super) fn kind_label(&self) -> &'static str { + pub fn kind_label(&self) -> &'static str { match self.kind { StoragePartitionKind::ProfileBacked => "profile-backed", StoragePartitionKind::Ephemeral => "ephemeral", } } - pub(super) fn cookie_store(&self) -> &SharedBrowserCookieStore { + pub fn cookie_store(&self) -> &SharedBrowserCookieStore { &self.handles.cookie_store } - pub(super) fn web_storage_store(&self) -> &SharedWebStorageStore { + pub fn web_storage_store(&self) -> &SharedWebStorageStore { &self.handles.web_storage_store } - pub(super) fn indexed_db_manager(&self) -> &SharedIndexedDbManager { + pub fn indexed_db_manager(&self) -> &SharedIndexedDbManager { &self.handles.indexed_db_manager } - pub(super) fn storage_bucket_store(&self) -> &SharedStorageBucketStore { + pub fn storage_bucket_store(&self) -> &SharedStorageBucketStore { &self.handles.storage_bucket_store } - #[cfg(test)] - pub(super) fn replace_storage_bucket_store( - &mut self, - storage_bucket_store: SharedStorageBucketStore, - ) { + #[cfg(any(test, feature = "test-support"))] + pub fn replace_storage_bucket_store(&mut self, storage_bucket_store: SharedStorageBucketStore) { self.handles.storage_bucket_store = storage_bucket_store; } - pub(super) fn storage_quota_for_origin(&self, origin: &str) -> (f64, bool) { + pub fn storage_quota_for_origin(&self, origin: &str) -> (f64, bool) { self.storage_quota_overrides .get(origin) .copied() .map(|quota| (quota, true)) .unwrap_or(( - moli_core::storage::DEFAULT_ORIGIN_STORAGE_QUOTA_BYTES as f64, + crate::storage::DEFAULT_ORIGIN_STORAGE_QUOTA_BYTES as f64, false, )) } - pub(super) fn set_storage_quota_override(&mut self, origin: String, quota: f64) { + pub fn set_storage_quota_override(&mut self, origin: String, quota: f64) { self.storage_quota_overrides.insert(origin, quota); } - pub(super) fn clear_storage_quota_override(&mut self, origin: &str) { + pub fn clear_storage_quota_override(&mut self, origin: &str) { self.storage_quota_overrides.remove(origin); } - pub(super) fn clear_http_cache(&self) -> Result<(), String> { + pub fn clear_http_cache(&self) -> Result<(), String> { let Some(cache_root) = self.http_cache_root.as_ref() else { return Ok(()); }; @@ -274,8 +268,8 @@ impl StoragePartition { .map_err(|error| format!("FailedToClearHttpCache: {error}")) } - #[cfg(test)] - pub(super) fn http_cache_configuration(&self) -> (Option<&std::path::Path>, Option) { + #[cfg(any(test, feature = "test-support"))] + pub fn http_cache_configuration(&self) -> (Option<&std::path::Path>, Option) { (self.http_cache_root.as_deref(), self.http_cache_max_bytes) } } diff --git a/moli-core/src/browser/browser_context/workers.rs b/moli-core/src/browser/browser_context/workers.rs new file mode 100644 index 0000000000..7209dcd25d --- /dev/null +++ b/moli-core/src/browser/browser_context/workers.rs @@ -0,0 +1,174 @@ +use crate::{browser::ServiceWorkerCommand, runtime::RendererBrowserContextRuntime}; +use moli_shared_worker::SharedWorkerInstanceId; + +use super::BrowserContext; + +impl BrowserContext { + pub fn execute_service_worker_command( + &self, + command: ServiceWorkerCommand, + ) -> Result<(), String> { + let runtime = self.renderer_runtime(); + match command { + ServiceWorkerCommand::SetForceUpdateOnPageLoad(force_update) => { + runtime.set_service_worker_force_update_on_page_load_for_devtools(force_update); + Ok(()) + } + ServiceWorkerCommand::Unregister { scope } => runtime + .unregister_service_worker_scope_for_devtools(&scope) + .map(drop), + ServiceWorkerCommand::Start { scope } => { + runtime.start_service_worker_for_devtools(&scope).map(drop) + } + ServiceWorkerCommand::StopVersion { version_id } => runtime + .stop_service_worker_for_devtools(version_id) + .map(drop), + ServiceWorkerCommand::StopAll => { + runtime.stop_all_service_workers_for_devtools().map(drop) + } + ServiceWorkerCommand::SkipWaiting { scope } => runtime + .skip_waiting_service_worker_for_devtools(&scope) + .map(drop), + ServiceWorkerCommand::UpdateRegistration { scope } => runtime + .update_service_worker_registration_for_devtools(&scope) + .map(drop), + ServiceWorkerCommand::DeliverPushMessage { + origin, + registration_id, + data, + } => runtime + .deliver_push_message_for_devtools(&origin, registration_id, data) + .map(drop), + ServiceWorkerCommand::DispatchSyncEvent { + origin, + registration_id, + tag, + last_chance, + } => runtime + .dispatch_sync_event_for_devtools(&origin, registration_id, tag, last_chance) + .map(drop), + ServiceWorkerCommand::DispatchPeriodicSyncEvent { + origin, + registration_id, + tag, + } => runtime + .dispatch_periodic_sync_event_for_devtools(&origin, registration_id, tag) + .map(drop), + } + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn service_worker_force_update_on_page_load_for_test(&self) -> bool { + self.renderer_runtime() + .service_worker_force_update_on_page_load_for_devtools() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn service_worker_pause_on_start_for_test(&self) -> bool { + self.renderer_runtime() + .service_worker_pause_on_start_for_devtools() + } + + pub fn controlled_service_worker_window_client_ids( + &self, + registration_id: u64, + version_id: u64, + ) -> Vec { + self.renderer_runtime() + .controlled_service_worker_window_client_ids_for_devtools(registration_id, version_id) + } + + pub fn set_service_worker_pause_on_start_for_version( + &self, + version_id: u64, + pause: bool, + ) -> bool { + self.renderer_runtime() + .set_service_worker_pause_on_start_for_version_for_devtools(version_id, pause) + } + + pub fn set_service_worker_pause_on_start(&self, pause: bool) { + self.renderer_runtime() + .set_service_worker_pause_on_start_for_devtools(pause); + } + + pub fn set_service_worker_related_pause_on_start_policies( + &self, + policies: Vec<(u64, u64, String, String)>, + ) { + self.renderer_runtime() + .set_service_worker_related_pause_on_start_policies_for_devtools(policies); + } + + pub fn set_dedicated_worker_pause_on_start(&self, pause: bool) { + self.renderer_runtime() + .set_dedicated_worker_pause_on_start_for_devtools(pause); + } + + pub fn set_service_worker_inspection_attached(&self, version_id: u64, attached: bool) { + self.renderer_runtime() + .set_service_worker_devtools_attached(version_id, attached); + } + + pub fn close_shared_worker(&self, instance_id: SharedWorkerInstanceId) -> bool { + self.renderer_runtime() + .close_shared_worker_for_target_close(instance_id) + } + + pub fn close_dedicated_worker(&self, instance_id: u64) -> bool { + self.renderer_runtime() + .close_dedicated_worker_for_devtools(instance_id) + } + + pub fn attach_dedicated_worker_inspector_session( + &self, + instance_id: u64, + session_id: Option, + ) -> bool { + self.renderer_runtime() + .attach_dedicated_worker_runtime_inspector_session(instance_id, session_id) + } + + pub fn detach_shared_worker_inspector_session( + &self, + instance_id: SharedWorkerInstanceId, + session_id: Option, + ) -> bool { + self.renderer_runtime() + .detach_shared_worker_runtime_inspector_session(instance_id, session_id) + } + + pub fn detach_dedicated_worker_inspector_session( + &self, + instance_id: u64, + session_id: Option, + ) -> bool { + self.renderer_runtime() + .detach_dedicated_worker_runtime_inspector_session(instance_id, session_id) + } + + pub fn detach_service_worker_inspector_session( + &self, + version_id: u64, + session_id: Option, + ) -> bool { + self.renderer_runtime() + .detach_service_worker_runtime_inspector_session(version_id, session_id) + } + + pub fn run_dedicated_worker_if_waiting_for_debugger(&self, instance_id: u64) -> bool { + self.renderer_runtime() + .run_dedicated_worker_if_waiting_for_debugger_for_devtools(instance_id) + } + + pub fn run_service_worker_if_waiting_for_debugger(&self, version_id: u64) -> bool { + self.renderer_runtime() + .run_service_worker_if_waiting_for_debugger_for_devtools(version_id) + } + + pub fn worker_runtime_inspection_endpoint(&self) -> RendererBrowserContextRuntime { + self.renderer_runtime() + } +} diff --git a/moli-protocol/src/conn/body_spool.rs b/moli-core/src/browser/captured_body.rs similarity index 91% rename from moli-protocol/src/conn/body_spool.rs rename to moli-core/src/browser/captured_body.rs index 66435726c9..70db9dbd6a 100644 --- a/moli-protocol/src/conn/body_spool.rs +++ b/moli-core/src/browser/captured_body.rs @@ -15,7 +15,7 @@ use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt}; use anyhow::{Context, Result, anyhow}; const DEFAULT_MEMORY_LIMIT: usize = 1024 * 1024; -pub(crate) const DEFAULT_BODY_MATERIALIZE_LIMIT: usize = 64 * 1024 * 1024; +pub const DEFAULT_BODY_MATERIALIZE_LIMIT: usize = 64 * 1024 * 1024; static NEXT_SPOOL_ID: AtomicU64 = AtomicU64::new(1); @@ -29,7 +29,7 @@ enum CapturedBodyInner { Memory(Vec), SharedMemory(Arc<[u8]>), File { path: PathBuf, len: usize }, - Subresource(moli_core::page::SubresourceResponseBody), + Subresource(crate::page::SubresourceResponseBody), } impl Drop for CapturedBodyInner { @@ -41,23 +41,23 @@ impl Drop for CapturedBodyInner { } impl CapturedBody { - pub(crate) fn from_string(body: String) -> Self { + pub fn from_string(body: String) -> Self { Self::from_bytes(body.into_bytes()) } - pub(crate) fn from_bytes(body: Vec) -> Self { + pub fn from_bytes(body: Vec) -> Self { Self { inner: Arc::new(CapturedBodyInner::Memory(body)), } } - pub(crate) fn from_shared_bytes(body: Arc<[u8]>) -> Self { + pub fn from_shared_bytes(body: Arc<[u8]>) -> Self { Self { inner: Arc::new(CapturedBodyInner::SharedMemory(body)), } } - pub(crate) fn from_bytes_spooled(body: Vec) -> Self { + pub fn from_bytes_spooled(body: Vec) -> Self { if body.len() <= DEFAULT_MEMORY_LIMIT { return Self::from_bytes(body); } @@ -70,21 +70,19 @@ impl CapturedBody { Self::from_bytes(body) } - pub(crate) fn from_optional_renderer_synthetic_response_body( - body: Option, + pub fn from_optional_renderer_synthetic_response_body( + body: Option, ) -> Self { Self::from_bytes(body.map(|body| body.into_body_bytes()).unwrap_or_default()) } - pub(crate) fn from_subresource_response_body( - body: &moli_core::page::SubresourceResponseBody, - ) -> Self { + pub fn from_subresource_response_body(body: &crate::page::SubresourceResponseBody) -> Self { Self { inner: Arc::new(CapturedBodyInner::Subresource(body.clone())), } } - pub(crate) fn len(&self) -> usize { + pub fn len(&self) -> usize { match self.inner.as_ref() { CapturedBodyInner::Memory(bytes) => bytes.len(), CapturedBodyInner::SharedMemory(bytes) => bytes.len(), @@ -93,12 +91,16 @@ impl CapturedBody { } } - pub(crate) fn materialize_bytes_limited(&self, limit: usize) -> Result> { + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn materialize_bytes_limited(&self, limit: usize) -> Result> { ensure_materialize_limit(self.len(), limit)?; self.materialize_bytes() } - pub(crate) fn materialize_bytes(&self) -> Result> { + pub fn materialize_bytes(&self) -> Result> { match self.inner.as_ref() { CapturedBodyInner::Memory(bytes) => Ok(bytes.clone()), CapturedBodyInner::SharedMemory(bytes) => Ok(bytes.to_vec()), @@ -118,7 +120,7 @@ impl CapturedBody { } } - pub(crate) fn read_range(&self, offset: usize, len: usize) -> Result> { + pub fn read_range(&self, offset: usize, len: usize) -> Result> { if len == 0 || offset >= self.len() { return Ok(Vec::new()); } @@ -150,13 +152,13 @@ impl CapturedBody { } } - #[cfg(test)] - pub(crate) fn materialize_lossy_string(&self) -> Result { + #[cfg(any(test, feature = "test-support"))] + pub fn materialize_lossy_string(&self) -> Result { self.materialize_bytes() .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()) } - pub(crate) fn chunk_reader(&self, chunk_size: usize) -> Result { + pub fn chunk_reader(&self, chunk_size: usize) -> Result { if chunk_size == 0 { return Err(anyhow!( "captured response body chunk size must be non-zero" @@ -195,7 +197,7 @@ impl CapturedBody { } } -pub(crate) struct CapturedBodyChunkReader { +pub struct CapturedBodyChunkReader { source: CapturedBodyChunkReaderSource, chunk_size: usize, } @@ -207,7 +209,7 @@ enum CapturedBodyChunkReaderSource { } impl CapturedBodyChunkReader { - pub(crate) fn next_chunk(&mut self) -> Result>> { + pub fn next_chunk(&mut self) -> Result>> { match &mut self.source { CapturedBodyChunkReaderSource::Memory { body, offset } => { let bytes: &[u8] = match body.inner.as_ref() { @@ -251,7 +253,7 @@ impl CapturedBodyChunkReader { } } -pub(crate) fn ensure_materialize_limit(len: usize, limit: usize) -> Result<()> { +pub fn ensure_materialize_limit(len: usize, limit: usize) -> Result<()> { if len > limit { Err(anyhow!( "response body is {len} bytes, exceeds CDP materialization limit of {limit} bytes" @@ -270,7 +272,7 @@ impl PartialEq for CapturedBody { impl Eq for CapturedBody {} #[derive(Debug)] -pub(crate) struct CapturedBodyWriter { +pub struct CapturedBodyWriter { memory_limit: usize, len: usize, memory: Vec, @@ -285,7 +287,7 @@ impl Default for CapturedBodyWriter { } impl CapturedBodyWriter { - pub(crate) fn new(memory_limit: usize) -> Self { + pub fn new(memory_limit: usize) -> Self { Self { memory_limit, len: 0, @@ -295,7 +297,7 @@ impl CapturedBodyWriter { } } - pub(crate) fn append(&mut self, bytes: &[u8]) -> Result<()> { + pub fn append(&mut self, bytes: &[u8]) -> Result<()> { if bytes.is_empty() { return Ok(()); } @@ -313,11 +315,11 @@ impl CapturedBodyWriter { Ok(()) } - pub(crate) fn finish(mut self) -> Result { + pub fn finish(mut self) -> Result { self.finish_in_place() } - pub(crate) fn finish_in_place(&mut self) -> Result { + pub fn finish_in_place(&mut self) -> Result { if let Some(file) = self.file.as_mut() { file.flush() .context("failed to flush captured response body spool")?; @@ -559,7 +561,7 @@ mod tests { #[test] fn captured_body_from_subresource_response_body_reads_shared_source() -> Result<()> { let subresource_body = - moli_core::page::SubresourceResponseBody::from_bytes(b"hello world".to_vec()); + crate::page::SubresourceResponseBody::from_bytes(b"hello world".to_vec()); let body = CapturedBody::from_subresource_response_body(&subresource_body); assert!(matches!( diff --git a/moli-protocol/src/conn/state/emulation.rs b/moli-core/src/browser/emulation.rs similarity index 72% rename from moli-protocol/src/conn/state/emulation.rs rename to moli-core/src/browser/emulation.rs index 20cb00d1ff..e12f8fb9cb 100644 --- a/moli-protocol/src/conn/state/emulation.rs +++ b/moli-core/src/browser/emulation.rs @@ -15,28 +15,28 @@ pub struct EmulatedDeviceMetrics { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct EmulatedNetworkConditions { +pub struct EmulatedNetworkConditions { offline: bool, } impl EmulatedNetworkConditions { - pub(crate) fn offline() -> Self { + pub fn offline() -> Self { Self { offline: true } } - pub(crate) fn navigator_online(&self) -> bool { + pub fn navigator_online(&self) -> bool { !self.offline } } -pub(crate) type EmulatedViewportSurface = moli_page_types::ViewportSurface; +pub type EmulatedViewportSurface = moli_page_types::ViewportSurface; impl EmulatedDeviceMetrics { - pub(crate) fn screen_avail_height(&self) -> u32 { + pub fn screen_avail_height(&self) -> u32 { self.screen_avail_height } - pub(crate) fn device_pixel_ratio(&self) -> f64 { + pub fn device_pixel_ratio(&self) -> f64 { if self.device_scale_factor.is_finite() && self.device_scale_factor > 0.0 { self.device_scale_factor } else { @@ -44,7 +44,7 @@ impl EmulatedDeviceMetrics { } } - pub(crate) fn viewport_surface(&self) -> EmulatedViewportSurface { + pub fn viewport_surface(&self) -> EmulatedViewportSurface { EmulatedViewportSurface { inner_width: self.width, inner_height: self.height, @@ -81,7 +81,7 @@ pub enum EmulatedGeolocationOverrideState { } impl EmulatedGeolocationOverrideState { - pub(crate) fn position(&self) -> Option<&EmulatedGeolocationOverride> { + pub fn position(&self) -> Option<&EmulatedGeolocationOverride> { match self { Self::Position(position) => Some(position), Self::PositionUnavailable => None, @@ -99,7 +99,7 @@ pub struct EmulatedMediaOverrides { pub contrast: Option, } -impl From for moli_core::page::EmulatedMediaOverrides { +impl From for crate::page::EmulatedMediaOverrides { fn from(value: EmulatedMediaOverrides) -> Self { Self { media: value.media, @@ -112,7 +112,7 @@ impl From for moli_core::page::EmulatedMediaOverrides { } } -impl From<&EmulatedMediaOverrides> for moli_core::page::EmulatedMediaOverrides { +impl From<&EmulatedMediaOverrides> for crate::page::EmulatedMediaOverrides { fn from(value: &EmulatedMediaOverrides) -> Self { Self { media: value.media.clone(), @@ -128,7 +128,6 @@ impl From<&EmulatedMediaOverrides> for moli_core::page::EmulatedMediaOverrides { #[cfg(test)] mod tests { use super::EmulatedDeviceMetrics; - use crate::conn::EmulationPolicy; #[test] fn device_pixel_ratio_normalizes_non_positive_and_non_finite_values() { @@ -158,30 +157,4 @@ mod tests { metrics.device_scale_factor = f64::NAN; assert_eq!(metrics.device_pixel_ratio(), 1.0); } - - #[test] - fn handler_disable_uses_raw_state_for_conditional_target_resets() { - let mut effective = EmulationPolicy { - focus_emulation_enabled: true, - script_execution_disabled: true, - emulated_media: super::EmulatedMediaOverrides { - color_scheme: Some("dark".to_owned()), - ..Default::default() - }, - ..Default::default() - }; - let mut raw = crate::conn::DevToolsEmulationSessionState::default(); - - effective.apply_changes(raw.disable_policy_changes()); - - assert!( - effective.focus_emulation_enabled, - "an untouched handler must not clear another session's focus setting" - ); - assert!( - effective.emulated_media.color_scheme.is_none(), - "Blink clears the shared media override on every handler disable" - ); - assert!(!effective.script_execution_disabled); - } } diff --git a/moli-core/src/browser/mod.rs b/moli-core/src/browser/mod.rs index 52c18b2523..a2c6fb56eb 100644 --- a/moli-core/src/browser/mod.rs +++ b/moli-core/src/browser/mod.rs @@ -4,20 +4,48 @@ //! CDP wire identifiers and their numeric representation does not define an //! ordering relationship. +mod browser_context; +mod captured_body; mod document_lifecycle; mod document_lifetime; mod downloads; +mod emulation; +mod navigation_error; +mod owner; +pub use navigation_error::{ + NavigationNetworkError, NavigationNetworkErrorKind, NavigationRequestBlocked, +}; mod permissions; mod renderer_residence; +mod service_workers; +pub mod web_contents; +pub use browser_context::*; +pub use captured_body::{ + CapturedBody, CapturedBodyChunkReader, CapturedBodyWriter, DEFAULT_BODY_MATERIALIZE_LIMIT, + ensure_materialize_limit, +}; pub use document_lifecycle::DocumentLifecycle; pub use document_lifetime::{DocumentLifetime, DocumentLifetimeObserver, DocumentRetirement}; pub use downloads::{ DownloadAccessError, DownloadBehavior, DownloadBody, DownloadManager, DownloadMetadata, DownloadObservation, DownloadPolicy, DownloadSnapshot, DownloadState, }; +pub use emulation::{ + EmulatedDeviceMetrics, EmulatedGeolocationOverride, EmulatedGeolocationOverrideState, + EmulatedMediaOverrides, EmulatedNetworkConditions, EmulatedViewportSurface, +}; +pub use owner::{ + BrowserBuiltInitialDocument, BrowserCommittedInitialDocument, BrowserContextHandle, + BrowserDocumentMaterialization, BrowserDocumentNavigationCommit, BrowserHandle, + BrowserInitialDocumentAdmission, BrowserInitialDocumentBuild, BrowserInterceptedNavigationLoad, + BrowserInterceptedNavigationResponse, BrowserNavigationLoad, BrowserPreparedDocumentNavigation, + BrowserPreparedNavigationResponse, BrowserService, PendingDocumentRetirement, + PendingWebContentsClose, WebContentsCreation, +}; pub use permissions::{PermissionDefaults, PermissionOverrides}; pub use renderer_residence::RendererPageResidenceIdentity; +pub use service_workers::ServiceWorkerCommand; /// Navigation semantics, independent of the protocol that requested the load. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -92,6 +120,60 @@ define_browser_identity!( "Identity of one browser-owned request decision, independent of protocol request IDs." ); +/// Stable routing capability for one browser-owned WebContents. +/// +/// This carries only physical Browser identities. Frontend target and session +/// identifiers are deliberately resolved before this capability is created. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct WebContentsHandle { + context: BrowserContextId, + web_contents: WebContentsId, +} + +impl WebContentsHandle { + pub const fn new(context: BrowserContextId, web_contents: WebContentsId) -> Self { + Self { + context, + web_contents, + } + } + + pub const fn context(self) -> BrowserContextId { + self.context + } + + pub const fn id(self) -> WebContentsId { + self.web_contents + } +} + +/// Exact routing capability for one replaceable browser-owned Document. +/// +/// A delayed operation must present this complete capability again at commit; +/// matching only the stable WebContents is insufficient after navigation. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct DocumentHandle { + web_contents: WebContentsHandle, + document: DocumentId, +} + +impl DocumentHandle { + pub const fn new(web_contents: WebContentsHandle, document: DocumentId) -> Self { + Self { + web_contents, + document, + } + } + + pub const fn web_contents(self) -> WebContentsHandle { + self.web_contents + } + + pub const fn id(self) -> DocumentId { + self.document + } +} + static NEXT_BROWSER_SEQUENCE: AtomicU64 = AtomicU64::new(1); /// Process-monotonic sequence of completed Browser semantic occurrences. @@ -176,4 +258,16 @@ mod tests { size_of::() ); } + + #[test] + fn document_handle_preserves_the_complete_physical_route() { + let context = BrowserContextId::allocate(); + let contents = WebContentsId::allocate(); + let document = DocumentId::allocate(); + let handle = DocumentHandle::new(WebContentsHandle::new(context, contents), document); + + assert_eq!(handle.web_contents().context(), context); + assert_eq!(handle.web_contents().id(), contents); + assert_eq!(handle.id(), document); + } } diff --git a/moli-core/src/browser/navigation_error.rs b/moli-core/src/browser/navigation_error.rs new file mode 100644 index 0000000000..593b6d2cd0 --- /dev/null +++ b/moli-core/src/browser/navigation_error.rs @@ -0,0 +1,57 @@ +use std::{error::Error, fmt}; + +use url::Url; + +/// Network policy failures produced by the navigation request loaders. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum NavigationNetworkErrorKind { + InternetDisconnected, +} + +impl NavigationNetworkErrorKind { + pub fn error_text(self) -> &'static str { + match self { + Self::InternetDisconnected => "net::ERR_INTERNET_DISCONNECTED", + } + } +} + +/// A navigation request rejected by emulated offline network policy. +/// +/// Transport failures retain `moli_fetch::NetworkFetchFailureContext` and use +/// their existing handling paths; this cause currently represents offline +/// enforcement only. +/// +/// This is an error cause rather than display context, so callers can recover +/// its request identity through `anyhow::Error::downcast_ref` after adding +/// diagnostic context along the navigation pipeline. +#[derive(Debug)] +pub struct NavigationNetworkError { + pub kind: NavigationNetworkErrorKind, + pub unreachable_url: Url, + pub request_method: String, + pub request_headers: moli_fetch::RequestHeaders, +} + +impl fmt::Display for NavigationNetworkError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.kind.error_text()) + } +} + +impl Error for NavigationNetworkError {} + +/// A navigation request rejected by `Network.setBlockedURLs`. +/// +/// The protocol boundary preserves its canonical error text independently of +/// diagnostic context on other navigation failures. +#[derive(Debug)] +pub struct NavigationRequestBlocked; + +impl fmt::Display for NavigationRequestBlocked { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("net::ERR_BLOCKED_BY_CLIENT") + } +} + +impl Error for NavigationRequestBlocked {} diff --git a/moli-core/src/browser/owner.rs b/moli-core/src/browser/owner.rs new file mode 100644 index 0000000000..64b0f69d23 --- /dev/null +++ b/moli-core/src/browser/owner.rs @@ -0,0 +1,1898 @@ +use std::{ + fmt, + future::Future, + path::PathBuf, + pin::Pin, + sync::{Arc, mpsc as std_mpsc}, + thread, +}; + +use indexmap::IndexMap; +use parking_lot::Mutex; +use tokio::sync::{mpsc, oneshot}; + +mod navigation; +pub use navigation::{ + BrowserBuiltInitialDocument, BrowserCommittedInitialDocument, BrowserDocumentMaterialization, + BrowserDocumentNavigationCommit, BrowserInitialDocumentAdmission, BrowserInitialDocumentBuild, + BrowserInterceptedNavigationLoad, BrowserInterceptedNavigationResponse, BrowserNavigationLoad, + BrowserPreparedDocumentNavigation, BrowserPreparedNavigationResponse, +}; + +use super::{ + BrowserContext, BrowserContextId, BrowserContextStoragePartitionHandles, MainFrameSlotId, + StoragePartitionKind, WebContentsHandle, + web_contents::{InitialDocumentCreator, SessionStorageNamespace, WebContents}, +}; + +const BROWSER_OWNER_STACK_SIZE: usize = 16 * 1024 * 1024; + +type BrowserOperation = Box; +type BrowserLocalOperation = Box; +type BrowserLocalSender = mpsc::UnboundedSender; + +enum BrowserOwnerMessage { + Execute(BrowserOperation), + Shutdown, +} + +macro_rules! forward_context_read { + ($(fn $name:ident($($argument:ident: $argument_type:ty),* $(,)?) -> $result:ty;)+) => { + $( + pub fn $name(&self, $($argument: $argument_type),*) -> $result { + self.read_live(move |context| context.$name($($argument),*)) + } + )+ + }; +} + +macro_rules! forward_context_update { + ($(fn $name:ident($($argument:ident: $argument_type:ty),* $(,)?) -> $result:ty;)+) => { + $( + pub fn $name(&self, $($argument: $argument_type),*) -> $result { + self.update_live(move |context| context.$name($($argument),*)) + } + )+ + }; +} + +macro_rules! forward_context_try_read { + ($(fn $name:ident($($argument:ident: $argument_type:ty),* $(,)?) -> $result:ty;)+) => { + $( + pub fn $name(&self, $($argument: $argument_type),*) -> Result<$result, String> { + self.try_read(move |context| context.$name($($argument),*)) + } + )+ + }; +} + +macro_rules! forward_context_try_update { + ($(fn $name:ident($($argument:ident: $argument_type:ty),* $(,)?) -> $result:ty;)+) => { + $( + pub fn $name(&self, $($argument: $argument_type),*) -> Result<$result, String> { + self.try_update(move |context| context.$name($($argument),*)) + } + )+ + }; +} + +/// The process-local Browser aggregate. +/// +/// It is intentionally neither cloneable nor exposed through shared mutable +/// access. Only the Browser owner sequence constructs and mutates it. +struct Browser { + contexts: IndexMap, + permission_defaults: super::PermissionDefaults, + navigation_work: navigation::NavigationWorkRegistry, + local_sender: BrowserLocalSender, +} + +impl Browser { + fn new(local_sender: BrowserLocalSender) -> Self { + Self { + contexts: IndexMap::new(), + permission_defaults: super::PermissionDefaults::default(), + navigation_work: navigation::NavigationWorkRegistry::default(), + local_sender, + } + } + + fn context(&self, id: BrowserContextId) -> Result<&BrowserContext, String> { + self.contexts + .get(&id) + .ok_or_else(|| "BrowserContext unavailable".to_owned()) + } + + fn context_mut(&mut self, id: BrowserContextId) -> Result<&mut BrowserContext, String> { + self.contexts + .get_mut(&id) + .ok_or_else(|| "BrowserContext unavailable".to_owned()) + } + + fn insert_context(&mut self, context: BrowserContext) -> BrowserContextId { + let id = context.id(); + let previous = self.contexts.insert(id, context); + debug_assert!(previous.is_none(), "BrowserContext identity must be unique"); + id + } + + fn remove_context(&mut self, id: BrowserContextId) -> bool { + let Some(context) = self.contexts.shift_remove(&id) else { + return false; + }; + self.navigation_work.remove_context(id); + context.shutdown(); + true + } + + fn shutdown(&mut self) { + self.navigation_work.clear(); + for (_, context) in std::mem::take(&mut self.contexts) { + context.shutdown(); + } + } + + fn spawn_local_context_operation( + &mut self, + id: BrowserContextId, + operation: impl FnOnce( + BrowserContext, + ) -> Pin + 'static>> + + 'static, + ) -> Result, String> { + let context = self + .contexts + .shift_remove(&id) + .ok_or_else(|| "BrowserContext unavailable".to_owned())?; + let local_sender = self.local_sender.clone(); + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + let (context, result) = operation(context).await; + let _ = local_sender.send(Box::new(move |browser| { + let previous = browser.contexts.insert(id, context); + debug_assert!( + previous.is_none(), + "BrowserContext identity must remain unique during local work" + ); + let _ = completion_tx.send(result); + })); + }); + Ok(completion) + } +} + +impl Drop for Browser { + fn drop(&mut self) { + self.shutdown(); + } +} + +struct BrowserOwnerEndpoint { + tx: mpsc::UnboundedSender, + join: Mutex>>, +} + +impl BrowserOwnerEndpoint { + fn shutdown_and_join(&self) { + let _ = self.tx.send(BrowserOwnerMessage::Shutdown); + if let Some(join) = self.join.lock().take() + && join.join().is_err() + { + tracing::error!("Browser owner thread panicked during shutdown"); + } + } +} + +impl Drop for BrowserOwnerEndpoint { + fn drop(&mut self) { + let _ = self.tx.send(BrowserOwnerMessage::Shutdown); + if let Some(join) = self.join.get_mut().take() + && join.join().is_err() + { + tracing::error!("Browser owner thread panicked during drop"); + } + } +} + +/// Cloneable command endpoint for the unique Browser owner sequence. +#[derive(Clone)] +pub struct BrowserHandle { + endpoint: Arc, +} + +impl fmt::Debug for BrowserHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BrowserHandle").finish_non_exhaustive() + } +} + +impl BrowserHandle { + fn start() -> Result { + let (tx, mut rx) = mpsc::unbounded_channel(); + let (ready_tx, ready_rx) = std_mpsc::sync_channel(1); + let join = thread::Builder::new() + .name("moli-browser-owner".to_owned()) + .stack_size(BROWSER_OWNER_STACK_SIZE) + .spawn(move || { + let runtime = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(runtime) => runtime, + Err(error) => { + let _ = ready_tx.send(Err(format!( + "failed to build Browser owner runtime: {error}" + ))); + return; + } + }; + let local = tokio::task::LocalSet::new(); + let _ = ready_tx.send(Ok(())); + local.block_on(&runtime, async move { + let (local_tx, mut local_rx) = mpsc::unbounded_channel(); + let mut browser = Browser::new(local_tx); + loop { + tokio::select! { + message = rx.recv() => match message { + Some(BrowserOwnerMessage::Execute(operation)) => operation(&mut browser), + Some(BrowserOwnerMessage::Shutdown) | None => break, + }, + operation = local_rx.recv() => { + if let Some(operation) = operation { + operation(&mut browser); + } + } + } + } + browser.shutdown(); + }); + }) + .map_err(|error| format!("failed to spawn Browser owner: {error}"))?; + ready_rx + .recv() + .map_err(|error| format!("Browser owner stopped during startup: {error}"))??; + Ok(Self { + endpoint: Arc::new(BrowserOwnerEndpoint { + tx, + join: Mutex::new(Some(join)), + }), + }) + } + + fn execute( + &self, + operation: impl FnOnce(&mut Browser) -> R + Send + 'static, + ) -> Result { + let (result_tx, result_rx) = std_mpsc::sync_channel(1); + self.endpoint + .tx + .send(BrowserOwnerMessage::Execute(Box::new(move |browser| { + let _ = result_tx.send(operation(browser)); + }))) + .map_err(|_| "Browser owner is unavailable".to_owned())?; + result_rx + .recv() + .map_err(|_| "Browser owner stopped before completing the operation".to_owned()) + } + + pub fn create_context( + &self, + handles: BrowserContextStoragePartitionHandles, + kind: StoragePartitionKind, + http_cache_root: Option, + http_cache_max_bytes: Option, + ) -> Result { + let id = self.execute(move |browser| { + browser.insert_context(BrowserContext::new( + handles, + kind, + http_cache_root, + http_cache_max_bytes, + )) + })?; + Ok(BrowserContextHandle { + browser: self.clone(), + id, + }) + } + + pub fn contains_context(&self, id: BrowserContextId) -> bool { + self.execute(move |browser| browser.contexts.contains_key(&id)) + .unwrap_or(false) + } + + pub fn remove_context(&self, id: BrowserContextId) -> Result { + self.execute(move |browser| browser.remove_context(id)) + } + + pub fn set_permission_default( + &self, + registration: crate::page::PermissionOverrideRegistration, + ) { + self.execute(move |browser| browser.permission_defaults.set(registration)) + .expect("live Browser owner must accept permission defaults"); + } + + pub fn clear_permission_defaults(&self) { + self.execute(|browser| browser.permission_defaults.clear()) + .expect("live Browser owner must clear permission defaults"); + } + + pub fn permission_default_count(&self) -> usize { + self.execute(|browser| browser.permission_defaults.override_count()) + .expect("live Browser owner must snapshot permission defaults") + } + + pub fn permission_defaults_snapshot(&self) -> Vec { + self.execute(|browser| browser.permission_defaults.snapshot()) + .expect("live Browser owner must snapshot permission defaults") + } +} + +/// Service lifetime guard for the Browser owner sequence. +/// +/// AppState keeps this value alive independently of every frontend socket. +#[derive(Clone, Debug)] +pub struct BrowserService { + handle: BrowserHandle, +} + +impl BrowserService { + pub fn start() -> Result { + Ok(Self { + handle: BrowserHandle::start()?, + }) + } + + pub fn handle(&self) -> BrowserHandle { + self.handle.clone() + } + + pub fn shutdown(&self) { + self.handle.endpoint.shutdown_and_join(); + } +} + +/// Exact Context capability backed by the Browser owner sequence. +#[derive(Clone, Debug)] +pub struct BrowserContextHandle { + browser: BrowserHandle, + id: BrowserContextId, +} + +/// Completion of a Browser-owned WebContents teardown. +pub struct PendingWebContentsClose { + completion: oneshot::Receiver<()>, +} + +impl PendingWebContentsClose { + pub async fn close_async(self) { + let _ = self.completion.await; + } +} + +/// Completion of a Browser-owned Document retirement. +pub struct PendingDocumentRetirement { + completion: oneshot::Receiver<()>, +} + +impl PendingDocumentRetirement { + pub async fn close(self) { + let _ = self.completion.await; + } +} + +/// Initial Browser-owned state for a newly created WebContents. +/// +/// Frontend target/session identities are projected only after this request +/// returns the physical handle. +#[derive(Default)] +pub struct WebContentsCreation { + initial_document: Option<( + String, + Option, + Option, + )>, + session_storage: Option, +} + +impl WebContentsCreation { + pub fn with_initial_document( + initial_url: String, + creator: Option, + storage_key: Option, + ) -> Self { + Self { + initial_document: Some((initial_url, creator, storage_key)), + session_storage: None, + } + } + + pub fn with_session_storage(mut self, namespace: SessionStorageNamespace) -> Self { + self.session_storage = Some(namespace); + self + } + + fn build(self) -> WebContents { + let mut contents = WebContents::default(); + if let Some((url, creator, storage_key)) = self.initial_document { + contents.begin_initial_empty_document(url, creator, storage_key); + } + if let Some(namespace) = self.session_storage { + contents.install_session_storage_namespace(namespace); + } + contents + } +} + +impl BrowserContextHandle { + pub fn id(&self) -> BrowserContextId { + self.id + } + + pub fn is_live(&self) -> bool { + self.browser.contains_context(self.id) + } + + pub fn remove(&self) -> Result { + self.browser.remove_context(self.id) + } + + fn read( + &self, + operation: impl FnOnce(&BrowserContext) -> R + Send + 'static, + ) -> Result { + let id = self.id; + self.browser + .execute(move |browser| browser.context(id).map(operation))? + } + + fn update( + &self, + operation: impl FnOnce(&mut BrowserContext) -> R + Send + 'static, + ) -> Result { + let id = self.id; + self.browser + .execute(move |browser| browser.context_mut(id).map(operation))? + } + + fn read_live( + &self, + operation: impl FnOnce(&BrowserContext) -> R + Send + 'static, + ) -> R { + self.read(operation) + .expect("live BrowserContext handle must resolve in its owner") + } + + fn update_live( + &self, + operation: impl FnOnce(&mut BrowserContext) -> R + Send + 'static, + ) -> R { + self.update(operation) + .expect("live BrowserContext handle must resolve in its owner") + } + + fn try_read( + &self, + operation: impl FnOnce(&BrowserContext) -> Result + Send + 'static, + ) -> Result { + self.read(operation)? + } + + fn try_update( + &self, + operation: impl FnOnce(&mut BrowserContext) -> Result + Send + 'static, + ) -> Result { + self.update(operation)? + } + + pub fn create_web_contents( + &self, + creation: WebContentsCreation, + ) -> Result<(WebContentsHandle, MainFrameSlotId), String> { + self.update(move |context| context.register_web_contents(creation.build()))? + } + + pub fn web_contents_window_name( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + self.try_read(move |context| { + context + .web_contents_window_name(handle) + .map(|name| name.map(str::to_owned)) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn suspend_navigation_resource_owner_for_test( + &self, + handle: WebContentsHandle, + ) -> Result + use<>, String> { + let (restore, restored) = self.browser.execute(move |browser| { + let mut retired_owner = crate::runtime::RendererBrowserContextRuntime::new(); + let retired_engine = + crate::runtime::NavigationEngine::new_with_fetch_config_and_browser_context_access( + Default::default(), + retired_owner.owner_access(), + moli_page_types::OptionalResourceFetchMask::NONE, + true, + ) + .map_err(|error| error.to_string())?; + retired_owner.shutdown_and_join(); + let original = std::mem::replace( + browser + .context_mut(handle.context())? + .navigation_engine_for_test_mut(handle) + .ok_or("navigation engine unavailable")?, + retired_engine, + ); + let (restore, resume) = oneshot::channel(); + let (completed, restored) = oneshot::channel(); + let sender = browser.local_sender.clone(); + tokio::task::spawn_local(async move { + let _ = resume.await; + let _ = sender.send(Box::new(move |browser| { + if let Ok(context) = browser.context_mut(handle.context()) + && let Some(engine) = context.navigation_engine_for_test_mut(handle) + { + *engine = original; + } + let _ = completed.send(()); + })); + }); + Ok::<_, String>((restore, restored)) + })??; + Ok(async move { + let _ = restore.send(()); + let _ = restored.await; + }) + } + + pub fn close_web_contents( + &self, + handle: WebContentsHandle, + ) -> Result { + self.try_update(move |context| { + let closing = context.close_web_contents(handle)?; + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + closing.close_async().await; + let _ = completion_tx.send(()); + }); + Ok(PendingWebContentsClose { completion }) + }) + } + + pub fn close_all_web_contents(&self) -> Vec { + self.update_live(|context| { + context + .close_all_web_contents() + .into_iter() + .map(|closing| { + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + closing.close_async().await; + let _ = completion_tx.send(()); + }); + PendingWebContentsClose { completion } + }) + .collect() + }) + } + + pub fn retire_document( + &self, + handle: WebContentsHandle, + ) -> Result { + self.try_update(move |context| { + let retiring = context.retire_document(handle)?; + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + retiring.close().await; + let _ = completion_tx.send(()); + }); + Ok(PendingDocumentRetirement { completion }) + }) + } + + pub fn bind_page_navigation_engines( + &self, + config: crate::runtime::NavigationRuntimeConfig, + sender: Option, + ) { + self.update_live(move |context| context.bind_page_navigation_engines(config, sender)); + } + + pub fn set_renderer_output_transport_sender( + &self, + sender: crate::RendererOutputTransportSender, + ) { + self.update_live(move |context| context.set_renderer_output_transport_sender(sender)); + } + + pub fn contains_web_contents(&self, handle: WebContentsHandle) -> bool { + self.read_live(move |context| context.contains_web_contents(handle)) + } + + pub fn select_web_contents(&self, id: super::WebContentsId) -> bool { + self.update_live(move |context| context.select_web_contents(id)) + } + + pub fn selected_web_contents_id(&self) -> Option { + self.read_live(BrowserContext::selected_web_contents_id) + } + + pub fn selected_web_contents_handle(&self) -> Option { + self.read_live(BrowserContext::selected_web_contents_handle) + } + + pub fn web_contents_count(&self) -> usize { + self.read_live(BrowserContext::web_contents_count) + } + + pub fn web_contents_handle_for_window_id(&self, window_id: u64) -> Option { + self.read_live(move |context| context.web_contents_handle_for_window_id(window_id)) + } + + pub fn web_contents_handle_for_window_name(&self, name: &str) -> Option { + let name = name.to_owned(); + self.read_live(move |context| context.web_contents_handle_for_window_name(&name)) + } + + pub fn clone_session_storage_namespace( + &self, + web_contents: super::WebContentsId, + ) -> Option { + self.read_live(move |context| context.clone_session_storage_namespace(web_contents)) + } + + pub fn web_contents_is_crashed(&self, handle: WebContentsHandle) -> Result { + self.try_read(move |context| context.web_contents_is_crashed(handle)) + } + + pub fn set_web_contents_crashed( + &self, + handle: WebContentsHandle, + crashed: bool, + ) -> Result<(), String> { + self.try_update(move |context| context.set_web_contents_crashed(handle, crashed)) + } + + pub fn performance_metric_snapshot( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + self.try_read(move |context| context.performance_metric_snapshot(handle)) + } + + pub fn observe_renderer_page_state( + &self, + handle: WebContentsHandle, + snapshot: Arc, + ) -> Result { + self.try_update(move |context| context.observe_renderer_page_state(handle, &snapshot)) + } + + pub fn web_contents_window_surface( + &self, + handle: WebContentsHandle, + ) -> Result { + self.try_read(move |context| context.web_contents_window_surface(handle)) + } + + #[allow(clippy::too_many_arguments)] + pub fn update_web_contents_window_surface( + &self, + handle: WebContentsHandle, + state: Option, + width: Option, + height: Option, + x: Option, + y: Option, + ) -> Result<(), String> { + self.try_update(move |context| { + context.update_web_contents_window_surface(handle, state, width, height, x, y) + }) + } + + pub fn set_web_contents_window_name( + &self, + handle: WebContentsHandle, + name: Option, + ) -> Result<(), String> { + self.try_update(move |context| context.set_web_contents_window_name(handle, name)) + } + + pub fn set_web_contents_opener( + &self, + handle: WebContentsHandle, + opener: Option, + can_access: bool, + ) -> Result<(), String> { + self.try_update(move |context| context.set_web_contents_opener(handle, opener, can_access)) + } + + pub fn set_web_contents_network_offline( + &self, + handle: WebContentsHandle, + offline: bool, + ) -> Result<(), String> { + self.try_update(move |context| context.set_web_contents_network_offline(handle, offline)) + } + + pub fn web_contents_network_offline(&self, handle: WebContentsHandle) -> Result { + self.try_read(move |context| context.web_contents_network_offline(handle)) + } + + pub fn web_contents_emulation_policy( + &self, + handle: WebContentsHandle, + ) -> Result { + self.try_read(move |context| context.web_contents_emulation_policy(handle)) + } + + pub fn apply_web_contents_emulation_policy_change( + &self, + handle: WebContentsHandle, + change: super::web_contents::EmulationPolicyChange, + ) -> Result<(), String> { + self.try_update(move |context| { + context.apply_web_contents_emulation_policy_change(handle, change) + }) + } + + pub fn apply_web_contents_emulation_policy_changes( + &self, + handle: WebContentsHandle, + changes: Vec, + ) -> Result<(), String> { + self.try_update(move |context| { + context.apply_web_contents_emulation_policy_changes(handle, changes) + }) + } + + pub fn web_contents_window_counts(&self) -> (usize, usize, usize) { + self.read_live(BrowserContext::web_contents_window_counts) + } + + pub fn loaded_document_count(&self) -> usize { + self.read_live(BrowserContext::loaded_document_count) + } + + pub fn has_pending_javascript_dialog(&self) -> bool { + self.read_live(BrowserContext::has_pending_javascript_dialog) + } + + forward_context_read! { + fn default_locale_override() -> Option; + fn default_timezone_override() -> Option; + fn has_loaded_document(handle: WebContentsHandle) -> bool; + fn document_renderer_matches(handle: WebContentsHandle, renderer: super::RendererPageResidenceIdentity) -> bool; + fn has_inflight_background_navigation() -> bool; + fn loaded_document_renderer_owner_ids() -> std::collections::HashSet; + fn dedicated_worker_running_isolate_count() -> usize; + fn routes_renderer_browser_context_runtime(runtime: crate::RendererBrowserContextRuntimeId) -> bool; + fn renderer_memory_diagnostics() -> serde_json::Value; + fn shared_worker_runtime_diagnostics() -> crate::runtime::RendererSharedWorkerRuntimeDiagnostics; + fn javascript_dialog_handler_enabled() -> bool; + } + + forward_context_update! { + fn invalidate_selected_resource_runtime() -> (); + fn clear_permission_overrides() -> (); + fn set_download_policy(policy: Option) -> (); + fn set_browser_identity_override(identity: Option) -> (); + fn set_default_network_conditions(conditions: Option) -> (); + fn set_default_geolocation_override(geolocation: Option) -> (); + fn set_default_device_metrics(metrics: super::EmulatedDeviceMetrics) -> bool; + fn set_default_extra_headers(headers: moli_fetch::RequestHeaders) -> (); + fn set_network_policy(policy: super::ContextNetworkPolicy) -> (); + fn set_context_tls_verify_host_override(enabled: bool) -> (); + } + + forward_context_try_read! { + fn initial_document_build_pending(handle: WebContentsHandle) -> bool; + fn web_contents_initial_document_state(handle: WebContentsHandle) -> Option; + fn document_handle(handle: WebContentsHandle) -> Option; + fn web_contents_identity(handle: WebContentsHandle) -> (super::WebContentsId, super::MainFrameSlotId); + fn resolve_history_traversal(handle: WebContentsHandle, destination: super::web_contents::HistoryTraversalDestination) -> super::web_contents::ResolvedHistoryTraversal; + fn document_service_worker_client_id(handle: super::DocumentHandle) -> u64; + fn document_renderer_residence(handle: super::DocumentHandle) -> super::RendererPageResidenceIdentity; + fn document_url(handle: super::DocumentHandle) -> url::Url; + fn document_title(handle: super::DocumentHandle) -> String; + fn document_lifecycle_snapshot(handle: super::DocumentHandle) -> Option; + fn initial_document_url(handle: WebContentsHandle) -> Option; + fn initial_document_storage_key(handle: WebContentsHandle) -> Option; + fn is_on_initial_document(handle: WebContentsHandle) -> Option; + fn initial_document_has_pending_navigation(handle: WebContentsHandle) -> bool; + fn navigation_history_snapshot(handle: WebContentsHandle) -> (usize, Vec); + fn navigation_history_entry_url(handle: WebContentsHandle, entry_id: i32) -> Option; + fn has_pending_document_navigation(handle: WebContentsHandle) -> bool; + fn navigation_is_default(handle: WebContentsHandle) -> bool; + fn navigation_retains(handle: WebContentsHandle, navigation: super::NavigationId) -> bool; + fn current_document_navigation(handle: WebContentsHandle) -> Option; + fn committed_document_navigation(handle: WebContentsHandle) -> Option; + fn has_inflight_background_navigation_for_web_contents(handle: WebContentsHandle) -> bool; + fn accepts_document_preparation(handle: WebContentsHandle, navigation: super::NavigationId, renderer: super::RendererPageResidenceIdentity) -> bool; + } + + forward_context_try_update! { + fn set_default_timezone_override(timezone: Option) -> (); + fn set_default_locale_override(locale: Option) -> (); + fn mark_next_navigation_history_replace_current(handle: WebContentsHandle) -> (); + fn mark_next_navigation_history_traverse_to_entry(handle: WebContentsHandle, entry_id: i32) -> (); + fn commit_same_document_navigation(handle: WebContentsHandle, document: super::DocumentId, url: url::Url, history_update: crate::page::SameDocumentHistoryUpdate) -> Option; + fn mark_renderer_crashed(handle: WebContentsHandle) -> (); + fn start_document_navigation(handle: WebContentsHandle) -> super::NavigationId; + fn clear_document_navigation_state(handle: WebContentsHandle) -> (); + fn begin_initial_empty_document(handle: WebContentsHandle, initial_url: String, creator: Option, storage_key: Option) -> (); + fn mark_initial_url_replaces_empty_document(handle: WebContentsHandle) -> (); + fn crash_web_contents_renderer_from_io(handle: WebContentsHandle) -> (); + fn pause_navigation_request(handle: WebContentsHandle, navigation: super::NavigationId, request: super::web_contents::NavigationRequestInterception) -> super::web_contents::NavigationInterceptionPermit; + fn pause_navigation_response(handle: WebContentsHandle, navigation: super::NavigationId, transfer: super::web_contents::PausedDocumentTransfer) -> super::web_contents::NavigationInterceptionPermit; + } + + pub fn take_navigation_request( + &self, + permit: super::web_contents::NavigationInterceptionPermit, + ) -> Option { + self.update_live(move |context| context.take_navigation_request(permit)) + } + + pub fn take_navigation_response( + &self, + permit: super::web_contents::NavigationInterceptionPermit, + ) -> Option { + self.update_live(move |context| context.take_navigation_response(permit)) + } + + pub fn restore_navigation_response( + &self, + permit: super::web_contents::NavigationInterceptionPermit, + transfer: super::web_contents::PausedDocumentTransfer, + ) -> Result<(), Box> { + self.update_live(move |context| context.restore_navigation_response(permit, transfer)) + } + + pub fn commit_document_title( + &self, + handle: WebContentsHandle, + change: &crate::RendererDocumentTitleChanged, + ) -> Result, String> { + let change = change.clone(); + self.try_update(move |context| context.commit_document_title(handle, &change)) + } + + pub fn arm_background_navigation_completion( + &self, + token: &super::NavigationId, + additional_cancellation: Option, + ) -> bool { + let token = *token; + self.update_live(move |context| { + context.arm_background_navigation_completion(&token, additional_cancellation) + }) + } + + pub fn accepts_any_pending_navigation_event(&self, token: &super::NavigationId) -> bool { + let token = *token; + self.read_live(move |context| context.accepts_any_pending_navigation_event(&token)) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn document_navigation_cancellation_handle_for_test( + &self, + token: &super::NavigationId, + ) -> Option { + let token = *token; + self.read_live(move |context| { + context.document_navigation_cancellation_handle_for_test(&token) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn commit_pending_document_navigation_for_test( + &self, + handle: WebContentsHandle, + token: &super::NavigationId, + ) -> bool { + let token = *token; + self.update_live(move |context| { + context.commit_pending_document_navigation_for_test(handle, &token) + }) + } + + pub fn settle_background_navigation_completion(&self, token: &super::NavigationId) -> bool { + let token = *token; + self.update_live(move |context| context.settle_background_navigation_completion(&token)) + } + + pub fn accepts_pending_navigation( + &self, + handle: WebContentsHandle, + navigation: &super::NavigationId, + ) -> Result { + let navigation = *navigation; + self.try_read(move |context| context.accepts_pending_navigation(handle, &navigation)) + } + + pub fn accepts_document_body_completion( + &self, + handle: WebContentsHandle, + navigation: &super::NavigationId, + ) -> Result { + let navigation = *navigation; + self.try_read(move |context| context.accepts_document_body_completion(handle, &navigation)) + } + + pub fn clear_pending_navigation_if_matches( + &self, + handle: WebContentsHandle, + navigation: &super::NavigationId, + ) -> Result { + let navigation = *navigation; + self.try_update(move |context| { + context.clear_pending_navigation_if_matches(handle, &navigation) + }) + } + + pub fn apply_renderer_document_lifecycle( + &self, + renderer_page: super::RendererPageResidenceIdentity, + event: crate::page::RendererDocumentLifecycleEvent, + ) -> Option { + self.update_live(move |context| { + context.apply_renderer_document_lifecycle(renderer_page, event) + }) + } + + forward_context_read! { + fn controlled_service_worker_window_client_ids(registration_id: u64, version_id: u64) -> Vec; + fn set_service_worker_pause_on_start_for_version(version_id: u64, pause: bool) -> bool; + fn close_shared_worker(instance_id: moli_shared_worker::SharedWorkerInstanceId) -> bool; + fn close_dedicated_worker(instance_id: u64) -> bool; + fn attach_dedicated_worker_inspector_session(instance_id: u64, session_id: Option) -> bool; + fn detach_shared_worker_inspector_session(instance_id: moli_shared_worker::SharedWorkerInstanceId, session_id: Option) -> bool; + fn detach_dedicated_worker_inspector_session(instance_id: u64, session_id: Option) -> bool; + fn detach_service_worker_inspector_session(version_id: u64, session_id: Option) -> bool; + fn run_dedicated_worker_if_waiting_for_debugger(instance_id: u64) -> bool; + fn run_service_worker_if_waiting_for_debugger(version_id: u64) -> bool; + fn worker_runtime_inspection_endpoint() -> crate::runtime::RendererBrowserContextRuntime; + fn permission_override_count() -> usize; + fn storage_partition_kind() -> super::StoragePartitionKind; + fn storage_partition_kind_label() -> &'static str; + fn page_storage_handles(web_contents: Option) -> Result; + fn clear_http_cache() -> Result<(), String>; + fn snapshot_cookies() -> Vec; + fn web_contents_for_renderer_owner(owner: crate::RendererOwnerLocalHostId) -> Option; + } + + forward_context_update! { + fn set_service_worker_pause_on_start(pause: bool) -> (); + fn set_service_worker_related_pause_on_start_policies(policies: Vec<(u64, u64, String, String)>) -> (); + fn set_dedicated_worker_pause_on_start(pause: bool) -> (); + fn set_service_worker_inspection_attached(version_id: u64, attached: bool) -> (); + fn set_storage_quota_override(origin: String, quota: f64) -> (); + fn set_javascript_dialog_handler_enabled(enabled: bool) -> (); + } + + pub fn execute_service_worker_command( + &self, + command: super::ServiceWorkerCommand, + ) -> Result<(), String> { + self.try_read(move |context| context.execute_service_worker_command(command)) + } + + pub fn download_policy(&self) -> Option { + self.read_live(|context| context.download_policy().cloned()) + } + + pub fn browser_identity_override( + &self, + ) -> Option { + self.read_live(|context| context.browser_identity_override().cloned()) + } + + pub fn emulation_defaults(&self) -> super::ContextEmulationDefaults { + self.read_live(|context| context.emulation_defaults().clone()) + } + + pub fn network_policy(&self) -> super::ContextNetworkPolicy { + self.read_live(|context| context.network_policy().clone()) + } + + pub fn observe_request_cookie_access_report( + &self, + request_url: &url::Url, + request_context: moli_cookie_jar::NetworkCookieRequestContext, + ) -> Option { + let request_url = request_url.clone(); + self.read_live(move |context| { + context.observe_request_cookie_access_report(&request_url, request_context) + }) + } + + pub fn clear_storage_quota_override(&self, origin: &str) { + let origin = origin.to_owned(); + self.update_live(move |context| context.clear_storage_quota_override(&origin)); + } + + pub fn storage_quota_for_origin(&self, origin: &str) -> (f64, bool) { + let origin = origin.to_owned(); + self.read_live(move |context| context.storage_quota_for_origin(&origin)) + } + + pub fn storage_usage_for_origin( + &self, + serialized_origin: &str, + ) -> Result { + let serialized_origin = serialized_origin.to_owned(); + self.try_read(move |context| context.storage_usage_for_origin(&serialized_origin)) + } + + pub fn clear_site_data_for_origin( + &self, + origin: &url::Url, + options: super::SiteDataClearOptions, + ) -> Result<(), String> { + let origin = origin.clone(); + self.try_update(move |context| context.clear_site_data_for_origin(&origin, options)) + } + + pub fn clear_site_data_for_storage_key( + &self, + storage_key: &moli_storage_key::MoliStorageKey, + options: super::SiteDataClearOptions, + ) -> Result<(), String> { + let storage_key = storage_key.clone(); + self.try_update(move |context| { + context.clear_site_data_for_storage_key(&storage_key, options) + }) + } + + pub fn store_cookie( + &self, + cookie: moli_cookie_jar::StoredCookie, + request_url: Option<&url::Url>, + source: moli_cookie_jar::CookieSource, + ) -> moli_cookie_jar::StoredCookieSetReport { + let request_url = request_url.cloned(); + self.read_live(move |context| context.store_cookie(cookie, request_url.as_ref(), source)) + } + + #[allow(clippy::too_many_arguments)] + pub fn delete_cookies_with_partition_key( + &self, + name: Option<&str>, + domain: Option<&str>, + path: Option<&str>, + url_host: Option<&str>, + partition_key: Option<&moli_cookie_jar::StoredCookiePartitionKey>, + ) { + let name = name.map(str::to_owned); + let domain = domain.map(str::to_owned); + let path = path.map(str::to_owned); + let url_host = url_host.map(str::to_owned); + let partition_key = partition_key.cloned(); + self.update_live(move |context| { + context.delete_cookies_with_partition_key( + name.as_deref(), + domain.as_deref(), + path.as_deref(), + url_host.as_deref(), + partition_key.as_ref(), + ); + }); + } + + pub fn start_download_request( + &self, + policy: &super::DownloadPolicy, + client: crate::network::ResourceRequestClient, + request: moli_fetch::Request, + suggested_filename: Option, + ) -> Result, String> { + let policy = policy.clone(); + self.try_update(move |context| { + context.start_download_request(&policy, client, request, suggested_filename) + }) + } + + pub fn start_download_response( + &self, + web_contents: WebContentsHandle, + policy: &super::DownloadPolicy, + url: url::Url, + headers: Vec<(String, String)>, + body: super::DownloadBody, + ) -> Result, String> { + let policy = policy.clone(); + self.try_update(move |context| { + context.start_download_response(web_contents, &policy, url, headers, body) + }) + } + + pub fn cancel_download(&self, guid: &str) -> Option> { + let guid = guid.to_owned(); + self.read_live(move |context| context.cancel_download(&guid)) + } + + pub fn read_download_artifact( + &self, + guid: &str, + ) -> Option, String>>, super::DownloadAccessError>> + { + let guid = guid.to_owned(); + self.read_live(move |context| context.read_download_artifact(&guid)) + } + + forward_context_try_read! { + fn web_contents_has_pending_javascript_dialog(handle: WebContentsHandle) -> bool; + fn ensure_document_current(document: super::DocumentHandle) -> (); + } + + forward_context_try_update! { + fn install_document_javascript_dialog(document: super::DocumentHandle, dialog: crate::page::RendererPendingJavaScriptDialog) -> super::web_contents::JavaScriptDialogKey; + } + + pub fn document_javascript_dialog_snapshot( + &self, + document: super::DocumentHandle, + key: super::web_contents::JavaScriptDialogKey, + ) -> Option { + self.read_live(move |context| context.document_javascript_dialog_snapshot(document, key)) + } + + pub fn set_document_javascript_dialog_prompt_text( + &self, + document: super::DocumentHandle, + key: super::web_contents::JavaScriptDialogKey, + prompt_text: String, + ) -> Result<(), super::web_contents::JavaScriptDialogError> { + self.update(move |context| { + context.set_document_javascript_dialog_prompt_text(document, key, prompt_text) + }) + .unwrap_or(Err(super::web_contents::JavaScriptDialogError::NotFound)) + } + + pub fn finish_document_javascript_dialog( + &self, + document: super::DocumentHandle, + key: super::web_contents::JavaScriptDialogKey, + accepted: bool, + prompt_text: Option, + ) -> Option { + self.update_live(move |context| { + context.finish_document_javascript_dialog(document, key, accepted, prompt_text) + }) + } + + pub fn dismiss_document_javascript_dialog( + &self, + document: super::DocumentHandle, + key: super::web_contents::JavaScriptDialogKey, + ) { + self.update_live(move |context| context.dismiss_document_javascript_dialog(document, key)); + } + + forward_context_try_read! { + fn document_handle_for_web_contents(handle: WebContentsHandle) -> Option; + fn start_set_document_content(document: super::DocumentHandle, frame_id: String, html: String) -> super::PendingSetDocumentContent; + fn start_top_level_same_document_navigation(document: super::DocumentHandle, url: String) -> super::PendingTopLevelSameDocumentNavigation; + fn start_capture_document_screencast_frame(document: super::DocumentHandle, request: crate::page::RendererCaptureScreencastFrameRequest) -> super::PendingCaptureDocumentScreencastFrame; + fn start_capture_document_snapshot(document: super::DocumentHandle) -> super::PendingCaptureDocumentSnapshot; + fn start_capture_document_image(document: super::DocumentHandle, request: crate::page::RendererCaptureScreenshotRequest) -> super::PendingCaptureDocumentImage; + fn start_top_level_history_traversal(document: super::DocumentHandle, delta: i64) -> super::PendingTopLevelHistoryTraversal; + fn start_navigation_history_reset(document: super::DocumentHandle) -> super::PendingNavigationHistoryReset; + fn start_child_frame_navigation(document: super::DocumentHandle, frame_id: String, url: String) -> super::PendingChildFrameNavigation; + fn start_child_frame_document_resource_text_search(document: super::DocumentHandle, frame_id: String, url: String, query: String, case_sensitive: bool, is_regex: bool) -> super::PendingDocumentResourceTextSearch; + fn start_document_text_search(document: super::DocumentHandle, text: String, query: String, case_sensitive: bool, is_regex: bool) -> super::PendingDocumentResourceTextSearch; + fn set_document_javascript_dialog_handler_enabled(document: super::DocumentHandle, enabled: bool) -> (); + fn start_document_csp_bypass_update(document: super::DocumentHandle, bypass: bool) -> super::PendingDocumentCspBypassUpdate; + fn document_subresource_network_records(document: super::DocumentHandle) -> Vec; + fn document_observable_output_snapshot(document: super::DocumentHandle) -> Vec; + fn start_document_lifecycle_stop(document: super::DocumentHandle) -> super::PendingDocumentLifecycleStop; + fn start_document_diagnostics_snapshot(document: super::DocumentHandle) -> super::PendingDocumentDiagnosticsSnapshot; + fn start_document_storage_key_snapshot(document: super::DocumentHandle) -> super::PendingDocumentStorageKeySnapshot; + fn start_child_frame_tree_snapshot(document: super::DocumentHandle) -> super::PendingChildFrameTreeSnapshot; + fn start_document_cookie_owner_snapshot(document: super::DocumentHandle) -> super::PendingDocumentCookieOwnerSnapshot; + fn start_document_blob_read(document: super::DocumentHandle, uuid: String) -> super::PendingDocumentBlobRead; + fn start_network_resource_load_preparation(document: super::DocumentHandle, frame_id: String, url: url::Url, disable_cache: bool, include_credentials: bool) -> super::PendingNetworkResourceLoadPreparation; + fn start_app_manifest_load_preparation(document: super::DocumentHandle) -> super::PendingAppManifestLoadPreparation; + fn start_app_manifest_publication(document: super::DocumentHandle, publication: crate::page::RendererAppManifestLoadPublication) -> super::PendingAppManifestPublication; + fn start_document_autofill_trigger(document: super::DocumentHandle, request: crate::page::RendererAutofillTriggerRequest) -> super::PendingDocumentAutofillTrigger; + fn start_document_input_command(document: super::DocumentHandle, command: super::PageInputCommand) -> super::PendingDocumentInputCommand; + } + + forward_context_try_update! { + fn finish_set_document_content(completed: super::CompletedSetDocumentContent) -> (crate::page::RendererSetDocumentContentResult, crate::page::RendererCommandTurnOutput); + fn finish_top_level_same_document_navigation(completed: super::CompletedTopLevelSameDocumentNavigation) -> (bool, crate::page::RendererCommandTurnOutput); + fn finish_capture_document_screencast_frame(completed: super::CompletedCaptureDocumentScreencastFrame) -> crate::page::RendererCaptureScreencastFrameReply; + fn finish_capture_document_snapshot(completed: super::CompletedCaptureDocumentSnapshot) -> super::DocumentSnapshot; + fn finish_capture_document_image(completed: super::CompletedCaptureDocumentImage) -> crate::page::RendererCaptureScreenshotReply; + fn finish_top_level_history_traversal(completed: super::CompletedTopLevelHistoryTraversal) -> bool; + fn finish_navigation_history_reset(completed: super::CompletedNavigationHistoryReset) -> bool; + fn finish_child_frame_navigation(completed: super::CompletedChildFrameNavigation) -> (bool, crate::page::RendererCommandTurnOutput); + fn finish_document_resource_text_search(completed: super::CompletedDocumentResourceTextSearch) -> crate::page::RendererResourceTextSearchOutcome; + fn finish_document_csp_bypass_update(completed: super::CompletedDocumentCspBypassUpdate) -> (); + fn finish_document_lifecycle_stop(completed: super::CompletedDocumentLifecycleStop) -> crate::page::RendererCommandTurnOutput; + fn finish_document_diagnostics_snapshot(completed: super::CompletedDocumentDiagnosticsSnapshot) -> crate::page::RendererPageDiagnosticsSnapshot; + fn start_document_child_frame_lifecycle_work(document: super::DocumentHandle, timeout: std::time::Duration) -> super::PendingChildFrameLifecycleWork; + fn finish_document_child_frame_lifecycle_work(completed: super::CompletedChildFrameLifecycleWork) -> (bool, crate::page::RendererCommandTurnOutput); + fn finish_document_storage_key_snapshot(completed: super::CompletedDocumentStorageKeySnapshot) -> String; + fn finish_child_frame_tree_snapshot(completed: super::CompletedChildFrameTreeSnapshot) -> Vec; + fn finish_document_cookie_owner_snapshot(completed: super::CompletedDocumentCookieOwnerSnapshot) -> crate::page::DocumentCookieOwnerSnapshot; + fn finish_document_blob_read(completed: super::CompletedDocumentBlobRead) -> Option>; + fn finish_network_resource_load_preparation(completed: super::CompletedNetworkResourceLoadPreparation) -> crate::page::RendererNetworkResourceLoadPreparation; + fn finish_app_manifest_load_preparation(completed: super::CompletedAppManifestLoadPreparation) -> super::BrowserAppManifestLoadPreparation; + fn finish_app_manifest_publication(completed: super::CompletedAppManifestPublication) -> crate::page::RendererCommandTurnOutput; + fn finish_document_autofill_trigger(completed: super::CompletedDocumentAutofillTrigger) -> crate::page::RendererAutofillTriggerOutcome; + fn finish_document_input_command(completed: super::CompletedDocumentInputCommand) -> crate::page::RendererCommandTurnOutput; + fn observe_document_lifetime(document: super::DocumentHandle) -> super::DocumentLifetimeObserver; + } + + pub fn document_response_headers( + &self, + document: super::DocumentHandle, + ) -> Result, String> { + self.try_read(move |context| { + context + .document_response_headers(document) + .map(<[(String, String)]>::to_vec) + }) + } + + forward_context_update! { + fn start_document_policy_batch(document: super::DocumentHandle, updates: Vec) -> super::PendingDocumentPolicyBatch; + fn start_document_runtime_policy_reconciliation(document: super::DocumentHandle, policy: super::DocumentRuntimePolicyReconciliation) -> super::PendingDocumentPolicyBatch; + } + + forward_context_try_update! { + fn start_document_policy_update(document: super::DocumentHandle, update: super::DocumentPolicyUpdate) -> super::PendingDocumentPolicyUpdate; + fn finish_document_policy_update(completed: super::CompletedDocumentPolicyUpdate) -> (); + fn finish_document_policy_batch(completed: super::CompletedDocumentPolicyBatch) -> (); + } + + pub fn start_document_policy_batch_with_surface( + &self, + document: super::DocumentHandle, + updates: Vec, + foreground: bool, + navigator_queries: moli_page_types::NavigatorQueryOverrides, + global_network_conditions: Option, + global_geolocation: Option<&super::EmulatedGeolocationOverrideState>, + ) -> super::PendingDocumentPolicyBatch { + let global_geolocation = global_geolocation.cloned(); + self.update_live(move |context| { + context.start_document_policy_batch_with_surface( + document, + updates, + foreground, + navigator_queries, + global_network_conditions, + global_geolocation.as_ref(), + ) + }) + } + + pub fn start_document_page_surface_update( + &self, + document: super::DocumentHandle, + foreground: bool, + navigator_queries: moli_page_types::NavigatorQueryOverrides, + global_network_conditions: Option, + global_geolocation: Option<&super::EmulatedGeolocationOverrideState>, + ) -> Result { + let global_geolocation = global_geolocation.cloned(); + self.try_update(move |context| { + context.start_document_page_surface_update( + document, + foreground, + navigator_queries, + global_network_conditions, + global_geolocation.as_ref(), + ) + }) + } + + pub fn page_surface_for_web_contents( + &self, + handle: WebContentsHandle, + foreground: bool, + global_network_conditions: Option, + global_geolocation: Option<&super::EmulatedGeolocationOverrideState>, + ) -> Result { + let global_geolocation = global_geolocation.cloned(); + self.try_read(move |context| { + context.page_surface_for_web_contents( + handle, + foreground, + global_network_conditions, + global_geolocation.as_ref(), + ) + }) + } + + forward_context_read! { + fn web_contents_has_navigation_engine(handle: WebContentsHandle) -> bool; + fn web_contents_navigation_layout_policy(handle: WebContentsHandle) -> Option; + fn web_contents_navigation_renderer_owner_id(handle: WebContentsHandle) -> Option; + fn web_contents_navigation_diagnostics(handle: WebContentsHandle) -> Option; + } + + forward_context_try_read! { + fn start_document_fetch_command(document: super::DocumentHandle, command: super::DocumentFetchCommand) -> super::PendingDocumentFetchCommand; + } + + forward_context_try_update! { + fn start_web_contents_fetch_interception_update(web_contents: WebContentsHandle, enabled: bool, resource_type: Option, accept_stale_completion: bool) -> Option; + fn install_web_contents_fetch_interception_policy(web_contents: WebContentsHandle, enabled: bool, resource_type: Option) -> (); + fn finish_document_fetch_command(completed: super::CompletedDocumentFetchCommand) -> super::DocumentFetchCommandOutcome; + fn finish_document_resource_runtime_update(completed: super::CompletedDocumentResourceRuntimeUpdate) -> (); + } + + pub fn web_contents_navigation_fetch_config( + &self, + handle: WebContentsHandle, + ) -> Option { + self.read_live(move |context| { + context + .web_contents_navigation_fetch_config(handle) + .cloned() + }) + } + + pub fn configure_selected_navigation_policy( + &self, + defaults: moli_fetch::FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> Result<(), String> { + let global_headers = global_headers.clone(); + self.try_update(move |context| { + context.configure_selected_navigation_policy( + defaults, + &global_headers, + global_network_conditions, + ) + }) + } + + pub fn ensure_web_contents_resource_request_client( + &self, + web_contents: WebContentsHandle, + defaults: moli_fetch::FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> Result { + let global_headers = global_headers.clone(); + self.try_update(move |context| { + context.ensure_web_contents_resource_request_client( + web_contents, + defaults, + &global_headers, + global_network_conditions, + ) + }) + } + + pub fn start_web_contents_resource_runtime_rebuild( + &self, + web_contents: WebContentsHandle, + defaults: moli_fetch::FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> Result, String> { + let global_headers = global_headers.clone(); + self.try_update(move |context| { + context.start_web_contents_resource_runtime_rebuild( + web_contents, + defaults, + &global_headers, + global_network_conditions, + ) + }) + } + + forward_context_read! { + fn selected_document_handle() -> Option; + fn selected_document_navigation_metadata() -> Option; + } + + forward_context_try_read! { + fn web_contents_tls_verify_host_override(handle: WebContentsHandle) -> Option; + fn web_contents_bypass_content_security_policy(handle: WebContentsHandle) -> bool; + fn web_contents_browser_identity_override(handle: WebContentsHandle) -> Option; + fn web_contents_network_request_policy(handle: WebContentsHandle) -> super::web_contents::NetworkRequestPolicy; + fn web_contents_locale_override(handle: WebContentsHandle) -> Option; + fn web_contents_timezone_override(handle: WebContentsHandle) -> Option; + fn web_contents_has_non_default_policy(handle: WebContentsHandle) -> bool; + fn web_contents_opener(handle: WebContentsHandle) -> Option<(super::WebContentsId, bool)>; + } + + forward_context_try_update! { + fn set_web_contents_tls_verify_host_override(handle: WebContentsHandle, enabled: Option) -> (); + fn set_web_contents_bypass_content_security_policy(handle: WebContentsHandle, bypass: bool) -> (); + fn set_web_contents_browser_identity_override(handle: WebContentsHandle, identity: Option) -> (); + fn set_web_contents_network_request_policy(handle: WebContentsHandle, policy: super::web_contents::NetworkRequestPolicy) -> (); + fn set_web_contents_locale_override(handle: WebContentsHandle, locale: Option) -> (); + fn set_web_contents_timezone_override(handle: WebContentsHandle, timezone: Option) -> (); + } + + pub fn inherited_document_policy( + &self, + fetch_config: moli_fetch::FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + global_geolocation: Option<&super::EmulatedGeolocationOverrideState>, + ) -> super::web_contents::InheritedDocumentPolicy { + let id = self.id; + let global_headers = global_headers.clone(); + let global_geolocation = global_geolocation.cloned(); + self.browser + .execute(move |browser| { + browser.context(id).map(|context| { + context.inherited_document_policy( + fetch_config, + &browser.permission_defaults, + &global_headers, + global_network_conditions, + global_geolocation.as_ref(), + ) + }) + }) + .expect("live Browser owner must accept a policy snapshot") + .expect("live BrowserContext handle must resolve in its owner") + } + + pub fn permission_snapshot(&self) -> Vec { + let id = self.id; + self.browser + .execute(move |browser| { + browser + .context(id) + .map(|context| context.permission_snapshot(&browser.permission_defaults)) + }) + .expect("live Browser owner must accept a permission snapshot") + .expect("live BrowserContext handle must resolve in its owner") + } + + pub fn set_permission_override( + &self, + registration: crate::page::PermissionOverrideRegistration, + ) -> Result<(), String> { + let id = self.id; + self.browser.execute(move |browser| { + let Browser { + contexts, + permission_defaults, + .. + } = browser; + let context = contexts + .get_mut(&id) + .ok_or_else(|| "BrowserContext unavailable".to_owned())?; + context.set_permission_override(permission_defaults, registration); + Ok(()) + })? + } + + pub fn start_permission_update( + &self, + ) -> Result, String> { + let id = self.id; + self.browser.execute(move |browser| { + browser + .context(id)? + .start_permission_update(&browser.permission_defaults) + })? + } + + forward_context_try_update! { + fn finish_permission_update(completed: super::CompletedContextPermissionUpdate) -> (); + } + + #[cfg(any(test, feature = "test-support"))] + async fn run_local_test_operation( + &self, + operation: impl FnOnce( + BrowserContext, + ) -> Pin + 'static>> + + Send + + 'static, + ) -> Result { + let id = self.id; + let completion = self + .browser + .execute(move |browser| browser.spawn_local_context_operation(id, operation))??; + completion + .await + .map_err(|_| "Browser owner stopped before completing local Context work".to_owned()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn document_runtime_heap_usage_for_test( + &self, + document: super::DocumentHandle, + ) -> Result { + self.run_local_test_operation(move |mut context| { + Box::pin(async move { + let result = context.document_runtime_heap_usage_for_test(document).await; + (context, result) + }) + }) + .await? + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn evaluate_document_expression_for_test( + &self, + document: super::DocumentHandle, + expression: &str, + await_promise: bool, + ) -> Result { + let expression = expression.to_owned(); + self.run_local_test_operation(move |mut context| { + Box::pin(async move { + let result = context + .evaluate_document_expression_for_test(document, &expression, await_promise) + .await; + (context, result) + }) + }) + .await? + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn reset_selected_resource_runtime_for_test(&self) -> bool { + self.run_local_test_operation(|mut context| { + Box::pin(async move { + let reset = context.reset_selected_resource_runtime_for_test().await; + (context, reset) + }) + }) + .await + .unwrap_or(false) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn apply_document_cookie_facade_overrides_for_test( + &self, + document: super::DocumentHandle, + overrides: Option, + ) -> Result<(), String> { + self.run_local_test_operation(move |mut context| { + Box::pin(async move { + let result = context + .apply_document_cookie_facade_overrides_for_test(document, overrides) + .await; + (context, result) + }) + }) + .await? + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn capture_document_policy_for_test( + &self, + handle: WebContentsHandle, + inherited: super::web_contents::InheritedDocumentPolicy, + final_url: &url::Url, + ) -> Result { + let final_url = final_url.clone(); + self.try_update(move |context| { + context.capture_document_policy_for_test(handle, inherited, &final_url) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn renderer_runtime_id_for_test(&self) -> crate::RendererBrowserContextRuntimeId { + self.read_live(BrowserContext::renderer_runtime_id_for_test) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_idle_override_for_test( + &self, + document: super::DocumentHandle, + ) -> Result, String> { + self.try_read(move |context| context.document_idle_override_for_test(document)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn web_contents_fetch_interception_for_test( + &self, + handle: WebContentsHandle, + ) -> Result<(bool, Option), String> { + self.try_read(move |context| context.web_contents_fetch_interception_for_test(handle)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn has_paused_navigation_auth_for_test(&self, handle: WebContentsHandle) -> bool { + self.read_live(move |context| context.has_paused_navigation_auth_for_test(handle)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn paused_navigation_response_renderer_agent_for_test( + &self, + handle: WebContentsHandle, + ) -> Option { + self.read_live(move |context| { + context + .paused_navigation_response_for_test(handle) + .and_then( + super::web_contents::PausedDocumentTransfer::prepared_renderer_agent_token, + ) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn record_navigation_history_for_test( + &self, + handle: WebContentsHandle, + snapshot: (String, String), + ) -> Result<(), String> { + self.try_update(move |context| context.record_navigation_history_for_test(handle, snapshot)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn set_web_contents_opener_for_test( + &self, + handle: WebContentsHandle, + opener: super::WebContentsId, + can_access: bool, + ) -> Result<(), String> { + self.try_update(move |context| { + context.set_web_contents_opener_for_test(handle, opener, can_access) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn pending_document_for_test( + &self, + handle: WebContentsHandle, + ) -> Result, String> { + self.try_read(move |context| context.pending_document(handle)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn install_document_lifecycle_for_test( + &self, + document: super::DocumentHandle, + lifecycle: super::DocumentLifecycle, + ) -> Result<(), String> { + self.try_update(move |context| { + context.install_document_lifecycle_for_test(document, lifecycle) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn apply_document_lifecycle_for_test( + &self, + document: super::DocumentHandle, + event: crate::page::RendererDocumentLifecycleEvent, + ) -> Result { + self.try_update(move |context| context.apply_document_lifecycle_for_test(document, event)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn replace_document_identity_for_test( + &self, + document: super::DocumentHandle, + replacement: super::DocumentId, + ) -> Result<(), String> { + self.try_update(move |context| { + context.replace_document_identity_for_test(document, replacement) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn clear_web_contents_javascript_dialogs_for_test( + &self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.try_update(move |context| { + context.clear_web_contents_javascript_dialogs_for_test(handle) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn mark_initial_empty_document_materialized_for_test( + &self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.try_update(move |context| { + context.mark_initial_empty_document_materialized_for_test(handle) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn mark_initial_empty_document_exited_for_test( + &self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.try_update(move |context| context.mark_initial_empty_document_exited_for_test(handle)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn accepts_any_document_body_completion_for_test( + &self, + navigation: &super::NavigationId, + ) -> bool { + let navigation = *navigation; + self.read_live(move |context| { + context.accepts_any_document_body_completion_for_test(&navigation) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn service_worker_force_update_on_page_load_for_test(&self) -> bool { + self.read_live(BrowserContext::service_worker_force_update_on_page_load_for_test) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn service_worker_pause_on_start_for_test(&self) -> bool { + self.read_live(BrowserContext::service_worker_pause_on_start_for_test) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn resource_storage_handles_for_test(&self) -> super::BrowserContextResourceStorageHandles { + self.read_live(BrowserContext::resource_storage_handles_for_test) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn http_cache_configuration_for_test(&self) -> (Option, Option) { + self.read_live(|context| { + let (path, max_bytes) = context.http_cache_configuration_for_test(); + (path.map(std::path::Path::to_path_buf), max_bytes) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn cookie_store_for_test(&self) -> moli_cookie_jar::SharedBrowserCookieStore { + self.read_live(|context| context.cookie_store_for_test().clone()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn web_storage_store_for_test(&self) -> crate::network::SharedWebStorageStore { + self.read_live(|context| context.web_storage_store_for_test().clone()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn selected_session_storage_store_for_test( + &self, + ) -> Option { + self.read_live(|context| context.selected_session_storage_store_for_test().cloned()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn indexed_db_manager_for_test(&self) -> crate::storage::SharedIndexedDbManager { + self.read_live(|context| context.indexed_db_manager_for_test().clone()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn storage_bucket_store_for_test(&self) -> crate::storage::SharedStorageBucketStore { + self.read_live(|context| context.storage_bucket_store_for_test().clone()) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn replace_storage_bucket_store_for_test( + &self, + storage: crate::storage::SharedStorageBucketStore, + ) { + self.update_live(move |context| context.replace_storage_bucket_store_for_test(storage)); + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn set_http_proxy_override_for_test(&self, proxy: Option) { + self.update_live(move |context| context.set_http_proxy_override_for_test(proxy)); + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_renderer_devtools_agent_token_for_test( + &self, + document: super::DocumentHandle, + ) -> Option { + self.read_live(move |context| { + context + .page_for_test(document) + .map(crate::page::Page::renderer_devtools_agent_token) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_response_status_for_test( + &self, + document: super::DocumentHandle, + ) -> Result { + self.try_read(move |context| context.document_response_status_for_test(document)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_script_execution_for_test( + &self, + document: super::DocumentHandle, + ) -> Result { + self.try_read(move |context| context.document_script_execution_for_test(document)) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_renderer_inspection_endpoint_for_test( + &self, + document: super::DocumentHandle, + ) -> Result { + self.try_read(move |context| { + context.document_renderer_inspection_endpoint_for_test(document) + }) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn web_contents_navigation_browser_context_runtime_id_for_test( + &self, + handle: WebContentsHandle, + ) -> Option { + self.read_live(move |context| { + context.web_contents_navigation_browser_context_runtime_id_for_test(handle) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn context_capability_does_not_own_the_physical_context() { + let service = BrowserService::start().expect("Browser owner should start"); + let handle = service + .handle() + .create_context( + BrowserContextStoragePartitionHandles::memory(), + StoragePartitionKind::Ephemeral, + None, + None, + ) + .expect("context creation should succeed"); + assert!(handle.is_live()); + assert!(handle.remove().expect("context removal should succeed")); + assert!(!handle.is_live()); + service.shutdown(); + } +} diff --git a/moli-core/src/browser/owner/navigation.rs b/moli-core/src/browser/owner/navigation.rs new file mode 100644 index 0000000000..043348d268 --- /dev/null +++ b/moli-core/src/browser/owner/navigation.rs @@ -0,0 +1,1278 @@ +use anyhow::Context as _; +use std::collections::HashMap; + +use tokio::sync::oneshot; +use url::Url; + +use crate::{ + browser::{ + BrowserContextId, DocumentId, NavigationId, NavigationRequestLoadPolicy, + RendererPageResidenceIdentity, WebContentsHandle, WebContentsId, + web_contents::{ + AdmittedDocumentMaterialization as PhysicalDocumentMaterialization, + AdmittedInitialDocumentBuild as PhysicalInitialDocumentBuild, + AdmittedNavigationLoad as PhysicalNavigationLoad, + BuiltInitialDocument as PhysicalBuiltInitialDocument, ClaimedNavigationRequest, + CommittedDocumentInfo, CommittedInitialDocument as PhysicalCommittedInitialDocument, + DocumentNavigationDestination, InheritedDocumentPolicy, + InitialDocumentAdmission as PhysicalInitialDocumentAdmission, InitialDocumentBuildKey, + InitialDocumentPageBuildWaiter, + InterceptedNavigationLoad as PhysicalInterceptedNavigationLoad, + NavigationInterceptionPermit, + PreparedDocumentNavigation as PhysicalPreparedDocumentNavigation, + PreparedNavigationResponse as PhysicalPreparedNavigationResponse, + }, + }, + page::SubresourceAuthCredentials, + runtime::{ + BuiltDocumentPage, CommittedDocumentResourceSource, ExternalRawDocumentBodyStream, + NavigationStreamingRawResponse, PageVmInitStage, RendererReplyBoundary, + RendererReservedServiceWorkerClient, + }, +}; +use moli_fetch::{ + NetworkFetchResult, NetworkObservationJournal, RawResponse, StreamingRawResponse, +}; + +use super::{BrowserContextHandle, BrowserHandle, BrowserOwnerMessage, PendingDocumentRetirement}; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct NavigationWorkId(u64); + +struct ContextWork { + context: BrowserContextId, + value: T, +} + +#[derive(Default)] +pub(super) struct NavigationWorkRegistry { + next_id: u64, + loads: HashMap>, + prepared_responses: HashMap>, + materializations: HashMap>, + prepared_documents: HashMap>, + initial_builds: HashMap>, + built_initial_documents: HashMap>, +} + +impl NavigationWorkRegistry { + fn allocate(&mut self) -> NavigationWorkId { + self.next_id = self + .next_id + .checked_add(1) + .expect("Browser navigation work identity exhausted"); + NavigationWorkId(self.next_id) + } + + fn insert_load( + &mut self, + context: BrowserContextId, + load: PhysicalNavigationLoad, + ) -> NavigationWorkId { + let id = self.allocate(); + self.loads.insert( + id, + ContextWork { + context, + value: load, + }, + ); + id + } + + fn remove(&mut self, id: NavigationWorkId) { + self.loads.remove(&id); + self.prepared_responses.remove(&id); + self.materializations.remove(&id); + self.prepared_documents.remove(&id); + self.initial_builds.remove(&id); + if let Some(built) = self.built_initial_documents.remove(&id) { + tokio::task::spawn_local(built.value.retire()); + } + } + + pub(super) fn remove_context(&mut self, context: BrowserContextId) { + self.loads.retain(|_, work| work.context != context); + self.prepared_responses + .retain(|_, work| work.context != context); + self.materializations + .retain(|_, work| work.context != context); + self.prepared_documents + .retain(|_, work| work.context != context); + self.initial_builds + .retain(|_, work| work.context != context); + let built = self + .built_initial_documents + .iter() + .filter_map(|(id, work)| (work.context == context).then_some(*id)) + .collect::>(); + for id in built { + if let Some(work) = self.built_initial_documents.remove(&id) { + tokio::task::spawn_local(work.value.retire()); + } + } + } + + pub(super) fn clear(&mut self) { + self.loads.clear(); + self.prepared_responses.clear(); + self.materializations.clear(); + self.prepared_documents.clear(); + self.initial_builds.clear(); + for (_, built) in self.built_initial_documents.drain() { + tokio::task::spawn_local(built.value.retire()); + } + } +} + +fn unavailable() -> String { + "Browser navigation operation is unavailable".to_owned() +} + +fn receive_error() -> anyhow::Error { + anyhow::anyhow!("Browser owner stopped before completing navigation work") +} + +impl BrowserHandle { + fn discard_navigation_work(&self, id: NavigationWorkId) { + let _ = self + .endpoint + .tx + .send(BrowserOwnerMessage::Execute(Box::new(move |browser| { + browser.navigation_work.remove(id) + }))); + } +} + +/// Move-only capability for one Browser-owned navigation load. +pub struct BrowserNavigationLoad { + context: BrowserContextHandle, + work: Option, + web_contents: WebContentsId, + navigation: NavigationId, + document: DocumentId, + renderer: RendererPageResidenceIdentity, +} + +impl BrowserNavigationLoad { + fn new( + context: BrowserContextHandle, + work: NavigationWorkId, + load: &PhysicalNavigationLoad, + ) -> Self { + Self { + context, + work: Some(work), + web_contents: load.web_contents_id(), + navigation: load.navigation_id(), + document: load.document_id(), + renderer: load.renderer_page(), + } + } + + fn work(&self) -> Result { + self.work.ok_or_else(unavailable) + } + + fn take_work(&mut self) -> Result { + self.work.take().ok_or_else(unavailable) + } + + pub fn web_contents_id(&self) -> WebContentsId { + self.web_contents + } + + pub fn navigation_id(&self) -> NavigationId { + self.navigation + } + + pub fn document_id(&self) -> DocumentId { + self.document + } + + pub fn renderer_page(&self) -> RendererPageResidenceIdentity { + self.renderer + } + + pub fn set_redirect_state( + &mut self, + headers: Option, + chain: Vec, + ) { + let id = self.work().expect("live navigation work"); + self.context + .browser + .execute(move |browser| { + let load = browser + .navigation_work + .loads + .get_mut(&id) + .expect("live navigation work"); + load.value.set_redirect_state(headers, chain); + }) + .expect("live Browser owner"); + } + + pub async fn fetch_navigation( + &mut self, + method: &str, + raw_url: &str, + body: Option>, + request_headers: moli_fetch::RequestHeaders, + ) -> anyhow::Result { + let id = self.work().map_err(anyhow::Error::msg)?; + let method = method.to_owned(); + let raw_url = raw_url.to_owned(); + let completion = self + .context + .browser + .execute(move |browser| { + let work = browser + .navigation_work + .loads + .remove(&id) + .ok_or_else(unavailable)?; + let local_sender = browser.local_sender.clone(); + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + let ContextWork { context, mut value } = work; + let result = value + .fetch_navigation(&method, &raw_url, body, request_headers) + .await; + let _ = local_sender.send(Box::new(move |browser| { + browser + .navigation_work + .loads + .insert(id, ContextWork { context, value }); + let _ = completion_tx.send(result); + })); + }); + Ok::<_, String>(completion) + }) + .map_err(anyhow::Error::msg)? + .map_err(anyhow::Error::msg)?; + completion.await.map_err(|_| receive_error())? + } + + async fn fetch_intercepted_response( + &mut self, + method: String, + raw_url: String, + body: Option>, + headers: moli_fetch::RequestHeaders, + auth: Option, + ) -> anyhow::Result> { + let id = self.work().map_err(anyhow::Error::msg)?; + let completion = self + .context + .browser + .execute(move |browser| { + let work = browser + .navigation_work + .loads + .remove(&id) + .ok_or_else(unavailable)?; + let local_sender = browser.local_sender.clone(); + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + let ContextWork { context, value } = work; + let result = value + .fetch_intercepted_response(&method, &raw_url, body, headers, auth) + .await; + let _ = local_sender.send(Box::new(move |browser| { + browser + .navigation_work + .loads + .insert(id, ContextWork { context, value }); + let _ = completion_tx.send(result); + })); + }); + Ok::<_, String>(completion) + }) + .map_err(anyhow::Error::msg)? + .map_err(anyhow::Error::msg)?; + completion.await.map_err(|_| receive_error())? + } + + async fn fetch_intercepted_auth_response( + &mut self, + method: String, + raw_url: String, + body: Option>, + headers: moli_fetch::RequestHeaders, + auth: SubresourceAuthCredentials, + ) -> anyhow::Result> { + let id = self.work().map_err(anyhow::Error::msg)?; + let completion = self + .context + .browser + .execute(move |browser| { + let work = browser + .navigation_work + .loads + .remove(&id) + .ok_or_else(unavailable)?; + let local_sender = browser.local_sender.clone(); + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + let ContextWork { context, value } = work; + let result = value + .fetch_intercepted_auth_response(&method, &raw_url, body, headers, auth) + .await; + let _ = local_sender.send(Box::new(move |browser| { + browser + .navigation_work + .loads + .insert(id, ContextWork { context, value }); + let _ = completion_tx.send(result); + })); + }); + Ok::<_, String>(completion) + }) + .map_err(anyhow::Error::msg)? + .map_err(anyhow::Error::msg)?; + completion.await.map_err(|_| receive_error())? + } + + #[allow(clippy::too_many_arguments)] + pub async fn prepare_document_response_async( + &mut self, + requested_url: Url, + final_url: Url, + redirected: bool, + redirect_count: usize, + response_status: u16, + response_headers: Vec<(String, String)>, + raw_body: ExternalRawDocumentBodyStream, + stage: PageVmInitStage, + reply_boundary: RendererReplyBoundary, + resource_source: CommittedDocumentResourceSource, + reserved_service_worker_client: Option, + ) -> anyhow::Result { + let id = self.take_work().map_err(anyhow::Error::msg)?; + let context_handle = self.context.clone(); + let completion = self + .context + .browser + .execute(move |browser| { + let work = browser + .navigation_work + .loads + .remove(&id) + .ok_or_else(unavailable)?; + let local_sender = browser.local_sender.clone(); + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + let ContextWork { context, mut value } = work; + let result = value + .prepare_document_response_async( + requested_url, + final_url, + redirected, + redirect_count, + response_status, + response_headers, + raw_body, + stage, + reply_boundary, + resource_source, + reserved_service_worker_client, + ) + .await; + let _ = local_sender.send(Box::new(move |browser| { + let result = match result { + Ok(prepared) => { + let renderer_token = prepared.renderer_devtools_agent_token(); + let inspection = prepared.inspection_configuration_endpoint(); + browser.navigation_work.prepared_responses.insert( + id, + ContextWork { + context, + value: prepared, + }, + ); + Ok(BrowserPreparedNavigationResponse { + context: context_handle, + work: Some(id), + renderer_token, + inspection, + }) + } + Err(error) => Err(error.to_string()), + }; + let _ = completion_tx.send(result); + })); + }); + Ok::<_, String>(completion) + }) + .map_err(anyhow::Error::msg)? + .map_err(anyhow::Error::msg)?; + completion + .await + .map_err(|_| receive_error())? + .map_err(anyhow::Error::msg) + } +} + +impl Drop for BrowserNavigationLoad { + fn drop(&mut self) { + if let Some(work) = self.work.take() { + self.context.browser.discard_navigation_work(work); + } + } +} + +/// Browser-owned intercepted request whose renderer reservation stays on the +/// Browser owner sequence while network transport is awaited. +pub struct BrowserInterceptedNavigationLoad { + pub load: BrowserNavigationLoad, + pub requested_url: Url, + pub method: String, + body: Option>, + pub headers: moli_fetch::RequestHeaders, + prior_observations: NetworkObservationJournal, +} + +impl std::fmt::Debug for BrowserInterceptedNavigationLoad { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("BrowserInterceptedNavigationLoad") + .field("renderer", &self.load.renderer_page()) + .field("requested_url", &self.requested_url) + .finish_non_exhaustive() + } +} + +impl BrowserInterceptedNavigationLoad { + pub fn new( + load: BrowserNavigationLoad, + requested_url: Url, + method: String, + body: Option>, + headers: moli_fetch::RequestHeaders, + ) -> Self { + Self { + load, + requested_url, + method, + body, + headers, + prior_observations: NetworkObservationJournal::default(), + } + } + + pub async fn fetch_streaming( + mut self, + auth: Option, + ) -> anyhow::Result> { + let response = self + .load + .fetch_intercepted_response( + self.method.clone(), + self.requested_url.to_string(), + self.body.clone(), + self.headers.clone(), + auth, + ) + .await + .with_context(|| format!("failed to fetch page `{}`", self.requested_url))?; + Ok(self.with_response(response)) + } + + pub async fn fetch_auth( + mut self, + auth: SubresourceAuthCredentials, + ) -> anyhow::Result> { + let response = self + .load + .fetch_intercepted_auth_response( + self.method.clone(), + self.requested_url.to_string(), + self.body.clone(), + self.headers.clone(), + auth, + ) + .await + .with_context(|| format!("failed to fetch page `{}`", self.requested_url))?; + Ok(self.with_response(response)) + } + + pub fn with_response( + mut self, + response: NetworkFetchResult, + ) -> BrowserInterceptedNavigationResponse { + let (response, observations) = response.into_parts_with_observation_journal(); + let mut prior = std::mem::take(&mut self.prior_observations); + prior.append(observations); + BrowserInterceptedNavigationResponse { + work: self, + response: NetworkFetchResult::with_observation_journal(response, prior), + } + } + + pub fn into_request_parts( + self, + ) -> ( + BrowserNavigationLoad, + Url, + String, + Option>, + moli_fetch::RequestHeaders, + ) { + ( + self.load, + self.requested_url, + self.method, + self.body, + self.headers, + ) + } +} + +#[derive(Debug)] +pub struct BrowserInterceptedNavigationResponse { + work: BrowserInterceptedNavigationLoad, + response: NetworkFetchResult, +} + +impl BrowserInterceptedNavigationResponse { + pub fn web_contents(&self) -> WebContentsId { + self.work.load.web_contents_id() + } + + pub fn response(&self) -> &R { + self.response.response() + } + + pub fn observation_journal(&self) -> &NetworkObservationJournal { + self.response.observation_journal() + } + + pub fn into_parts(self) -> (BrowserInterceptedNavigationLoad, NetworkFetchResult) { + (self.work, self.response) + } +} + +impl BrowserInterceptedNavigationResponse { + pub async fn materialize( + self, + ) -> anyhow::Result> { + let (response, observations) = self.response.into_parts_with_observation_journal(); + let response = response + .into_materialized_raw_response() + .await + .context("failed to read page body from stream")?; + Ok(BrowserInterceptedNavigationResponse { + work: self.work, + response: NetworkFetchResult::with_observation_journal(response, observations), + }) + } +} + +impl BrowserInterceptedNavigationResponse { + pub fn retry(self) -> BrowserInterceptedNavigationLoad { + let (mut work, response) = self.into_parts(); + let (response, observations) = response.into_parts_with_observation_journal(); + let mut request = moli_fetch::Request::get_with_url(work.requested_url); + request.method = work.method; + request.body = work.body; + request.request_headers = work.headers; + let id = work.load.work().expect("live navigation work"); + let request = work + .load + .context + .browser + .execute(move |browser| { + browser + .navigation_work + .loads + .get_mut(&id) + .expect("live navigation work") + .value + .redirected_request(response, request) + }) + .expect("live Browser owner"); + work.requested_url = request.url; + work.method = request.method; + work.body = request.body; + work.headers = request.request_headers; + work.prior_observations = observations; + work + } +} + +pub enum BrowserInitialDocumentAdmission { + Present, + Join(InitialDocumentPageBuildWaiter), + Build(Box), +} + +pub struct BrowserInitialDocumentBuild { + context: BrowserContextHandle, + work: Option, + key: InitialDocumentBuildKey, + inspection: moli_renderer_v8::RendererPreparedDocumentInspectionEndpoint, +} + +impl BrowserInitialDocumentBuild { + pub fn key(&self) -> InitialDocumentBuildKey { + self.key + } + + pub fn inspection_endpoint( + &self, + ) -> moli_renderer_v8::RendererPreparedDocumentInspectionEndpoint { + self.inspection.clone() + } + + pub fn start_preparation(&mut self) -> anyhow::Result<()> { + let work = self.work.ok_or_else(|| anyhow::anyhow!(unavailable()))?; + self.context + .browser + .execute(move |browser| { + browser + .navigation_work + .initial_builds + .get_mut(&work) + .ok_or_else(unavailable)? + .value + .start_preparation() + .map_err(|error| error.to_string()) + }) + .map_err(anyhow::Error::msg)? + .map_err(anyhow::Error::msg) + } + + pub async fn materialize(mut self) -> anyhow::Result { + let id = self + .work + .take() + .ok_or_else(|| anyhow::anyhow!(unavailable()))?; + let context_handle = self.context.clone(); + let completion = self + .context + .browser + .execute(move |browser| { + let work = browser + .navigation_work + .initial_builds + .remove(&id) + .ok_or_else(unavailable)?; + let local_sender = browser.local_sender.clone(); + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + let ContextWork { context, value } = work; + let result = value.materialize().await; + let _ = local_sender.send(Box::new(move |browser| { + let result = match result { + Ok(built) => { + let key = built.key(); + browser.navigation_work.built_initial_documents.insert( + id, + ContextWork { + context, + value: built, + }, + ); + Ok(BrowserBuiltInitialDocument { + context: context_handle, + work: Some(id), + key, + }) + } + Err(error) => Err(error.to_string()), + }; + let _ = completion_tx.send(result); + })); + }); + Ok::<_, String>(completion) + }) + .map_err(anyhow::Error::msg)? + .map_err(anyhow::Error::msg)?; + completion + .await + .map_err(|_| receive_error())? + .map_err(anyhow::Error::msg) + } +} + +impl Drop for BrowserInitialDocumentBuild { + fn drop(&mut self) { + if let Some(work) = self.work.take() { + self.context.browser.discard_navigation_work(work); + } + } +} + +pub struct BrowserBuiltInitialDocument { + context: BrowserContextHandle, + work: Option, + key: InitialDocumentBuildKey, +} + +impl BrowserBuiltInitialDocument { + pub fn key(&self) -> InitialDocumentBuildKey { + self.key + } + + pub async fn retire(mut self) { + let Some(work) = self.work.take() else { + return; + }; + let completion = self.context.browser.execute(move |browser| { + let built = browser + .navigation_work + .built_initial_documents + .remove(&work) + .map(|work| work.value); + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + if let Some(built) = built { + built.retire().await; + } + let _ = completion_tx.send(()); + }); + completion + }); + if let Ok(completion) = completion { + let _ = completion.await; + } + } +} + +impl Drop for BrowserBuiltInitialDocument { + fn drop(&mut self) { + if let Some(work) = self.work.take() { + self.context.browser.discard_navigation_work(work); + } + } +} + +pub struct BrowserCommittedInitialDocument { + pub key: InitialDocumentBuildKey, + pub lifecycle: crate::browser::web_contents::CommittedDocumentLifecycle, + pub diagnostics: crate::page::RendererPageCreationDiagnostics, + pub inspection_endpoint: moli_renderer_v8::RendererInspectionEndpoint, +} + +/// Exact prepared renderer reservation retained by the Browser owner. +pub struct BrowserPreparedNavigationResponse { + context: BrowserContextHandle, + work: Option, + renderer_token: crate::page::RendererDevToolsAgentToken, + inspection: moli_renderer_v8::RendererPreparedDocumentInspectionEndpoint, +} + +impl BrowserPreparedNavigationResponse { + pub fn renderer_devtools_agent_token(&self) -> crate::page::RendererDevToolsAgentToken { + self.renderer_token + } + + pub fn inspection_configuration_endpoint( + &self, + ) -> moli_renderer_v8::RendererPreparedDocumentInspectionEndpoint { + self.inspection.clone() + } + + fn take_work(&mut self) -> Result { + self.work.take().ok_or_else(unavailable) + } +} + +impl Drop for BrowserPreparedNavigationResponse { + fn drop(&mut self) { + if let Some(work) = self.work.take() { + self.context.browser.discard_navigation_work(work); + } + } +} + +/// Materialization admitted against an exact prepared navigation response. +pub struct BrowserDocumentMaterialization { + context: BrowserContextHandle, + work: Option, +} + +impl BrowserDocumentMaterialization { + pub async fn materialize( + mut self, + ) -> anyhow::Result> { + let id = self + .work + .take() + .ok_or_else(|| anyhow::anyhow!(unavailable()))?; + let context_handle = self.context.clone(); + let completion = self + .context + .browser + .execute(move |browser| { + let work = browser + .navigation_work + .materializations + .remove(&id) + .ok_or_else(unavailable)?; + let local_sender = browser.local_sender.clone(); + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + let ContextWork { context, value } = work; + let result = value.materialize().await; + let _ = local_sender.send(Box::new(move |browser| { + let result = match result { + Ok(BuiltDocumentPage { + page, + page_creation_diagnostics, + page_creation_artifacts, + pending_download, + }) => { + let navigation = page.navigation(); + let web_contents = page.web_contents_id(); + let renderer = page.renderer_residence(); + browser.navigation_work.prepared_documents.insert( + id, + ContextWork { + context, + value: page, + }, + ); + Ok(BuiltDocumentPage { + page: BrowserPreparedDocumentNavigation { + context: context_handle, + work: Some(id), + navigation, + web_contents, + renderer, + }, + page_creation_diagnostics, + page_creation_artifacts, + pending_download, + }) + } + Err(error) => Err(error.to_string()), + }; + let _ = completion_tx.send(result); + })); + }); + Ok::<_, String>(completion) + }) + .map_err(anyhow::Error::msg)? + .map_err(anyhow::Error::msg)?; + completion + .await + .map_err(|_| receive_error())? + .map_err(anyhow::Error::msg) + } +} + +impl Drop for BrowserDocumentMaterialization { + fn drop(&mut self) { + if let Some(work) = self.work.take() { + self.context.browser.discard_navigation_work(work); + } + } +} + +/// Move-only capability for a materialized navigation awaiting Browser commit. +pub struct BrowserPreparedDocumentNavigation { + context: BrowserContextHandle, + work: Option, + navigation: NavigationId, + web_contents: WebContentsId, + renderer: RendererPageResidenceIdentity, +} + +impl BrowserPreparedDocumentNavigation { + pub fn navigation(&self) -> NavigationId { + self.navigation + } + + pub fn web_contents_id(&self) -> WebContentsId { + self.web_contents + } + + pub fn renderer_residence(&self) -> RendererPageResidenceIdentity { + self.renderer + } + + fn take_work(&mut self) -> Result { + self.work.take().ok_or_else(unavailable) + } +} + +impl Drop for BrowserPreparedDocumentNavigation { + fn drop(&mut self) { + if let Some(work) = self.work.take() { + self.context.browser.discard_navigation_work(work); + } + } +} + +/// Sendable outcome of a completed Browser document navigation. +pub struct BrowserDocumentNavigationCommit { + pub web_contents: WebContentsId, + pub frame_slot: crate::browser::MainFrameSlotId, + pub navigation: NavigationId, + pub document: DocumentId, + pub previous_document: Option, + pub previous_renderer: Option, + pub inspection_endpoint: moli_renderer_v8::RendererInspectionEndpoint, + pub lifecycle: crate::browser::web_contents::CommittedDocumentLifecycle, + pub info: CommittedDocumentInfo, + pub retirement: PendingDocumentRetirement, + pub post_response_continuation: + Option, +} + +impl BrowserContextHandle { + pub fn start_initial_document( + &self, + handle: WebContentsHandle, + inherited: InheritedDocumentPolicy, + ) -> Result { + let context_handle = self.clone(); + self.browser.execute(move |browser| { + match browser + .context_mut(context_handle.id)? + .start_initial_document(handle, inherited)? + { + PhysicalInitialDocumentAdmission::Present => { + Ok(BrowserInitialDocumentAdmission::Present) + } + PhysicalInitialDocumentAdmission::Join(waiter) => { + Ok(BrowserInitialDocumentAdmission::Join(waiter)) + } + PhysicalInitialDocumentAdmission::Build(build) => { + let key = build.key(); + let inspection = build.inspection_endpoint(); + let work = browser.navigation_work.allocate(); + browser.navigation_work.initial_builds.insert( + work, + ContextWork { + context: context_handle.id, + value: *build, + }, + ); + Ok(BrowserInitialDocumentAdmission::Build(Box::new( + BrowserInitialDocumentBuild { + context: context_handle, + work: Some(work), + key, + inspection, + }, + ))) + } + } + })? + } + + pub fn commit_initial_document( + &self, + mut built: BrowserBuiltInitialDocument, + ) -> Result> { + if built.context.id != self.id { + return Err(Box::new(built)); + } + let Some(work) = built.work.take() else { + return Err(Box::new(built)); + }; + let key = built.key; + let context_handle = self.clone(); + let fallback_context = self.clone(); + match self.browser.execute(move |browser| { + let Some(stored) = browser + .navigation_work + .built_initial_documents + .remove(&work) + else { + return Err(Box::new(BrowserBuiltInitialDocument { + context: context_handle, + work: None, + key, + })); + }; + if stored.context != context_handle.id { + browser + .navigation_work + .built_initial_documents + .insert(work, stored); + return Err(Box::new(BrowserBuiltInitialDocument { + context: context_handle, + work: Some(work), + key, + })); + } + let committed = match browser.context_mut(context_handle.id) { + Ok(context) => context.commit_initial_document(stored.value), + Err(_) => Err(Box::new(stored.value)), + }; + match committed { + Ok(PhysicalCommittedInitialDocument { + key, + lifecycle, + diagnostics, + inspection_endpoint, + }) => Ok(BrowserCommittedInitialDocument { + key, + lifecycle, + diagnostics, + inspection_endpoint, + }), + Err(stale) => { + browser.navigation_work.built_initial_documents.insert( + work, + ContextWork { + context: context_handle.id, + value: *stale, + }, + ); + Err(Box::new(BrowserBuiltInitialDocument { + context: context_handle, + work: Some(work), + key, + })) + } + } + }) { + Ok(result) => result, + Err(_) => Err(Box::new(BrowserBuiltInitialDocument { + context: fallback_context, + work: Some(work), + key, + })), + } + } + + pub fn start_navigation_load( + &self, + handle: WebContentsHandle, + navigation: NavigationId, + policy: NavigationRequestLoadPolicy, + inherited: InheritedDocumentPolicy, + ) -> Result { + let context_handle = self.clone(); + self.browser.execute(move |browser| { + let load = browser + .context_mut(context_handle.id)? + .start_navigation_load(handle, navigation, policy, inherited)?; + let work = browser.navigation_work.insert_load(context_handle.id, load); + let load = &browser + .navigation_work + .loads + .get(&work) + .expect("inserted navigation load") + .value; + Ok(BrowserNavigationLoad::new(context_handle, work, load)) + })? + } + + pub fn start_claimed_navigation_request( + &self, + request: ClaimedNavigationRequest, + inherited: InheritedDocumentPolicy, + ) -> Result { + let context_handle = self.clone(); + self.browser.execute(move |browser| { + let work = browser + .context_mut(context_handle.id)? + .start_claimed_navigation_request(request, inherited)?; + let (load, requested_url, method, body, headers) = work.into_owner_parts(); + let id = browser.navigation_work.insert_load(context_handle.id, load); + let stored = &browser + .navigation_work + .loads + .get(&id) + .expect("inserted intercepted navigation load") + .value; + Ok(BrowserInterceptedNavigationLoad::new( + BrowserNavigationLoad::new(context_handle, id, stored), + requested_url, + method, + body, + headers, + )) + })? + } + + pub fn start_navigation_load_for_interception( + &self, + permit: NavigationInterceptionPermit, + policy: NavigationRequestLoadPolicy, + inherited: InheritedDocumentPolicy, + ) -> Result { + let context_handle = self.clone(); + self.browser.execute(move |browser| { + let load = browser + .context_mut(context_handle.id)? + .start_navigation_load_for_interception(permit, policy, inherited)?; + let id = browser.navigation_work.insert_load(context_handle.id, load); + let stored = &browser + .navigation_work + .loads + .get(&id) + .expect("inserted intercepted navigation load") + .value; + Ok(BrowserNavigationLoad::new(context_handle, id, stored)) + })? + } + + pub fn pause_navigation_auth( + &self, + response: BrowserInterceptedNavigationResponse, + ) -> Result { + let (work, response) = response.into_parts(); + let BrowserInterceptedNavigationLoad { + mut load, + requested_url, + method, + body, + headers, + prior_observations: _, + } = work; + if load.context.id != self.id { + return Err("navigation auth belongs to another BrowserContext".to_owned()); + } + let id = load.take_work()?; + let context = self.id; + self.browser.execute(move |browser| { + let load = browser + .navigation_work + .loads + .remove(&id) + .ok_or_else(unavailable)?; + if load.context != context { + return Err("navigation auth belongs to another BrowserContext".to_owned()); + } + let work = PhysicalInterceptedNavigationLoad::new( + load.value, + requested_url, + method, + body, + headers, + ); + browser + .context_mut(context)? + .pause_navigation_auth(work.with_response(response)) + })? + } + + pub fn take_navigation_auth( + &self, + permit: NavigationInterceptionPermit, + ) -> Option> { + let context_handle = self.clone(); + self.browser + .execute(move |browser| { + let response = browser + .context_mut(context_handle.id) + .ok()? + .take_navigation_auth(permit)?; + let (work, response) = response.into_parts(); + let (load, requested_url, method, body, headers) = work.into_owner_parts(); + let id = browser.navigation_work.insert_load(context_handle.id, load); + let stored = &browser + .navigation_work + .loads + .get(&id) + .expect("restored intercepted navigation load") + .value; + Some(BrowserInterceptedNavigationResponse { + work: BrowserInterceptedNavigationLoad::new( + BrowserNavigationLoad::new(context_handle, id, stored), + requested_url, + method, + body, + headers, + ), + response, + }) + }) + .ok() + .flatten() + } + + pub fn start_document_materialization( + &self, + handle: WebContentsHandle, + navigation: NavigationId, + mut page: BrowserPreparedNavigationResponse, + destination: DocumentNavigationDestination, + inherited: InheritedDocumentPolicy, + ) -> Result { + if page.context.id != self.id { + return Err("navigation response belongs to another BrowserContext".to_owned()); + } + let work = page.take_work()?; + let context_handle = self.clone(); + self.browser.execute(move |browser| { + let prepared = browser + .navigation_work + .prepared_responses + .remove(&work) + .ok_or_else(unavailable)?; + if prepared.context != context_handle.id { + return Err("navigation response belongs to another BrowserContext".to_owned()); + } + let materialization = browser + .context_mut(context_handle.id)? + .start_document_materialization( + handle, + navigation, + prepared.value, + destination, + inherited, + )?; + browser.navigation_work.materializations.insert( + work, + ContextWork { + context: context_handle.id, + value: materialization, + }, + ); + Ok(BrowserDocumentMaterialization { + context: context_handle, + work: Some(work), + }) + })? + } + + pub fn commit_document_navigation( + &self, + mut prepared: BrowserPreparedDocumentNavigation, + ) -> Result { + if prepared.context.id != self.id { + return Err("navigation document belongs to another BrowserContext".to_owned()); + } + let work = prepared.take_work()?; + let context = self.id; + self.browser.execute(move |browser| { + let prepared = browser + .navigation_work + .prepared_documents + .remove(&work) + .ok_or_else(unavailable)?; + if prepared.context != context { + return Err("navigation document belongs to another BrowserContext".to_owned()); + } + let commit = browser + .context_mut(context)? + .commit_document_navigation(prepared.value)?; + let (completion_tx, completion) = oneshot::channel(); + tokio::task::spawn_local(async move { + commit.retirement.close().await; + let _ = completion_tx.send(()); + }); + Ok(BrowserDocumentNavigationCommit { + web_contents: commit.web_contents, + frame_slot: commit.frame_slot, + navigation: commit.navigation, + document: commit.document, + previous_document: commit.previous_document, + previous_renderer: commit.previous_renderer, + inspection_endpoint: commit.inspection_endpoint, + lifecycle: commit.lifecycle, + info: commit.info, + retirement: PendingDocumentRetirement { completion }, + post_response_continuation: commit.post_response_continuation, + }) + })? + } +} diff --git a/moli-core/src/browser/service_workers.rs b/moli-core/src/browser/service_workers.rs new file mode 100644 index 0000000000..14bf8dc3e7 --- /dev/null +++ b/moli-core/src/browser/service_workers.rs @@ -0,0 +1,39 @@ +use url::Url; + +/// BrowserContext-scoped ServiceWorker control with no frontend attribution. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ServiceWorkerCommand { + SetForceUpdateOnPageLoad(bool), + Unregister { + scope: Url, + }, + Start { + scope: Url, + }, + StopVersion { + version_id: u64, + }, + StopAll, + SkipWaiting { + scope: Url, + }, + UpdateRegistration { + scope: Url, + }, + DeliverPushMessage { + origin: Url, + registration_id: u64, + data: Option>, + }, + DispatchSyncEvent { + origin: Url, + registration_id: u64, + tag: String, + last_chance: bool, + }, + DispatchPeriodicSyncEvent { + origin: Url, + registration_id: u64, + tag: String, + }, +} diff --git a/moli-protocol/src/conn/state/web_contents.rs b/moli-core/src/browser/web_contents.rs similarity index 63% rename from moli-protocol/src/conn/state/web_contents.rs rename to moli-core/src/browser/web_contents.rs index 9c7a4554b8..c5870f6987 100644 --- a/moli-protocol/src/conn/state/web_contents.rs +++ b/moli-core/src/browser/web_contents.rs @@ -1,4 +1,4 @@ -use moli_core::{ +use crate::{ browser::{MainFrameSlotId, WebContentsId}, page::{Page, RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind}, runtime::NavigationEngine, @@ -7,56 +7,64 @@ use moli_core::{ mod navigation_controller; use navigation_controller::NavigationController; pub use navigation_controller::PageNavigationHistoryEntry; -pub(crate) use navigation_controller::{ - HistoryTraversalDestination, InitialDocument, InitialDocumentCreator, ResolvedHistoryTraversal, +pub use navigation_controller::{ + HistoryTraversalDestination, InitialDocument, InitialDocumentCreator, InitialDocumentSnapshot, + ResolvedHistoryTraversal, }; mod document_host; mod document_policy; -pub(in crate::conn::state) use document_policy::InheritedDocumentPolicy; +pub use document_policy::InheritedDocumentPolicy; mod emulation_policy; mod initial_document; mod javascript_dialog; -pub(in crate::conn) use initial_document::InitialDocumentAdmission; -pub(in crate::conn::state) use initial_document::InitialDocumentBuildState; -pub(crate) use initial_document::{ - BuiltInitialDocument, InitialDocumentBuildKey, InitialDocumentPageBuildWaiter, +pub use initial_document::InitialDocumentAdmission; +pub use initial_document::InitialDocumentBuildState; +pub use initial_document::{ + AdmittedInitialDocumentBuild, BuiltInitialDocument, CommittedInitialDocument, + InitialDocumentBuildKey, InitialDocumentPageBuildWaiter, }; mod navigation_commit; -pub(in crate::conn) use navigation_commit::AdmittedDocumentMaterialization; +pub use navigation_commit::AdmittedDocumentMaterialization; mod navigation_history; mod navigation_interception; -pub(crate) use navigation_interception::{ +pub use navigation_interception::{ ClaimedNavigationRequest, InterceptedNavigationLoad, InterceptedNavigationResponse, NavigationInterceptionPermit, NavigationRequestInterception, }; mod navigation_load; -pub(crate) use navigation_history::SameDocumentNavigationCommitted; -pub(in crate::conn) use navigation_load::{AdmittedNavigationLoad, PreparedNavigationResponse}; +pub use navigation_history::SameDocumentNavigationCommitted; +pub use navigation_load::{AdmittedNavigationLoad, PreparedNavigationResponse}; mod network_request_policy; -pub(crate) use navigation_commit::{ - CommittedDocumentLifecycle, DocumentNavigationDestination, PreparedDocumentNavigation, - RetiringDocument, +pub use navigation_commit::{ + CommittedDocumentInfo, CommittedDocumentLifecycle, CommittedDocumentNavigation, + DocumentNavigationDestination, PreparedDocumentNavigation, RetiringDocument, }; mod page_surface; +mod paused_document_transfer; mod resource_runtime; mod session_storage; #[cfg(test)] mod tests; mod window; -pub(in crate::conn) use document_host::DocumentHost; -pub(crate) use document_host::DocumentLifecycleEvent; -pub(crate) use emulation_policy::{EmulationPolicy, EmulationPolicyChange}; +pub use document_host::DocumentHost; +pub use document_host::DocumentLifecycleEvent; +pub use emulation_policy::{EmulationPolicy, EmulationPolicyChange}; use javascript_dialog::JavaScriptDialogs; -pub(crate) use javascript_dialog::{ +pub use javascript_dialog::{ JavaScriptDialogClosed, JavaScriptDialogError, JavaScriptDialogKey, JavaScriptDialogSnapshot, }; -pub(in crate::conn) use network_request_policy::NetworkRequestPolicy; -pub(in crate::conn) use network_request_policy::merge_extra_header_layers; -pub(in crate::conn) use page_surface::PageSurface; -pub(crate) use session_storage::SessionStorageNamespace; -pub(in crate::conn) use window::{Window, WindowOpener}; -pub(crate) use window::{WindowSurface, WindowSurfaceState}; +pub use network_request_policy::NetworkRequestPolicy; +pub use network_request_policy::merge_extra_header_layers; +pub use page_surface::PageSurface; +pub use paused_document_transfer::{ + DocumentBodySource, OpenBodyStreamError, PausedDocumentTransfer, + PausedResponsePreparedDocument, PausedStreamingDocumentResponse, + PendingFetchResponseOpenedBodyStream, SyntheticDocumentResponseContext, +}; +pub use session_storage::SessionStorageNamespace; +pub use window::{Window, WindowOpener}; +pub use window::{WindowSurface, WindowSurfaceState}; /// Stable Browser page ownership, independent of DevTools bindings. /// @@ -65,25 +73,24 @@ pub(crate) use window::{WindowSurface, WindowSurfaceState}; /// Declaration order cancels pending work and retires the Document before /// releasing the engine and storage. This owner is deliberately not Clone. #[derive(Debug)] -pub(in crate::conn) struct WebContents { +pub struct WebContents { id: WebContentsId, navigation: NavigationController, // Dismiss modal renderer work before Document/Page teardown. - pub(in crate::conn) javascript_dialogs: JavaScriptDialogs, - pub(in crate::conn) main_frame: MainFrameSlot, + pub(crate) javascript_dialogs: JavaScriptDialogs, + pub(crate) main_frame: MainFrameSlot, navigation_engine: Option, - pub(in crate::conn) session_storage: SessionStorageNamespace, - pub(in crate::conn) window: Window, - pub(in crate::conn) crashed: bool, - pub(in crate::conn) emulation_policy: EmulationPolicy, - pub(in crate::conn) network_request_policy: NetworkRequestPolicy, - fetch_subresource_interception: (bool, Option), - pub(in crate::conn) network_offline: bool, - pub(in crate::conn) tls_verify_host_override: Option, - pub(in crate::conn) bypass_content_security_policy: bool, - pub(in crate::conn) browser_identity_override: - Option, - pub(in crate::conn) environment_owner: moli_core::ProcessEnvironmentOwner, + pub(crate) session_storage: SessionStorageNamespace, + pub(crate) window: Window, + pub(crate) crashed: bool, + pub(crate) emulation_policy: EmulationPolicy, + pub(crate) network_request_policy: NetworkRequestPolicy, + fetch_subresource_interception: (bool, Option), + pub(crate) network_offline: bool, + pub(crate) tls_verify_host_override: Option, + pub(crate) bypass_content_security_policy: bool, + pub(crate) browser_identity_override: Option, + pub(crate) environment_owner: crate::ProcessEnvironmentOwner, } impl Default for WebContents { @@ -110,18 +117,22 @@ impl Default for WebContents { } impl WebContents { - #[cfg(test)] - pub(in crate::conn::state) fn fetch_subresource_interception( + pub fn install_session_storage_namespace(&mut self, namespace: SessionStorageNamespace) { + self.session_storage = namespace; + } + + #[cfg(any(test, feature = "test-support"))] + pub fn fetch_subresource_interception( &self, - ) -> (bool, Option) { + ) -> (bool, Option) { self.fetch_subresource_interception } - pub(in crate::conn::state) fn start_fetch_interception_update( + pub fn start_fetch_interception_update( &mut self, enabled: bool, - resource_type: Option, - ) -> Result, String> { + resource_type: Option, + ) -> Result, String> { // Install effective Browser policy even before the first Document, and // retain it if the outgoing renderer has already stopped accepting work. self.install_fetch_interception_policy(enabled, resource_type); @@ -137,17 +148,17 @@ impl WebContents { .map_err(|error| error.to_string()) } - pub(in crate::conn::state) fn install_fetch_interception_policy( + pub fn install_fetch_interception_policy( &mut self, enabled: bool, - resource_type: Option, + resource_type: Option, ) { self.fetch_subresource_interception = (enabled, resource_type); } /// Retire Browser authority synchronously, then close the renderer without /// retaining any Context/registry borrow across await. - pub(in crate::conn) fn begin_close(mut self) -> ClosingWebContents { + pub fn begin_close(mut self) -> ClosingWebContents { self.navigation.clear_document_navigation_state(); let page = self.replace_document(None); ClosingWebContents { @@ -156,9 +167,9 @@ impl WebContents { } } - pub(in crate::conn) fn performance_metric_snapshot( + pub fn performance_metric_snapshot( &self, - ) -> Option { + ) -> Option { Some( self.main_frame .current_document @@ -171,7 +182,7 @@ impl WebContents { /// Browser observation is independent of the DevTools command that /// produced this snapshot. The Page cache validates physical residence /// and revision; a rejected observation never invalidates a frozen reply. - pub(in crate::conn) fn observe_renderer_page_state( + pub fn observe_renderer_page_state( &mut self, snapshot: &std::sync::Arc, ) -> bool { @@ -181,7 +192,7 @@ impl WebContents { .is_some_and(|document| document.page.observe_renderer_page_state(snapshot)) } - pub(in crate::conn) fn apply_document_lifecycle( + pub fn apply_document_lifecycle( &mut self, event: RendererDocumentLifecycleEvent, ) -> Option { @@ -204,8 +215,8 @@ impl WebContents { if matches!( event.kind, RendererDocumentLifecycleEventKind::Started { - reason: moli_core::page::RendererLifecycleStartReason::ExplicitDocumentOpen - | moli_core::page::RendererLifecycleStartReason::JavascriptDocumentReplacement + reason: crate::page::RendererLifecycleStartReason::ExplicitDocumentOpen + | crate::page::RendererLifecycleStartReason::JavascriptDocumentReplacement } ) { self.navigation.mark_initial_empty_document_exited(); @@ -213,7 +224,7 @@ impl WebContents { Some(DocumentLifecycleEvent::new(document.id, event)) } - pub(in crate::conn) fn replace_document(&mut self, next: Option) -> Option { + pub fn replace_document(&mut self, next: Option) -> Option { self.navigation.cancel_initial_document_build(); self.javascript_dialogs.clear(); if let Some(document) = &next { @@ -225,37 +236,37 @@ impl WebContents { self.main_frame.replace_document(next) } - pub(in crate::conn) fn id(&self) -> WebContentsId { + pub fn id(&self) -> WebContentsId { self.id } - pub(in crate::conn) fn set_network_request_policy(&mut self, policy: NetworkRequestPolicy) { + pub fn set_network_request_policy(&mut self, policy: NetworkRequestPolicy) { if let Some(engine) = self.navigation_engine.as_mut() { engine.set_cache_disabled(policy.cache_disabled); } self.network_request_policy = policy; } - pub(in crate::conn) fn set_network_offline(&mut self, offline: bool) { + pub fn set_network_offline(&mut self, offline: bool) { self.network_offline = offline; } - pub(in crate::conn) fn set_tls_verify_host_override(&mut self, enabled: Option) { + pub fn set_tls_verify_host_override(&mut self, enabled: Option) { self.tls_verify_host_override = enabled; } - pub(in crate::conn) fn set_bypass_content_security_policy(&mut self, bypass: bool) { + pub fn set_bypass_content_security_policy(&mut self, bypass: bool) { self.bypass_content_security_policy = bypass; } - pub(in crate::conn) fn set_browser_identity_override( + pub fn set_browser_identity_override( &mut self, identity: Option, ) { self.browser_identity_override = identity; } - pub(in crate::conn) fn install_navigation_engine(&mut self, mut engine: NavigationEngine) { + pub fn install_navigation_engine(&mut self, mut engine: NavigationEngine) { assert!( self.navigation_engine.is_none(), "WebContents must retain its first installed NavigationEngine" @@ -268,13 +279,13 @@ impl WebContents { /// A move-owned Browser teardown participant, not a mutable WebContents handle. /// Its Page retires before the engine and storage even if cleanup is cancelled. -pub(in crate::conn) struct ClosingWebContents { +pub struct ClosingWebContents { page: Option, _contents: WebContents, } impl ClosingWebContents { - pub(in crate::conn) async fn close_async(mut self) { + pub async fn close_async(mut self) { if let Some(page) = self.page.take() { let _ = page.close_async().await; } @@ -283,9 +294,9 @@ impl ClosingWebContents { /// Stable main-frame slot; only the current Document is replaced on navigation. #[derive(Debug)] -pub(in crate::conn) struct MainFrameSlot { +pub struct MainFrameSlot { id: MainFrameSlotId, - pub(in crate::conn) current_document: Option, + pub current_document: Option, } impl Default for MainFrameSlot { @@ -298,7 +309,7 @@ impl Default for MainFrameSlot { } impl MainFrameSlot { - pub(in crate::conn) fn id(&self) -> MainFrameSlotId { + pub fn id(&self) -> MainFrameSlotId { self.id } diff --git a/moli-core/src/browser/web_contents/document_host.rs b/moli-core/src/browser/web_contents/document_host.rs new file mode 100644 index 0000000000..125bc7958d --- /dev/null +++ b/moli-core/src/browser/web_contents/document_host.rs @@ -0,0 +1,112 @@ +use crate::{ + browser::{DocumentId, DocumentLifecycle, DocumentLifetime}, + page::{Page, RendererDocumentLifecycleEvent}, +}; + +/// Exact native occurrence carried to a possibly delayed DevTools projection. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DocumentLifecycleEvent { + document: DocumentId, + event: RendererDocumentLifecycleEvent, +} + +impl DocumentLifecycleEvent { + pub fn new(document: DocumentId, event: RendererDocumentLifecycleEvent) -> Self { + Self { document, event } + } + + pub fn document(&self) -> DocumentId { + self.document + } + pub fn event(&self) -> RendererDocumentLifecycleEvent { + self.event + } +} + +/// One browser Document incarnation, including its concrete renderer Page. +/// +/// Private in the current residence until the typed API cutover (Commit 24b). +/// No Target/session state or public mutable Page capability belongs here. +#[derive(Debug)] +pub struct DocumentHost { + pub(crate) id: DocumentId, + pub(crate) page: Page, + pub(crate) lifecycle: DocumentLifecycle, + pub(crate) lifetime: DocumentLifetime, +} + +impl DocumentHost { + pub fn new(id: DocumentId, page: Page) -> Self { + Self { + id, + page, + lifecycle: DocumentLifecycle::default(), + lifetime: DocumentLifetime::default(), + } + } + + pub fn id(&self) -> DocumentId { + self.id + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn evaluate_runtime_expression_for_test( + &mut self, + expression: &str, + await_promise: bool, + ) -> anyhow::Result { + self.page + .evaluate_runtime_expression_without_navigation_follow_with_await_async( + expression, + await_promise, + ) + .await + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub async fn runtime_heap_usage_for_test( + &mut self, + ) -> anyhow::Result { + self.page.runtime_heap_usage_async().await + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn start_blob_bytes_for_uuid_for_test( + &self, + uuid: String, + ) -> anyhow::Result { + self.page.start_blob_bytes_for_uuid(uuid) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn finish_blob_bytes_for_uuid_for_test( + &mut self, + completion: crate::page::CompletedPageCommand, + ) -> anyhow::Result>> { + self.page.finish_blob_bytes_for_uuid(completion) + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn document_title_for_test(&self) -> String { + self.page.document_title() + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn renderer_page_residence_identity_for_test( + &self, + ) -> crate::browser::RendererPageResidenceIdentity { + crate::browser::RendererPageResidenceIdentity::from_page(&self.page) + } + + /// Retire the browser incarnation before handing its Page to async cleanup. + pub(super) fn retire(self) -> Page { + self.lifetime.supersede(); + self.page + } +} diff --git a/moli-protocol/src/conn/state/web_contents/document_policy.rs b/moli-core/src/browser/web_contents/document_policy.rs similarity index 86% rename from moli-protocol/src/conn/state/web_contents/document_policy.rs rename to moli-core/src/browser/web_contents/document_policy.rs index eebb04e467..2dfd0dd1c8 100644 --- a/moli-protocol/src/conn/state/web_contents/document_policy.rs +++ b/moli-core/src/browser/web_contents/document_policy.rs @@ -1,22 +1,20 @@ -use moli_core::{page::PermissionOverrideRegistration, runtime::PreparedDocumentPagePolicy}; +use crate::{page::PermissionOverrideRegistration, runtime::PreparedDocumentPagePolicy}; use moli_fetch::FetchConfig; use url::Url; use super::{WebContents, network_request_policy::merge_extra_header_layers}; -use crate::conn::state::browser_context::{ - BrowserContextStoragePartitionHandles, ContextEmulationDefaults, -}; +use crate::browser::{BrowserContextStoragePartitionHandles, ContextEmulationDefaults}; /// Source-free context inheritance, captured during Browser admission. It is /// not a second stored policy and contains no inspection/session configuration. -pub(in crate::conn::state) struct InheritedDocumentPolicy { - pub(in crate::conn::state) selected_web_contents: Option, - pub(in crate::conn::state) navigator_queries: moli_page_types::NavigatorQueryOverrides, - pub(in crate::conn::state) fetch_config: FetchConfig, - pub(in crate::conn::state) extra_headers: moli_fetch::RequestHeaders, - pub(in crate::conn::state) emulation: ContextEmulationDefaults, - pub(in crate::conn::state) permissions: Vec, - pub(in crate::conn::state) storage: BrowserContextStoragePartitionHandles, +pub struct InheritedDocumentPolicy { + pub selected_web_contents: Option, + pub navigator_queries: moli_page_types::NavigatorQueryOverrides, + pub fetch_config: FetchConfig, + pub extra_headers: moli_fetch::RequestHeaders, + pub emulation: ContextEmulationDefaults, + pub permissions: Vec, + pub storage: BrowserContextStoragePartitionHandles, } impl WebContents { @@ -45,7 +43,7 @@ impl WebContents { Ok(policy) } - pub(in crate::conn::state) fn configure_navigation_engine_policy( + pub fn configure_navigation_engine_policy( &mut self, inherited: &InheritedDocumentPolicy, ) -> Result< @@ -90,7 +88,7 @@ impl WebContents { Ok((navigator_identity, extra_http_headers, network_offline)) } - pub(in crate::conn::state) fn capture_document_policy( + pub fn capture_document_policy( &mut self, inherited: InheritedDocumentPolicy, final_url: &Url, diff --git a/moli-protocol/src/conn/state/web_contents/emulation_policy.rs b/moli-core/src/browser/web_contents/emulation_policy.rs similarity index 69% rename from moli-protocol/src/conn/state/web_contents/emulation_policy.rs rename to moli-core/src/browser/web_contents/emulation_policy.rs index e72d61c43f..9a7298377b 100644 --- a/moli-protocol/src/conn/state/web_contents/emulation_policy.rs +++ b/moli-core/src/browser/web_contents/emulation_policy.rs @@ -1,4 +1,4 @@ -use super::super::emulation::{ +use crate::browser::{ EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, EmulatedMediaOverrides, EmulatedNetworkConditions, }; @@ -6,20 +6,20 @@ use super::super::emulation::{ /// Source-free values applied to one Browser page. Session contributions use /// the same value representation, but do not own the installed policy. #[derive(Debug, Clone, Default, PartialEq)] -pub(crate) struct EmulationPolicy { - pub(crate) default_background_color: Option<[u8; 4]>, - pub(crate) network_conditions: Option, - pub(crate) geolocation_override: Option, - pub(crate) emulated_media: EmulatedMediaOverrides, - pub(crate) emulated_device_metrics: Option, - pub(crate) max_touch_points: u32, - pub(crate) emit_touch_events_for_mouse: bool, - pub(crate) focus_emulation_enabled: bool, - pub(crate) script_execution_disabled: bool, +pub struct EmulationPolicy { + pub default_background_color: Option<[u8; 4]>, + pub network_conditions: Option, + pub geolocation_override: Option, + pub emulated_media: EmulatedMediaOverrides, + pub emulated_device_metrics: Option, + pub max_touch_points: u32, + pub emit_touch_events_for_mouse: bool, + pub focus_emulation_enabled: bool, + pub script_execution_disabled: bool, } #[derive(Debug, Clone)] -pub(crate) enum EmulationPolicyChange { +pub enum EmulationPolicyChange { DefaultBackgroundColor(Option<[u8; 4]>), NetworkConditions(Option), Geolocation(Option), @@ -32,7 +32,7 @@ pub(crate) enum EmulationPolicyChange { } impl EmulationPolicy { - pub(in crate::conn) fn apply(&mut self, change: EmulationPolicyChange) { + pub fn apply(&mut self, change: EmulationPolicyChange) { match change { EmulationPolicyChange::DefaultBackgroundColor(value) => { self.default_background_color = value @@ -52,7 +52,7 @@ impl EmulationPolicy { } } - pub(in crate::conn) fn apply_changes(&mut self, changes: Vec) { + pub fn apply_changes(&mut self, changes: Vec) { for change in changes { self.apply(change); } diff --git a/moli-protocol/src/conn/state/web_contents/initial_document.rs b/moli-core/src/browser/web_contents/initial_document.rs similarity index 80% rename from moli-protocol/src/conn/state/web_contents/initial_document.rs rename to moli-core/src/browser/web_contents/initial_document.rs index cd0396c5a6..4d741048e0 100644 --- a/moli-protocol/src/conn/state/web_contents/initial_document.rs +++ b/moli-core/src/browser/web_contents/initial_document.rs @@ -1,4 +1,4 @@ -use moli_core::{ +use crate::{ browser::{ BrowserSequence, DocumentId, DocumentLifecycle, MainFrameSlotId, RendererPageResidenceIdentity, WebContentsId, @@ -7,13 +7,10 @@ use moli_core::{ }; use tokio::sync::watch; -#[cfg(test)] -mod tests; - use super::{CommittedDocumentLifecycle, DocumentHost, InheritedDocumentPolicy, WebContents}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct InitialDocumentBuildKey { +pub struct InitialDocumentBuildKey { web_contents: WebContentsId, frame_slot: MainFrameSlotId, document: DocumentId, @@ -21,13 +18,13 @@ pub(crate) struct InitialDocumentBuildKey { } impl InitialDocumentBuildKey { - pub(in crate::conn) fn web_contents(&self) -> WebContentsId { + pub fn web_contents(&self) -> WebContentsId { self.web_contents } - pub(in crate::conn) fn document(&self) -> DocumentId { + pub fn document(&self) -> DocumentId { self.document } - pub(in crate::conn) fn renderer(&self) -> RendererPageResidenceIdentity { + pub fn renderer(&self) -> RendererPageResidenceIdentity { self.renderer } } @@ -36,7 +33,7 @@ impl InitialDocumentBuildKey { /// Dropping either finishes the same one-shot observation without borrowing the /// registry. A stale worker can never complete a later build's waiters. #[derive(Debug)] -pub(in crate::conn::state) struct InitialDocumentBuildCompletion { +pub struct InitialDocumentBuildCompletion { sender: watch::Sender>>, } @@ -60,7 +57,7 @@ impl InitialDocumentBuildCompletion { true }); } - pub(in crate::conn::state) fn pending(&self) -> bool { + pub fn pending(&self) -> bool { self.sender.borrow().is_none() } fn waiter(&self) -> InitialDocumentPageBuildWaiter { @@ -77,12 +74,12 @@ impl Drop for InitialDocumentBuildCompletion { } #[derive(Debug, Clone)] -pub(crate) struct InitialDocumentPageBuildWaiter { +pub struct InitialDocumentPageBuildWaiter { receiver: watch::Receiver>>, } impl InitialDocumentPageBuildWaiter { - pub(crate) async fn wait(mut self) -> Result<(), String> { + pub async fn wait(mut self) -> Result<(), String> { loop { if let Some(result) = self.receiver.borrow().clone() { return result; @@ -96,47 +93,47 @@ impl InitialDocumentPageBuildWaiter { } #[derive(Debug)] -pub(in crate::conn::state) struct InitialDocumentBuildState { - pub(in crate::conn::state) key: InitialDocumentBuildKey, - pub(in crate::conn::state) completion: InitialDocumentBuildCompletion, +pub struct InitialDocumentBuildState { + pub key: InitialDocumentBuildKey, + pub completion: InitialDocumentBuildCompletion, } impl InitialDocumentBuildState { - pub(in crate::conn::state) fn waiter(&self) -> InitialDocumentPageBuildWaiter { + pub fn waiter(&self) -> InitialDocumentPageBuildWaiter { self.completion.waiter() } } -pub(in crate::conn) struct AdmittedInitialDocumentBuild { +pub struct AdmittedInitialDocumentBuild { key: InitialDocumentBuildKey, completion: InitialDocumentBuildCompletion, page: PendingPreparedDocumentPage, policy: PreparedDocumentPagePolicy, } -pub(in crate::conn) enum InitialDocumentAdmission { +pub enum InitialDocumentAdmission { Present, Join(InitialDocumentPageBuildWaiter), Build(Box), } impl AdmittedInitialDocumentBuild { - pub(in crate::conn) fn start_preparation(&mut self) -> anyhow::Result<()> { + pub fn start_preparation(&mut self) -> anyhow::Result<()> { anyhow::ensure!( self.completion.pending(), "InitialDocumentPageBuildCancelled" ); self.page.start_preparation() } - pub(in crate::conn) fn key(&self) -> InitialDocumentBuildKey { + pub fn key(&self) -> InitialDocumentBuildKey { self.key } - pub(in crate::conn) fn inspection_endpoint( + pub fn inspection_endpoint( &self, ) -> moli_renderer_v8::RendererPreparedDocumentInspectionEndpoint { self.page.inspection_configuration_endpoint() } - pub(in crate::conn) async fn materialize(self) -> anyhow::Result { + pub async fn materialize(self) -> anyhow::Result { let Self { key, completion, @@ -167,7 +164,7 @@ impl AdmittedInitialDocumentBuild { } } -pub(crate) struct BuiltInitialDocument { +pub struct BuiltInitialDocument { key: InitialDocumentBuildKey, completion: InitialDocumentBuildCompletion, built: BuiltDocumentPage, @@ -175,23 +172,41 @@ pub(crate) struct BuiltInitialDocument { } impl BuiltInitialDocument { - pub(in crate::conn) fn key(&self) -> InitialDocumentBuildKey { + pub fn key(&self) -> InitialDocumentBuildKey { self.key } - pub(in crate::conn) async fn retire(self) { + pub async fn retire(self) { let _ = self.built.page.close_async().await; } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn built_mut_for_test(&mut self) -> &mut BuiltDocumentPage { + &mut self.built + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn set_lifecycle_for_test(&mut self, lifecycle: DocumentLifecycle) { + self.lifecycle = lifecycle; + } + + #[cfg(any(test, feature = "test-support"))] + #[doc(hidden)] + pub fn lifecycle_for_test(&self) -> &DocumentLifecycle { + &self.lifecycle + } } -pub(in crate::conn) struct CommittedInitialDocument { - pub(in crate::conn) key: InitialDocumentBuildKey, - pub(in crate::conn) lifecycle: CommittedDocumentLifecycle, - pub(in crate::conn) diagnostics: moli_core::page::RendererPageCreationDiagnostics, - pub(in crate::conn) inspection_endpoint: moli_renderer_v8::RendererInspectionEndpoint, +pub struct CommittedInitialDocument { + pub key: InitialDocumentBuildKey, + pub lifecycle: CommittedDocumentLifecycle, + pub diagnostics: crate::page::RendererPageCreationDiagnostics, + pub inspection_endpoint: moli_renderer_v8::RendererInspectionEndpoint, } impl WebContents { - pub(in crate::conn::state) fn start_initial_document_build( + pub fn start_initial_document_build( &mut self, inherited: InheritedDocumentPolicy, ) -> Result { @@ -255,7 +270,7 @@ impl WebContents { ))) } - pub(in crate::conn::state) fn commit_initial_document( + pub fn commit_initial_document( &mut self, candidate: BuiltInitialDocument, ) -> Result> { @@ -301,9 +316,9 @@ impl WebContents { .iter() .any(|event| { matches!(event.kind, - moli_core::page::RendererDocumentLifecycleEventKind::Started { reason: - moli_core::page::RendererLifecycleStartReason::ExplicitDocumentOpen - | moli_core::page::RendererLifecycleStartReason::JavascriptDocumentReplacement }) + crate::page::RendererDocumentLifecycleEventKind::Started { reason: + crate::page::RendererLifecycleStartReason::ExplicitDocumentOpen + | crate::page::RendererLifecycleStartReason::JavascriptDocumentReplacement }) }) { self.navigation.mark_initial_empty_document_exited(); diff --git a/moli-protocol/src/conn/state/web_contents/javascript_dialog.rs b/moli-core/src/browser/web_contents/javascript_dialog.rs similarity index 78% rename from moli-protocol/src/conn/state/web_contents/javascript_dialog.rs rename to moli-core/src/browser/web_contents/javascript_dialog.rs index 350bdfc66d..28c1a1016f 100644 --- a/moli-protocol/src/conn/state/web_contents/javascript_dialog.rs +++ b/moli-core/src/browser/web_contents/javascript_dialog.rs @@ -1,4 +1,4 @@ -use moli_core::{ +use crate::{ browser::DocumentId, page::{ RendererDocumentLifecycleIdentity, RendererJavaScriptDialogId, @@ -9,14 +9,14 @@ use moli_core::{ /// Reuses the Browser incarnation and exact renderer source; no new allocator. /// The source distinguishes a parked popup from its later materialized Page. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct JavaScriptDialogKey { - pub(in crate::conn) document: DocumentId, +pub struct JavaScriptDialogKey { + pub document: DocumentId, source: RendererDocumentLifecycleIdentity, dialog: RendererJavaScriptDialogId, } impl JavaScriptDialogKey { - pub(crate) fn new(document: DocumentId, dialog: &RendererPendingJavaScriptDialog) -> Self { + pub fn new(document: DocumentId, dialog: &RendererPendingJavaScriptDialog) -> Self { Self { document, source: dialog.source_document(), @@ -26,20 +26,20 @@ impl JavaScriptDialogKey { } #[derive(Debug, PartialEq, Eq)] -pub(crate) struct JavaScriptDialogSnapshot { - pub(crate) dialog_type: String, - pub(crate) message: String, - pub(crate) default_prompt: String, +pub struct JavaScriptDialogSnapshot { + pub dialog_type: String, + pub message: String, + pub default_prompt: String, } #[derive(Debug)] -pub(crate) struct JavaScriptDialogClosed { - pub(crate) dialog_type: String, - pub(crate) user_input: String, +pub struct JavaScriptDialogClosed { + pub dialog_type: String, + pub user_input: String, } #[derive(Debug, PartialEq, Eq)] -pub(crate) enum JavaScriptDialogError { +pub enum JavaScriptDialogError { NotFound, NotPrompt, } @@ -59,12 +59,12 @@ impl JavaScriptDialog { /// Browser-owned modal state. Session snapshots can copy keys, never this owner. #[derive(Debug, Default)] -pub(in crate::conn) struct JavaScriptDialogs { +pub struct JavaScriptDialogs { pending: Vec, } impl JavaScriptDialogs { - pub(in crate::conn) fn install( + pub fn install( &mut self, document: DocumentId, renderer: RendererPendingJavaScriptDialog, @@ -83,10 +83,7 @@ impl JavaScriptDialogs { key } - pub(in crate::conn) fn snapshot( - &self, - key: JavaScriptDialogKey, - ) -> Option { + pub fn snapshot(&self, key: JavaScriptDialogKey) -> Option { let dialog = &self .pending .iter() @@ -99,7 +96,7 @@ impl JavaScriptDialogs { }) } - pub(in crate::conn) fn set_prompt_text( + pub fn set_prompt_text( &mut self, key: JavaScriptDialogKey, prompt_text: String, @@ -116,7 +113,7 @@ impl JavaScriptDialogs { Ok(()) } - pub(in crate::conn) fn finish( + pub fn finish( &mut self, key: JavaScriptDialogKey, accepted: bool, @@ -134,15 +131,15 @@ impl JavaScriptDialogs { }) } - pub(in crate::conn) fn dismiss(&mut self, key: JavaScriptDialogKey) { + pub fn dismiss(&mut self, key: JavaScriptDialogKey) { let _ = self.finish(key, false, Some(String::new())); } - pub(in crate::conn) fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.pending.is_empty() } - pub(in crate::conn) fn clear(&mut self) { + pub fn clear(&mut self) { for dialog in self.pending.drain(..) { let _ = dialog.renderer.finish(false, String::new()); } diff --git a/moli-protocol/src/conn/state/web_contents/navigation_commit.rs b/moli-core/src/browser/web_contents/navigation_commit.rs similarity index 92% rename from moli-protocol/src/conn/state/web_contents/navigation_commit.rs rename to moli-core/src/browser/web_contents/navigation_commit.rs index 10fe60a4d3..d931c135e4 100644 --- a/moli-protocol/src/conn/state/web_contents/navigation_commit.rs +++ b/moli-core/src/browser/web_contents/navigation_commit.rs @@ -1,4 +1,4 @@ -use moli_core::{ +use crate::{ browser::{ BrowserSequence, DocumentId, DocumentLifecycle, MainFrameSlotId, NavigationId, RendererPageResidenceIdentity, WebContentsId, @@ -13,7 +13,7 @@ use super::{DocumentHost, InheritedDocumentPolicy, WebContents}; /// Browser-only commit participant in the private migration residence (20/24b). /// No frontend identity, renderer attachment, arbitrary callback or Page lease. #[derive(Debug)] -pub(crate) struct PreparedDocumentNavigation { +pub struct PreparedDocumentNavigation { identity: DocumentNavigationIdentity, page: Page, lifecycle: DocumentLifecycle, @@ -39,15 +39,15 @@ impl DocumentNavigationIdentity { } } -pub(crate) struct DocumentNavigationDestination { - pub(crate) url: Url, - pub(crate) security_origin: String, - pub(crate) secure_context_type: String, +pub struct DocumentNavigationDestination { + pub url: Url, + pub security_origin: String, + pub secure_context_type: String, } /// An admitted operation owns the candidate, not a reusable permission to /// configure or commit a different renderer reservation. -pub(in crate::conn) struct AdmittedDocumentMaterialization { +pub struct AdmittedDocumentMaterialization { identity: DocumentNavigationIdentity, page: PreparedDocumentPage, destination: DocumentNavigationDestination, @@ -55,7 +55,7 @@ pub(in crate::conn) struct AdmittedDocumentMaterialization { } impl AdmittedDocumentMaterialization { - pub(in crate::conn) async fn materialize( + pub async fn materialize( self, ) -> anyhow::Result> { anyhow::ensure!( @@ -89,21 +89,25 @@ impl AdmittedDocumentMaterialization { } #[derive(Debug)] -pub(in crate::conn) struct CommittedDocumentInfo { - pub(in crate::conn) url: Url, - pub(in crate::conn) title: String, - pub(in crate::conn) security_origin: String, - pub(in crate::conn) secure_context_type: String, +pub struct CommittedDocumentInfo { + pub url: Url, + pub title: String, + pub security_origin: String, + pub secure_context_type: String, } impl PreparedDocumentNavigation { + pub(crate) fn renderer_residence(&self) -> RendererPageResidenceIdentity { + RendererPageResidenceIdentity::from_page(&self.page) + } + /// Configure the move-owned Browser participant without borrowing its owner. /// These are effective values, never frontend registration/session identities. - #[cfg(test)] + #[cfg(any(test, feature = "test-support"))] async fn apply_document_policy( mut self, - interception: (bool, Option), - permissions: &[moli_core::page::PermissionOverrideRegistration], + interception: (bool, Option), + permissions: &[crate::page::PermissionOverrideRegistration], ) -> anyhow::Result { use anyhow::Context; // Empty/disabled is an effective value too: policy may have been @@ -119,11 +123,11 @@ impl PreparedDocumentNavigation { Ok(self) } - pub(in crate::conn) fn navigation(&self) -> NavigationId { + pub fn navigation(&self) -> NavigationId { self.identity.navigation } - pub(in crate::conn) fn web_contents_id(&self) -> WebContentsId { + pub fn web_contents_id(&self) -> WebContentsId { self.identity.web_contents } @@ -155,34 +159,37 @@ impl PreparedDocumentNavigation { } /// Self-contained Browser occurrence plus a move-owned post-commit retirement. -pub(in crate::conn) struct CommittedDocumentNavigation { - pub(in crate::conn) web_contents: WebContentsId, - pub(in crate::conn) frame_slot: MainFrameSlotId, - pub(in crate::conn) navigation: NavigationId, - pub(in crate::conn) document: DocumentId, - pub(in crate::conn) previous_document: Option, - pub(in crate::conn) previous_renderer: Option, - pub(in crate::conn) inspection_endpoint: moli_renderer_v8::RendererInspectionEndpoint, - pub(in crate::conn) lifecycle: CommittedDocumentLifecycle, - pub(in crate::conn) info: CommittedDocumentInfo, - pub(in crate::conn) retirement: RetiringDocument, - pub(in crate::conn) post_response_continuation: - Option, +pub struct CommittedDocumentNavigation { + pub web_contents: WebContentsId, + pub frame_slot: MainFrameSlotId, + pub navigation: NavigationId, + pub document: DocumentId, + pub previous_document: Option, + pub previous_renderer: Option, + pub inspection_endpoint: moli_renderer_v8::RendererInspectionEndpoint, + pub lifecycle: CommittedDocumentLifecycle, + pub info: CommittedDocumentInfo, + pub retirement: RetiringDocument, + pub post_response_continuation: Option, } /// Retirement has no commit authority and never retains a Browser registry borrow. -pub(crate) struct RetiringDocument(Option); +pub struct RetiringDocument(Option); /// A creation prefix published by the Browser commit, never a request to /// initialize or rewind native state from a frontend projection. -pub(crate) struct CommittedDocumentLifecycle { - pub(crate) document: DocumentId, - pub(crate) browser_sequence: BrowserSequence, - pub(crate) artifacts: RendererPageCreationArtifacts, +pub struct CommittedDocumentLifecycle { + pub document: DocumentId, + pub browser_sequence: BrowserSequence, + pub artifacts: RendererPageCreationArtifacts, } impl RetiringDocument { - pub(crate) async fn close(self) { + pub(crate) fn from_page(page: Option) -> Self { + Self(page) + } + + pub async fn close(self) { if let Some(page) = self.0 { let _ = page.close_async().await; } @@ -218,14 +225,14 @@ impl WebContents { /// Construct a commit participant for fixtures with an already built Page. /// Production navigation must materialize an admitted prepared Document. - #[cfg(test)] - pub(in crate::conn::state) fn start_loaded_document_navigation( + #[cfg(any(test, feature = "test-support"))] + pub fn start_loaded_document_navigation( &self, navigation: NavigationId, page: Page, destination: DocumentNavigationDestination, artifacts: &RendererPageCreationArtifacts, - permissions: Vec, + permissions: Vec, ) -> Result< impl std::future::Future> + use<>, &'static str, @@ -249,7 +256,7 @@ impl WebContents { }) } - pub(in crate::conn::state) fn start_document_materialization( + pub fn start_document_materialization( &mut self, navigation: NavigationId, response: super::PreparedNavigationResponse, @@ -285,7 +292,7 @@ impl WebContents { }) } - pub(in crate::conn) fn commit_document_navigation( + pub fn commit_document_navigation( &mut self, mut prepared: PreparedDocumentNavigation, ) -> Result { @@ -353,7 +360,7 @@ impl WebContents { #[cfg(test)] mod tests { use super::*; - use moli_core::{ + use crate::{ browser::DocumentRetirement, runtime::{Browser, BrowserConfig}, }; @@ -402,7 +409,7 @@ mod tests { .evaluate_runtime_expression_with_await_async(query, true) .await .unwrap(); - page.set_permission_overrides_async(&[moli_core::page::PermissionOverrideRegistration { + page.set_permission_overrides_async(&[crate::page::PermissionOverrideRegistration { permission: serde_json::json!({"name": "geolocation"}), setting: "granted".into(), origin: None, @@ -606,7 +613,7 @@ mod tests { ((true, None), Vec::new()), ( (false, None), - vec![moli_core::page::PermissionOverrideRegistration { + vec![crate::page::PermissionOverrideRegistration { permission: serde_json::json!({"name": "geolocation"}), setting: "granted".into(), origin: None, diff --git a/moli-protocol/src/conn/state/web_contents/navigation_controller.rs b/moli-core/src/browser/web_contents/navigation_controller.rs similarity index 86% rename from moli-protocol/src/conn/state/web_contents/navigation_controller.rs rename to moli-core/src/browser/web_contents/navigation_controller.rs index 75ea5f1443..6f136b36a5 100644 --- a/moli-protocol/src/conn/state/web_contents/navigation_controller.rs +++ b/moli-core/src/browser/web_contents/navigation_controller.rs @@ -1,22 +1,22 @@ //! Browser navigation state, private in the current residence until Commit 24b. //! Protocol loader correlation and renderer output binding stay in TargetPageSlot. -use moli_core::browser::{DocumentId, NavigationId, WebContentsId}; +use crate::browser::{DocumentId, NavigationId, WebContentsId}; mod history; pub use history::PageNavigationHistoryEntry; -pub(crate) use history::{HistoryTraversalDestination, ResolvedHistoryTraversal}; +pub use history::{HistoryTraversalDestination, ResolvedHistoryTraversal}; use history::{NavigationHistoryState, PendingNavigationHistoryUpdate}; #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct InitialDocumentCreator { +pub struct InitialDocumentCreator { web_contents_id: WebContentsId, security_origin: String, secure_context_type: String, } impl InitialDocumentCreator { - pub(crate) fn new( + pub fn new( web_contents_id: WebContentsId, security_origin: String, secure_context_type: String, @@ -28,13 +28,13 @@ impl InitialDocumentCreator { } } - pub(crate) fn web_contents_id(&self) -> WebContentsId { + pub fn web_contents_id(&self) -> WebContentsId { self.web_contents_id } - pub(crate) fn security_origin(&self) -> &str { + pub fn security_origin(&self) -> &str { &self.security_origin } - pub(crate) fn secure_context_type(&self) -> &str { + pub fn secure_context_type(&self) -> &str { &self.secure_context_type } } @@ -49,13 +49,61 @@ enum InitialDocumentLifecycle { /// Browser seed and lifecycle; no Target, loader or mirrored pending state. #[derive(Debug)] -pub(crate) struct InitialDocument { +pub struct InitialDocument { initial_url: String, creator: Option, storage_key: Option, lifecycle: InitialDocumentLifecycle, } +/// Owned diagnostic projection of an initial empty Document. +#[derive(Debug, Clone)] +pub struct InitialDocumentSnapshot { + initial_url: String, + creator: Option, + storage_key: Option, + materialized: bool, + exited: bool, +} + +impl InitialDocumentSnapshot { + pub fn initial_url(&self) -> &str { + &self.initial_url + } + + pub fn creator(&self) -> Option<&InitialDocumentCreator> { + self.creator.as_ref() + } + + pub fn storage_key(&self) -> Option<&moli_storage_key::MoliStorageKey> { + self.storage_key.as_ref() + } + + pub fn materialized(&self) -> bool { + self.materialized + } + + pub fn exited(&self) -> bool { + self.exited + } + + pub fn is_on_initial_empty_document(&self) -> bool { + !self.exited + } +} + +impl From<&InitialDocument> for InitialDocumentSnapshot { + fn from(document: &InitialDocument) -> Self { + Self { + initial_url: document.initial_url.clone(), + creator: document.creator.clone(), + storage_key: document.storage_key.clone(), + materialized: document.materialized(), + exited: document.exited(), + } + } +} + impl InitialDocument { fn new( initial_url: String, @@ -69,25 +117,25 @@ impl InitialDocument { lifecycle: InitialDocumentLifecycle::Unmaterialized, } } - pub(crate) fn initial_url(&self) -> &str { + pub fn initial_url(&self) -> &str { &self.initial_url } - pub(crate) fn creator(&self) -> Option<&InitialDocumentCreator> { + pub fn creator(&self) -> Option<&InitialDocumentCreator> { self.creator.as_ref() } - pub(crate) fn storage_key(&self) -> Option<&moli_storage_key::MoliStorageKey> { + pub fn storage_key(&self) -> Option<&moli_storage_key::MoliStorageKey> { self.storage_key.as_ref() } - pub(crate) fn materialized(&self) -> bool { + pub fn materialized(&self) -> bool { matches!(self.lifecycle, InitialDocumentLifecycle::Materialized) } - pub(crate) fn exited(&self) -> bool { + pub fn exited(&self) -> bool { matches!(self.lifecycle, InitialDocumentLifecycle::Exited) } - pub(crate) fn is_on_initial_empty_document(&self) -> bool { + pub fn is_on_initial_empty_document(&self) -> bool { !self.exited() } - #[cfg(test)] + #[cfg(any(test, feature = "test-support"))] fn mark_materialized(&mut self) { if !self.exited() { self.lifecycle = InitialDocumentLifecycle::Materialized; @@ -112,7 +160,7 @@ struct PendingNavigationRequest { history_update: Option, paused_interception: Option, document_preparation: Option<( - moli_core::browser::RendererPageResidenceIdentity, + crate::browser::RendererPageResidenceIdentity, moli_fetch::FetchCancelHandle, )>, cancellation_handles: Vec, @@ -180,7 +228,7 @@ impl Drop for PendingNavigationRequest { } #[derive(Debug, Default)] -pub(in crate::conn) struct NavigationController { +pub struct NavigationController { pending_navigation_request: Option, committed_document_navigation: Option, history: NavigationHistoryState, @@ -188,8 +236,8 @@ pub(in crate::conn) struct NavigationController { } impl NavigationController { - #[cfg(test)] - pub(in crate::conn::state) fn has_paused_request_for_test(&self) -> bool { + #[cfg(any(test, feature = "test-support"))] + pub fn has_paused_request_for_test(&self) -> bool { self.pending_navigation_request .as_ref() .and_then(|request| request.paused_interception.as_ref()) @@ -201,8 +249,8 @@ impl NavigationController { }) } - #[cfg(test)] - pub(in crate::conn::state) fn has_paused_auth_for_test(&self) -> bool { + #[cfg(any(test, feature = "test-support"))] + pub fn has_paused_auth_for_test(&self) -> bool { self.pending_navigation_request .as_ref() .and_then(|request| request.paused_interception.as_ref()) @@ -214,10 +262,8 @@ impl NavigationController { }) } - #[cfg(test)] - pub(in crate::conn::state) fn paused_response_for_test( - &self, - ) -> Option<&crate::conn::PausedDocumentTransfer> { + #[cfg(any(test, feature = "test-support"))] + pub fn paused_response_for_test(&self) -> Option<&super::PausedDocumentTransfer> { match self .pending_navigation_request .as_ref()? @@ -250,7 +296,7 @@ impl NavigationController { if pending.paused_interception.is_some() { return Err("navigation already has a paused interception".to_owned()); } - let request = moli_core::browser::BrowserRequestId::allocate(); + let request = crate::browser::BrowserRequestId::allocate(); let permit = super::NavigationInterceptionPermit { web_contents, navigation, @@ -322,7 +368,7 @@ impl NavigationController { if pending.paused_interception.is_some() { return Err("navigation already has a paused interception".to_owned()); } - let request = moli_core::browser::BrowserRequestId::allocate(); + let request = crate::browser::BrowserRequestId::allocate(); let permit = super::NavigationInterceptionPermit { web_contents: identity.web_contents, navigation: identity.navigation, @@ -376,7 +422,7 @@ impl NavigationController { &mut self, web_contents: WebContentsId, navigation: NavigationId, - transfer: crate::conn::PausedDocumentTransfer, + transfer: super::PausedDocumentTransfer, ) -> Result { let pending = self .pending_navigation_request @@ -390,7 +436,7 @@ impl NavigationController { if pending.paused_interception.is_some() { return Err("navigation already has a paused interception".to_owned()); } - let request = moli_core::browser::BrowserRequestId::allocate(); + let request = crate::browser::BrowserRequestId::allocate(); let permit = super::NavigationInterceptionPermit { web_contents, navigation, @@ -408,7 +454,7 @@ impl NavigationController { pub(super) fn take_response( &mut self, permit: super::NavigationInterceptionPermit, - ) -> Option { + ) -> Option { let pending = self.pending_navigation_request.as_mut().filter(|pending| { pending.navigation_id == permit.navigation && pending.document_id == permit.document @@ -443,8 +489,8 @@ impl NavigationController { pub(super) fn restore_response( &mut self, permit: super::NavigationInterceptionPermit, - transfer: crate::conn::PausedDocumentTransfer, - ) -> Result<(), Box> { + transfer: super::PausedDocumentTransfer, + ) -> Result<(), Box> { let Some(pending) = self.pending_navigation_request.as_mut().filter(|pending| { pending.navigation_id == permit.navigation && pending.document_id == permit.document @@ -472,16 +518,14 @@ impl NavigationController { self.history.resolve_traversal(destination) } - pub(in crate::conn::state) fn initial_document_build( - &self, - ) -> Option<&super::InitialDocumentBuildState> { + pub fn initial_document_build(&self) -> Option<&super::InitialDocumentBuildState> { match &self.initial_empty_document.as_ref()?.lifecycle { InitialDocumentLifecycle::Building(build) => Some(build), _ => None, } } - pub(in crate::conn::state) fn admit_initial_document_build( + pub fn admit_initial_document_build( &mut self, url: &str, build: super::InitialDocumentBuildState, @@ -492,7 +536,7 @@ impl NavigationController { initial.lifecycle = InitialDocumentLifecycle::Building(build); } - pub(in crate::conn::state) fn cancel_initial_document_build(&mut self) { + pub fn cancel_initial_document_build(&mut self) { if let Some(initial) = self.initial_empty_document.as_mut() && matches!(initial.lifecycle, InitialDocumentLifecycle::Building(_)) { @@ -500,9 +544,7 @@ impl NavigationController { } } - pub(in crate::conn::state) fn take_initial_document_build_for_commit( - &mut self, - ) -> super::InitialDocumentBuildState { + pub fn take_initial_document_build_for_commit(&mut self) -> super::InitialDocumentBuildState { let initial = self .initial_empty_document .as_mut() @@ -516,24 +558,24 @@ impl NavigationController { build } - pub(in crate::conn::state) fn pending_document(&self) -> Option<(NavigationId, DocumentId)> { + pub fn pending_document(&self) -> Option<(NavigationId, DocumentId)> { self.pending_navigation_request .as_ref() .filter(|request| !request.committed) .map(|request| (request.navigation_id, request.document_id)) } - pub(in crate::conn::state) fn current_document_navigation(&self) -> Option { + pub fn current_document_navigation(&self) -> Option { self.pending_document() .map(|(navigation, _)| navigation) .or(self.committed_document_navigation) } - pub(in crate::conn::state) fn committed_document_navigation(&self) -> Option { + pub fn committed_document_navigation(&self) -> Option { self.committed_document_navigation } - pub(in crate::conn::state) fn retains_navigation(&self, navigation: NavigationId) -> bool { + pub fn retains_navigation(&self, navigation: NavigationId) -> bool { self.pending_navigation_request .as_ref() .is_some_and(|request| request.matches(&navigation)) @@ -592,7 +634,7 @@ impl NavigationController { self.history.clear_pending_update(); } - pub(crate) fn initial_empty_document_pending_cross_document_navigation(&self) -> bool { + pub fn initial_empty_document_pending_cross_document_navigation(&self) -> bool { self.is_on_initial_empty_document() == Some(true) && self.has_pending_document_navigation() } @@ -600,18 +642,18 @@ impl NavigationController { self.history.clear(); } - pub(crate) fn is_default(&self) -> bool { + pub fn is_default(&self) -> bool { self.pending_navigation_request.is_none() && self.committed_document_navigation.is_none() && self.initial_empty_document.is_none() && self.history == NavigationHistoryState::default() } - pub(in crate::conn::state) fn current_url(&self) -> Option<&str> { + pub fn current_url(&self) -> Option<&str> { self.history.current_url() } - pub(crate) fn document_navigation_cancellation_handle( + pub fn document_navigation_cancellation_handle( &self, token: &NavigationId, ) -> Option { @@ -638,7 +680,7 @@ impl NavigationController { pub(super) fn admit_document_load( &mut self, navigation: NavigationId, - renderer: moli_core::browser::RendererPageResidenceIdentity, + renderer: crate::browser::RendererPageResidenceIdentity, cancellation: moli_fetch::FetchCancelHandle, request_cancellation: moli_fetch::FetchCancelHandle, ) -> bool { @@ -662,10 +704,10 @@ impl NavigationController { true } - pub(in crate::conn::state) fn accepts_document_preparation( + pub fn accepts_document_preparation( &self, navigation: NavigationId, - renderer: moli_core::browser::RendererPageResidenceIdentity, + renderer: crate::browser::RendererPageResidenceIdentity, ) -> bool { self.pending_navigation_request .as_ref() @@ -714,26 +756,26 @@ impl NavigationController { true } - pub(crate) fn has_inflight_background_navigation(&self) -> bool { + pub fn has_inflight_background_navigation(&self) -> bool { self.pending_navigation_request .as_ref() .is_some_and(|request| request.background_completion_pending) } - pub(crate) fn accepts_pending_document_navigation_event(&self, token: &NavigationId) -> bool { + pub fn accepts_pending_document_navigation_event(&self, token: &NavigationId) -> bool { self.pending_navigation_request .as_ref() .is_some_and(|request| request.matches(token) && !request.committed) } - pub(crate) fn accepts_document_body_completion_event(&self, token: &NavigationId) -> bool { + pub fn accepts_document_body_completion_event(&self, token: &NavigationId) -> bool { match self.pending_navigation_request.as_ref() { Some(pending) => pending.matches(token), None => self.committed_document_navigation.as_ref() == Some(token), } } - pub(crate) fn has_pending_document_navigation(&self) -> bool { + pub fn has_pending_document_navigation(&self) -> bool { self.pending_navigation_request .as_ref() .is_some_and(|request| !request.committed) @@ -763,7 +805,7 @@ impl NavigationController { self.initial_empty_document = Some(InitialDocument::new(initial_url, creator, storage_key)); } - #[cfg(test)] + #[cfg(any(test, feature = "test-support"))] pub(super) fn mark_initial_empty_document_materialized(&mut self) { if let Some(state) = self.initial_empty_document.as_mut() { state.mark_materialized(); @@ -776,17 +818,17 @@ impl NavigationController { } } - pub(crate) fn initial_empty_document_state(&self) -> Option<&InitialDocument> { + pub fn initial_empty_document_state(&self) -> Option<&InitialDocument> { self.initial_empty_document.as_ref() } - pub(crate) fn initial_empty_document_url_if_current(&self) -> Option<&str> { + pub fn initial_empty_document_url_if_current(&self) -> Option<&str> { self.initial_empty_document_state() .filter(|state| state.is_on_initial_empty_document()) .map(InitialDocument::initial_url) } - pub(crate) fn initial_empty_document_storage_key_if_current( + pub fn initial_empty_document_storage_key_if_current( &self, ) -> Option<&moli_storage_key::MoliStorageKey> { self.initial_empty_document_state() @@ -794,20 +836,20 @@ impl NavigationController { .and_then(InitialDocument::storage_key) } - pub(crate) fn is_on_initial_empty_document(&self) -> Option { + pub fn is_on_initial_empty_document(&self) -> Option { self.initial_empty_document_state() .map(InitialDocument::is_on_initial_empty_document) } - pub(crate) fn can_install_current_initial_empty_document_page(&self) -> bool { + pub fn can_install_current_initial_empty_document_page(&self) -> bool { !self.has_pending_document_navigation() && self .initial_empty_document_state() .is_none_or(InitialDocument::is_on_initial_empty_document) } - #[cfg(test)] - pub(crate) fn has_materialized_current_initial_empty_document(&self) -> bool { + #[cfg(any(test, feature = "test-support"))] + pub fn has_materialized_current_initial_empty_document(&self) -> bool { self.initial_empty_document_state() .is_some_and(|state| state.is_on_initial_empty_document() && state.materialized()) } @@ -855,11 +897,11 @@ impl NavigationController { self.history.mark_traverse_to_entry(entry_id); } - pub(crate) fn navigation_history_entry_url(&self, entry_id: i32) -> Option { + pub fn navigation_history_entry_url(&self, entry_id: i32) -> Option { self.history.entry_url(entry_id) } - pub(crate) fn navigation_history_snapshot(&self) -> (usize, Vec) { + pub fn navigation_history_snapshot(&self) -> (usize, Vec) { self.history.snapshot() } @@ -890,7 +932,7 @@ impl NavigationController { self.history.record_loaded_entry(entry, update); } - #[cfg(test)] + #[cfg(any(test, feature = "test-support"))] pub(super) fn record_navigation_history_for_test(&mut self, snapshot: (String, String)) { let entry = self.navigation_history_entry_for_page_snapshot(snapshot); let update = self.history.take_pending_update(); @@ -901,7 +943,7 @@ impl NavigationController { &mut self, url: String, title: String, - history_update: moli_core::page::SameDocumentHistoryUpdate, + history_update: crate::page::SameDocumentHistoryUpdate, ) -> bool { self.history .record_same_document_update(url, title, history_update) @@ -909,36 +951,30 @@ impl NavigationController { } impl super::WebContents { - pub(in crate::conn::state) fn navigation(&self) -> &NavigationController { + pub fn navigation(&self) -> &NavigationController { &self.navigation } - pub(in crate::conn::state) fn start_document_navigation(&mut self) -> NavigationId { + pub fn start_document_navigation(&mut self) -> NavigationId { self.navigation.start_document_navigation() } - #[cfg(test)] - pub(in crate::conn::state) fn commit_pending_document_navigation_if_matches( - &mut self, - token: &NavigationId, - ) -> bool { + #[cfg(any(test, feature = "test-support"))] + pub fn commit_pending_document_navigation_if_matches(&mut self, token: &NavigationId) -> bool { self.navigation .commit_pending_document_navigation_if_matches(token) } - pub(in crate::conn::state) fn clear_pending_document_navigation_if_matches( - &mut self, - token: &NavigationId, - ) -> bool { + pub fn clear_pending_document_navigation_if_matches(&mut self, token: &NavigationId) -> bool { self.navigation .clear_pending_document_navigation_if_matches(token) } - pub(in crate::conn::state) fn clear_document_navigation_state(&mut self) { + pub fn clear_document_navigation_state(&mut self) { self.navigation.clear_document_navigation_state() } - pub(in crate::conn::state) fn arm_background_navigation_completion( + pub fn arm_background_navigation_completion( &mut self, token: &NavigationId, additional_cancellation: Option, @@ -947,15 +983,12 @@ impl super::WebContents { .arm_background_navigation_completion(token, additional_cancellation) } - pub(in crate::conn::state) fn settle_background_navigation_completion( - &mut self, - token: &NavigationId, - ) -> bool { + pub fn settle_background_navigation_completion(&mut self, token: &NavigationId) -> bool { self.navigation .settle_background_navigation_completion(token) } - pub(in crate::conn::state) fn begin_initial_empty_document( + pub fn begin_initial_empty_document( &mut self, initial_url: String, creator: Option, @@ -965,32 +998,27 @@ impl super::WebContents { .begin_initial_empty_document(initial_url, creator, storage_key) } - #[cfg(test)] - pub(in crate::conn::state) fn mark_initial_empty_document_materialized(&mut self) { + #[cfg(any(test, feature = "test-support"))] + pub fn mark_initial_empty_document_materialized(&mut self) { self.navigation.mark_initial_empty_document_materialized() } - #[cfg(test)] - pub(in crate::conn::state) fn mark_initial_empty_document_exited(&mut self) { + #[cfg(any(test, feature = "test-support"))] + pub fn mark_initial_empty_document_exited(&mut self) { self.navigation.mark_initial_empty_document_exited() } - pub(in crate::conn::state) fn mark_next_navigation_history_replace_current(&mut self) { + pub fn mark_next_navigation_history_replace_current(&mut self) { self.navigation .mark_next_navigation_history_replace_current() } - pub(in crate::conn::state) fn mark_next_navigation_history_replace_initial_empty_document( - &mut self, - ) { + pub fn mark_next_navigation_history_replace_initial_empty_document(&mut self) { self.navigation .mark_next_navigation_history_replace_initial_empty_document() } - pub(in crate::conn::state) fn mark_next_navigation_history_traverse_to_entry( - &mut self, - entry_id: i32, - ) { + pub fn mark_next_navigation_history_traverse_to_entry(&mut self, entry_id: i32) { self.navigation .mark_next_navigation_history_traverse_to_entry(entry_id) } @@ -1006,7 +1034,7 @@ fn is_initial_empty_document_url(raw_url: &str) -> bool { #[cfg(test)] mod tests { use super::*; - use moli_core::page::SameDocumentHistoryUpdate; + use crate::page::SameDocumentHistoryUpdate; #[test] fn navigation_history_seed_entry_preserves_pending_update() { diff --git a/moli-protocol/src/conn/state/web_contents/navigation_controller/history.rs b/moli-core/src/browser/web_contents/navigation_controller/history.rs similarity index 99% rename from moli-protocol/src/conn/state/web_contents/navigation_controller/history.rs rename to moli-core/src/browser/web_contents/navigation_controller/history.rs index b76044125a..bd954e84c4 100644 --- a/moli-protocol/src/conn/state/web_contents/navigation_controller/history.rs +++ b/moli-core/src/browser/web_contents/navigation_controller/history.rs @@ -1,12 +1,12 @@ -use moli_core::page::SessionHistoryUpdateKind; +use crate::page::SessionHistoryUpdateKind; -pub(crate) enum HistoryTraversalDestination { +pub enum HistoryTraversalDestination { Entry(i32), Delta(i64), } #[derive(Debug, PartialEq, Eq)] -pub(crate) enum ResolvedHistoryTraversal { +pub enum ResolvedHistoryTraversal { Noop, Entry { entry_id: i32, diff --git a/moli-protocol/src/conn/state/web_contents/navigation_history.rs b/moli-core/src/browser/web_contents/navigation_history.rs similarity index 72% rename from moli-protocol/src/conn/state/web_contents/navigation_history.rs rename to moli-core/src/browser/web_contents/navigation_history.rs index 6f9b0c8a6d..1419f99b6e 100644 --- a/moli-protocol/src/conn/state/web_contents/navigation_history.rs +++ b/moli-core/src/browser/web_contents/navigation_history.rs @@ -1,7 +1,6 @@ use super::WebContents; -use crate::conn::state::PageNavigationHistoryEntry; -use crate::conn::state::{HistoryTraversalDestination, ResolvedHistoryTraversal}; -use moli_core::{ +use super::{HistoryTraversalDestination, PageNavigationHistoryEntry, ResolvedHistoryTraversal}; +use crate::{ browser::{DocumentId, WebContentsId}, page::{CompletedPageCommand, PendingPageCommand, SameDocumentHistoryUpdate}, }; @@ -10,49 +9,41 @@ use url::Url; #[cfg(test)] mod tests; -pub(crate) struct SameDocumentNavigationCommitted { - pub(crate) web_contents: WebContentsId, - pub(crate) document: DocumentId, - pub(crate) url: Url, +pub struct SameDocumentNavigationCommitted { + pub web_contents: WebContentsId, + pub document: DocumentId, + pub url: Url, } impl WebContents { - #[cfg(test)] - pub(in crate::conn::state) fn record_navigation_history_for_test( - &mut self, - snapshot: (String, String), - ) { + #[cfg(any(test, feature = "test-support"))] + pub fn record_navigation_history_for_test(&mut self, snapshot: (String, String)) { self.navigation.record_navigation_history_for_test(snapshot); } - pub(in crate::conn::state) fn mark_renderer_crashed(&mut self) { + pub fn mark_renderer_crashed(&mut self) { self.crashed = true; self.navigation.clear_navigation_history(); } - pub(in crate::conn::state) fn resolve_history_traversal( + pub fn resolve_history_traversal( &self, destination: HistoryTraversalDestination, ) -> Result { self.navigation.resolve_history_traversal(destination) } - pub(in crate::conn::state) fn navigation_history_snapshot( - &self, - ) -> (usize, Vec) { + pub fn navigation_history_snapshot(&self) -> (usize, Vec) { self.navigation.navigation_history_snapshot() } - pub(in crate::conn::state) fn navigation_history_entry_url( - &self, - entry_id: i32, - ) -> Option { + pub fn navigation_history_entry_url(&self, entry_id: i32) -> Option { self.navigation.navigation_history_entry_url(entry_id) } - pub(in crate::conn::state) fn commit_document_title( + pub fn commit_document_title( &mut self, - change: &moli_core::RendererDocumentTitleChanged, + change: &crate::RendererDocumentTitleChanged, ) -> Option { let document = self.main_frame.current_document.as_ref()?; let snapshot = document.lifecycle.snapshot()?; @@ -71,7 +62,7 @@ impl WebContents { ) } - pub(in crate::conn::state) fn commit_same_document_navigation( + pub fn commit_same_document_navigation( &mut self, document_id: DocumentId, url: Url, @@ -102,9 +93,7 @@ impl WebContents { }) } - pub(in crate::conn::state) fn start_reset_navigation_history( - &self, - ) -> Result { + pub fn start_reset_navigation_history(&self) -> Result { if !self.navigation.can_reset_navigation_history() { return Err("History cannot be pruned".to_owned()); } @@ -117,7 +106,7 @@ impl WebContents { .map_err(|error| error.to_string()) } - pub(in crate::conn::state) fn finish_reset_navigation_history( + pub fn finish_reset_navigation_history( &mut self, completion: CompletedPageCommand, ) -> Result { diff --git a/moli-protocol/src/conn/state/web_contents/navigation_history/tests.rs b/moli-core/src/browser/web_contents/navigation_history/tests.rs similarity index 98% rename from moli-protocol/src/conn/state/web_contents/navigation_history/tests.rs rename to moli-core/src/browser/web_contents/navigation_history/tests.rs index 6417763b63..2fd740e2db 100644 --- a/moli-protocol/src/conn/state/web_contents/navigation_history/tests.rs +++ b/moli-core/src/browser/web_contents/navigation_history/tests.rs @@ -1,6 +1,6 @@ use super::*; -use crate::conn::state::web_contents::tests::BrowserFixture; -use moli_core::{RendererDocumentTitleChanged, page::RendererDocumentLifecycleIdentity}; +use crate::browser::web_contents::tests::BrowserFixture; +use crate::{RendererDocumentTitleChanged, page::RendererDocumentLifecycleIdentity}; use serde_json::json; fn title_change(browser: &BrowserFixture, title: &str) -> RendererDocumentTitleChanged { diff --git a/moli-protocol/src/conn/state/web_contents/navigation_interception.rs b/moli-core/src/browser/web_contents/navigation_interception.rs similarity index 87% rename from moli-protocol/src/conn/state/web_contents/navigation_interception.rs rename to moli-core/src/browser/web_contents/navigation_interception.rs index f076bc6bb3..ab05ed8ab4 100644 --- a/moli-protocol/src/conn/state/web_contents/navigation_interception.rs +++ b/moli-core/src/browser/web_contents/navigation_interception.rs @@ -1,10 +1,10 @@ -use anyhow::Context; -use moli_core::{ +use crate::{ browser::{ BrowserRequestId, DocumentId, NavigationId, NavigationRequestLoadPolicy, WebContentsId, }, page::SubresourceAuthCredentials, }; +use anyhow::Context; use moli_fetch::{ NetworkFetchResult, NetworkObservationJournal, RawResponse, StreamingRawResponse, }; @@ -18,7 +18,7 @@ use super::{ /// A single Browser decision. Copying a protocol correlation cannot duplicate /// its authority: the owning pending navigation consumes this request once. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct NavigationInterceptionPermit { +pub struct NavigationInterceptionPermit { pub(super) web_contents: WebContentsId, pub(super) navigation: NavigationId, pub(super) document: DocumentId, @@ -26,16 +26,16 @@ pub(crate) struct NavigationInterceptionPermit { } impl NavigationInterceptionPermit { - pub(crate) fn navigation(self) -> NavigationId { + pub fn navigation(self) -> NavigationId { self.navigation } - pub(in crate::conn) fn web_contents(self) -> WebContentsId { + pub fn web_contents(self) -> WebContentsId { self.web_contents } - #[cfg(test)] - pub(crate) fn for_correlation_test() -> Self { + #[cfg(any(test, feature = "test-support"))] + pub fn for_correlation_test() -> Self { Self { web_contents: WebContentsId::allocate(), navigation: NavigationId::allocate(), @@ -49,7 +49,7 @@ impl NavigationInterceptionPermit { /// and how to resume it. It deliberately contains no Target, session, loader, /// protocol request id, or frontend result state. #[derive(Debug)] -pub(crate) struct NavigationRequestInterception { +pub struct NavigationRequestInterception { requested_url: Url, method: String, body: Option>, @@ -59,7 +59,7 @@ pub(crate) struct NavigationRequestInterception { } impl NavigationRequestInterception { - pub(crate) fn new( + pub fn new( requested_url: Url, method: String, body: Option>, @@ -117,7 +117,7 @@ impl NavigationRequestInterception { /// Navigation identified by `permit`; supersession makes admission fail rather /// than falling back to a current Target/loader selection. #[derive(Debug)] -pub(crate) struct ClaimedNavigationRequest { +pub struct ClaimedNavigationRequest { permit: NavigationInterceptionPermit, request: NavigationRequestInterception, } @@ -130,11 +130,11 @@ impl ClaimedNavigationRequest { Self { permit, request } } - pub(crate) fn permit(&self) -> NavigationInterceptionPermit { + pub fn permit(&self) -> NavigationInterceptionPermit { self.permit } - pub(crate) fn apply_overrides( + pub fn apply_overrides( &mut self, requested_url: Option, method: Option, @@ -149,12 +149,12 @@ impl ClaimedNavigationRequest { /// The admitted Browser operation retains the actual request across auth /// pauses and retries. No Target, session, loader ID or frontend configuration /// is needed to resume it, and no Browser borrow is held while fetching. -pub(crate) struct InterceptedNavigationLoad { - pub(in crate::conn) load: AdmittedNavigationLoad, - pub(in crate::conn) requested_url: Url, - pub(in crate::conn) method: String, +pub struct InterceptedNavigationLoad { + pub load: AdmittedNavigationLoad, + pub requested_url: Url, + pub method: String, body: Option>, - pub(in crate::conn) headers: moli_fetch::RequestHeaders, + pub headers: moli_fetch::RequestHeaders, prior_observations: NetworkObservationJournal, } @@ -168,7 +168,7 @@ impl std::fmt::Debug for InterceptedNavigationLoad { } impl InterceptedNavigationLoad { - pub(in crate::conn) fn new( + pub fn new( load: AdmittedNavigationLoad, requested_url: Url, method: String, @@ -185,7 +185,7 @@ impl InterceptedNavigationLoad { } } - pub(crate) async fn fetch_streaming( + pub async fn fetch_streaming( self, auth: Option, ) -> anyhow::Result> { @@ -203,7 +203,7 @@ impl InterceptedNavigationLoad { Ok(self.with_response(response)) } - pub(crate) async fn fetch_auth( + pub async fn fetch_auth( self, auth: SubresourceAuthCredentials, ) -> anyhow::Result> { @@ -221,7 +221,7 @@ impl InterceptedNavigationLoad { Ok(self.with_response(response)) } - pub(in crate::conn) fn with_response( + pub fn with_response( mut self, response: NetworkFetchResult, ) -> InterceptedNavigationResponse { @@ -234,7 +234,7 @@ impl InterceptedNavigationLoad { } } - pub(in crate::conn) fn into_request_parts( + pub fn into_request_parts( self, ) -> ( AdmittedNavigationLoad, @@ -251,28 +251,40 @@ impl InterceptedNavigationLoad { self.headers, ) } + + pub(crate) fn into_owner_parts( + self, + ) -> ( + AdmittedNavigationLoad, + Url, + String, + Option>, + moli_fetch::RequestHeaders, + ) { + self.into_request_parts() + } } #[derive(Debug)] -pub(crate) struct InterceptedNavigationResponse { +pub struct InterceptedNavigationResponse { work: InterceptedNavigationLoad, response: NetworkFetchResult, } impl InterceptedNavigationResponse { - pub(in crate::conn) fn web_contents(&self) -> WebContentsId { + pub fn web_contents(&self) -> WebContentsId { self.identity().web_contents } - pub(crate) fn response(&self) -> &R { + pub fn response(&self) -> &R { self.response.response() } - pub(crate) fn observation_journal(&self) -> &NetworkObservationJournal { + pub fn observation_journal(&self) -> &NetworkObservationJournal { self.response.observation_journal() } - pub(crate) fn into_parts(self) -> (InterceptedNavigationLoad, NetworkFetchResult) { + pub fn into_parts(self) -> (InterceptedNavigationLoad, NetworkFetchResult) { (self.work, self.response) } @@ -281,48 +293,19 @@ impl InterceptedNavigationResponse { } } -impl InterceptedNavigationResponse { - pub(crate) async fn materialize( - self, - ) -> anyhow::Result> { - let (response, observations) = self.response.into_parts_with_observation_journal(); - let response = response - .into_materialized_raw_response() - .await - .context("failed to read page body from stream")?; - Ok(InterceptedNavigationResponse { - work: self.work, - response: NetworkFetchResult::with_observation_journal(response, observations), - }) - } -} - impl InterceptedNavigationResponse { - pub(crate) fn retry(self) -> InterceptedNavigationLoad { + pub fn retry(self) -> InterceptedNavigationLoad { let (mut work, response) = self.into_parts(); let (response, observations) = response.into_parts_with_observation_journal(); - let redirect_headers = if response.redirect_chain.len() > work.load.redirect_chain.len() { - work.load.redirect_headers.take() - } else { - None - }; - let mut request = moli_fetch::Request::get_with_url(work.requested_url.clone()) - .with_redirect_headers(redirect_headers); + let mut request = moli_fetch::Request::get_with_url(work.requested_url); request.method = work.method; request.body = work.body; request.request_headers = work.headers; - for redirect in response - .redirect_chain - .iter() - .skip(work.load.redirect_chain.len()) - { - request.apply_redirect_status(redirect.status); - } - work.requested_url = response.final_url; + let request = work.load.redirected_request(response, request); + work.requested_url = request.url; work.method = request.method; work.body = request.body; work.headers = request.request_headers; - work.load.redirect_chain = response.redirect_chain; work.prior_observations = observations; work } @@ -349,7 +332,7 @@ pub(super) struct PausedNavigationRequest { #[derive(Debug)] pub(super) struct PausedNavigationResponse { pub(super) request: BrowserRequestId, - pub(super) transfer: crate::conn::PausedDocumentTransfer, + pub(super) transfer: super::PausedDocumentTransfer, } #[derive(Debug)] @@ -360,7 +343,7 @@ pub(super) enum PausedNavigationInterception { } impl WebContents { - pub(in crate::conn::state) fn pause_navigation_request( + pub fn pause_navigation_request( &mut self, navigation: NavigationId, request: NavigationRequestInterception, @@ -368,7 +351,7 @@ impl WebContents { self.navigation.pause_request(self.id, navigation, request) } - pub(in crate::conn::state) fn take_navigation_request( + pub fn take_navigation_request( &mut self, permit: NavigationInterceptionPermit, ) -> Option { @@ -378,7 +361,7 @@ impl WebContents { self.navigation.take_request(permit) } - pub(in crate::conn::state) fn start_claimed_navigation_request( + pub fn start_claimed_navigation_request( &mut self, request: ClaimedNavigationRequest, inherited: InheritedDocumentPolicy, @@ -398,7 +381,7 @@ impl WebContents { )) } - pub(in crate::conn::state) fn start_navigation_load_for_interception( + pub fn start_navigation_load_for_interception( &mut self, permit: NavigationInterceptionPermit, policy: NavigationRequestLoadPolicy, @@ -410,7 +393,7 @@ impl WebContents { self.start_navigation_load(permit.navigation, policy, inherited) } - pub(in crate::conn::state) fn pause_navigation_auth( + pub fn pause_navigation_auth( &mut self, response: InterceptedNavigationResponse, ) -> Result { @@ -420,7 +403,7 @@ impl WebContents { self.navigation.pause_auth_response(response) } - pub(in crate::conn::state) fn take_navigation_auth( + pub fn take_navigation_auth( &mut self, permit: NavigationInterceptionPermit, ) -> Option> { @@ -430,30 +413,30 @@ impl WebContents { self.navigation.take_auth_response(permit) } - pub(in crate::conn::state) fn pause_navigation_response( + pub fn pause_navigation_response( &mut self, navigation: NavigationId, - transfer: crate::conn::PausedDocumentTransfer, + transfer: super::PausedDocumentTransfer, ) -> Result { self.navigation .pause_response(self.id, navigation, transfer) } - pub(in crate::conn::state) fn take_navigation_response( + pub fn take_navigation_response( &mut self, permit: NavigationInterceptionPermit, - ) -> Option { + ) -> Option { if permit.web_contents != self.id { return None; } self.navigation.take_response(permit) } - pub(in crate::conn::state) fn restore_navigation_response( + pub fn restore_navigation_response( &mut self, permit: NavigationInterceptionPermit, - transfer: crate::conn::PausedDocumentTransfer, - ) -> Result<(), Box> { + transfer: super::PausedDocumentTransfer, + ) -> Result<(), Box> { if permit.web_contents != self.id { return Err(Box::new(transfer)); } @@ -464,8 +447,10 @@ impl WebContents { #[cfg(test)] mod tests { use super::*; - use crate::conn::state::web_contents::tests::BrowserFixture; - use moli_core::page::SubresourceAuthScheme; + use crate::browser::web_contents::{ + DocumentBodySource, PausedDocumentTransfer, tests::BrowserFixture, + }; + use crate::page::SubresourceAuthScheme; use moli_fetch::ResponseHead; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, @@ -523,7 +508,7 @@ mod tests { ) } - fn paused_response(url: Url) -> crate::conn::PausedDocumentTransfer { + fn paused_response(url: Url) -> PausedDocumentTransfer { let response = RawResponse::from_head_and_body( ResponseHead { final_url: url.clone(), @@ -538,9 +523,9 @@ mod tests { }, b"response body".to_vec(), ); - crate::conn::PausedDocumentTransfer::pending( + PausedDocumentTransfer::pending( NavigationRequestLoadPolicy::DocumentInitiated, - crate::conn::DocumentBodySource::BufferedRaw { + DocumentBodySource::BufferedRaw { requested_url: url, request_method: "GET".to_owned(), request_headers: Default::default(), @@ -793,7 +778,7 @@ mod tests { let mut browser = BrowserFixture::new(); let work = request(&mut browser, url); let auth = SubresourceAuthCredentials { - target: moli_core::page::SubresourceAuthTarget::Server, + target: crate::page::SubresourceAuthTarget::Server, username: "user".to_owned(), password: "pass".to_owned(), scheme: SubresourceAuthScheme::Digest, diff --git a/moli-protocol/src/conn/state/web_contents/navigation_load.rs b/moli-core/src/browser/web_contents/navigation_load.rs similarity index 86% rename from moli-protocol/src/conn/state/web_contents/navigation_load.rs rename to moli-core/src/browser/web_contents/navigation_load.rs index 09f21cb2a7..9ca51e7b78 100644 --- a/moli-protocol/src/conn/state/web_contents/navigation_load.rs +++ b/moli-core/src/browser/web_contents/navigation_load.rs @@ -1,4 +1,4 @@ -use moli_core::{ +use crate::{ browser::{ DocumentId, NavigationId, NavigationRequestLoadPolicy, RendererPageResidenceIdentity, WebContentsId, @@ -24,7 +24,7 @@ mod tests; /// Move-owned navigation work. No caller can borrow or reconfigure its engine, /// replace its storage, or bind its response to a different Browser navigation. -pub(in crate::conn) struct AdmittedNavigationLoad { +pub struct AdmittedNavigationLoad { // Response preparation consumes admission even if renderer preparation fails. identity: Option, engine: NavigationEngine, @@ -42,35 +42,33 @@ pub(in crate::conn) struct AdmittedNavigationLoad { /// The identity is inherited from load admission, never reconstructed from the /// current Target, session, or pending navigation when the response completes. -pub(in crate::conn) struct PreparedNavigationResponse { +pub struct PreparedNavigationResponse { pub(super) identity: DocumentNavigationIdentity, pub(super) page: PreparedDocumentPage, } impl PreparedNavigationResponse { - pub(in crate::conn) fn renderer_devtools_agent_token( - &self, - ) -> moli_core::page::RendererDevToolsAgentToken { + pub fn renderer_devtools_agent_token(&self) -> crate::page::RendererDevToolsAgentToken { self.page.renderer_devtools_agent_token() } - pub(in crate::conn) fn inspection_configuration_endpoint( + pub fn inspection_configuration_endpoint( &self, ) -> moli_renderer_v8::RendererPreparedDocumentInspectionEndpoint { self.page.inspection_configuration_endpoint() } - #[cfg(test)] - pub(in crate::conn) async fn materialize( + #[cfg(any(test, feature = "test-support"))] + pub async fn materialize( self, - policy: Option, - ) -> anyhow::Result { + policy: Option, + ) -> anyhow::Result { self.page.materialize(policy).await } } impl WebContents { - pub(in crate::conn::state) fn start_navigation_load( + pub fn start_navigation_load( &mut self, navigation: NavigationId, policy: NavigationRequestLoadPolicy, @@ -141,23 +139,43 @@ impl AdmittedNavigationLoad { .expect("navigation response admission already consumed") } - pub(in crate::conn) fn web_contents_id(&self) -> WebContentsId { + pub fn web_contents_id(&self) -> WebContentsId { self.identity().web_contents } - pub(in crate::conn) fn navigation_id(&self) -> NavigationId { + pub fn navigation_id(&self) -> NavigationId { self.identity().navigation } - pub(in crate::conn) fn document_id(&self) -> DocumentId { + pub fn document_id(&self) -> DocumentId { self.identity().document } - pub(in crate::conn) fn renderer_page(&self) -> RendererPageResidenceIdentity { + pub fn renderer_page(&self) -> RendererPageResidenceIdentity { RendererPageResidenceIdentity::from_parts( self.reservation.local_host_id(), self.reservation.page_id(), ) } - pub(in crate::conn) fn set_redirect_state( + pub(crate) fn redirected_request( + &mut self, + response: moli_fetch::RawResponse, + mut request: moli_fetch::Request, + ) -> moli_fetch::Request { + if response.redirect_chain.len() > self.redirect_chain.len() { + request = request.with_redirect_headers(self.redirect_headers.take()); + } + for redirect in response + .redirect_chain + .iter() + .skip(self.redirect_chain.len()) + { + request.apply_redirect_status(redirect.status); + } + request.url = response.final_url; + self.redirect_chain = response.redirect_chain; + request + } + + pub fn set_redirect_state( &mut self, headers: Option, chain: Vec, @@ -181,11 +199,11 @@ impl AdmittedNavigationLoad { .iter() .any(|pattern| moli_fetch::url_pattern_matches(pattern, raw_url)) { - return Err(crate::conn::NavigationRequestBlocked.into()); + return Err(crate::browser::NavigationRequestBlocked.into()); } if self.network_offline { - return Err(crate::conn::NavigationNetworkError { - kind: crate::conn::NavigationNetworkErrorKind::InternetDisconnected, + return Err(crate::browser::NavigationNetworkError { + kind: crate::browser::NavigationNetworkErrorKind::InternetDisconnected, unreachable_url: Url::parse(raw_url)?, request_method: method.to_owned(), request_headers: headers.clone(), @@ -206,7 +224,7 @@ impl AdmittedNavigationLoad { self.policy != NavigationRequestLoadPolicy::BrowserInitiated } - pub(in crate::conn) async fn fetch_navigation( + pub async fn fetch_navigation( &mut self, method: &str, raw_url: &str, @@ -256,7 +274,7 @@ impl AdmittedNavigationLoad { Ok(request) } - pub(in crate::conn) async fn fetch_intercepted_response( + pub async fn fetch_intercepted_response( &self, method: &str, raw_url: &str, @@ -275,7 +293,7 @@ impl AdmittedNavigationLoad { .await } - pub(in crate::conn) async fn fetch_intercepted_auth_response( + pub async fn fetch_intercepted_auth_response( &self, method: &str, raw_url: &str, @@ -298,7 +316,7 @@ impl AdmittedNavigationLoad { Ok(response) } - pub(in crate::conn) async fn prepare_document_response_async( + pub async fn prepare_document_response_async( &mut self, requested_url: Url, final_url: Url, @@ -347,8 +365,8 @@ impl AdmittedNavigationLoad { } /// Direct Page-builder fixtures have no Browser transaction to commit. - #[cfg(test)] - pub(in crate::conn) fn for_fixture( + #[cfg(any(test, feature = "test-support"))] + pub fn for_fixture( engine: NavigationEngine, resource_storage: NavigationResourceStorageHandles, page_storage: NavigationPageStorageHandles, @@ -361,7 +379,7 @@ impl AdmittedNavigationLoad { Self { identity: Some(DocumentNavigationIdentity { web_contents: WebContentsId::allocate(), - frame_slot: moli_core::browser::MainFrameSlotId::allocate(), + frame_slot: crate::browser::MainFrameSlotId::allocate(), navigation: NavigationId::allocate(), document: DocumentId::allocate(), cancellation: FetchCancelHandle::new(), diff --git a/moli-protocol/src/conn/state/web_contents/navigation_load/tests.rs b/moli-core/src/browser/web_contents/navigation_load/tests.rs similarity index 99% rename from moli-protocol/src/conn/state/web_contents/navigation_load/tests.rs rename to moli-core/src/browser/web_contents/navigation_load/tests.rs index c397eedc45..25d37ed4e0 100644 --- a/moli-protocol/src/conn/state/web_contents/navigation_load/tests.rs +++ b/moli-core/src/browser/web_contents/navigation_load/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::conn::state::web_contents::tests::{BrowserFixture, prepare}; +use crate::browser::web_contents::tests::{BrowserFixture, prepare}; #[tokio::test] async fn precommit_retirement_preserves_document_and_retires_request_history() { diff --git a/moli-protocol/src/conn/state/web_contents/network_request_policy.rs b/moli-core/src/browser/web_contents/network_request_policy.rs similarity index 64% rename from moli-protocol/src/conn/state/web_contents/network_request_policy.rs rename to moli-core/src/browser/web_contents/network_request_policy.rs index 0a4aee4953..c8dee72f87 100644 --- a/moli-protocol/src/conn/state/web_contents/network_request_policy.rs +++ b/moli-core/src/browser/web_contents/network_request_policy.rs @@ -1,13 +1,13 @@ /// Installed page-level request policy. Frontends resolve their contributions /// before supplying this value; context header defaults remain context-owned. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub(in crate::conn) struct NetworkRequestPolicy { - pub(in crate::conn) cache_disabled: bool, - pub(in crate::conn) bypass_service_worker: bool, - pub(in crate::conn) blocked_url_patterns: Vec, - pub(in crate::conn) extra_headers: moli_fetch::RequestHeaders, +pub struct NetworkRequestPolicy { + pub cache_disabled: bool, + pub bypass_service_worker: bool, + pub blocked_url_patterns: Vec, + pub extra_headers: moli_fetch::RequestHeaders, } -pub(in crate::conn) fn merge_extra_header_layers( +pub fn merge_extra_header_layers( layers: &[&moli_fetch::RequestHeaders], ) -> moli_fetch::RequestHeaders { let mut headers = moli_fetch::RequestHeaders::default(); diff --git a/moli-protocol/src/conn/state/web_contents/page_surface.rs b/moli-core/src/browser/web_contents/page_surface.rs similarity index 91% rename from moli-protocol/src/conn/state/web_contents/page_surface.rs rename to moli-core/src/browser/web_contents/page_surface.rs index 350a0071f8..a3e28058ee 100644 --- a/moli-protocol/src/conn/state/web_contents/page_surface.rs +++ b/moli-core/src/browser/web_contents/page_surface.rs @@ -1,23 +1,23 @@ -use super::super::emulation::{ +use super::WebContents; +use crate::browser::{ EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, EmulatedNetworkConditions, }; -use super::WebContents; #[cfg(test)] use serde_json::json; /// A derived Browser value, never stored alongside the installed policy. -pub(in crate::conn) struct PageSurface { +pub struct PageSurface { network_conditions: Option, geolocation_override: Option, max_touch_points: u32, - pub(in crate::conn) navigator_queries: moli_page_types::NavigatorQueryOverrides, + pub navigator_queries: moli_page_types::NavigatorQueryOverrides, focus_emulation_enabled: bool, foreground: bool, window_document_hidden: bool, } impl WebContents { - pub(in crate::conn) fn page_surface( + pub fn page_surface( &self, foreground: bool, default_network_conditions: Option, @@ -49,15 +49,15 @@ impl PageSurface { self.max_touch_points } - pub(in crate::conn) fn document_has_focus(&self) -> bool { + pub fn document_has_focus(&self) -> bool { self.document_is_focused() } - pub(in crate::conn) fn document_hidden(&self) -> bool { + pub fn document_hidden(&self) -> bool { !self.document_is_visible() } - pub(in crate::conn) fn document_visibility_state(&self) -> &'static str { + pub fn document_visibility_state(&self) -> &'static str { if self.document_is_visible() { "visible" } else { @@ -65,7 +65,7 @@ impl PageSurface { } } - pub(in crate::conn) fn navigator_overrides(&self) -> moli_page_types::NavigatorOverrides { + pub fn navigator_overrides(&self) -> moli_page_types::NavigatorOverrides { moli_page_types::NavigatorOverrides { online: self .network_conditions @@ -98,7 +98,7 @@ impl PageSurface { (self.foreground && !self.window_document_hidden) || self.focus_emulation_enabled } - pub(in crate::conn) fn document_activity(&self) -> moli_page_types::DocumentActivity { + pub fn document_activity(&self) -> moli_page_types::DocumentActivity { moli_page_types::DocumentActivity::new( self.document_is_focused(), self.document_is_visible(), @@ -138,7 +138,7 @@ mod tests { #[tokio::test] async fn browser_page_surface_runs_without_devtools_registration() { - use moli_core::runtime::{Browser, BrowserConfig}; + use crate::runtime::{Browser, BrowserConfig}; let browser = Browser::new(BrowserConfig::default()).unwrap(); let mut page = browser .fetch("data:text/html,

page surface

") diff --git a/moli-core/src/browser/web_contents/paused_document_transfer.rs b/moli-core/src/browser/web_contents/paused_document_transfer.rs new file mode 100644 index 0000000000..cade2c76f1 --- /dev/null +++ b/moli-core/src/browser/web_contents/paused_document_transfer.rs @@ -0,0 +1,709 @@ +use anyhow::Context as _; +use tokio::sync::{mpsc, oneshot}; +use url::Url; + +use crate::browser::{ + CapturedBody, CapturedBodyWriter, NavigationRequestLoadPolicy, ensure_materialize_limit, +}; +use moli_cookie_jar::StoredCookieQueryReport; +use moli_fetch::{NetworkObservationJournal, RawResponse, ResponseHead, StreamingRawResponse}; + +use crate::browser::BrowserPreparedNavigationResponse; + +/// Renderer resources prepared before a response-stage interception decision. +pub struct PausedResponsePreparedDocument { + prepared_page: BrowserPreparedNavigationResponse, + renderer_body_tx: mpsc::Sender>, + renderer_completion_tx: oneshot::Sender>, +} + +impl std::fmt::Debug for PausedResponsePreparedDocument { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("PausedResponsePreparedDocument") + .field( + "renderer_devtools_agent_token", + &self.prepared_page.renderer_devtools_agent_token(), + ) + .finish_non_exhaustive() + } +} + +impl PausedResponsePreparedDocument { + pub fn new( + prepared_page: BrowserPreparedNavigationResponse, + renderer_body_tx: mpsc::Sender>, + renderer_completion_tx: oneshot::Sender>, + ) -> Self { + Self { + prepared_page, + renderer_body_tx, + renderer_completion_tx, + } + } + + pub fn renderer_devtools_agent_token(&self) -> crate::page::RendererDevToolsAgentToken { + self.prepared_page.renderer_devtools_agent_token() + } + + pub fn into_parts( + self, + ) -> ( + BrowserPreparedNavigationResponse, + mpsc::Sender>, + oneshot::Sender>, + ) { + ( + self.prepared_page, + self.renderer_body_tx, + self.renderer_completion_tx, + ) + } +} + +/// Browser-owned response body and renderer preparation retained while a +/// response-stage interception decision is pending. +#[derive(Debug)] +pub enum DocumentBodySource { + BufferedRaw { + requested_url: Url, + request_method: String, + request_headers: moli_fetch::RequestHeaders, + response: RawResponse, + network_observation_journal: NetworkObservationJournal, + }, + StreamingRaw { + requested_url: Url, + request_method: String, + request_headers: moli_fetch::RequestHeaders, + response: StreamingRawResponse, + network_observation_journal: NetworkObservationJournal, + prepared_document: Option>, + }, + CapturedRaw { + requested_url: Url, + request_method: String, + request_headers: moli_fetch::RequestHeaders, + head: ResponseHead, + body: CapturedBody, + network_observation_journal: NetworkObservationJournal, + }, +} + +#[derive(Debug)] +pub struct PausedDocumentTransfer { + request_load_policy: NavigationRequestLoadPolicy, + state: PausedDocumentTransferState, +} + +#[derive(Debug)] +enum PausedDocumentTransferState { + Pending { + body: DocumentBodySource, + }, + ActiveBodyStream { + stream: ActiveDocumentBodyStreamState, + }, +} + +#[derive(Debug)] +struct ActiveDocumentBodyStreamState { + requested_url: Url, + request_method: String, + request_headers: moli_fetch::RequestHeaders, + response: StreamingRawResponse, + network_observation_journal: NetworkObservationJournal, + captured_body: CapturedBodyWriter, + unread_body: Vec, + offset: usize, + finished: bool, +} + +#[derive(Debug)] +pub struct PendingFetchResponseOpenedBodyStream { + pub handle: String, + pub buffered_bytes: Option>, + pub transfer: PausedDocumentTransfer, +} + +#[derive(Debug)] +pub enum OpenBodyStreamError { + NotOpenable(Box), + Failed { + transfer: Box, + error: anyhow::Error, + }, +} + +pub struct PausedStreamingDocumentResponse { + pub request_load_policy: NavigationRequestLoadPolicy, + pub requested_url: Url, + pub request_method: String, + pub request_headers: moli_fetch::RequestHeaders, + pub response: StreamingRawResponse, + pub network_observation_journal: NetworkObservationJournal, + pub prepared_document: Option>, +} + +pub struct SyntheticDocumentResponseContext { + pub request_load_policy: NavigationRequestLoadPolicy, + pub requested_url: Url, + pub request_method: String, + pub request_headers: moli_fetch::RequestHeaders, + pub final_url: Url, + pub request_cookie_report: Option, +} + +impl PausedDocumentTransfer { + pub fn pending( + request_load_policy: NavigationRequestLoadPolicy, + body: DocumentBodySource, + ) -> Self { + Self { + request_load_policy, + state: PausedDocumentTransferState::Pending { body }, + } + } + + pub fn prepared_renderer_agent_token(&self) -> Option { + match &self.state { + PausedDocumentTransferState::Pending { + body: + DocumentBodySource::StreamingRaw { + prepared_document: Some(prepared_document), + .. + }, + } => Some(prepared_document.renderer_devtools_agent_token()), + _ => None, + } + } + + pub fn into_pending( + self, + ) -> Result<(NavigationRequestLoadPolicy, DocumentBodySource), Box> { + let Self { + request_load_policy, + state, + } = self; + match state { + PausedDocumentTransferState::Pending { body } => Ok((request_load_policy, body)), + state => Err(Box::new(Self { + request_load_policy, + state, + })), + } + } + + pub fn into_streaming_response(self) -> Result> { + let (request_load_policy, body) = self.into_pending()?; + match body { + DocumentBodySource::StreamingRaw { + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + prepared_document, + } => Ok(PausedStreamingDocumentResponse { + request_load_policy, + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + prepared_document, + }), + body => Err(Box::new(Self::pending(request_load_policy, body))), + } + } + + pub fn open_body_stream( + self, + handle: String, + ) -> Result { + let (request_load_policy, body) = self + .into_pending() + .map_err(OpenBodyStreamError::NotOpenable)?; + match body { + DocumentBodySource::StreamingRaw { + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + prepared_document: _, + } => Ok(PendingFetchResponseOpenedBodyStream { + handle, + buffered_bytes: None, + transfer: Self { + request_load_policy, + state: PausedDocumentTransferState::ActiveBodyStream { + stream: ActiveDocumentBodyStreamState::new( + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + ), + }, + }, + }), + DocumentBodySource::BufferedRaw { + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + } => { + let bytes = response.clone_body_bytes(); + Ok(PendingFetchResponseOpenedBodyStream { + handle, + buffered_bytes: Some(bytes), + transfer: Self::pending( + request_load_policy, + DocumentBodySource::BufferedRaw { + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + }, + ), + }) + } + DocumentBodySource::CapturedRaw { + requested_url, + request_method, + request_headers, + head, + body, + network_observation_journal, + } => { + let bytes = match body.materialize_bytes() { + Ok(bytes) => bytes, + Err(error) => { + return Err(OpenBodyStreamError::Failed { + transfer: Box::new(Self::pending( + request_load_policy, + DocumentBodySource::CapturedRaw { + requested_url, + request_method, + request_headers, + head, + body, + network_observation_journal, + }, + )), + error: error.context("failed to materialize captured response body"), + }); + } + }; + Ok(PendingFetchResponseOpenedBodyStream { + handle, + buffered_bytes: Some(bytes), + transfer: Self::pending( + request_load_policy, + DocumentBodySource::CapturedRaw { + requested_url, + request_method, + request_headers, + head, + body, + network_observation_journal, + }, + ), + }) + } + } + } + + pub fn body_stream_offset(&self) -> Option { + match &self.state { + PausedDocumentTransferState::ActiveBodyStream { stream } => Some(stream.offset), + PausedDocumentTransferState::Pending { .. } => None, + } + } + + pub async fn read_body_stream_async( + self, + size: Option, + ) -> Result<(Vec, bool, Self), (Self, anyhow::Error)> { + let Self { + request_load_policy, + state, + } = self; + let PausedDocumentTransferState::ActiveBodyStream { mut stream } = state else { + return Err(( + Self { + request_load_policy, + state, + }, + anyhow::anyhow!("StreamHandleNotFound"), + )); + }; + match stream.read_async(size).await { + Ok((bytes, eof)) => { + let state = if eof { + match stream.finish_pending_body_source() { + Ok(body) => PausedDocumentTransferState::Pending { body }, + Err(message) => { + return Err(( + Self { + request_load_policy, + state: PausedDocumentTransferState::ActiveBodyStream { stream }, + }, + message, + )); + } + } + } else { + PausedDocumentTransferState::ActiveBodyStream { stream } + }; + Ok(( + bytes, + eof, + Self { + request_load_policy, + state, + }, + )) + } + Err(message) => Err(( + Self { + request_load_policy, + state: PausedDocumentTransferState::ActiveBodyStream { stream }, + }, + message, + )), + } + } + + pub async fn finish_body_stream_async( + self, + ) -> Result<(NavigationRequestLoadPolicy, DocumentBodySource), (Self, anyhow::Error)> { + let Self { + request_load_policy, + state, + } = self; + match state { + PausedDocumentTransferState::Pending { body } => Ok((request_load_policy, body)), + PausedDocumentTransferState::ActiveBodyStream { mut stream } => { + if let Err(message) = stream.read_async(None).await { + return Err(( + Self { + request_load_policy, + state: PausedDocumentTransferState::ActiveBodyStream { stream }, + }, + message, + )); + } + match stream.finish_pending_body_source() { + Ok(body) => Ok((request_load_policy, body)), + Err(message) => Err(( + Self { + request_load_policy, + state: PausedDocumentTransferState::ActiveBodyStream { stream }, + }, + message, + )), + } + } + } + } + + pub fn into_synthetic_response_context(self) -> SyntheticDocumentResponseContext { + let Self { + request_load_policy, + state, + } = self; + let (requested_url, request_method, request_headers, final_url, request_cookie_report) = + match state { + PausedDocumentTransferState::Pending { body } => match body { + DocumentBodySource::BufferedRaw { + requested_url, + request_method, + request_headers, + response, + .. + } => ( + requested_url, + request_method, + request_headers, + response.final_url.clone(), + response.request_cookie_report.clone(), + ), + DocumentBodySource::StreamingRaw { + requested_url, + request_method, + request_headers, + response, + .. + } => ( + requested_url, + request_method, + request_headers, + response.final_url.clone(), + response.request_cookie_report.clone(), + ), + DocumentBodySource::CapturedRaw { + requested_url, + request_method, + request_headers, + head, + .. + } => ( + requested_url, + request_method, + request_headers, + head.final_url.clone(), + head.request_cookie_report.clone(), + ), + }, + PausedDocumentTransferState::ActiveBodyStream { stream } => ( + stream.requested_url, + stream.request_method, + stream.request_headers, + stream.response.final_url.clone(), + stream.response.request_cookie_report.clone(), + ), + }; + SyntheticDocumentResponseContext { + request_load_policy, + requested_url, + request_method, + request_headers, + final_url, + request_cookie_report, + } + } + + pub async fn materialize_body_limited_async( + self, + limit: usize, + ) -> Result<(Option>, Self), (anyhow::Error, Self)> { + let (request_load_policy, body) = match self.into_pending() { + Ok(parts) => parts, + Err(transfer) => return Ok((None, *transfer)), + }; + match body.materialize_body_limited_async(limit).await { + Ok((bytes, body)) => Ok((Some(bytes), Self::pending(request_load_policy, body))), + Err((message, body)) => Err((message, Self::pending(request_load_policy, body))), + } + } +} + +impl DocumentBodySource { + pub async fn materialize_body_limited_async( + self, + limit: usize, + ) -> Result<(Vec, Self), (anyhow::Error, Self)> { + match self { + Self::BufferedRaw { + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + } => { + if let Err(error) = ensure_materialize_limit(response.body_bytes().len(), limit) { + return Err(( + error, + Self::BufferedRaw { + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + }, + )); + } + let bytes = response.clone_body_bytes(); + Ok(( + bytes, + Self::BufferedRaw { + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + }, + )) + } + Self::StreamingRaw { + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + prepared_document: _, + } => { + let preserved_head = response.head(); + let (head, body) = match capture_streaming_raw_response(response).await { + Ok(captured) => captured, + Err(message) => { + return Err(( + message, + Self::CapturedRaw { + requested_url, + request_method, + request_headers, + head: preserved_head, + body: CapturedBody::from_bytes(Vec::new()), + network_observation_journal, + }, + )); + } + }; + let result = body + .materialize_bytes_limited(limit) + .context("failed to materialize captured response body"); + let source = Self::CapturedRaw { + requested_url, + request_method, + request_headers, + head, + body, + network_observation_journal, + }; + match result { + Ok(bytes) => Ok((bytes, source)), + Err(message) => Err((message, source)), + } + } + Self::CapturedRaw { + requested_url, + request_method, + request_headers, + head, + body, + network_observation_journal, + } => { + let result = body + .materialize_bytes_limited(limit) + .context("failed to materialize captured response body"); + let source = Self::CapturedRaw { + requested_url, + request_method, + request_headers, + head, + body, + network_observation_journal, + }; + match result { + Ok(bytes) => Ok((bytes, source)), + Err(message) => Err((message, source)), + } + } + } + } +} + +impl ActiveDocumentBodyStreamState { + fn new( + requested_url: Url, + request_method: String, + request_headers: moli_fetch::RequestHeaders, + response: StreamingRawResponse, + network_observation_journal: NetworkObservationJournal, + ) -> Self { + Self { + requested_url, + request_method, + request_headers, + response, + network_observation_journal, + captured_body: CapturedBodyWriter::default(), + unread_body: Vec::new(), + offset: 0, + finished: false, + } + } + + async fn read_async(&mut self, size: Option) -> Result<(Vec, bool), anyhow::Error> { + let mut bytes = Vec::new(); + match size { + Some(limit) => { + while bytes.len() < limit { + if self.unread_body.is_empty() && !self.finished { + self.read_next_chunk_async().await?; + } + if self.unread_body.is_empty() { + break; + } + let remaining = limit.saturating_sub(bytes.len()); + self.drain_unread(&mut bytes, remaining); + } + } + None => { + while !self.unread_body.is_empty() || !self.finished { + if self.unread_body.is_empty() { + self.read_next_chunk_async().await?; + } + let remaining = self.unread_body.len(); + self.drain_unread(&mut bytes, remaining); + } + } + } + self.offset = self.offset.saturating_add(bytes.len()); + Ok((bytes, self.finished && self.unread_body.is_empty())) + } + + fn drain_unread(&mut self, bytes: &mut Vec, limit: usize) { + let take = limit.min(self.unread_body.len()); + bytes.extend(self.unread_body.drain(..take)); + } + + async fn read_next_chunk_async(&mut self) -> anyhow::Result<()> { + if self.finished { + return Ok(()); + } + if let Some(chunk) = self.response.next_chunk().await { + self.captured_body + .append(&chunk) + .context("failed to capture response body stream")?; + self.unread_body.extend(chunk); + return Ok(()); + } + self.response + .finish() + .await + .context("failed to read page body from stream")?; + self.finished = true; + Ok(()) + } + + fn finish_pending_body_source(&mut self) -> anyhow::Result { + let head = self.response.head(); + let body = self + .captured_body + .finish_in_place() + .context("failed to finish captured response body")?; + Ok(DocumentBodySource::CapturedRaw { + requested_url: self.requested_url.clone(), + request_method: self.request_method.clone(), + request_headers: self.request_headers.clone(), + head, + body, + network_observation_journal: self.network_observation_journal.clone(), + }) + } +} + +async fn capture_streaming_raw_response( + mut response: StreamingRawResponse, +) -> anyhow::Result<(ResponseHead, CapturedBody)> { + let head = response.head(); + let mut body = CapturedBodyWriter::default(); + while let Some(chunk) = response.next_chunk().await { + body.append(&chunk) + .context("failed to capture response body stream")?; + } + response + .finish() + .await + .context("failed to read page body from stream")?; + let body = body + .finish() + .context("failed to finish captured response body")?; + Ok((head, body)) +} diff --git a/moli-protocol/src/conn/state/web_contents/resource_runtime.rs b/moli-core/src/browser/web_contents/resource_runtime.rs similarity index 77% rename from moli-protocol/src/conn/state/web_contents/resource_runtime.rs rename to moli-core/src/browser/web_contents/resource_runtime.rs index 86bb436ed4..4ef9f773fc 100644 --- a/moli-protocol/src/conn/state/web_contents/resource_runtime.rs +++ b/moli-core/src/browser/web_contents/resource_runtime.rs @@ -1,66 +1,60 @@ -use moli_core::{ +use crate::{ network::ResourceRequestClient, page::{CompletedPageCommand, PendingPageCommand, RendererCommandTurnOutput}, runtime::{NavigationEngine, NavigationEngineDiagnostics}, }; use super::{InheritedDocumentPolicy, WebContents}; -use crate::conn::state::browser_context::BrowserContextStoragePartitionHandles; +use crate::browser::BrowserContextStoragePartitionHandles; #[cfg(test)] mod tests; impl WebContents { - pub(in crate::conn::state) fn has_navigation_engine(&self) -> bool { + pub fn has_navigation_engine(&self) -> bool { self.navigation_engine.is_some() } - pub(in crate::conn::state) fn navigation_fetch_config( - &self, - ) -> Option<&moli_fetch::FetchConfig> { + pub fn navigation_fetch_config(&self) -> Option<&moli_fetch::FetchConfig> { self.navigation_engine .as_ref() .map(NavigationEngine::fetch_config) } - pub(in crate::conn::state) fn navigation_layout_policy( - &self, - ) -> Option { + pub fn navigation_layout_policy(&self) -> Option { self.navigation_engine .as_ref() .map(NavigationEngine::layout_policy) } - pub(in crate::conn::state) fn navigation_renderer_owner_id(&self) -> Option { + pub fn navigation_renderer_owner_id(&self) -> Option { self.navigation_engine .as_ref() .map(NavigationEngine::renderer_owner_id_for_diagnostics) } - pub(in crate::conn::state) fn navigation_diagnostics( - &self, - ) -> Option { + pub fn navigation_diagnostics(&self) -> Option { self.navigation_engine .as_ref() .map(NavigationEngine::diagnostics) } - pub(in crate::conn::state) fn set_renderer_output_transport_sender( + pub fn set_renderer_output_transport_sender( &self, - sender: moli_core::RendererOutputTransportSender, + sender: crate::RendererOutputTransportSender, ) { if let Some(engine) = &self.navigation_engine { engine.set_renderer_output_transport_sender(sender); } } - pub(in crate::conn::state) fn invalidate_resource_runtime(&mut self) { + pub fn invalidate_resource_runtime(&mut self) { if let Some(engine) = self.navigation_engine.as_mut() { engine.reset_resource_runtime_without_loaded_page(); } } - pub(in crate::conn::state) fn ensure_resource_request_client( + pub fn ensure_resource_request_client( &mut self, inherited: &InheritedDocumentPolicy, ) -> Result { @@ -72,7 +66,7 @@ impl WebContents { .ok_or_else(|| "resource request client unavailable".to_owned()) } - pub(in crate::conn::state) fn start_resource_runtime_rebuild( + pub fn start_resource_runtime_rebuild( &mut self, inherited: &InheritedDocumentPolicy, ) -> Result, String> { @@ -103,7 +97,7 @@ impl WebContents { .map_err(|error| format!("failed to update page resource runtime: {error}")) } - pub(in crate::conn::state) fn finish_resource_runtime_update( + pub fn finish_resource_runtime_update( &mut self, completion: CompletedPageCommand, ) -> Result<(), String> { @@ -127,7 +121,7 @@ impl WebContents { result } - pub(in crate::conn::state) fn finish_unobserved_resource_runtime_update( + pub fn finish_unobserved_resource_runtime_update( completion: CompletedPageCommand, ) -> Result<(), String> { completion @@ -138,7 +132,7 @@ impl WebContents { }) } - pub(in crate::conn::state) fn start_child_frame_lifecycle_work( + pub fn start_child_frame_lifecycle_work( &mut self, storage: &BrowserContextStoragePartitionHandles, timeout: std::time::Duration, @@ -161,7 +155,7 @@ impl WebContents { .map_err(|error| error.to_string()) } - pub(in crate::conn::state) fn complete_child_frame_lifecycle_work( + pub fn complete_child_frame_lifecycle_work( &mut self, completion: CompletedPageCommand, ) -> Result<(bool, RendererCommandTurnOutput), String> { @@ -177,20 +171,18 @@ impl WebContents { .map_err(|error| error.to_string()) } - #[cfg(test)] - pub(in crate::conn::state) fn navigation_engine_for_test(&self) -> Option<&NavigationEngine> { + #[cfg(any(test, feature = "test-support"))] + pub fn navigation_engine_for_test(&self) -> Option<&NavigationEngine> { self.navigation_engine.as_ref() } - #[cfg(test)] - pub(in crate::conn::state) fn navigation_engine_for_test_mut( - &mut self, - ) -> Option<&mut NavigationEngine> { + #[cfg(any(test, feature = "test-support"))] + pub fn navigation_engine_for_test_mut(&mut self) -> Option<&mut NavigationEngine> { self.navigation_engine.as_mut() } - #[cfg(test)] - pub(in crate::conn::state) async fn reset_resource_runtime_for_test(&mut self) { + #[cfg(any(test, feature = "test-support"))] + pub async fn reset_resource_runtime_for_test(&mut self) { if let Some(engine) = self.navigation_engine.as_mut() { engine .reset_resource_runtime_async( diff --git a/moli-protocol/src/conn/state/web_contents/resource_runtime/tests.rs b/moli-core/src/browser/web_contents/resource_runtime/tests.rs similarity index 99% rename from moli-protocol/src/conn/state/web_contents/resource_runtime/tests.rs rename to moli-core/src/browser/web_contents/resource_runtime/tests.rs index 31d1b2f1fc..0cb4b14998 100644 --- a/moli-protocol/src/conn/state/web_contents/resource_runtime/tests.rs +++ b/moli-core/src/browser/web_contents/resource_runtime/tests.rs @@ -1,4 +1,4 @@ -use crate::conn::state::web_contents::tests::BrowserFixture; +use crate::browser::web_contents::tests::BrowserFixture; use moli_browser_profile::BrowserIdentityProfile; use serde_json::json; diff --git a/moli-protocol/src/conn/state/web_contents/session_storage.rs b/moli-core/src/browser/web_contents/session_storage.rs similarity index 75% rename from moli-protocol/src/conn/state/web_contents/session_storage.rs rename to moli-core/src/browser/web_contents/session_storage.rs index 9be42502fa..1f34526286 100644 --- a/moli-protocol/src/conn/state/web_contents/session_storage.rs +++ b/moli-core/src/browser/web_contents/session_storage.rs @@ -1,11 +1,11 @@ use std::sync::OnceLock; -use moli_core::network::{ +use crate::network::{ SharedWebStorageStore, deep_clone_shared_web_storage_store, new_shared_web_storage_store, }; #[derive(Default)] -pub(crate) struct SessionStorageNamespace { +pub struct SessionStorageNamespace { store: OnceLock, } @@ -18,17 +18,17 @@ impl std::fmt::Debug for SessionStorageNamespace { } impl SessionStorageNamespace { - pub(crate) fn from_store(store: SharedWebStorageStore) -> Self { + pub fn from_store(store: SharedWebStorageStore) -> Self { Self { store: OnceLock::from(store), } } - pub(crate) fn store(&self) -> &SharedWebStorageStore { + pub fn store(&self) -> &SharedWebStorageStore { self.store.get_or_init(new_shared_web_storage_store) } - pub(crate) fn deep_clone(&self) -> Self { + pub fn deep_clone(&self) -> Self { Self { store: OnceLock::from(deep_clone_shared_web_storage_store(self.store())), } diff --git a/moli-protocol/src/conn/state/web_contents/tests.rs b/moli-core/src/browser/web_contents/tests.rs similarity index 92% rename from moli-protocol/src/conn/state/web_contents/tests.rs rename to moli-core/src/browser/web_contents/tests.rs index 9d727a09c5..7cc90c2ece 100644 --- a/moli-protocol/src/conn/state/web_contents/tests.rs +++ b/moli-core/src/browser/web_contents/tests.rs @@ -1,9 +1,6 @@ use super::*; -use crate::conn::state::{ - NavigationRequestLoadPolicy, browser_context::BrowserContextStoragePartitionHandles, -}; -use moli_core::{ - browser::NavigationId, +use crate::{ + browser::{BrowserContextStoragePartitionHandles, NavigationId, NavigationRequestLoadPolicy}, runtime::{ CommittedDocumentResourceSource, ExternalRawDocumentBodyStream, PageVmInitStage, RendererReplyBoundary, @@ -17,7 +14,7 @@ pub(super) struct BrowserFixture { } impl BrowserFixture { - pub(super) async fn navigate(&mut self, title: &str) -> moli_core::browser::DocumentId { + pub(super) async fn navigate(&mut self, title: &str) -> crate::browser::DocumentId { let navigation = self.contents.navigation.start_document_navigation(); self.complete_navigation(navigation, title).await } @@ -26,7 +23,7 @@ impl BrowserFixture { &mut self, navigation: NavigationId, title: &str, - ) -> moli_core::browser::DocumentId { + ) -> crate::browser::DocumentId { let response = prepare(&mut self.start(navigation).unwrap(), title) .await .unwrap(); diff --git a/moli-protocol/src/conn/state/web_contents/window.rs b/moli-core/src/browser/web_contents/window.rs similarity index 50% rename from moli-protocol/src/conn/state/web_contents/window.rs rename to moli-core/src/browser/web_contents/window.rs index 28e1df4ad4..c9000b2f45 100644 --- a/moli-protocol/src/conn/state/web_contents/window.rs +++ b/moli-core/src/browser/web_contents/window.rs @@ -1,22 +1,22 @@ -use moli_core::browser::WebContentsId; +use crate::browser::WebContentsId; /// Browser window state has the lifetime of its owning WebContents. #[derive(Debug, Default)] -pub(in crate::conn) struct Window { - pub(in crate::conn) name: Option, - pub(in crate::conn) opener: Option, - pub(in crate::conn) surface: WindowSurface, +pub struct Window { + pub name: Option, + pub opener: Option, + pub surface: WindowSurface, } #[derive(Debug, Clone, Copy)] -pub(in crate::conn) struct WindowOpener { - pub(in crate::conn) web_contents_id: WebContentsId, +pub struct WindowOpener { + pub web_contents_id: WebContentsId, // A noopener window still has creator attribution, but no script access. - pub(in crate::conn) can_access: bool, + pub can_access: bool, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub(crate) enum WindowSurfaceState { +pub enum WindowSurfaceState { #[default] Normal, Maximized, @@ -25,11 +25,21 @@ pub(crate) enum WindowSurfaceState { } impl WindowSurfaceState { - pub(crate) fn document_hidden(self) -> bool { + pub fn from_label(label: &str) -> Option { + match label { + "normal" => Some(Self::Normal), + "maximized" => Some(Self::Maximized), + "minimized" => Some(Self::Minimized), + "fullscreen" => Some(Self::Fullscreen), + _ => None, + } + } + + pub fn document_hidden(self) -> bool { matches!(self, Self::Minimized) } - pub(crate) fn label(self) -> &'static str { + pub fn label(self) -> &'static str { match self { Self::Normal => "normal", Self::Maximized => "maximized", @@ -41,16 +51,16 @@ impl WindowSurfaceState { /// A value snapshot; it confers no mutable access to Browser state. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub(crate) struct WindowSurface { - pub(crate) state: WindowSurfaceState, - pub(crate) width: u32, - pub(crate) height: u32, - pub(crate) x: i32, - pub(crate) y: i32, +pub struct WindowSurface { + pub state: WindowSurfaceState, + pub width: u32, + pub height: u32, + pub x: i32, + pub y: i32, } impl WindowSurface { - pub(in crate::conn) fn set_geometry( + pub fn set_geometry( &mut self, width: Option, height: Option, @@ -70,4 +80,18 @@ impl WindowSurface { self.y = y; } } + + pub fn update( + &mut self, + state: Option, + width: Option, + height: Option, + x: Option, + y: Option, + ) { + if let Some(state) = state { + self.state = state; + } + self.set_geometry(width, height, x, y); + } } diff --git a/moli-core/src/page/js_evaluation.rs b/moli-core/src/page/js_evaluation.rs index d1fb31be33..5f543e3948 100644 --- a/moli-core/src/page/js_evaluation.rs +++ b/moli-core/src/page/js_evaluation.rs @@ -227,6 +227,25 @@ impl Page { ) .await } + + pub fn start_page_surface_override_script(&self, source: &str) -> Result { + self.start_page_command(RendererPageCommand::RunPageSurfaceOverrideScript { + source: source.to_owned(), + }) + } + + pub fn finish_page_surface_override_script( + &mut self, + completion: CompletedPageCommand, + ) -> Result<()> { + let reply = self.finish_page_command(completion); + expect_page_reply!( + reply, + "run page surface override script", + "a unit reply", + RendererPageReply::Unit => Ok(()), + ) + } } // Decoding a frozen inspection reply requires no Browser Page residence. diff --git a/moli-core/src/page/settings_support.rs b/moli-core/src/page/settings_support.rs index 84e7fc2dc1..4a2689d135 100644 --- a/moli-core/src/page/settings_support.rs +++ b/moli-core/src/page/settings_support.rs @@ -239,6 +239,26 @@ impl Page { .await } + pub fn start_set_script_execution_disabled( + &self, + disabled: bool, + ) -> Result { + self.start_page_command(RendererPageCommand::SetScriptExecutionDisabled(disabled)) + } + + pub fn finish_set_script_execution_disabled( + &mut self, + completion: CompletedPageCommand, + ) -> Result<()> { + let reply = self.finish_page_command(completion); + expect_page_reply!( + reply, + "set script execution disabled", + "a unit reply", + RendererPageReply::Unit => Ok(()), + ) + } + pub async fn set_bypass_content_security_policy_async(&mut self, bypass: bool) -> Result<()> { self.dispatch_unit_page_command_async( RendererPageCommand::SetBypassContentSecurityPolicy(bypass), diff --git a/moli-core/src/page/subresource_support.rs b/moli-core/src/page/subresource_support.rs index 7e1cfc3f6c..c9ba4ad3ad 100644 --- a/moli-core/src/page/subresource_support.rs +++ b/moli-core/src/page/subresource_support.rs @@ -1,8 +1,8 @@ -use anyhow::Result; +use anyhow::{Result, bail}; use url::Url; use super::protocol_support::{PendingSubresourceContinueOutcome, SubresourceAuthCredentials}; -use super::{CompletedPageCommand, Page, PendingPageCommand}; +use super::{CompletedPageCommand, Page, PendingPageCommand, RendererCommandTurnOutput}; use super::{RendererPendingJavaScriptDialog, RendererSyntheticResponseBody}; use crate::RendererOutputFence; use crate::renderer::{RendererPageCommand, RendererPageReply}; @@ -12,16 +12,19 @@ impl Page { self.handle.take_pending_modal_javascript_dialogs() } - pub async fn stop_document_lifecycle_async(&mut self) -> Result<()> { - let reply = self - .dispatch_page_command_async(RendererPageCommand::StopDocumentLifecycle) - .await?; - expect_page_reply!( - reply, - "stop document lifecycle page command", - "a unit reply", - RendererPageReply::Unit => Ok(()), - ) + pub fn start_stop_document_lifecycle(&self) -> Result { + self.start_page_command(RendererPageCommand::StopDocumentLifecycle) + } + + pub fn finish_stop_document_lifecycle( + &mut self, + completion: CompletedPageCommand, + ) -> Result { + let output = self.finish_page_command_turn(completion); + if !matches!(output.completion().reply(), RendererPageReply::Unit) { + bail!("stop document lifecycle page command expected a unit reply"); + } + Ok(output) } pub async fn continue_pending_subresource_fetch_async( diff --git a/moli-core/src/runtime/storage_partition.rs b/moli-core/src/runtime/storage_partition.rs index 5bc1da92f8..87659a78a3 100644 --- a/moli-core/src/runtime/storage_partition.rs +++ b/moli-core/src/runtime/storage_partition.rs @@ -334,7 +334,8 @@ pub(crate) fn import_cookies_into_store( let mut cookie_store = cookie_store.lock(); let mut accepted = 0usize; for cookie in cookies { - let report = cookie_store.upsert_with_request_url_report(cookie, None, CookieSource::Cdp); + let report = + cookie_store.upsert_with_request_url_report(cookie, None, CookieSource::Management); if report.is_accepted() { accepted += 1; } @@ -374,7 +375,8 @@ fn commit_cookie_delta_to_store( } for cookie in final_cookies { - let report = cookie_store.upsert_with_request_url_report(cookie, None, CookieSource::Cdp); + let report = + cookie_store.upsert_with_request_url_report(cookie, None, CookieSource::Management); if !report.is_accepted() { return Err(anyhow!("failed to commit cookie into storage partition")); } diff --git a/moli-protocol-server/src/cdp_scheduler.rs b/moli-protocol-server/src/cdp_scheduler.rs index b7a15a2107..7485525f88 100644 --- a/moli-protocol-server/src/cdp_scheduler.rs +++ b/moli-protocol-server/src/cdp_scheduler.rs @@ -7,8 +7,8 @@ use std::{ use moli_cookie_jar::StoredCookie; use moli_core::page::RendererVisualStateToken; use moli_core::{ - RendererOutputTransportMessage, page::RendererDocumentLifecycleMilestone, - runtime::NavigationRuntimeConfig, + RendererOutputTransportMessage, browser::BrowserHandle, + page::RendererDocumentLifecycleMilestone, runtime::NavigationRuntimeConfig, }; use moli_protocol::{ AgentHostDispatchResult, BackgroundNavigationCompletion, BackgroundProtocolEvent, @@ -717,10 +717,12 @@ impl CdpScheduler { } pub(crate) fn new_with_initial_state_runtime_config( + browser: BrowserHandle, initial_storage_partition: CdpInitialStoragePartition, navigation_runtime_config: NavigationRuntimeConfig, ) -> (Self, CdpSchedulerEventReceivers) { Self::new_with_initial_state_runtime_config_and_target_host_integration( + browser, initial_storage_partition, navigation_runtime_config, None, @@ -728,11 +730,13 @@ impl CdpScheduler { } pub(crate) fn new_with_initial_state_runtime_config_and_target_host_integration( + browser: BrowserHandle, initial_storage_partition: CdpInitialStoragePartition, navigation_runtime_config: NavigationRuntimeConfig, target_host_integration: Option, ) -> (Self, CdpSchedulerEventReceivers) { Self::new_with_default_target_runtime_initialization( + browser, initial_storage_partition, navigation_runtime_config, target_host_integration, @@ -742,12 +746,14 @@ impl CdpScheduler { } pub(crate) fn new_with_deferred_default_target_runtime_and_screencast_interval( + browser: BrowserHandle, initial_storage_partition: CdpInitialStoragePartition, navigation_runtime_config: NavigationRuntimeConfig, target_host_integration: Option, page_screencast_interval_ms: u32, ) -> (Self, CdpSchedulerEventReceivers) { Self::new_with_default_target_runtime_initialization( + browser, initial_storage_partition, navigation_runtime_config, target_host_integration, @@ -757,26 +763,18 @@ impl CdpScheduler { } fn new_with_default_target_runtime_initialization( + browser: BrowserHandle, initial_storage_partition: CdpInitialStoragePartition, navigation_runtime_config: NavigationRuntimeConfig, target_host_integration: Option, initialization: DefaultTargetRuntimeInitialization, page_screencast_interval_ms: u32, ) -> (Self, CdpSchedulerEventReceivers) { - let conn = match initialization { - DefaultTargetRuntimeInitialization::Materialized => { - CdpConnection::new_with_initial_storage_partition_and_runtime_config( - initial_storage_partition, - navigation_runtime_config, - ) - } - DefaultTargetRuntimeInitialization::Deferred => { - CdpConnection::new_with_deferred_navigation_runtime( - initial_storage_partition, - navigation_runtime_config, - ) - } - }; + let conn = CdpConnection::new( + browser, + initial_storage_partition, + navigation_runtime_config, + ); let mut scheduler = Self::new_with_screencast_interval(conn, page_screencast_interval_ms); if let Some(target_host_integration) = target_host_integration { target_host_integration.install(&mut scheduler.conn); diff --git a/moli-protocol-server/src/cdp_scheduler/actor.rs b/moli-protocol-server/src/cdp_scheduler/actor.rs index efc0678134..17a1398497 100644 --- a/moli-protocol-server/src/cdp_scheduler/actor.rs +++ b/moli-protocol-server/src/cdp_scheduler/actor.rs @@ -2046,7 +2046,7 @@ fn trace_command( #[cfg(test)] mod tests { use moli_core::RendererRuntimeInspectorAsyncCompletion; - use moli_protocol::CdpConnection; + use serde_json::json; use super::*; @@ -2132,7 +2132,7 @@ mod tests { #[test] fn unmatched_runtime_inspector_response_is_not_downgraded_to_protocol_output() { - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); let output = route_unmatched_runtime_inspector_response( &mut scheduler, diff --git a/moli-protocol-server/src/cdp_scheduler/adapter_scheduler.rs b/moli-protocol-server/src/cdp_scheduler/adapter_scheduler.rs index d34b7a2f3f..fb234bdc6f 100644 --- a/moli-protocol-server/src/cdp_scheduler/adapter_scheduler.rs +++ b/moli-protocol-server/src/cdp_scheduler/adapter_scheduler.rs @@ -282,7 +282,7 @@ impl ProtocolAdapterScheduler { mod tests { use moli_core::{PageId, RendererOutputResidenceIdentity, RendererOwnerLocalHostId}; use moli_protocol::{ - CdpConnection, CdpSchedulerEvent, ProtocolSchedulerWork, + CdpSchedulerEvent, ProtocolSchedulerWork, test_support::{ deferred_main_document_load_observation_id, deferred_main_document_load_output_interest, root_frame_stopped_loading_work, @@ -315,7 +315,7 @@ mod tests { #[test] fn pending_exact_load_observation_allows_independent_protocol_residence() { let observation_id = deferred_main_document_load_observation_id(1); - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); scheduler.apply_scheduler_events(vec![CdpSchedulerEvent::ProtocolWorkPublished { work: protocol_observation(1), }]); @@ -414,7 +414,7 @@ mod tests { async fn self_turn_is_coalesced_and_preserves_the_client_turn_boundary() { LocalSet::new() .run_until(async { - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); scheduler.apply_scheduler_events(vec![CdpSchedulerEvent::ProtocolWorkPublished { work: protocol_observation(1), }]); diff --git a/moli-protocol-server/src/cdp_scheduler/tests.rs b/moli-protocol-server/src/cdp_scheduler/tests.rs index b2e83206ca..c18c9654f5 100644 --- a/moli-protocol-server/src/cdp_scheduler/tests.rs +++ b/moli-protocol-server/src/cdp_scheduler/tests.rs @@ -11,8 +11,7 @@ use moli_core::{ }, }; use moli_protocol::{ - BackgroundProtocolEvent, CdpConnection, CdpSchedulerEvent, - DeferredMainDocumentLoadCompletionOutputAction, + BackgroundProtocolEvent, CdpSchedulerEvent, DeferredMainDocumentLoadCompletionOutputAction, DeferredMainDocumentLoadCompletionOutputInterest, DeferredMainDocumentLoadPredecessorCandidate, ProtocolSchedulerWork, conn::RuntimeInspectorResponseReady, @@ -397,7 +396,7 @@ fn network_finished_event_for_target( #[test] fn mixed_owner_protocol_output_keeps_the_conservative_global_gate() { - let mut conn = CdpConnection::new(); + let mut conn = moli_protocol::test_support::connection(); let navigation = arm_background_navigation_request(&mut conn, "LOADER-known"); let target_id = navigation.target_id().to_owned(); let known = network_response_event_for_target( @@ -622,7 +621,7 @@ fn concrete_renderer_output_precedes_work_published_by_the_same_ingress() { #[test] fn protocol_work_published_by_held_ingress_inherits_exact_load_predecessor() { let load = deferred_main_document_load_observation_id(1); - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); scheduler.apply_scheduler_events_with_load_predecessors( vec![CdpSchedulerEvent::ProtocolWorkPublished { work: root_frame_stopped_loading_work(1, "FRAME-1"), @@ -851,7 +850,7 @@ async fn runtime_response_releases_its_frozen_renderer_publication_predecessor() let publication = post_load_renderer_publication(PageId::new_for_testing(1), 1); let candidate = future_load_candidate(&publication); let cursor = publication_cursor(&publication); - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); scheduler.queues.enqueue_renderer_output_publication( cursor, ProtocolOutputSequence::from_messages(vec![json!({ @@ -880,7 +879,7 @@ async fn runtime_response_cannot_release_renderer_output_with_an_exact_load_pred let publication = post_load_renderer_publication(PageId::new_for_testing(1), 1); let load = deferred_main_document_load_observation_id(1); let cursor = publication_cursor(&publication); - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); scheduler.queues.enqueue_renderer_output_publication( cursor, ProtocolOutputSequence::from_messages(vec![json!({ @@ -936,7 +935,7 @@ async fn specialized_wait_yield_closes_candidate_retained_in_the_main_fifo() { let candidate = future_load_candidate(&publication); let interest = load_interest_for_publication(&publication); let unrelated_later_load = deferred_main_document_load_observation_id(1); - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); scheduler.queues.enqueue_renderer_output_publication( cursor, ProtocolOutputSequence::from_messages(vec![json!({"method": "Runtime.consoleAPICalled"})]), @@ -965,7 +964,7 @@ fn scheduler_holds_concrete_renderer_output_until_its_exact_load_predecessor_fin let publication = post_load_renderer_publication(PageId::new_for_testing(7), 1); let cursor = publication_cursor(&publication); let load = deferred_main_document_load_observation_id(1); - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); scheduler.queues.enqueue_renderer_output_publication( cursor, ProtocolOutputSequence::from_messages(vec![json!({"method": "Runtime.consoleAPICalled"})]), @@ -994,7 +993,7 @@ async fn renderer_stream_control_is_consumed_without_protocol_residence() { )), } .into(); - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); let output = scheduler.ingest_renderer_publication_now(control).await; assert!(output.is_empty()); @@ -1021,7 +1020,7 @@ async fn closed_renderer_transport_fails_an_unprojected_command_fence() { RendererOutputStreamIdentity::new_page_for_protocol_test(PageId::new_for_testing(90)); let predecessor = RendererOutputFence::new_for_test(RendererOutputCursor::new_for_test(stream, 1)); - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); let failure = scheduler .project_renderer_output_predecessor_before_devtools_result(&mut receivers, &predecessor) @@ -1038,7 +1037,7 @@ async fn closed_renderer_transport_fails_an_unprojected_command_fence() { #[test] fn concrete_protocol_output_waits_for_its_client_turn_predecessor() { - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); scheduler.apply_scheduler_events(vec![CdpSchedulerEvent::ProtocolWorkPublished { work: root_frame_stopped_loading_work(1, "FRAME-1"), }]); @@ -1067,7 +1066,7 @@ fn concrete_protocol_output_waits_for_its_client_turn_predecessor() { #[test] fn concrete_protocol_output_preserves_protocol_publish_order() { - let mut scheduler = CdpScheduler::new(CdpConnection::new()); + let mut scheduler = CdpScheduler::new(moli_protocol::test_support::connection()); scheduler.apply_scheduler_events(vec![ CdpSchedulerEvent::ProtocolWorkPublished { work: root_frame_stopped_loading_work(1, "FRAME-1"), @@ -1122,7 +1121,7 @@ fn concrete_protocol_output_rejects_missing_earlier_publication() { #[tokio::test] async fn background_navigation_blocks_only_its_target_protocol_residences() { - let mut conn = CdpConnection::new(); + let mut conn = moli_protocol::test_support::connection(); let navigation = arm_background_navigation_request(&mut conn, "LOADER-nav"); let navigation_target_id = navigation.target_id().to_owned(); let browser_context_id = conn.default_browser_context_id().to_owned(); @@ -1185,7 +1184,7 @@ async fn background_navigation_blocks_only_its_target_protocol_residences() { #[tokio::test] async fn protocol_residence_snapshot_skips_another_targets_navigation_gate() { - let mut conn = CdpConnection::new(); + let mut conn = moli_protocol::test_support::connection(); let navigation = arm_background_navigation_request(&mut conn, "LOADER-A"); let target_a = navigation.target_id().to_owned(); let browser_context_id = conn.default_browser_context_id().to_owned(); @@ -1234,7 +1233,7 @@ async fn protocol_residence_snapshot_skips_another_targets_navigation_gate() { #[test] fn replacement_navigation_cancels_and_exactly_settles_the_target_owned_request() { - let mut conn = CdpConnection::new(); + let mut conn = moli_protocol::test_support::connection(); let source = arm_background_navigation_request(&mut conn, "LOADER-source"); let replacement = arm_background_navigation_request(&mut conn, "LOADER-replacement"); assert!(source.is_cancelled()); @@ -1254,7 +1253,7 @@ fn replacement_navigation_cancels_and_exactly_settles_the_target_owned_request() #[test] fn scheduler_defers_subresource_network_events_until_background_navigation_gate_clears() { - let mut conn = CdpConnection::new(); + let mut conn = moli_protocol::test_support::connection(); let navigation = arm_background_navigation_request(&mut conn, "LOADER-nav"); let target_id = navigation.target_id().to_owned(); let mut scheduler = CdpScheduler::new(conn); @@ -1319,7 +1318,7 @@ fn scheduler_defers_subresource_network_events_until_background_navigation_gate_ #[tokio::test] async fn target_a_navigation_does_not_defer_target_b_network_events() { - let mut conn = CdpConnection::new(); + let mut conn = moli_protocol::test_support::connection(); conn.install_default_browser_target(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, @@ -1408,7 +1407,7 @@ async fn target_a_navigation_does_not_defer_target_b_network_events() { #[test] fn navigation_gate_release_precedes_later_renderer_boundary_network_output() { - let mut conn = CdpConnection::new(); + let mut conn = moli_protocol::test_support::connection(); let navigation = arm_background_navigation_request(&mut conn, "LOADER-nav"); let target_id = navigation.target_id().to_owned(); let mut scheduler = CdpScheduler::new(conn); diff --git a/moli-protocol-server/src/protocol_server/cdp_owner.rs b/moli-protocol-server/src/protocol_server/cdp_owner.rs index e9be8602d0..bccf5514fd 100644 --- a/moli-protocol-server/src/protocol_server/cdp_owner.rs +++ b/moli-protocol-server/src/protocol_server/cdp_owner.rs @@ -4,7 +4,10 @@ use std::{ }; use anyhow::{Context, Result, bail}; -use moli_core::runtime::{NavigationRuntimeConfig, storage_partition::StoragePartitionState}; +use moli_core::{ + browser::BrowserHandle, + runtime::{NavigationRuntimeConfig, storage_partition::StoragePartitionState}, +}; use moli_protocol::CdpInitialStoragePartition; use parking_lot::Mutex; use tokio::sync::{Notify, mpsc, oneshot}; @@ -51,6 +54,7 @@ struct CdpOwnerRecord { #[derive(Clone)] struct CdpOwnerRuntimeConfig { + browser: BrowserHandle, directory: SharedCdpAgentHostDirectory, target_id_allocator: Arc, tab_target_id_allocator: Arc, @@ -62,6 +66,7 @@ struct CdpOwnerRuntimeConfig { impl SharedCdpOwnerRegistry { pub(super) fn new( + browser: BrowserHandle, directory: SharedCdpAgentHostDirectory, target_id_allocator: Arc, tab_target_id_allocator: Arc, @@ -73,6 +78,7 @@ impl SharedCdpOwnerRegistry { Self { inner: Arc::new(CdpOwnerRegistryInner { config: CdpOwnerRuntimeConfig { + browser, directory, target_id_allocator, tab_target_id_allocator, @@ -133,6 +139,7 @@ impl SharedCdpOwnerRegistry { checkpoint_rx, ); let owner_finished_rx = spawn_owner_task( + self.inner.config.browser.clone(), receivers, initial_storage_partition, self.inner.config.navigation_runtime_config.clone(), @@ -201,6 +208,7 @@ impl Drop for CdpOwnerRegistryInner { #[allow(clippy::too_many_arguments)] fn spawn_owner_task( + browser: BrowserHandle, receivers: CdpFrontendReceivers, initial_storage_partition: CdpInitialStoragePartition, navigation_runtime_config: NavigationRuntimeConfig, @@ -212,6 +220,7 @@ fn spawn_owner_task( spawn_protocol_local_task("cdp-owner", move || async move { let (scheduler, scheduler_receivers) = CdpScheduler::new_with_deferred_default_target_runtime_and_screencast_interval( + browser, initial_storage_partition, navigation_runtime_config, target_host_integration, diff --git a/moli-protocol-server/src/protocol_server/mod.rs b/moli-protocol-server/src/protocol_server/mod.rs index 200e763d31..6118f50837 100644 --- a/moli-protocol-server/src/protocol_server/mod.rs +++ b/moli-protocol-server/src/protocol_server/mod.rs @@ -19,6 +19,7 @@ use axum::{ use moli_cookie_jar::StoredCookie; use moli_core::{ LayoutPolicy, OptionalResourceFetchMask, + browser::BrowserService, runtime::{NavigationRuntimeConfig, storage_partition::StoragePartitionState}, }; use moli_fetch::FetchConfig; @@ -159,6 +160,7 @@ impl ProtocolServer { )?; let cdp_owner_registry = app_state.cdp_owner_registry.clone(); + let browser_service = app_state.browser_service.clone(); let app = build_router(app_state); let listener = listener.tap_io(|tcp_stream| { @@ -166,6 +168,7 @@ impl ProtocolServer { }); let result = axum::serve(listener, app).await; cdp_owner_registry.shutdown().await; + browser_service.shutdown(); result.context("protocol server failed") } } @@ -737,6 +740,7 @@ fn is_websocket_upgrade_request(request: &Request) -> bool { #[derive(Clone)] struct AppState { + browser_service: BrowserService, browser_ws_url: String, page_ws_url: String, bidi_ws_url: String, @@ -781,13 +785,13 @@ impl AppState { navigation_runtime_config: NavigationRuntimeConfig, screencast_interval_ms: u32, ) -> anyhow::Result { - Ok(Self::from_parts( + Self::from_parts( addr, SharedCookieProfile::from_storage_partition(storage_partition.clone()), storage_partition, navigation_runtime_config, screencast_interval_ms, - )) + ) } fn from_parts( @@ -796,11 +800,13 @@ impl AppState { storage_partition: Arc, navigation_runtime_config: NavigationRuntimeConfig, screencast_interval_ms: u32, - ) -> Self { + ) -> anyhow::Result { + let browser_service = BrowserService::start().map_err(anyhow::Error::msg)?; let cdp_agent_host_directory = SharedCdpAgentHostDirectory::default(); let cdp_target_id_allocator = Arc::new(AtomicU64::new(0)); let cdp_tab_target_id_allocator = Arc::new(AtomicU64::new(0)); let cdp_owner_registry = SharedCdpOwnerRegistry::new( + browser_service.handle(), cdp_agent_host_directory.clone(), cdp_target_id_allocator, cdp_tab_target_id_allocator, @@ -809,7 +815,8 @@ impl AppState { navigation_runtime_config.clone(), screencast_interval_ms, ); - Self { + Ok(Self { + browser_service, browser_ws_url: format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}"), page_ws_url: format!("ws://{addr}/devtools/page/{DEFAULT_TARGET_ID}"), bidi_ws_url: format!("ws://{addr}/session"), @@ -826,7 +833,7 @@ impl AppState { optional_resource_fetch_mask: navigation_runtime_config.optional_resource_fetch_mask(), subframe_loading_enabled: navigation_runtime_config.subframe_loading_enabled(), layout_policy: navigation_runtime_config.layout_policy(), - } + }) } fn initial_storage_partition( diff --git a/moli-protocol-server/src/protocol_server/tests/misc.rs b/moli-protocol-server/src/protocol_server/tests/misc.rs index d2012b9dc4..7ad3625edd 100644 --- a/moli-protocol-server/src/protocol_server/tests/misc.rs +++ b/moli-protocol-server/src/protocol_server/tests/misc.rs @@ -198,8 +198,10 @@ fn app_state_storage_partition_backing_derives_initial_partition_from_owner() -> )?; let initial_storage_partition = state.initial_storage_partition(vec![stored_cookie("owner-cookie", "owner-value")]); - let mut conn = - moli_protocol::CdpConnection::new_with_initial_storage_partition(initial_storage_partition); + let mut conn = moli_protocol::test_support::connection_with_config( + initial_storage_partition, + Default::default(), + ); assert!(conn.snapshot_profile_backed_cookies().is_none()); conn.install_default_browser_target(); diff --git a/moli-protocol-server/src/protocol_server/webdriver_bidi.rs b/moli-protocol-server/src/protocol_server/webdriver_bidi.rs index eb7179e52a..f376696766 100644 --- a/moli-protocol-server/src/protocol_server/webdriver_bidi.rs +++ b/moli-protocol-server/src/protocol_server/webdriver_bidi.rs @@ -14,7 +14,9 @@ use axum::{ }; use futures_util::SinkExt; use moli_cookie_jar::StoredCookie; -use moli_core::{RendererOutputTransportMessage, runtime::NavigationRuntimeConfig}; +use moli_core::{ + RendererOutputTransportMessage, browser::BrowserHandle, runtime::NavigationRuntimeConfig, +}; use moli_protocol::{ BackgroundCommandResponsePayload, BackgroundProtocolEvent, CdpInitialStoragePartition, conn::RuntimeInspectorResponseReady, @@ -71,6 +73,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 cookie_profile = state.cookie_profile; + let browser = state.browser_service.handle(); let navigation_runtime_config = NavigationRuntimeConfig::new( state.fetch_config, state.optional_resource_fetch_mask, @@ -79,6 +82,7 @@ pub(super) async fn ws_bidi_session_upgrade_handler( ); ws.on_upgrade(move |socket| { handle_bidi_session_socket( + browser, socket, web_socket_url, session_registry, @@ -130,6 +134,7 @@ pub(super) async fn ws_bidi_existing_session_upgrade_handler( } async fn handle_bidi_session_socket( + browser: BrowserHandle, socket: WebSocket, web_socket_url: String, session_registry: SharedBidiSessionRegistry, @@ -140,6 +145,7 @@ async fn handle_bidi_session_socket( ) { let cookie_commit = spawn_protocol_local_task("bidi-socket", move || { handle_bidi_session_socket_local( + browser, socket, web_socket_url, session_registry, @@ -163,6 +169,7 @@ async fn handle_bidi_session_socket( } async fn handle_bidi_session_socket_local( + browser: BrowserHandle, socket: WebSocket, web_socket_url: String, session_registry: SharedBidiSessionRegistry, @@ -172,6 +179,7 @@ async fn handle_bidi_session_socket_local( ) -> CookieProfileCommit { let mut actor = BidiSocketActor::new(socket, web_socket_url); let (mut scheduler, mut receivers) = CdpScheduler::new_with_initial_state_runtime_config( + browser, initial_storage_partition, navigation_runtime_config, ); diff --git a/moli-protocol-server/src/protocol_server/webdriver_classic.rs b/moli-protocol-server/src/protocol_server/webdriver_classic.rs index 7a6af3962f..299661f2c9 100644 --- a/moli-protocol-server/src/protocol_server/webdriver_classic.rs +++ b/moli-protocol-server/src/protocol_server/webdriver_classic.rs @@ -1661,6 +1661,7 @@ pub(super) async fn webdriver_classic_new_session( let initial_cookie_snapshot = initial_cookies.clone(); let initial_storage_partition = state.initial_storage_partition(initial_cookies); let runtime = ClassicSessionRuntimeHandle::spawn( + state.browser_service.handle(), initial_cookie_snapshot, initial_storage_partition, moli_core::runtime::NavigationRuntimeConfig::new( diff --git a/moli-protocol-server/src/protocol_server/webdriver_classic/state.rs b/moli-protocol-server/src/protocol_server/webdriver_classic/state.rs index 02b7b9bb0f..a002058cbe 100644 --- a/moli-protocol-server/src/protocol_server/webdriver_classic/state.rs +++ b/moli-protocol-server/src/protocol_server/webdriver_classic/state.rs @@ -8,7 +8,10 @@ use std::{ use axum::extract::ws::WebSocket; use moli_cookie_jar::StoredCookie; -use moli_core::{page::RendererDocumentLifecycleMilestone, runtime::NavigationRuntimeConfig}; +use moli_core::{ + browser::BrowserHandle, page::RendererDocumentLifecycleMilestone, + runtime::NavigationRuntimeConfig, +}; use moli_protocol::{ CdpInitialStoragePartition, DevToolsPageResidenceIdentity, devtools_runtime::{ @@ -477,6 +480,7 @@ pub(in crate::protocol_server) struct ClassicSessionRuntimeHandle { impl ClassicSessionRuntimeHandle { pub(super) fn spawn( + browser: BrowserHandle, initial_cookie_snapshot: Vec, initial_storage_partition: CdpInitialStoragePartition, navigation_runtime_config: NavigationRuntimeConfig, @@ -484,6 +488,7 @@ impl ClassicSessionRuntimeHandle { let (tx, rx) = mpsc::unbounded_channel(); let _runtime_finished_rx = spawn_protocol_local_task("classic-session", move || { classic_session_runtime_loop( + browser, rx, initial_cookie_snapshot, initial_storage_partition, @@ -1320,12 +1325,14 @@ fn classic_pending_navigation_timeout_error() -> DevToolsError { } async fn classic_session_runtime_loop( + browser: BrowserHandle, mut rx: mpsc::UnboundedReceiver, initial_cookie_snapshot: Vec, initial_storage_partition: CdpInitialStoragePartition, navigation_runtime_config: NavigationRuntimeConfig, ) -> CookieProfileCommit { let (mut scheduler, mut receivers) = CdpScheduler::new_with_initial_state_runtime_config( + browser, initial_storage_partition, navigation_runtime_config, ); diff --git a/moli-protocol/Cargo.toml b/moli-protocol/Cargo.toml index 2f63e9c96f..9ecabd004d 100644 --- a/moli-protocol/Cargo.toml +++ b/moli-protocol/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" build = "build.rs" [features] -test-support = [] +test-support = ["moli-core/test-support"] [dependencies] anyhow = "1.0.100" diff --git a/moli-protocol/src/conn.rs b/moli-protocol/src/conn.rs index 81e87df276..c0bd856b7a 100644 --- a/moli-protocol/src/conn.rs +++ b/moli-protocol/src/conn.rs @@ -1,5 +1,4 @@ use std::{ - cell::OnceCell, collections::{HashMap, HashSet}, path::PathBuf, sync::{ @@ -10,7 +9,6 @@ use std::{ use indexmap::IndexMap; use moli_cookie_jar::{StoredCookie, StoredCookieQueryReport}; -use moli_fetch::FetchConfig; use parking_lot::Mutex; use serde_json::json; @@ -20,12 +18,10 @@ use crate::devtools_runtime::{ use crate::domains::command_output::{BackgroundProtocolEventBuffer, CommandOutputBuffer}; use moli_core::{ - LayoutPolicy, OptionalResourceFetchMask, RendererOutputPublicationOrdering, - RendererOutputTransportMessage, + LayoutPolicy, RendererOutputPublicationOrdering, RendererOutputTransportMessage, + browser::BrowserHandle, network::{SharedWebStorageStore, new_shared_web_storage_store}, - runtime::{ - NavigationEngine, NavigationRuntimeConfig, storage_partition::StoragePartitionState, - }, + runtime::{NavigationRuntimeConfig, storage_partition::StoragePartitionState}, }; pub const DEFAULT_CDP_PAGE_TARGET_ID: &str = "moli-default"; @@ -33,8 +29,8 @@ pub const DEFAULT_CDP_TAB_TARGET_ID: &str = "moli-default-tab"; mod activity_source; mod bidi_channel_work; -mod body_spool; mod browser_context; +mod browser_worker_commands; mod command_owner_scope; mod command_view; mod cookie_manager_surface; @@ -48,6 +44,7 @@ mod dispatch; mod download_policy; mod downloads; pub(crate) use download_policy::parse_download_behavior; +pub(crate) use downloads::PreparedDownloadActivation; #[cfg(test)] mod download_policy_tests; mod fetch_support; @@ -55,6 +52,8 @@ mod fetch_support; mod inspection_binding_tests; mod inspector_route; mod navigation_error; +#[cfg(test)] +pub(crate) use navigation_error::NavigationNetworkErrorKind; mod output; #[cfg(test)] mod permission_tests; @@ -72,12 +71,39 @@ mod settings; #[cfg(test)] mod site_data_manager_surface; mod state; +pub(crate) use state::PageInputCommand; +pub(crate) use state::{ + BrowserAppManifestLoadPreparation, CompletedAppManifestLoadPreparation, + CompletedAppManifestPublication, CompletedCaptureDocumentImage, + CompletedCaptureDocumentScreencastFrame, CompletedCaptureDocumentSnapshot, + CompletedChildFrameNavigation, CompletedChildFrameTreeSnapshot, + CompletedDocumentAutofillTrigger, CompletedDocumentBlobRead, + CompletedDocumentCookieOwnerSnapshot, CompletedDocumentCspBypassUpdate, + CompletedDocumentDiagnosticsSnapshot, CompletedDocumentFetchCommand, + CompletedDocumentInputCommand, CompletedDocumentLifecycleStop, CompletedDocumentPolicyUpdate, + CompletedDocumentResourceRuntimeUpdate, CompletedDocumentResourceTextSearch, + CompletedDocumentStorageKeySnapshot, CompletedNavigationHistoryReset, + CompletedNetworkResourceLoadPreparation, CompletedSetDocumentContent, + CompletedTopLevelHistoryTraversal, CompletedTopLevelSameDocumentNavigation, + DocumentFetchCommand, DocumentFetchCommandOutcome, DocumentPolicyUpdate, + DocumentRuntimePolicyReconciliation, PendingAppManifestLoadPreparation, + PendingAppManifestPublication, PendingCaptureDocumentImage, + PendingCaptureDocumentScreencastFrame, PendingCaptureDocumentSnapshot, + PendingChildFrameLifecycleWork, PendingChildFrameNavigation, PendingChildFrameTreeSnapshot, + PendingDocumentAutofillTrigger, PendingDocumentBlobRead, PendingDocumentCookieOwnerSnapshot, + PendingDocumentCspBypassUpdate, PendingDocumentDiagnosticsSnapshot, + PendingDocumentFetchCommand, PendingDocumentInputCommand, PendingDocumentLifecycleStop, + PendingDocumentPolicyBatch, PendingDocumentPolicyUpdate, PendingDocumentResourceRuntimeUpdate, + PendingDocumentResourceTextSearch, PendingDocumentStorageKeySnapshot, + PendingNavigationHistoryReset, PendingNetworkResourceLoadPreparation, + PendingSetDocumentContent, PendingTopLevelHistoryTraversal, + PendingTopLevelSameDocumentNavigation, +}; pub(crate) use state::{ ClaimedNavigationRequest, InterceptedNavigationLoad, InterceptedNavigationResponse, NavigationRequestInterception, }; pub(crate) use state::{CompletedContextPermissionUpdate, PendingContextPermissionUpdate}; -pub(crate) use state::{NetworkPolicyUpdateKind, PageInputCommand, PagePolicyUpdateKind}; mod target; mod top_level_navigation_work; @@ -88,7 +114,6 @@ pub(crate) use bidi_channel_work::{ BidiChannelListenerResidence, BidiChannelOwnerAction, BidiChannelOwnerActionBody, BidiChannelPageOwner, }; -pub(crate) use body_spool::{CapturedBody, CapturedBodyWriter}; pub(crate) use browser_context::{ PageLifecycleEventsEnableResult, SessionOwnerInspectorEnableResult, SessionOwnerRuntimeFrontendEnableResult, TargetNavigationLoadInputs, @@ -122,9 +147,8 @@ pub(crate) use fetch_support::{ PendingFetchResponseNavigation, PendingSubresourceFetchResidence, }; pub use fetch_support::{ - DocumentBodySource, FetchAuthChallenge, FetchInterceptionPattern, FetchRequestStage, - FetchResourceTypeFilter, InFlightSubresourceFetchRequest, PausedDocumentTransfer, - PendingFetchAuthNavigation, PendingFetchNavigation, PendingFetchResponseOpenedBodyStream, + FetchAuthChallenge, FetchInterceptionPattern, FetchRequestStage, FetchResourceTypeFilter, + InFlightSubresourceFetchRequest, PendingFetchAuthNavigation, PendingFetchNavigation, PendingSubresourceFetchAuthRequest, PendingSubresourceFetchAuthStage, PendingSubresourceFetchAuthStageChain, PendingSubresourceFetchOwnerKind, PendingSubresourceFetchRequest, PendingSubresourceFetchRequestStage, @@ -133,6 +157,11 @@ pub use fetch_support::{ ResponseStageUrlMatchPolicy, fetch_subresource_interception_config, fetch_subresource_interception_config_for_patterns, }; +pub(crate) use moli_core::browser::web_contents::OpenBodyStreamError; +pub use moli_core::browser::web_contents::{ + DocumentBodySource, PausedDocumentTransfer, PendingFetchResponseOpenedBodyStream, +}; +pub(crate) use moli_core::browser::{CapturedBody, CapturedBodyWriter}; pub use moli_protocol_cdp::{ CdpRendererCommandPolicy, CdpRendererCommandReplacement, CdpRendererCommandReplayDispatch, CdpRequest, ParsedCdpCommand, @@ -511,9 +540,7 @@ impl CommandDispatchContext { } pub(crate) use moli_protocol_cdp::{DEFAULT_LOADER_ID, monotonic_timestamp_seconds}; -pub(crate) use navigation_error::{ - NavigationNetworkError, NavigationNetworkErrorKind, NavigationRequestBlocked, -}; +pub(crate) use navigation_error::{NavigationNetworkError, NavigationRequestBlocked}; pub(crate) use output::NavigationBackgroundEvent; pub use output::{ BackgroundCommandResponsePayload, BackgroundEventSender, BackgroundProtocolEvent, @@ -560,11 +587,11 @@ pub(crate) use site_data_manager_surface::{ pub(crate) use state::BrowserContextResourceStorageHandles; pub(crate) use state::JavaScriptDialogError; pub use state::{ - BrowserContext, BrowserWindowBounds, DevToolsPageResidenceIdentity, DocumentStartScript, - DownloadNavigation, EmulatedDeviceMetrics, EmulatedGeolocationOverride, - EmulatedGeolocationOverrideState, EmulatedMediaOverrides, IsolatedWorldDefinition, - LoadedNavigation, NavigationDispatchState, NavigationLoadOutcome, NavigationRequestLoadPolicy, - PageAgentHost, PageNavigationHistoryEntry, RuntimeBindingDefinition, TargetInfo, URL_BASE, + BrowserContext, DevToolsPageResidenceIdentity, DocumentStartScript, DownloadNavigation, + EmulatedDeviceMetrics, EmulatedGeolocationOverride, EmulatedGeolocationOverrideState, + EmulatedMediaOverrides, IsolatedWorldDefinition, LoadedNavigation, NavigationDispatchState, + NavigationLoadOutcome, NavigationRequestLoadPolicy, PageAgentHost, PageNavigationHistoryEntry, + RuntimeBindingDefinition, TargetInfo, URL_BASE, }; pub(crate) use state::{ BrowserContextPageStorageHandles, BrowserContextStoragePartitionHandles, @@ -687,25 +714,6 @@ impl ConnectionNetworkRequestIdAllocator { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct IdleNavigationEngineReleaseResult { - pub(crate) reset: bool, - pub(crate) reason: &'static str, - pub(crate) loaded_browser_context_count: usize, - pub(crate) live_target_browser_context_count: usize, -} - -impl IdleNavigationEngineReleaseResult { - pub(crate) fn to_protocol_json(self) -> serde_json::Value { - json!({ - "reset": self.reset, - "reason": self.reason, - "loadedBrowserContextCount": self.loaded_browser_context_count, - "liveTargetBrowserContextCount": self.live_target_browser_context_count, - }) - } -} - impl PendingDeferredMainDocumentLoadCompletion { pub(crate) fn new( inner: crate::domains::activity::PendingDeferredMainDocumentLoadCompletionActivity, @@ -899,11 +907,13 @@ impl CdpInitialStoragePartitionOwner { fn new_default_browser_context( &self, + browser: &BrowserHandle, id: String, http_cache_root: Option, http_cache_max_bytes: Option, ) -> BrowserContext { BrowserContext::new_with_storage_partition_handles_and_http_cache( + browser, id, self.handles.clone(), http_cache_root, @@ -1022,126 +1032,19 @@ pub(crate) struct ServiceWorkerAutoAttachRelatedOwnerSession { pub(crate) wait_for_debugger_on_start: bool, } -/// Drop-addressable fallback engine for commands without a concrete Page host. -/// -/// `CdpConnection::drop` must release this engine before joining its extracted -/// BrowserContext network roots. A plain field would only be dropped after the -/// Drop implementation returned, which reverses that order. -struct StandaloneNavigationEngineSlot { - engine: OnceCell, - runtime_config: NavigationRuntimeConfig, - renderer_publication_sender: Option, -} - -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(), - runtime_config, - renderer_publication_sender: None, - } - } - - #[cfg(test)] - fn is_materialized(&self) -> bool { - self.engine.get().is_some() - } - - fn runtime_config(&self) -> NavigationRuntimeConfig { - self.engine - .get() - .map(NavigationEngine::runtime_config) - .unwrap_or_else(|| self.runtime_config.clone()) - } - - fn fetch_config(&self) -> &moli_fetch::FetchConfig { - self.engine - .get() - .map(NavigationEngine::fetch_config) - .unwrap_or_else(|| self.runtime_config.fetch_config()) - } - - fn layout_policy(&self) -> LayoutPolicy { - self.engine - .get() - .map(NavigationEngine::layout_policy) - .unwrap_or_else(|| self.runtime_config.layout_policy()) - } - - fn apply_fetch_defaults(&mut self, defaults: moli_fetch::FetchConfig) { - let config = self.runtime_config.fetch_config_mut(); - config.set_browser_identity(defaults.browser_identity().clone()); - config.set_http_proxy(defaults.http_proxy().map(str::to_owned)); - config.set_http_no_proxy(defaults.http_no_proxy().map(str::to_owned)); - config.set_tls_verify_host(defaults.tls_verify_host()); - if let Some(engine) = self.engine.get_mut() { - engine.set_browser_identity_override(defaults.browser_identity().clone()); - engine.set_http_proxy_override(defaults.http_proxy().map(str::to_owned)); - engine.set_http_no_proxy_override(defaults.http_no_proxy().map(str::to_owned)); - engine.set_tls_verify_host(defaults.tls_verify_host()); - } - } - - fn set_renderer_output_transport_sender( - &mut self, - sender: moli_core::RendererOutputTransportSender, - ) { - if let Some(engine) = self.engine.get() { - engine.set_renderer_output_transport_sender(sender.clone()); - } - self.renderer_publication_sender = Some(sender); - } - - fn replace(&mut self, engine: NavigationEngine) -> Option { - self.runtime_config = engine.runtime_config(); - if let Some(sender) = self.renderer_publication_sender.as_ref() { - engine.set_renderer_output_transport_sender(sender.clone()); - } - let previous = self.engine.take(); - self.engine - .set(engine) - .expect("standalone navigation engine slot must be empty after take"); - previous - } - - fn take(&mut self) -> Option { - self.engine.take() - } - - fn ensure(&self) -> &NavigationEngine { - self.engine.get_or_init(|| { - let engine = NavigationEngine::new_with_runtime_config(self.runtime_config.clone()); - if let Some(sender) = self.renderer_publication_sender.as_ref() { - engine.set_renderer_output_transport_sender(sender.clone()); - } - engine - }) - } - - #[cfg(test)] - fn ensure_mut(&mut self) -> &mut NavigationEngine { - self.ensure(); - self.engine - .get_mut() - .expect("standalone navigation engine was just materialized") - } +/// The single Browser-wide source for defaults inherited by every Context. +/// Clones are short-lived operation snapshots and never Context residents. +#[derive(Clone, Default)] +pub(crate) struct BrowserGlobalOverrides { + pub(crate) extra_headers: moli_fetch::RequestHeaders, + pub(crate) network_conditions: Option, + pub(crate) geolocation: Option, + pub(crate) cache_disabled: bool, } /// Persistent per-connection state. pub struct CdpConnection { + browser: BrowserHandle, // Browser/session routing state. pub browser_context: Option, pub inactive_browser_contexts: Vec, @@ -1175,20 +1078,13 @@ pub struct CdpConnection { next_page_domain_subscription_generation: u64, next_internal_runtime_command_id: u64, network_request_id_allocator: ConnectionNetworkRequestIdAllocator, - // Browser profile, permissions, download and global IO state. - pub window_bounds: BrowserWindowBounds, + // Browser profile, download and global IO state. download_policy: moli_core::browser::DownloadPolicy, download_subscriptions: download_policy::DownloadSubscriptions, - // Browser defaults remain embedded until the Browser aggregate cutover. - // Each physical Context owns its scoped rules; no wire-id registry lives here. - permission_defaults: moli_core::browser::PermissionDefaults, next_global_io_stream_id: u64, base_browser_identity: moli_browser_profile::BrowserIdentityProfile, - global_extra_headers: moli_fetch::RequestHeaders, + pub(crate) browser_global_overrides: BrowserGlobalOverrides, global_browser_identity_override: Option, - global_network_conditions: Option, - global_geolocation_override: Option, - global_cache_disabled: bool, pub(crate) network_data_collectors: crate::domains::network::NetworkDataCollectorStore, base_http_proxy: Option, base_http_no_proxy: Option, @@ -1207,110 +1103,16 @@ pub struct CdpConnection { // source-specific queue ownership is being migrated outward. scheduler_state: CdpConnectionSchedulerState, - // Standalone navigation state for commands that have no concrete Page - // owner. Every page-owned engine lives in its stable PageAgentHost. - standalone_navigation_engine: StandaloneNavigationEngineSlot, -} - -impl Default for CdpConnection { - fn default() -> Self { - Self::new() - } -} - -impl Drop for CdpConnection { - fn drop(&mut self) { - // Separate per-context roots from target/page state so teardown can - // follow the observable producer order even though NavigationEngines - // and detached local tasks retain only weak owner access. - let mut contexts = Vec::new(); - if let Some(context) = self.browser_context.take() { - contexts.push(context); - } - contexts.append(&mut self.inactive_browser_contexts); - let roots = contexts - .iter_mut() - .filter_map(BrowserContext::take_renderer_runtime_owner_for_teardown) - .collect::>(); - - let mut roots = roots; - for root in &mut roots { - root.terminate_renderer_producers_for_owner_shutdown(); - } - - // Dropping contexts releases every PageAgentHost and its Page state. - drop(contexts); - - // RenderRuntimeOwner joins happen when the last JsRuntime-backed - // NavigationEngine handle is released. Do that explicitly here rather - // than relying on field drop after this method returns. - drop(self.standalone_navigation_engine.take()); - - // Only after every Page engine and the standalone fallback are gone - // may the context roots close fetch admission and join network owners. - for root in &mut roots { - root.shutdown_network_and_join(); - } - drop(roots); - } + // Defaults only. Live engines belong to Core WebContents, never DevTools. + navigation_runtime_config: NavigationRuntimeConfig, } impl CdpConnection { - pub fn new() -> Self { - Self::new_with_initial_storage_partition(CdpInitialStoragePartition::memory()) - } - pub(crate) fn layout_policy(&self) -> LayoutPolicy { self.browser_context .as_ref() .and_then(|context| context.page_navigation_layout_policy(context.active_target_id()?)) - .unwrap_or_else(|| self.standalone_navigation_engine.layout_policy()) - } - - #[cfg(test)] - pub(crate) fn active_navigation_engine(&self) -> &NavigationEngine { - if let Some(engine) = self - .browser_context - .as_ref() - .and_then(|context| context.page_navigation_engine(context.active_target_id()?)) - { - return engine; - } - self.standalone_navigation_engine.ensure() - } - - #[cfg(test)] - pub(crate) fn active_navigation_engine_mut(&mut self) -> &mut NavigationEngine { - let active_owner = self - .browser_context - .as_ref() - .and_then(|context| Some((context.id.clone(), context.active_target_id()?.to_owned()))); - if let Some((browser_context_id, target_id)) = active_owner { - return self - .ensure_page_navigation_engine_for_target(&browser_context_id, &target_id) - .expect("active PageAgentHost navigation engine disappeared"); - } - self.standalone_navigation_engine.ensure_mut() - } - - #[cfg(test)] - pub(crate) fn ensure_page_navigation_engine_for_target( - &mut self, - browser_context_id: &str, - target_id: &str, - ) -> Option<&mut NavigationEngine> { - let needs_engine = self - .browser_context_by_id(browser_context_id)? - .page_navigation_engine(target_id) - .is_none(); - if needs_engine { - let config = self.standalone_navigation_engine.runtime_config(); - let sender = self.scheduler_hooks.renderer_publication_sender(); - self.browser_context_by_id_mut(browser_context_id)? - .bind_page_navigation_engines(config, sender); - } - self.browser_context_by_id_mut(browser_context_id)? - .page_navigation_engine_mut(target_id) + .unwrap_or_else(|| self.navigation_runtime_config.layout_policy()) } pub fn has_pending_javascript_dialog(&self) -> bool { @@ -1324,25 +1126,10 @@ impl CdpConnection { let Some(browser_context) = self.browser_context.as_ref() else { return false; }; - browser_context - .renderer_runtime() - .set_javascript_dialog_handler_enabled(enabled); + browser_context.set_javascript_dialog_handler_enabled(enabled); true } - pub fn new_with_initial_cookies(initial_cookies: Vec) -> Self { - Self::new_with_initial_storage_partition(CdpInitialStoragePartition::with_cookies( - initial_cookies, - )) - } - - pub fn new_with_fetch_config(fetch_config: FetchConfig) -> Self { - Self::new_with_initial_storage_partition_and_fetch_config( - CdpInitialStoragePartition::memory(), - fetch_config, - ) - } - pub fn enable_webdriver_bidi_download_events(&mut self) -> bool { self.download_subscriptions.enable_webdriver_bidi_events() } @@ -1351,80 +1138,10 @@ impl CdpConnection { self.download_subscriptions.disable_webdriver_bidi_events() } - pub fn new_with_initial_storage_partition( - initial_storage_partition: CdpInitialStoragePartition, - ) -> Self { - Self::new_with_initial_storage_partition_and_fetch_config( - initial_storage_partition, - FetchConfig::default(), - ) - } - - pub fn new_with_initial_storage_partition_and_fetch_config( - initial_storage_partition: CdpInitialStoragePartition, - fetch_config: FetchConfig, - ) -> Self { - Self::new_with_initial_storage_partition_fetch_config_and_resource_loading( - initial_storage_partition, - fetch_config, - OptionalResourceFetchMask::NONE, - true, - ) - } - - pub fn new_with_initial_storage_partition_fetch_config_and_image_fetch_enabled( - initial_storage_partition: CdpInitialStoragePartition, - fetch_config: FetchConfig, - image_fetch_enabled: bool, - ) -> Self { - let optional_resource_fetch_mask = if image_fetch_enabled { - OptionalResourceFetchMask::IMAGE - } else { - OptionalResourceFetchMask::NONE - }; - Self::new_with_initial_storage_partition_fetch_config_and_resource_loading( - initial_storage_partition, - fetch_config, - optional_resource_fetch_mask, - true, - ) - } - - pub fn new_with_initial_storage_partition_fetch_config_and_resource_loading( - initial_storage_partition: CdpInitialStoragePartition, - fetch_config: FetchConfig, - optional_resource_fetch_mask: OptionalResourceFetchMask, - subframe_loading_enabled: bool, - ) -> Self { - Self::new_with_initial_storage_partition_and_runtime_config( - initial_storage_partition, - NavigationRuntimeConfig::new( - fetch_config, - optional_resource_fetch_mask, - subframe_loading_enabled, - LayoutPolicy::default(), - ), - ) - } - - pub fn new_with_initial_storage_partition_and_runtime_config( - initial_storage_partition: CdpInitialStoragePartition, - navigation_runtime_config: NavigationRuntimeConfig, - ) -> Self { - Self::new_with_initial_storage_partition_owner_and_runtime_config( - CdpInitialStoragePartitionOwner::from_initial_storage_partition( - initial_storage_partition, - ), - navigation_runtime_config, - ) - } - - /// Creates protocol state without starting the renderer runtime. - /// - /// The runtime is materialized on first engine-backed operation. This is - /// intended for browser-level CDP owners that publish a target before any - /// frontend attaches to its page. - pub fn new_with_deferred_navigation_runtime( + /// Creates DevTools state attached to an externally owned Browser service. + /// Renderer resources are allocated when a concrete target is installed. + pub fn new( + browser: BrowserHandle, initial_storage_partition: CdpInitialStoragePartition, navigation_runtime_config: NavigationRuntimeConfig, ) -> Self { @@ -1432,34 +1149,13 @@ impl CdpConnection { CdpInitialStoragePartitionOwner::from_initial_storage_partition( initial_storage_partition, ); - Self::new_with_initial_storage_partition_owner_and_engine( - initial_storage_partition, - StandaloneNavigationEngineSlot::deferred(navigation_runtime_config), - ) - } - - fn new_with_initial_storage_partition_owner_and_runtime_config( - initial_storage_partition: CdpInitialStoragePartitionOwner, - navigation_runtime_config: NavigationRuntimeConfig, - ) -> Self { - Self::new_with_initial_storage_partition_owner_and_engine( - initial_storage_partition, - StandaloneNavigationEngineSlot::materialized( - NavigationEngine::new_with_runtime_config(navigation_runtime_config), - ), - ) - } - - fn new_with_initial_storage_partition_owner_and_engine( - initial_storage_partition: CdpInitialStoragePartitionOwner, - standalone_navigation_engine: StandaloneNavigationEngineSlot, - ) -> Self { - let fetch_config = standalone_navigation_engine.fetch_config(); + let fetch_config = navigation_runtime_config.fetch_config(); let base_browser_identity = fetch_config.browser_identity().clone(); let base_http_proxy = fetch_config.http_proxy().map(str::to_owned); let base_http_no_proxy = fetch_config.http_no_proxy().map(str::to_owned); let base_tls_verify_host = fetch_config.tls_verify_host(); Self { + browser, browser_context: None, inactive_browser_contexts: Vec::new(), target_discovery_enabled: false, @@ -1472,10 +1168,8 @@ impl CdpConnection { service_worker_pause_on_start_owner_sessions: HashSet::new(), dedicated_worker_pause_on_start_owner_sessions: HashSet::new(), install_default_target_on_auto_attach: false, - window_bounds: BrowserWindowBounds::default(), download_policy: moli_core::browser::DownloadPolicy::default(), download_subscriptions: download_policy::DownloadSubscriptions::default(), - permission_defaults: moli_core::browser::PermissionDefaults::default(), next_bc_id: 0, next_global_io_stream_id: 0, next_target_id: 0, @@ -1487,11 +1181,8 @@ impl CdpConnection { next_internal_runtime_command_id: 902_000_000, network_request_id_allocator: ConnectionNetworkRequestIdAllocator::default(), base_browser_identity, - global_extra_headers: Default::default(), + browser_global_overrides: BrowserGlobalOverrides::default(), global_browser_identity_override: None, - global_network_conditions: None, - global_geolocation_override: None, - global_cache_disabled: false, network_data_collectors: crate::domains::network::NetworkDataCollectorStore::default(), base_http_proxy, base_http_no_proxy, @@ -1502,7 +1193,7 @@ impl CdpConnection { scheduler_hooks: CdpSchedulerHooks::default(), target_host_lifecycle_observer: None, scheduler_state: CdpConnectionSchedulerState::default(), - standalone_navigation_engine, + navigation_runtime_config, } } @@ -1546,8 +1237,6 @@ impl CdpConnection { ) { self.scheduler_hooks .set_renderer_publication_sender(sender.clone()); - self.standalone_navigation_engine - .set_renderer_output_transport_sender(sender.clone()); for context in self .browser_context .iter_mut() @@ -1781,17 +1470,6 @@ impl CdpConnection { Ok(()) } - /// Validates ordinary document-command admission without lending Browser state. - /// Configuration and interruptible work intentionally do not use this gate. - pub(crate) fn resolve_document_command_owner( - &self, - owner: &CommandOwnerScope, - ) -> Result<(String, String), String> { - self.ensure_document_accessible_for_owner(owner)?; - self.loaded_document_owner_identity_for_owner(owner) - .ok_or_else(|| "NoDocumentLoaded".to_owned()) - } - pub(crate) fn current_document_loader_id_for_session_owner( &self, session_id: Option<&str>, @@ -2608,18 +2286,6 @@ impl CdpConnection { .await } - pub(crate) fn replace_standalone_navigation_engine(&mut self, engine: NavigationEngine) { - let engine = engine; - self.apply_scheduler_senders_to_navigation_engine(&engine); - drop(self.standalone_navigation_engine.replace(engine)); - } - - pub(crate) fn apply_scheduler_senders_to_navigation_engine(&self, engine: &NavigationEngine) { - if let Some(sender) = self.scheduler_hooks.renderer_publication_sender() { - engine.set_renderer_output_transport_sender(sender); - } - } - pub(crate) fn enqueue_deferred_main_document_load_completion( &mut self, admission: crate::domains::activity::DeferredMainDocumentLoadCompletionAdmission, @@ -2894,7 +2560,7 @@ impl CdpConnection { pub(crate) fn response_body_materialize_limit(&self) -> usize { self.fetch_config() .http_max_response_size() - .unwrap_or(body_spool::DEFAULT_BODY_MATERIALIZE_LIMIT) + .unwrap_or(moli_core::browser::DEFAULT_BODY_MATERIALIZE_LIMIT) } pub(crate) fn moli_memory_diagnostics(&self) -> serde_json::Value { @@ -3012,19 +2678,20 @@ impl CdpConnection { let active_engine = self .browser_context .as_ref() - .and_then(|context| context.page_navigation_diagnostics(context.active_target_id()?)) - .unwrap_or_else(|| self.standalone_navigation_engine.ensure().diagnostics()); - let active_renderer_owner_id = active_engine.renderer_owner_id; + .and_then(|context| context.page_navigation_diagnostics(context.active_target_id()?)); + let active_renderer_owner_id = active_engine + .as_ref() + .map(|engine| engine.renderer_owner_id); let mut page_navigation_engine_renderer_owner_ids = HashSet::new(); let mut estimated_renderer_owner_ids = HashSet::new(); - estimated_renderer_owner_ids.insert(active_renderer_owner_id); + estimated_renderer_owner_ids.extend(active_renderer_owner_id); estimated_renderer_owner_ids.extend(document_renderer_owner_ids.iter().copied()); for renderer_owner_id in self.browser_contexts().flat_map(|browser_context| { browser_context.page_targets.iter().filter_map(|target| { browser_context.page_navigation_renderer_owner_id(target.target_id()) }) }) { - if renderer_owner_id != active_renderer_owner_id { + if Some(renderer_owner_id) != active_renderer_owner_id { page_navigation_engine_renderer_owner_ids.insert(renderer_owner_id); } estimated_renderer_owner_ids.insert(renderer_owner_id); @@ -3032,10 +2699,12 @@ impl CdpConnection { let page_navigation_engine_renderer_owner_count = page_navigation_engine_renderer_owner_ids.len(); let estimated_renderer_owner_count = estimated_renderer_owner_ids.len(); - let document_isolate_model = active_engine.document_isolate_model; + let document_isolate_model = + moli_core::page::RendererDocumentIsolateAccountingDiagnostics::MODEL; let estimated_document_isolate_count = loaded_document_page_count + pending_document_page_build_count; - let document_isolate_accounting = active_engine.document_isolate_accounting; + let document_isolate_accounting = + moli_core::page::RendererDocumentIsolateAccountingDiagnostics::snapshot(); let document_isolate_accounting = json!({ "scope": "renderer-process", "created": document_isolate_accounting.created, @@ -3047,13 +2716,15 @@ impl CdpConnection { + shared_worker_running_worker_isolate_count; let estimated_live_v8_isolate_count = estimated_document_isolate_count + estimated_worker_isolate_count; - let active_navigation_engine_resource_runtime = active_engine.resource_runtime; - let active_navigation_engine_resource_runtime_id = - active_navigation_engine_resource_runtime - .as_ref() - .map(|diagnostics| diagnostics.runtime_id); - let active_navigation_engine_memory_cache = - active_navigation_engine_resource_runtime.map(|diagnostics| diagnostics.memory_cache); + let active_navigation_engine = active_engine.map(|engine| json!({ + "imageFetchEnabled": engine.image_fetch_enabled, + "optionalResourceFetchMask": engine.optional_resource_fetch_mask.bits(), + "subframeLoadingEnabled": engine.subframe_loading_enabled, + "resourceRuntime": engine.resource_runtime, + "resourceRuntimeId": engine.resource_runtime.as_ref().map(|runtime| runtime.runtime_id), + "networkMemoryCache": engine.resource_runtime.map(|runtime| runtime.memory_cache), + "browserContextRuntime": engine.browser_context_runtime, + })); json!({ "connection": { "hasActiveBrowserContext": self.browser_context.is_some(), @@ -3067,20 +2738,12 @@ impl CdpConnection { "autoAttach": self.auto_attach_enabled(), "targetDiscoveryEnabled": self.target_discovery_enabled, "targetInfoChangeEventsEnabled": self.target_info_change_events_enabled, - "activeNavigationEngine": { - "imageFetchEnabled": active_engine.image_fetch_enabled, - "optionalResourceFetchMask": active_engine.optional_resource_fetch_mask.bits(), - "subframeLoadingEnabled": active_engine.subframe_loading_enabled, - "resourceRuntime": active_navigation_engine_resource_runtime, - "resourceRuntimeId": active_navigation_engine_resource_runtime_id, - "networkMemoryCache": active_navigation_engine_memory_cache, - "browserContextRuntime": active_engine.browser_context_runtime, - }, + "activeNavigationEngine": active_navigation_engine, }, "isolateScope": { "documentIsolateModel": document_isolate_model, "workerIsolateModel": "per-worker-thread", - "activeNavigationEngineRendererOwnerCount": 1, + "activeNavigationEngineRendererOwnerCount": usize::from(active_renderer_owner_id.is_some()), "pageNavigationEngineRendererOwnerCount": page_navigation_engine_renderer_owner_count, "estimatedRendererOwnerCount": estimated_renderer_owner_count, "browserContextCount": browser_context_count, @@ -3123,7 +2786,7 @@ impl CdpConnection { }) } - fn idle_navigation_engine_release_counts(&self) -> (usize, usize) { + pub(crate) fn moli_reset_idle_navigation_engine_for_diagnostics(&self) -> serde_json::Value { let loaded_browser_context_count = self .browser_contexts() .filter(|browser_context| browser_context.loaded_document_page_count() != 0) @@ -3136,65 +2799,24 @@ impl CdpConnection { }) .count(); - ( - loaded_browser_context_count, - live_target_browser_context_count, - ) - } - - pub(crate) fn release_idle_navigation_engine_memory_if_idle( - &mut self, - ) -> IdleNavigationEngineReleaseResult { - let (loaded_browser_context_count, live_target_browser_context_count) = - self.idle_navigation_engine_release_counts(); let eligible = loaded_browser_context_count == 0 && live_target_browser_context_count == 0; - if !eligible { - return IdleNavigationEngineReleaseResult { - reset: false, - reason: "not-idle", - loaded_browser_context_count, - live_target_browser_context_count, - }; - } - - let replacement = NavigationEngine::new_with_runtime_config( - self.standalone_navigation_engine.runtime_config(), - ); - self.replace_standalone_navigation_engine(replacement); - IdleNavigationEngineReleaseResult { - reset: true, - reason: "idle-engine-replaced", - loaded_browser_context_count, - live_target_browser_context_count, - } - } - - pub(crate) fn release_idle_navigation_engine_memory_after_target_close(&mut self) { - let result = self.release_idle_navigation_engine_memory_if_idle(); - if result.reset { - tracing::debug!( - target: "moli_cdp_memory", - reason = result.reason, - "released idle navigation engine after final target close" - ); - } - } - - pub(crate) fn moli_reset_idle_navigation_engine_for_diagnostics( - &mut self, - ) -> serde_json::Value { - self.release_idle_navigation_engine_memory_if_idle() - .to_protocol_json() + // Retain the diagnostic command without allocating a replacement for + // an engine DevTools no longer owns. Core releases target resources. + json!({ + "reset": false, + "reason": if eligible { "no-standalone-engine" } else { "not-idle" }, + "loadedBrowserContextCount": loaded_browser_context_count, + "liveTargetBrowserContextCount": live_target_browser_context_count, + }) } pub(crate) fn new_browser_context(&self, id: String) -> BrowserContext { - let mut browser_context = self.initial_storage_partition.new_default_browser_context( + self.initial_storage_partition.new_default_browser_context( + &self.browser, id, self.fetch_config().http_cache_dir().map(PathBuf::from), self.fetch_config().http_cache_max_bytes(), - ); - self.apply_global_browser_context_state(&mut browser_context); - browser_context + ) } pub(crate) fn ensure_browser_context_for_implicit_target_creation(&mut self) { @@ -3209,21 +2831,13 @@ impl CdpConnection { self.insert_browser_context(self.new_browser_context(browser_context_id)); } - fn apply_global_browser_context_state(&self, browser_context: &mut BrowserContext) { - 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; - browser_context.global_geolocation_override = self.global_geolocation_override.clone(); - } - pub(crate) fn new_ephemeral_browser_context(&self, id: String) -> BrowserContext { - let mut browser_context = BrowserContext::new_ephemeral_with_http_cache( + BrowserContext::new_ephemeral_with_http_cache( + &self.browser, id, self.fetch_config().http_cache_dir().map(PathBuf::from), self.fetch_config().http_cache_max_bytes(), - ); - self.apply_global_browser_context_state(&mut browser_context); - browser_context + ) } pub fn snapshot_cookies(&mut self) -> Vec { @@ -3875,6 +3489,7 @@ impl CdpConnection { let was_placeholder = self.default_target_lifecycle.is_placeholder(); let default_browser_context_id = self.default_browser_context_id().to_owned(); let default_target_id = self.default_target_id().to_owned(); + let browser_cache_disabled = self.browser_global_overrides.cache_disabled; if !self.has_browser_context_id(&default_browser_context_id) { let mut browser_context = self.new_browser_context(default_browser_context_id.clone()); browser_context.set_active_target_id(default_target_id.clone()); @@ -3904,6 +3519,8 @@ impl CdpConnection { browser_context .begin_active_target_initial_empty_document("about:blank".to_owned()); } + browser_context + .set_base_cache_disabled_for_target(&default_target_id, browser_cache_disabled); } } if !self diff --git a/moli-protocol/src/conn/activity_source.rs b/moli-protocol/src/conn/activity_source.rs index 8bb022553c..907041e180 100644 --- a/moli-protocol/src/conn/activity_source.rs +++ b/moli-protocol/src/conn/activity_source.rs @@ -1,101 +1,16 @@ -use moli_core::page::{CompletedPageCommand, PendingPageCommand, RendererPageDiagnosticsSnapshot}; +use moli_core::page::RendererPageDiagnosticsSnapshot; use std::time::Instant; -use super::{CdpConnection, CdpSessionRoute, CommandOwnerScope}; - -pub(crate) struct PendingChildFrameLifecycleWork { - owner: CommandOwnerScope, - pending: PendingPageCommand, -} - -pub(crate) struct CompletedChildFrameLifecycleWork { - owner: CommandOwnerScope, - completion: CompletedPageCommand, -} - -impl PendingChildFrameLifecycleWork { - pub(crate) async fn wait(self) -> Result { - let completion = self - .pending - .wait() - .await - .map_err(|error| error.to_string())?; - Ok(CompletedChildFrameLifecycleWork { - owner: self.owner, - completion, - }) - } -} +use super::{CdpConnection, CommandOwnerScope, PendingChildFrameLifecycleWork}; impl CdpConnection { - fn activity_source_page_route(&mut self, session_id: Option<&str>) -> Option<(String, String)> { - let owner = CommandOwnerScope::capture(self, session_id); - self.activity_source_page_route_for_owner(&owner) - } - - fn activity_source_page_route_for_owner( - &mut self, - owner: &CommandOwnerScope, - ) -> Option<(String, String)> { - let route = owner.resolve_route(self)?; - let (browser_context_id, target_id) = match &route { - CdpSessionRoute::PageTarget { - browser_context_id, - target_id, - .. - } => (browser_context_id.clone(), target_id.clone()), - CdpSessionRoute::Browser => { - let context = self.browser_context.as_ref()?; - (context.id.clone(), context.active_target_id()?.to_owned()) - } - CdpSessionRoute::BrowserContext { browser_context_id } => ( - browser_context_id.clone(), - self.browser_context_by_id(browser_context_id)? - .active_target_id()? - .to_owned(), - ), - CdpSessionRoute::TabTarget { .. } - | CdpSessionRoute::SharedWorkerTarget { .. } - | CdpSessionRoute::DedicatedWorkerTarget { .. } - | CdpSessionRoute::ServiceWorkerTarget { .. } => return None, - }; - Some((browser_context_id, target_id)) - } - pub(crate) fn start_child_frame_lifecycle_work_for_owner( &mut self, owner: CommandOwnerScope, timeout: std::time::Duration, ) -> Result { - let (context_id, target_id) = self - .activity_source_page_route_for_owner(&owner) - .ok_or("NoDocumentLoaded")?; - let pending = self - .browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .start_target_child_frame_lifecycle_work(&target_id, timeout)?; - Ok(PendingChildFrameLifecycleWork { owner, pending }) - } - - pub(crate) fn complete_child_frame_lifecycle_work_command_turn_for_session_owner( - &mut self, - pending: CompletedChildFrameLifecycleWork, - ) -> Result<(bool, moli_core::page::RendererCommandTurnOutput), String> { - let (context_id, target_id) = self - .activity_source_page_route_for_owner(&pending.owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .complete_target_child_frame_lifecycle_work(&target_id, pending.completion) - } - - #[cfg(test)] - pub(crate) fn complete_child_frame_lifecycle_work_for_session_owner( - &mut self, - pending: CompletedChildFrameLifecycleWork, - ) -> Result { - self.complete_child_frame_lifecycle_work_command_turn_for_session_owner(pending) - .map(|(completed, _output)| completed) + let document = self.loaded_browser_document_for_owner(&owner)?; + self.start_document_child_frame_lifecycle_work(document, timeout) } pub async fn page_diagnostics_snapshot_for_session_owner_async( @@ -108,7 +23,8 @@ impl CdpConnection { session_id, trace_started, ); - let Some((context_id, target_id)) = self.activity_source_page_route(session_id) else { + let owner = CommandOwnerScope::capture(self, session_id); + let Ok(document) = self.loaded_browser_document_for_owner(&owner) else { trace_activity_source_stage( "conn_page_diagnostics_snapshot_missing_owner", session_id, @@ -116,34 +32,23 @@ impl CdpConnection { ); return Ok(RendererPageDiagnosticsSnapshot::default()); }; - let context = self - .browser_context_by_id_mut(&context_id) - .expect("resolved BrowserContext"); - if !context.target_has_loaded_page(&target_id) { - trace_activity_source_stage( - "conn_page_diagnostics_snapshot_missing_page", - session_id, - trace_started, - ); - return Ok(RendererPageDiagnosticsSnapshot::default()); - }; let renderer_started = moli_trace::cdp_runtime_trace_enabled().then(Instant::now); - let snapshot = context - .target_page_diagnostics_snapshot_async(&target_id) - .await - .map_err(|error| error.to_string())?; + let completed = self + .start_document_diagnostics_snapshot(document)? + .wait() + .await; trace_activity_source_stage( "conn_page_diagnostics_snapshot_renderer_done", session_id, renderer_started, ); let ingest_started = moli_trace::cdp_runtime_trace_enabled().then(Instant::now); - let ingested = context.ingest_owner_page_observable_output_updates_for_target(&target_id); + let snapshot = self.finish_document_diagnostics_snapshot(completed)?; trace_activity_source_stage_with_bool( "conn_page_diagnostics_snapshot_ingest_done", session_id, ingest_started, - ingested, + true, ); trace_activity_source_stage( "conn_page_diagnostics_snapshot_done", diff --git a/moli-protocol/src/conn/bidi_channel_work.rs b/moli-protocol/src/conn/bidi_channel_work.rs index f3c0fe73b0..e963ed1f16 100644 --- a/moli-protocol/src/conn/bidi_channel_work.rs +++ b/moli-protocol/src/conn/bidi_channel_work.rs @@ -158,11 +158,10 @@ impl BidiChannelOwnerAction { #[cfg(test)] mod tests { use super::*; - use crate::conn::BrowserContext; fn connection_with_page_session() -> CdpConnection { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-owner".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-owner".to_owned()); browser_context.set_active_target_id("TID-owner"); browser_context.attach_active_session("SID-owner".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); diff --git a/moli-protocol/src/conn/browser_context/dialog_owner.rs b/moli-protocol/src/conn/browser_context/dialog_owner.rs index 71b0961d80..e4f44069d3 100644 --- a/moli-protocol/src/conn/browser_context/dialog_owner.rs +++ b/moli-protocol/src/conn/browser_context/dialog_owner.rs @@ -5,8 +5,8 @@ use crate::conn::{ state::{JavaScriptDialogClosed, JavaScriptDialogError, JavaScriptDialogSnapshot}, }; -// Exact routing remains in DevTools; the value-only Browser calls replace this -// in-place bridge at Commit 22. No renderer dispatch lane or Page borrow here. +// DevTools resolves its projection once; Browser calls below receive only the +// exact physical Document capability and neutral dialog key. impl CdpConnection { pub(crate) fn install_javascript_dialog_for_session( &mut self, @@ -19,13 +19,34 @@ impl CdpConnection { let _ = dialog.finish(false, String::new()); return false; }; - owner.browser_context.install_javascript_dialog_for_target( + let Some(document) = owner + .browser_context + .document_handle_for_target(&owner.target_id) + .filter(|document| document.id() == page_owner.document_id()) + else { + let _ = dialog.finish(false, String::new()); + return false; + }; + let Ok(key) = owner + .browser_context + .install_document_javascript_dialog(document, dialog) + else { + return false; + }; + if owner.browser_context.project_javascript_dialog_for_session( &owner.target_id, &owner.session_key, - page_owner, source_frame_id, - dialog, - ) + document, + key, + ) { + true + } else { + owner + .browser_context + .dismiss_document_javascript_dialog(document, key); + false + } } pub(crate) fn javascript_dialog_snapshot_for_owner( @@ -33,9 +54,12 @@ impl CdpConnection { owner: &CommandOwnerScope, ) -> Option { let owner = self.target_session_owner_ref_for_owner(owner)?; + let (document, key) = owner + .browser_context + .projected_javascript_dialog_for_session(&owner.target_id, &owner.session_key)?; owner .browser_context - .javascript_dialog_snapshot_for_target(&owner.target_id, &owner.session_key) + .document_javascript_dialog_snapshot(document, key) } pub(crate) fn set_javascript_dialog_prompt_text_for_owner( @@ -46,13 +70,13 @@ impl CdpConnection { let owner = self .target_session_owner_mut_for_owner(owner) .ok_or(JavaScriptDialogError::NotFound)?; + let (document, key) = owner + .browser_context + .projected_javascript_dialog_for_session(&owner.target_id, &owner.session_key) + .ok_or(JavaScriptDialogError::NotFound)?; owner .browser_context - .set_javascript_dialog_prompt_text_for_target( - &owner.target_id, - &owner.session_key, - prompt_text, - ) + .set_document_javascript_dialog_prompt_text(document, key, prompt_text) } pub(crate) fn handle_javascript_dialog_for_owner( @@ -62,11 +86,33 @@ impl CdpConnection { prompt_text: Option, ) -> Option<(String, JavaScriptDialogClosed)> { let owner = self.target_session_owner_mut_for_owner(owner)?; - owner.browser_context.handle_javascript_dialog_for_target( - &owner.target_id, - &owner.session_key, + let Some((document, key)) = owner + .browser_context + .projected_javascript_dialog_for_session(&owner.target_id, &owner.session_key) + else { + let projections = owner + .browser_context + .take_projected_javascript_dialogs_for_session( + &owner.target_id, + &owner.session_key, + ); + owner + .browser_context + .dismiss_projected_javascript_dialogs(projections); + return None; + }; + let projection = owner + .browser_context + .pop_projected_javascript_dialog_for_session(&owner.target_id, &owner.session_key)?; + let (source_frame_id, projected_document, projected_key) = projection.into_parts(); + debug_assert_eq!(projected_document, document); + debug_assert_eq!(projected_key, key); + let outcome = owner.browser_context.finish_document_javascript_dialog( + document, + key, accepted, prompt_text, - ) + )?; + Some((source_frame_id, outcome)) } } diff --git a/moli-protocol/src/conn/browser_context/emulation_owner.rs b/moli-protocol/src/conn/browser_context/emulation_owner.rs index c50fbf1e8c..9c83733238 100644 --- a/moli-protocol/src/conn/browser_context/emulation_owner.rs +++ b/moli-protocol/src/conn/browser_context/emulation_owner.rs @@ -53,7 +53,7 @@ impl TargetSessionOwnerMut<'_> { &mut self, locale_override: Option, fallback_identity: &moli_browser_profile::BrowserIdentityProfile, - ) -> Result<(), &'static str> { + ) -> Result<(), String> { self.browser_context .set_base_locale_override_for_target(&self.target_id, locale_override.clone())?; self.browser_context @@ -68,7 +68,7 @@ impl TargetSessionOwnerMut<'_> { fn set_base_timezone_override( &mut self, timezone_override: Option, - ) -> Result<(), &'static str> { + ) -> Result<(), String> { self.browser_context .set_base_timezone_override_for_target(&self.target_id, timezone_override) } @@ -89,7 +89,7 @@ impl TargetSessionOwnerRef<'_> { Some( target .devtools_sessions - .emulation_disposal_is_effectively_noop(&self.session_key, effective), + .emulation_disposal_is_effectively_noop(&self.session_key, &effective), ) } @@ -160,7 +160,7 @@ impl CdpConnection { &mut self, owner: &crate::conn::CommandOwnerScope, locale_override: Option, - ) -> Result<(), &'static str> { + ) -> Result<(), String> { let fallback_identity = self.base_browser_identity.clone(); self.target_session_owner_mut_for_owner(owner) .ok_or("BrowserContextNotLoaded")? @@ -171,7 +171,7 @@ impl CdpConnection { &mut self, owner: &crate::conn::CommandOwnerScope, timezone_override: Option, - ) -> Result<(), &'static str> { + ) -> Result<(), String> { self.target_session_owner_mut_for_owner(owner) .ok_or("BrowserContextNotLoaded")? .set_base_timezone_override(timezone_override) diff --git a/moli-protocol/src/conn/browser_context/fetch_owner.rs b/moli-protocol/src/conn/browser_context/fetch_owner.rs index 841599ed47..ded60044bb 100644 --- a/moli-protocol/src/conn/browser_context/fetch_owner.rs +++ b/moli-protocol/src/conn/browser_context/fetch_owner.rs @@ -1,6 +1,6 @@ use super::target_session_owner::TargetSessionOwnerMut; use super::*; -use crate::conn::fetch_support::OpenBodyStreamError; +use crate::conn::OpenBodyStreamError; use crate::conn::state::{ ClaimedNavigationRequest, InterceptedNavigationResponse, NavigationInterceptionPermit, NavigationRequestInterception, @@ -11,8 +11,8 @@ use crate::conn::state::{ use crate::conn::{ CapturedBody, ClaimedFetchNavigation, ClaimedFetchResponseNavigation, CommandOwnerScope, CompletedFetchResponseBodyStreamReadDispatch, FetchInterceptionPattern, FetchRequestStage, - InFlightSubresourceFetchRequest, PausedDocumentTransfer, PendingFetchAuthNavigation, - PendingFetchNavigation, PendingFetchResponseBodyStreamRead, + InFlightSubresourceFetchRequest, PausedDocumentTransfer, PendingDocumentFetchCommand, + PendingFetchAuthNavigation, PendingFetchNavigation, PendingFetchResponseBodyStreamRead, PendingFetchResponseBodyStreamReadDispatch, PendingFetchResponseBodyStreamReadStart, PendingFetchResponseNavigation, PendingSubresourceFetchAuthRequest, PendingSubresourceFetchRequest, PendingSubresourceFetchResponseRequest, TargetRuntimeSlot, @@ -213,20 +213,28 @@ fn target_scoped_stream_owner_matches_session( fn remove_network_intercept_from_browser_context( browser_context: &mut BrowserContext, intercept_id: &str, -) -> Result>, String> { +) -> Result>, String> { let target_ids = browser_context .page_targets .iter() .map(|target| target.target_id().to_owned()) .collect::>(); for target_id in target_ids { + let web_contents = browser_context + .web_contents_handle_for_target(&target_id) + .ok_or("WebContents unavailable")?; let target = browser_context .page_target_mut(&target_id) .expect("target must remain registered"); if target.fetch_owner.remove_network_intercept(intercept_id) { let (enabled, resource_type) = target.fetch_owner.subresource_interception_config(); return browser_context - .start_target_fetch_interception_update(&target_id, enabled, resource_type) + .start_web_contents_fetch_interception_update( + web_contents, + enabled, + resource_type, + true, + ) .map(Some) .map_err(|error| format!("failed to update page fetch interception: {error}")); } @@ -236,28 +244,6 @@ fn remove_network_intercept_from_browser_context( } impl CdpConnection { - pub(crate) fn finish_removed_network_interception( - &mut self, - completion: moli_core::page::CompletedPageCommand, - ) -> Result<(), String> { - // BiDi's global removal may refer to a background or inactive page. - // Observe only the physical origin of the completion, not the command - // caller's selected page. Retirement does not undo the installed policy. - for context in self - .browser_context - .iter_mut() - .chain(self.inactive_browser_contexts.iter_mut()) - { - if context.observe_renderer_page_state(completion.page_state()) { - break; - } - } - completion - .into_unit_page_command_turn() - .map(drop) - .map_err(|error| error.to_string()) - } - pub(crate) fn target_fetch_subresource_interception_snapshot_for_owner( &self, owner: &CommandOwnerScope, @@ -587,6 +573,8 @@ impl CdpConnection { document_navigation_token: Option, navigation: crate::conn::NavigationDispatchState, body: crate::conn::DocumentBodySource, + body_progress_source: crate::domains::network::MainDocumentBodyProgressSource, + prepared_document: Option>, ) -> bool { let Some((browser_context_id, target_id)) = self.resolved_page_owner_identity_for_owner(owner) @@ -615,7 +603,12 @@ impl CdpConnection { .fetch_owner .register_pending_fetch_response_navigation( request_id, - PendingFetchResponseNavigation::new(navigation, permit), + PendingFetchResponseNavigation::new_with_response_projection( + navigation, + permit, + body_progress_source, + prepared_document, + ), ); true } @@ -1360,7 +1353,7 @@ impl CdpConnection { session_id: Option<&str>, handle_auth_requests: bool, patterns: Vec, - ) -> Result, String> { + ) -> Result, String> { let owner = CommandOwnerScope::capture(self, session_id); self.start_enable_fetch_for_owner(&owner, handle_auth_requests, patterns) } @@ -1370,7 +1363,7 @@ impl CdpConnection { command_owner: &CommandOwnerScope, handle_auth_requests: bool, patterns: Vec, - ) -> Result, String> { + ) -> Result, String> { let Some(mut owner) = self.target_session_owner_mut_for_owner(command_owner) else { return Err("BrowserContextNotLoaded".to_owned()); }; @@ -1379,12 +1372,17 @@ impl CdpConnection { handle_auth_requests, patterns, ); + let web_contents = owner + .browser_context + .web_contents_handle_for_target(&owner.target_id) + .ok_or("WebContents unavailable")?; owner .browser_context - .start_target_fetch_interception_update( - &owner.target_id, + .start_web_contents_fetch_interception_update( + web_contents, subresource_enabled, subresource_resource_type, + false, ) .map_err(|error| format!("failed to update page fetch interception: {error}")) } @@ -1398,7 +1396,7 @@ impl CdpConnection { handle_auth_requests: bool, auth_url_patterns: Vec, patterns: Vec, - ) -> Result, String> { + ) -> Result, String> { let Some(mut owner) = self.target_session_owner_mut_for_owner(command_owner) else { return Err("BrowserContextNotLoaded".to_owned()); }; @@ -1409,12 +1407,17 @@ impl CdpConnection { auth_url_patterns, patterns, ); + let web_contents = owner + .browser_context + .web_contents_handle_for_target(&owner.target_id) + .ok_or("WebContents unavailable")?; owner .browser_context - .start_target_fetch_interception_update( - &owner.target_id, + .start_web_contents_fetch_interception_update( + web_contents, subresource_enabled, subresource_resource_type, + false, ) .map_err(|error| format!("failed to update page fetch interception: {error}")) } @@ -1424,7 +1427,7 @@ impl CdpConnection { command_owner: &CommandOwnerScope, intercept_id: &str, allow_global_lookup: bool, - ) -> Result, String> { + ) -> Result, String> { let Some(mut owner) = self.target_session_owner_mut_for_owner(command_owner) else { return Err("BrowserContextNotLoaded".to_owned()); }; @@ -1436,12 +1439,17 @@ impl CdpConnection { } return Err("NetworkInterceptNotFound".to_owned()); }; + let web_contents = owner + .browser_context + .web_contents_handle_for_target(&owner.target_id) + .ok_or("WebContents unavailable")?; owner .browser_context - .start_target_fetch_interception_update( - &owner.target_id, + .start_web_contents_fetch_interception_update( + web_contents, subresource_enabled, subresource_resource_type, + false, ) .map_err(|error| format!("failed to update page fetch interception: {error}")) } @@ -1449,7 +1457,7 @@ impl CdpConnection { fn start_remove_network_intercept_from_any_target( &mut self, intercept_id: &str, - ) -> Result, String> { + ) -> Result, String> { if let Some(browser_context) = self.browser_context.as_mut() && let Some(pending) = remove_network_intercept_from_browser_context(browser_context, intercept_id)? @@ -1471,7 +1479,7 @@ impl CdpConnection { session_id: Option<&str>, ) -> Option<( SessionOwnerPendingFetchState, - Result, String>, + Result, String>, )> { let owner = CommandOwnerScope::capture(self, session_id); self.start_disable_fetch_for_owner(&owner, true) @@ -1483,7 +1491,7 @@ impl CdpConnection { renderer_policy_reconciled: bool, ) -> Option<( SessionOwnerPendingFetchState, - Result, String>, + Result, String>, )> { let owner = CommandOwnerScope::capture(self, session_id); self.start_disable_fetch_for_owner(&owner, !renderer_policy_reconciled) @@ -1495,7 +1503,7 @@ impl CdpConnection { enqueue_renderer_update: bool, ) -> Option<( SessionOwnerPendingFetchState, - Result, String>, + Result, String>, )> { let mut owner = self.target_session_owner_mut_for_owner(command_owner)?; let (pending, (subresource_enabled, subresource_resource_type), removed) = owner @@ -1506,21 +1514,34 @@ impl CdpConnection { // renderer lifecycle interrupt, so only publish it to the Browser // owner here and never wait behind active JavaScript. let page_command = if enqueue_renderer_update { - owner + let web_contents = owner .browser_context - .start_target_fetch_interception_update( - &owner.target_id, - subresource_enabled, - subresource_resource_type, - ) + .web_contents_handle_for_target(&owner.target_id) + .ok_or_else(|| "WebContents unavailable".to_owned()); + web_contents.and_then(|web_contents| { + owner + .browser_context + .start_web_contents_fetch_interception_update( + web_contents, + subresource_enabled, + subresource_resource_type, + false, + ) + }) } else if removed { owner .browser_context - .install_target_fetch_interception_policy( - &owner.target_id, - subresource_enabled, - subresource_resource_type, - ) + .web_contents_handle_for_target(&owner.target_id) + .ok_or_else(|| "WebContents unavailable".to_owned()) + .and_then(|web_contents| { + owner + .browser_context + .install_web_contents_fetch_interception_policy( + web_contents, + subresource_enabled, + subresource_resource_type, + ) + }) .map(|()| None) } else { Ok(None) diff --git a/moli-protocol/src/conn/browser_context/lifecycle.rs b/moli-protocol/src/conn/browser_context/lifecycle.rs index d8c79c8440..e49adfe3ad 100644 --- a/moli-protocol/src/conn/browser_context/lifecycle.rs +++ b/moli-protocol/src/conn/browser_context/lifecycle.rs @@ -1,6 +1,19 @@ use super::*; impl CdpConnection { + pub(crate) fn active_browser_context_id(&self) -> Option { + self.browser_context + .as_ref() + .map(BrowserContext::browser_context_id) + } + + pub(crate) fn activate_browser_context_by_browser_id( + &mut self, + browser_context_id: moli_core::browser::BrowserContextId, + ) -> bool { + self.activate_matching_browser_context(|bc| bc.browser_context_id() == browser_context_id) + } + pub fn activate_browser_context_by_id(&mut self, browser_context_id: &str) -> bool { self.activate_matching_browser_context(|bc| bc.id == browser_context_id) } @@ -40,18 +53,14 @@ impl CdpConnection { } pub fn insert_browser_context(&mut self, mut browser_context: BrowserContext) { + browser_context.apply_browser_cache_disabled(self.browser_global_overrides.cache_disabled); browser_context - .renderer_runtime() - .set_service_worker_pause_on_start_for_devtools( - self.service_worker_pause_on_start_for_devtools(), - ); - browser_context - .renderer_runtime() - .set_dedicated_worker_pause_on_start_for_devtools( - self.dedicated_worker_pause_on_start_for_devtools(), - ); + .set_service_worker_pause_on_start(self.service_worker_pause_on_start_for_devtools()); + browser_context.set_dedicated_worker_pause_on_start( + self.dedicated_worker_pause_on_start_for_devtools(), + ); browser_context.bind_page_navigation_engines( - self.standalone_navigation_engine.runtime_config(), + self.navigation_runtime_config.clone(), self.scheduler_hooks.renderer_publication_sender(), ); if self.browser_context.is_none() { @@ -66,22 +75,33 @@ impl CdpConnection { &mut self, browser_context_id: &str, restore_browser_context_id: Option<&str>, + ) -> Option { + let browser_context_id = self + .browser_context_by_id(browser_context_id) + .map(BrowserContext::browser_context_id)?; + let restore_browser_context_id = restore_browser_context_id.and_then(|id| { + self.browser_context_by_id(id) + .map(BrowserContext::browser_context_id) + }); + self.remove_browser_context_restoring_active(browser_context_id, restore_browser_context_id) + } + + pub(crate) fn remove_browser_context_restoring_active( + &mut self, + browser_context_id: moli_core::browser::BrowserContextId, + restore_browser_context_id: Option, ) -> Option { if self .browser_context .as_ref() - .is_some_and(|bc| bc.id == browser_context_id) + .is_some_and(|bc| bc.browser_context_id() == browser_context_id) { let removed = self.browser_context.take(); if self.browser_context.is_none() && !self.inactive_browser_contexts.is_empty() { self.select_inactive_browser_context_as_active(0); } self.invalidate_resource_runtime(); - self.restore_preferred_browser_context_async( - restore_browser_context_id, - browser_context_id, - ) - .await; + self.restore_preferred_browser_context(restore_browser_context_id, browser_context_id); self.apply_active_engine_fetch_overrides(); return removed; } @@ -89,14 +109,10 @@ impl CdpConnection { if let Some(index) = self .inactive_browser_contexts .iter() - .position(|bc| bc.id == browser_context_id) + .position(|bc| bc.browser_context_id() == browser_context_id) { let removed = self.inactive_browser_contexts.swap_remove(index); - self.restore_preferred_browser_context_async( - restore_browser_context_id, - browser_context_id, - ) - .await; + self.restore_preferred_browser_context(restore_browser_context_id, browser_context_id); Some(removed) } else { None @@ -137,10 +153,10 @@ impl CdpConnection { true } - async fn restore_preferred_browser_context_async( + fn restore_preferred_browser_context( &mut self, - restore_browser_context_id: Option<&str>, - removed_browser_context_id: &str, + restore_browser_context_id: Option, + removed_browser_context_id: moli_core::browser::BrowserContextId, ) { let Some(restore_browser_context_id) = restore_browser_context_id else { return; @@ -151,12 +167,10 @@ impl CdpConnection { if self .browser_context .as_ref() - .is_some_and(|bc| bc.id == restore_browser_context_id) + .is_some_and(|bc| bc.browser_context_id() == restore_browser_context_id) { return; } - let _ = self - .activate_browser_context_by_id_async(restore_browser_context_id) - .await; + let _ = self.activate_browser_context_by_browser_id(restore_browser_context_id); } } diff --git a/moli-protocol/src/conn/browser_context/lookup.rs b/moli-protocol/src/conn/browser_context/lookup.rs index e4dc34377c..cf8ad2be66 100644 --- a/moli-protocol/src/conn/browser_context/lookup.rs +++ b/moli-protocol/src/conn/browser_context/lookup.rs @@ -80,18 +80,4 @@ impl CdpConnection { .find(|bc| bc.page_target(target_id).is_some()) .map(|bc| bc.id.as_str()) } - - pub(crate) async fn apply_background_target_surface_overrides_async( - &mut self, - target_id: &str, - ) -> anyhow::Result { - let browser_context_id = self - .browser_context_id_for_target(target_id) - .map(str::to_owned) - .ok_or_else(|| anyhow::anyhow!("UnknownTargetId"))?; - self.browser_context_by_id_mut(&browser_context_id) - .ok_or_else(|| anyhow::anyhow!("UnknownBrowserContextId"))? - .apply_background_target_surface_overrides_async(target_id) - .await - } } diff --git a/moli-protocol/src/conn/browser_context/network_owner.rs b/moli-protocol/src/conn/browser_context/network_owner.rs index 2b3e59e4d5..3ed1757aff 100644 --- a/moli-protocol/src/conn/browser_context/network_owner.rs +++ b/moli-protocol/src/conn/browser_context/network_owner.rs @@ -2,13 +2,15 @@ use super::target_session_owner::{TargetSessionOwnerMut, TargetSessionOwnerRef}; use super::*; use crate::conn::CdpSessionRoute; use crate::conn::{CapturedBody, TargetRuntimeSlot}; +use crate::conn::{ + DocumentPolicyUpdate, PendingDocumentPolicyUpdate, PendingDocumentResourceRuntimeUpdate, +}; use crate::devtools_runtime::DevToolsNetworkDataType; use crate::domains::network::{ CapturedRequestBody, CapturedResponseBody, CollectedNetworkDataArtifact, NetworkBacklogPreferredRequestId, PendingNetworkBacklogDeliverySnapshot, TargetNetworkBacklogPreparedDelivery, }; -use moli_core::page::PendingPageCommand; use moli_page_types::DevToolsSessionKey; struct TargetNetworkListenerOwnerMut<'a> { @@ -163,61 +165,115 @@ impl TargetSessionOwnerMut<'_> { fn start_set_cache_disabled( mut self, cache_disabled: bool, - ) -> Result, String> { + global_extra_headers: &moli_fetch::RequestHeaders, + ) -> Result, String> { self.mutate_network_policy_session_state(|state| { state.cache_disabled = cache_disabled; }); - self.start_replay_effective_network_request_policy() + self.start_replay_effective_network_request_policy(global_extra_headers) } fn start_set_bypass_service_worker( mut self, bypass_service_worker: bool, - ) -> Result, String> { + ) -> Result, String> { self.mutate_network_policy_session_state(|state| { state.bypass_service_worker = bypass_service_worker; }); - self.browser_context - .start_target_service_worker_bypass_refresh(&self.target_id) + let effective = self + .browser_context + .effective_policy_for_target(&self.target_id); + self.start_document_policy_update(DocumentPolicyUpdate::BypassServiceWorker( + effective.bypass_service_worker(), + )) } fn start_set_blocked_url_patterns( mut self, blocked_url_patterns: Vec, - ) -> Result, String> { + ) -> Result, String> { self.mutate_network_policy_session_state(|state| { state.blocked_url_patterns = blocked_url_patterns; }); - self.browser_context - .start_target_blocked_urls_refresh(&self.target_id) + let effective = self + .browser_context + .effective_policy_for_target(&self.target_id); + self.start_document_policy_update(DocumentPolicyUpdate::BlockedUrls( + effective.blocked_url_patterns().to_vec(), + )) } fn start_set_extra_http_headers( mut self, extra_headers: moli_fetch::RequestHeaders, - ) -> Result, String> { + global_extra_headers: &moli_fetch::RequestHeaders, + ) -> Result, String> { self.mutate_network_policy_session_state(|state| { state.extra_headers = extra_headers; }); - self.browser_context - .start_target_extra_headers_refresh(&self.target_id) + self.start_effective_extra_http_headers_update(global_extra_headers) } fn start_set_target_extra_http_headers( - self, + mut self, extra_headers: moli_fetch::RequestHeaders, - ) -> Result, String> { + global_extra_headers: &moli_fetch::RequestHeaders, + ) -> Result, String> { self.browser_context .set_base_extra_headers_for_target(&self.target_id, extra_headers); + self.start_effective_extra_http_headers_update(global_extra_headers) + } + + fn start_effective_extra_http_headers_update( + &mut self, + global_extra_headers: &moli_fetch::RequestHeaders, + ) -> Result, String> { + let headers = self + .browser_context + .effective_extra_headers_for_target(&self.target_id, global_extra_headers); + let Some(document) = self + .browser_context + .document_handle_for_target(&self.target_id) + else { + return Ok(None); + }; self.browser_context - .start_target_extra_headers_refresh(&self.target_id) + .start_document_policy_update(document, DocumentPolicyUpdate::ExtraHttpHeaders(headers)) + .map(Some) } fn start_replay_effective_network_request_policy( &mut self, - ) -> Result, String> { + global_extra_headers: &moli_fetch::RequestHeaders, + ) -> Result, String> { + let effective = self + .browser_context + .effective_policy_for_target(&self.target_id); + let headers = self.browser_context.merged_extra_headers_for_target_policy( + global_extra_headers, + effective.extra_headers(), + ); + self.start_document_policy_update(DocumentPolicyUpdate::NetworkRequestPolicy { + extra_headers: headers, + bypass_service_worker: effective.bypass_service_worker(), + cache_disabled: effective.cache_disabled(), + blocked_url_patterns: effective.blocked_url_patterns().to_vec(), + }) + } + + fn start_document_policy_update( + &mut self, + update: DocumentPolicyUpdate, + ) -> Result, String> { + let Some(document) = self + .browser_context + .document_handle_for_target(&self.target_id) + else { + return Ok(None); + }; self.browser_context - .start_target_network_request_policy_refresh(&self.target_id) + .start_document_policy_update(document, update) + .map(Some) } fn set_devtools_browser_identity_override( @@ -261,11 +317,24 @@ impl TargetSessionOwnerMut<'_> { fn start_set_network_offline( self, offline: bool, - ) -> Result, String> { + ) -> Result, String> { + let Some(handle) = self + .browser_context + .web_contents_handle_for_target(&self.target_id) + else { + return Ok(None); + }; self.browser_context - .set_network_offline_for_target(&self.target_id, offline); + .set_web_contents_network_offline(handle, offline)?; + let Some(document) = self + .browser_context + .document_handle_for_target(&self.target_id) + else { + return Ok(None); + }; self.browser_context - .start_target_network_offline_refresh(&self.target_id) + .start_document_policy_update(document, DocumentPolicyUpdate::NetworkOffline(offline)) + .map(Some) } } @@ -539,32 +608,18 @@ impl CdpConnection { } pub(crate) fn set_global_cache_disabled(&mut self, cache_disabled: bool) { - self.global_cache_disabled = cache_disabled; + self.browser_global_overrides.cache_disabled = cache_disabled; for browser_context in self .browser_context .iter_mut() .chain(self.inactive_browser_contexts.iter_mut()) { - browser_context.global_cache_disabled = cache_disabled; - let targets = browser_context - .page_targets - .iter() - .map(|target| target.target_id().to_owned()) - .collect::>(); - for target_id in targets { - browser_context.set_base_cache_disabled_for_target(&target_id, cache_disabled); - } + browser_context.apply_browser_cache_disabled(cache_disabled); } } pub(crate) fn set_global_extra_headers(&mut self, extra_headers: moli_fetch::RequestHeaders) { - self.global_extra_headers = extra_headers.clone(); - if let Some(browser_context) = self.browser_context.as_mut() { - browser_context.global_extra_headers = extra_headers.clone(); - } - for browser_context in &mut self.inactive_browser_contexts { - browser_context.global_extra_headers = extra_headers.clone(); - } + self.browser_global_overrides.extra_headers = extra_headers; } pub(crate) fn disable_network_listener_for_session_owner( @@ -579,11 +634,12 @@ impl CdpConnection { &mut self, session_id: Option<&str>, cache_disabled: bool, - ) -> Result, String> { + ) -> Result, String> { + let global_extra_headers = self.browser_global_overrides.extra_headers.clone(); let Some(owner) = self.target_session_owner_mut(session_id) else { return Err("BrowserContextNotLoaded".to_owned()); }; - owner.start_set_cache_disabled(cache_disabled) + owner.start_set_cache_disabled(cache_disabled, &global_extra_headers) } pub(crate) fn set_cache_disabled_for_target( @@ -608,7 +664,7 @@ impl CdpConnection { &mut self, session_id: Option<&str>, bypass_service_worker: bool, - ) -> Result, String> { + ) -> Result, String> { let Some(owner) = self.target_session_owner_mut(session_id) else { return Err("BrowserContextNotLoaded".to_owned()); }; @@ -619,7 +675,7 @@ impl CdpConnection { &mut self, session_id: Option<&str>, blocked_url_patterns: Vec, - ) -> Result, String> { + ) -> Result, String> { let Some(owner) = self.target_session_owner_mut(session_id) else { return Err("BrowserContextNotLoaded".to_owned()); }; @@ -630,28 +686,30 @@ impl CdpConnection { &mut self, session_id: Option<&str>, extra_headers: moli_fetch::RequestHeaders, - ) -> Result, String> { + ) -> Result, String> { + let global_extra_headers = self.browser_global_overrides.extra_headers.clone(); let Some(owner) = self.target_session_owner_mut(session_id) else { return Err("BrowserContextNotLoaded".to_owned()); }; - owner.start_set_extra_http_headers(extra_headers) + owner.start_set_extra_http_headers(extra_headers, &global_extra_headers) } pub(crate) fn start_set_target_extra_http_headers_for_owner( &mut self, command_owner: &crate::conn::CommandOwnerScope, extra_headers: moli_fetch::RequestHeaders, - ) -> Result, String> { + ) -> Result, String> { + let global_extra_headers = self.browser_global_overrides.extra_headers.clone(); let Some(owner) = self.target_session_owner_mut_for_owner(command_owner) else { return Err("BrowserContextNotLoaded".to_owned()); }; - owner.start_set_target_extra_http_headers(extra_headers) + owner.start_set_target_extra_http_headers(extra_headers, &global_extra_headers) } pub(crate) fn start_replay_effective_network_request_policy_for_session_owner( &mut self, session_id: Option<&str>, - ) -> Result, String> { + ) -> Result, String> { if matches!( self.session_route(session_id), Some( @@ -662,17 +720,18 @@ impl CdpConnection { ) { return Ok(None); } + let global_extra_headers = self.browser_global_overrides.extra_headers.clone(); let Some(mut owner) = self.target_session_owner_mut(session_id) else { return Err("BrowserContextNotLoaded".to_owned()); }; - owner.start_replay_effective_network_request_policy() + owner.start_replay_effective_network_request_policy(&global_extra_headers) } pub(crate) fn start_set_base_user_agent_override_for_owner( &mut self, command_owner: &crate::conn::CommandOwnerScope, user_agent: Option, - ) -> Result, String> { + ) -> Result, String> { let browser_identity = user_agent.as_ref().map(|user_agent| { moli_browser_profile::BrowserIdentityProfile::new( user_agent.clone(), @@ -700,7 +759,7 @@ impl CdpConnection { &mut self, session_id: Option<&str>, browser_identity: Option, - ) -> Option, String>> { + ) -> Option, String>> { let is_browser_session = matches!( self.session_route(session_id), Some(CdpSessionRoute::Browser) @@ -728,7 +787,7 @@ impl CdpConnection { &mut self, session_id: Option<&str>, browser_identity: Option, - ) -> Result, String> { + ) -> Result, String> { let non_page_identity = browser_identity .as_ref() .map(crate::conn::DevToolsBrowserIdentityOverride::to_browser_identity); @@ -752,7 +811,7 @@ impl CdpConnection { &mut self, session_id: Option<&str>, enabled: bool, - ) -> Result, String> { + ) -> Result, String> { if session_id.is_none() || matches!( self.session_route(session_id), @@ -779,7 +838,7 @@ impl CdpConnection { &mut self, session_id: Option<&str>, offline: bool, - ) -> Result, String> { + ) -> Result, String> { let owner = crate::conn::CommandOwnerScope::capture(self, session_id); self.start_set_network_offline_for_owner(&owner, offline) } @@ -788,7 +847,7 @@ impl CdpConnection { &mut self, command_owner: &crate::conn::CommandOwnerScope, offline: bool, - ) -> Result, String> { + ) -> Result, String> { let Some(owner) = self.target_session_owner_mut_for_owner(command_owner) else { return Err("BrowserContextNotLoaded".to_owned()); }; @@ -801,8 +860,9 @@ mod tests { use super::*; fn connection_with_background_attached_session() -> CdpConnection { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-background".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-background".to_owned()); browser_context.register_page_target_url_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -816,6 +876,69 @@ mod tests { conn } + #[test] + fn browser_globals_apply_to_context_inserted_after_configuration() { + let mut conn = crate::test_support::connection(); + conn.set_global_extra_headers(vec![("X-Browser".into(), "global".into())].into()); + conn.set_global_network_conditions(Some(crate::conn::EmulatedNetworkConditions::offline())); + conn.set_global_geolocation_override(Some( + crate::conn::EmulatedGeolocationOverrideState::Position( + crate::conn::EmulatedGeolocationOverride { + latitude: 12.5, + longitude: 34.5, + accuracy: 1.0, + altitude: None, + altitude_accuracy: None, + heading: None, + speed: None, + }, + ), + )); + conn.set_global_cache_disabled(true); + + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-late".to_owned()); + browser_context.set_active_target_id("TID-late"); + browser_context.attach_active_session("SID-late"); + conn.insert_browser_context(browser_context); + + let inputs = conn.navigation_load_inputs_for_session_owner(Some("SID-late")); + assert_eq!( + inputs.extra_http_headers, + vec![("X-Browser".into(), "global".into())].into() + ); + assert!(inputs.network_offline); + // This fixture inserts the Context without registering its wire session. + // Resolve the exact Page route for the native policy, just as the + // former inspection-preload assertion did. + let inputs = conn.navigation_load_inputs_for_owner( + &crate::conn::CommandOwnerScope::for_route(CdpSessionRoute::PageTarget { + browser_context_id: "BID-late".into(), + target_id: "TID-late".into(), + session_key: DevToolsSessionKey::Primary, + }), + ); + assert_eq!(inputs.navigator_overrides.online, Some(false)); + assert_eq!( + inputs.navigator_overrides.geolocation, + Some(moli_page_types::GeolocationPositionOverride { + latitude: 12.5, + longitude: 34.5, + accuracy: 1.0, + altitude: None, + altitude_accuracy: None, + heading: None, + speed: None, + }) + ); + assert!( + conn.browser_context + .as_ref() + .unwrap() + .effective_policy_for_target("TID-late") + .cache_disabled() + ); + } + #[tokio::test] async fn tls_policy_is_shared_by_page_sessions_and_primary_cleanup_reveals_context_default() { let mut conn = connection_with_background_attached_session(); @@ -848,11 +971,14 @@ mod tests { ("SID-attached", Some(false), false), ("SID-background", None, true), ] { + conn.reset_primary_page_session_target_state_async( + "BID-background", + "TID-background", + session, + ) + .await + .unwrap(); let context = conn.browser_context.as_mut().unwrap(); - context - .reset_primary_page_session_target_state_async("TID-background", session) - .await - .unwrap(); let target = context.page_target("TID-background").unwrap(); assert!(target.is_session("SID-background")); assert_eq!( @@ -946,23 +1072,30 @@ mod tests { let mut conn = connection_with_background_attached_session(); conn.start_set_network_offline_for_session_owner(Some("SID-attached"), true) .unwrap(); - let context = conn.browser_context.as_mut().unwrap(); - context - .reset_primary_page_session_target_state_async("TID-background", "SID-attached") - .await - .unwrap(); + conn.reset_primary_page_session_target_state_async( + "BID-background", + "TID-background", + "SID-attached", + ) + .await + .unwrap(); + let context = conn.browser_context.as_ref().unwrap(); assert!(context.network_offline_for_target("TID-background")); - context - .reset_primary_page_session_target_state_async("TID-background", "SID-background") - .await - .unwrap(); + conn.reset_primary_page_session_target_state_async( + "BID-background", + "TID-background", + "SID-background", + ) + .await + .unwrap(); + let context = conn.browser_context.as_ref().unwrap(); assert!(!context.network_offline_for_target("TID-background")); } #[test] fn subresource_fetch_network_request_ids_are_connection_global_across_target_owners() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-mixed".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-mixed".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -1216,8 +1349,9 @@ mod tests { #[test] fn network_target_listener_can_be_disabled_after_enable() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-network".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-network".to_owned())); conn.browser_context .as_mut() .expect("browser context") diff --git a/moli-protocol/src/conn/browser_context/page_owner.rs b/moli-protocol/src/conn/browser_context/page_owner.rs index 543739ce89..b23c184c76 100644 --- a/moli-protocol/src/conn/browser_context/page_owner.rs +++ b/moli-protocol/src/conn/browser_context/page_owner.rs @@ -637,11 +637,9 @@ impl CdpConnection { let Some(browser_context) = self.browser_context_by_id(browser_context_id) else { return; }; - browser_context - .renderer_runtime() - .set_javascript_dialog_handler_enabled( - browser_context_has_page_domain_enabled_session(browser_context), - ); + browser_context.set_javascript_dialog_handler_enabled( + browser_context_has_page_domain_enabled_session(browser_context), + ); } pub(crate) fn set_console_enabled_for_session_owner( @@ -994,7 +992,7 @@ mod tests { for session in ["SID-attached", "SID-observer"] { assert!(context.assign_attached_session_to_target("TID-csp", session.to_owned())); } - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.install_browser_context_fixture_for_test(context); for (session, enabled, expected) in [ ("SID-primary", true, true), @@ -1211,13 +1209,12 @@ mod tests { #[test] fn page_domain_dialog_handler_tracks_the_session_owner_browser_context() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); - let mut active = BrowserContext::new_with_page_for_test("BID-active", "TID-active"); + let mut active = conn.new_page_target_fixture_for_test("BID-active", "TID-active"); active.set_active_target_id("TID-active".to_owned()); - let active_runtime = active.renderer_runtime(); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); assert!( inactive @@ -1227,33 +1224,46 @@ mod tests { inactive .assign_attached_session_to_target("TID-inactive", "SID-inactive-b".to_owned(),) ); - let inactive_runtime = inactive.renderer_runtime(); - conn.install_browser_context_fixture_for_test(active); conn.push_inactive_browser_context_fixture_for_test(inactive); assert!(conn.set_page_domain_enabled_for_session_owner(Some("SID-inactive-a"), true)); - assert!(inactive_runtime.javascript_dialog_handler_enabled()); assert!( - !active_runtime.javascript_dialog_handler_enabled(), + conn.browser_context_by_id("BID-inactive") + .unwrap() + .javascript_dialog_handler_enabled() + ); + assert!( + !conn + .browser_context_by_id("BID-active") + .unwrap() + .javascript_dialog_handler_enabled(), "enabling an inactive target session must not mutate the active browser context" ); assert!(conn.set_page_domain_enabled_for_session_owner(Some("SID-inactive-b"), true)); assert!(conn.disable_page_domain_for_session_owner(Some("SID-inactive-a"))); assert!( - inactive_runtime.javascript_dialog_handler_enabled(), + conn.browser_context_by_id("BID-inactive") + .unwrap() + .javascript_dialog_handler_enabled(), "one frontend must not disable dialog handling while a peer remains subscribed" ); assert!(conn.disable_page_domain_for_session_owner(Some("SID-inactive-b"))); - assert!(!inactive_runtime.javascript_dialog_handler_enabled()); + assert!( + !conn + .browser_context_by_id("BID-inactive") + .unwrap() + .javascript_dialog_handler_enabled() + ); } #[test] fn file_dialog_opened_target_listener_can_be_disabled_after_enable() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-file-dialog".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-file-dialog".to_owned())); conn.browser_context .as_mut() .expect("browser context") diff --git a/moli-protocol/src/conn/browser_context/runtime_owner.rs b/moli-protocol/src/conn/browser_context/runtime_owner.rs index 15691eba83..9cac288f38 100644 --- a/moli-protocol/src/conn/browser_context/runtime_owner.rs +++ b/moli-protocol/src/conn/browser_context/runtime_owner.rs @@ -437,7 +437,7 @@ mod tests { use moli_shared_worker::SharedWorkerInstanceId; use serde_json::json; - use crate::conn::{BrowserContext, CdpConnection, CdpSessionRoute, SharedWorkerTargetState}; + use crate::conn::{CdpSessionRoute, SharedWorkerTargetState}; #[test] fn runtime_listener_session_ids_are_worker_type_scoped() { @@ -457,8 +457,9 @@ mod tests { #[tokio::test] async fn runtime_listener_enable_uses_shared_worker_target_session() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-shared".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-shared".to_owned()); browser_context.insert_shared_worker_target(SharedWorkerTargetState::new( RendererOwnerLocalHostId::new_for_testing(1), SharedWorkerInstanceId::from_u64(91), diff --git a/moli-protocol/src/conn/browser_context/session_owner.rs b/moli-protocol/src/conn/browser_context/session_owner.rs index ec5c790bdb..d2cff3ba2e 100644 --- a/moli-protocol/src/conn/browser_context/session_owner.rs +++ b/moli-protocol/src/conn/browser_context/session_owner.rs @@ -99,12 +99,12 @@ impl CdpConnection { #[cfg(test)] mod tests { use super::*; - use crate::conn::BrowserContext; #[test] fn empty_browser_context_does_not_materialize_a_page_owner() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-empty-owner".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-empty-owner".to_owned())); assert!(conn.target_session_owner(None).is_none()); assert!(conn.accepts_unmaterialized_page_command(&CommandOwnerScope::capture(&conn, None))); @@ -117,11 +117,9 @@ mod tests { #[test] fn stale_page_route_is_not_reinterpreted_as_the_active_page() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-stale-owner", - "TID-live", - )); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_page_target_fixture_for_test("BID-stale-owner", "TID-live")); let stale_route = CdpSessionRoute::PageTarget { browser_context_id: "BID-stale-owner".to_owned(), target_id: "TID-stale".to_owned(), diff --git a/moli-protocol/src/conn/browser_context/target_session_owner.rs b/moli-protocol/src/conn/browser_context/target_session_owner.rs index 5fc606cee9..bbd21aea6f 100644 --- a/moli-protocol/src/conn/browser_context/target_session_owner.rs +++ b/moli-protocol/src/conn/browser_context/target_session_owner.rs @@ -26,10 +26,8 @@ use moli_core::page::{ }; #[cfg(test)] use moli_core::page::{ - Page, RendererInspectorSessionRestoreSnapshot, RendererPageCreationArtifacts, - RendererServiceWorkerVersionStatus, + RendererInspectorSessionRestoreSnapshot, RendererServiceWorkerVersionStatus, }; -use moli_core::runtime::RendererBrowserContextRuntimeOwnerAccess; use moli_fetch::BrowserNavigationRequestKind; use moli_page_types::DevToolsSessionKey; use url::Url; @@ -99,6 +97,7 @@ pub(super) struct TargetSessionStateMut<'a> { } pub(crate) struct TargetNavigationRequestPreflight { + pub(crate) web_contents: moli_core::browser::WebContentsHandle, pub(crate) frame_id: String, pub(crate) session_id: Option, pub(crate) document_fetch_event_session_id: Option, @@ -147,7 +146,6 @@ pub(crate) struct TargetNavigationLoadInputs { storage_handles: TargetNavigationStorageHandles, #[cfg(test)] pub(crate) root_frame_id: Option, - pub(crate) renderer_runtime: RendererBrowserContextRuntimeOwnerAccess, /// One identity for navigation request headers and the Document's Navigator. pub(crate) browser_identity_override: Option, pub(crate) http_proxy_override: Option, @@ -176,8 +174,6 @@ pub(crate) struct TargetNavigationLoadInputs { #[cfg(test)] pub(crate) bypass_service_worker: bool, #[cfg(test)] - pub(crate) cache_disabled: bool, - #[cfg(test)] pub(crate) blocked_url_patterns: Vec, #[cfg(test)] pub(crate) fetch_subresource_interception: @@ -191,6 +187,7 @@ pub(crate) struct TargetNavigationLoadInputs { fn prepared_document_inspection( context: &BrowserContext, target_id: &str, + _browser_globals: &crate::conn::BrowserGlobalOverrides, ) -> moli_renderer_v8::RendererPreparedDocumentInspectionConfiguration { let target = context .page_target(target_id) @@ -282,16 +279,20 @@ impl TargetNavigationLoadInputs { .then_some(report) } - fn from_browser_context_target(browser_context: &BrowserContext, target_id: &str) -> Self { + fn from_browser_context_target( + browser_context: &BrowserContext, + target_id: &str, + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> Self { #[cfg(test)] - let inspection = prepared_document_inspection(browser_context, target_id); + let inspection = prepared_document_inspection(browser_context, target_id, browser_globals); let effective_network_conditions = browser_context .target_emulation_policy(target_id) .expect("live WebContents") .network_conditions .or(browser_context.emulation_defaults().network_conditions) - .or(browser_context.global_network_conditions); + .or(browser_globals.network_conditions); let emulated_device_metrics = browser_context .target_emulation_policy(target_id) .expect("live WebContents") @@ -314,7 +315,6 @@ impl TargetNavigationLoadInputs { ), #[cfg(test)] root_frame_id: Some(target_id.to_owned()), - renderer_runtime: browser_context.renderer_runtime_owner_access(), browser_identity_override: effective_policy .browser_identity_override() .cloned() @@ -335,8 +335,10 @@ impl TargetNavigationLoadInputs { #[cfg(test)] runtime_inspector_session_restore_snapshots: inspection .runtime_inspector_session_restore_snapshots, - extra_http_headers: browser_context - .merged_extra_headers_for_target_policy(effective_policy.extra_headers()), + extra_http_headers: browser_context.merged_extra_headers_for_target_policy( + &browser_globals.extra_headers, + effective_policy.extra_headers(), + ), script_execution_disabled: browser_context .target_emulation_policy(target_id) .expect("live WebContents") @@ -357,13 +359,11 @@ impl TargetNavigationLoadInputs { || effective_network_conditions .is_some_and(|conditions| !conditions.navigator_online()), navigator_overrides: browser_context - .navigator_overrides_for_target(target_id) + .navigator_overrides_for_target(target_id, browser_globals) .expect("resolved Page target retains its navigator state"), #[cfg(test)] bypass_service_worker: effective_policy.bypass_service_worker(), #[cfg(test)] - cache_disabled: effective_policy.cache_disabled(), - #[cfg(test)] blocked_url_patterns: effective_policy.blocked_url_patterns().to_vec(), #[cfg(test)] fetch_subresource_interception: browser_context @@ -381,10 +381,12 @@ impl TargetNavigationLoadInputs { } } - fn from_browser_context_fallback(browser_context: &BrowserContext) -> Self { + fn from_browser_context_fallback( + browser_context: &BrowserContext, + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> Self { let mut inputs = TargetNavigationLoadInputs::no_loaded_browser_context( browser_context.page_storage_handles(), - browser_context.renderer_runtime_owner_access(), ); inputs.browser_context_id = Some(browser_context.id.clone()); inputs.browser_identity_override = @@ -398,21 +400,20 @@ impl TargetNavigationLoadInputs { inputs.document_start_scripts = browser_context.default_document_start_script_descriptors(); } - inputs.extra_http_headers = browser_context.effective_extra_headers(); + inputs.extra_http_headers = + browser_context.effective_extra_headers(&browser_globals.extra_headers); inputs.viewport_surface = browser_context .emulation_defaults() .device_metrics .as_ref() .map(|metrics| metrics.viewport_surface()); inputs.document_activity = browser_context.active_document_activity(); - inputs.network_offline = browser_context.effective_active_network_offline(); + inputs.network_offline = + browser_context.effective_active_network_offline(browser_globals.network_conditions); inputs } - fn no_loaded_browser_context( - page_handles: BrowserContextPageStorageHandles, - renderer_runtime: RendererBrowserContextRuntimeOwnerAccess, - ) -> Self { + fn no_loaded_browser_context(page_handles: BrowserContextPageStorageHandles) -> Self { Self { redirect_chain: Vec::new(), redirect_headers: None, @@ -420,7 +421,6 @@ impl TargetNavigationLoadInputs { storage_handles: TargetNavigationStorageHandles::from_page_handles(page_handles), #[cfg(test)] root_frame_id: None, - renderer_runtime, browser_identity_override: None, http_proxy_override: None, http_no_proxy_override: None, @@ -447,8 +447,6 @@ impl TargetNavigationLoadInputs { #[cfg(test)] bypass_service_worker: false, #[cfg(test)] - cache_disabled: false, - #[cfg(test)] blocked_url_patterns: Vec::new(), #[cfg(test)] fetch_subresource_interception: (false, None), @@ -604,7 +602,7 @@ impl<'a> TargetSessionOwnerRef<'a> { // Only a browser-owned network error Document diverges from the // user-visible Target/history URL. .filter(|url| url.as_str() == NETWORK_ERROR_PAGE_URL) - .map(ToString::to_string) + .map(|url| url.to_string()) .unwrap_or_else(|| target.target_identity().url().to_owned()); ( target.target_id().to_owned(), @@ -638,10 +636,14 @@ impl<'a> TargetSessionOwnerRef<'a> { }) } - pub(super) fn navigation_load_inputs(&self) -> TargetNavigationLoadInputs { + pub(super) fn navigation_load_inputs( + &self, + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> TargetNavigationLoadInputs { TargetNavigationLoadInputs::from_browser_context_target( self.browser_context, &self.target_id, + browser_globals, ) } } @@ -837,8 +839,12 @@ impl<'a> TargetSessionOwnerMut<'a> { referrer: Option<&str>, is_data_url: bool, fallback_browser_identity: &moli_browser_profile::BrowserIdentityProfile, + global_extra_headers: &moli_fetch::RequestHeaders, network_request_id_allocator: &mut ConnectionNetworkRequestIdAllocator, ) -> Option { + let web_contents = self + .browser_context + .web_contents_handle_for_target(&self.target_id)?; { let target = self.target(); let frame_id = self.target_id.clone(); @@ -851,15 +857,16 @@ impl<'a> TargetSessionOwnerMut<'a> { let effective_policy = self .browser_context .effective_policy_for_target(&self.target_id); - let mut request_headers = self - .browser_context - .merged_extra_headers_for_target_policy(effective_policy.extra_headers()); - let user_agent = effective_policy + let mut request_headers = self.browser_context.merged_extra_headers_for_target_policy( + global_extra_headers, + effective_policy.extra_headers(), + ); + let identity = effective_policy .browser_identity_override() + .cloned() .or_else(|| self.browser_context.default_browser_identity_override()) - .unwrap_or(fallback_browser_identity) - .user_agent(); - apply_user_agent_header(&mut request_headers, user_agent); + .unwrap_or_else(|| fallback_browser_identity.clone()); + apply_user_agent_header(&mut request_headers, identity.user_agent()); apply_referrer_header(&mut request_headers, referrer); let fetch_config = target.fetch_owner.config_snapshot(); let fetch_snapshot = fetch_config.subresource_interception_snapshot(); @@ -930,6 +937,7 @@ impl<'a> TargetSessionOwnerMut<'a> { needs_fetch_navigation_request_id, ); Some(TargetNavigationRequestPreflight { + web_contents, frame_id, session_id: target_session_id, document_fetch_event_session_id, @@ -973,16 +981,18 @@ impl CdpConnection { None => self .browser_context .as_ref() - .map(TargetNavigationLoadInputs::from_browser_context_fallback) + .map(|context| { + TargetNavigationLoadInputs::from_browser_context_fallback( + context, + &self.browser_global_overrides, + ) + }) .unwrap_or_else(|| { TargetNavigationLoadInputs::no_loaded_browser_context( self.initial_storage_partition.page_storage_handles(), - self.standalone_navigation_engine - .ensure() - .browser_context_owner_access(), ) }), - Some(owner) => owner.navigation_load_inputs(), + Some(owner) => owner.navigation_load_inputs(&self.browser_global_overrides), }; #[cfg(test)] let inputs = { @@ -1001,7 +1011,11 @@ impl CdpConnection { owner: &CommandOwnerScope, ) -> moli_renderer_v8::RendererPreparedDocumentInspectionConfiguration { match self.target_session_owner_ref_for_owner(owner) { - Some(owner) => prepared_document_inspection(owner.browser_context, &owner.target_id), + Some(owner) => prepared_document_inspection( + owner.browser_context, + &owner.target_id, + &self.browser_global_overrides, + ), None => moli_renderer_v8::RendererPreparedDocumentInspectionConfiguration { document_start_scripts: self .browser_context @@ -1032,6 +1046,7 @@ impl CdpConnection { .global_browser_identity_override .clone() .unwrap_or_else(|| self.base_browser_identity.clone()); + let global_extra_headers = self.browser_global_overrides.extra_headers.clone(); let mut network_request_id_allocator = std::mem::take(&mut self.network_request_id_allocator); let result = self @@ -1042,6 +1057,7 @@ impl CdpConnection { referrer, is_data_url, &fallback_browser_identity, + &global_extra_headers, &mut network_request_id_allocator, ) }); @@ -1058,35 +1074,6 @@ impl CdpConnection { .register_pending_fetch_navigation_request(pending) } - #[cfg(test)] - pub(crate) fn start_loaded_document_navigation_for_owner( - &self, - owner: &CommandOwnerScope, - navigation: crate::conn::NavigationId, - page: Page, - destination: crate::conn::DocumentNavigationDestination, - artifacts: &RendererPageCreationArtifacts, - ) -> Result< - impl std::future::Future> - + use<>, - String, - > { - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(owner) - .ok_or("navigation WebContents unavailable")?; - self.browser_context_by_id(&context_id) - .ok_or("navigation BrowserContext unavailable")? - .start_loaded_document_navigation_for_target( - &target_id, - navigation, - page, - destination, - artifacts, - &self.permission_defaults, - ) - .map_err(str::to_owned) - } - pub(crate) fn commit_loaded_navigation( &mut self, prepared: crate::conn::PreparedDocumentNavigation, @@ -1109,12 +1096,18 @@ impl CdpConnection { .await } - async fn close_page_target_for_target_close_async( + pub(crate) async fn close_web_contents_for_target_close_async( &mut self, target_id: &str, + handle: moli_core::browser::WebContentsHandle, out: &mut Vec, reason: &'static str, ) -> Option { + let context = self.browser_context_by_browser_id(handle.context())?; + if context.web_contents_handle_for_target(target_id) != Some(handle) { + return None; + } + let was_selected = context.selected_web_contents_handle() == Some(handle); let primary_route = self.target_session_route_for_target_id(target_id)?; let primary_owner = CommandOwnerScope::for_route(primary_route); let session_owners = self @@ -1139,8 +1132,8 @@ impl CdpConnection { out.extend(claimed_await_events); let (closing, primary_session_id, attached_session_ids, collected_network_data_artifacts) = { - let browser_context = self.browser_context.as_mut()?; - let (target, closing) = browser_context.take_page_target_for_close(target_id)?; + let browser_context = self.browser_context_by_browser_id_mut(handle.context())?; + let (target, closing) = browser_context.begin_web_contents_close(handle).ok()?; let collected_network_data_artifacts = target.runtime_slot.collected_network_data_artifacts(); let attached_session_ids = target @@ -1159,48 +1152,37 @@ impl CdpConnection { closing.close_async().await; self.record_collected_network_data_artifacts(collected_network_data_artifacts); - Some(ClosedPageTarget { + let closed = ClosedPageTarget { target_id: target_id.to_owned(), primary_session_id, attached_session_ids, - }) - } - - pub(crate) async fn close_background_page_target_for_target_close_async( - &mut self, - target_id: &str, - out: &mut Vec, - reason: &'static str, - ) -> Option { - let is_background = self - .browser_context - .as_ref() - .is_some_and(|browser_context| { - browser_context.page_target(target_id).is_some() - && !browser_context.is_active_target(target_id) - }); - if !is_background { - return None; + }; + if !was_selected { + return Some(closed); } - self.close_page_target_for_target_close_async(target_id, out, reason) - .await - } - pub(crate) async fn close_active_page_target_for_target_close_async( - &mut self, - out: &mut Vec, - reason: &'static str, - ) -> Option { - let target_id = self.browser_context.as_ref()?.active_target_id_owned()?; - let closed = self - .close_page_target_for_target_close_async(&target_id, out, reason) - .await?; - let selected_target_id = if let Some(browser_context) = self.browser_context.as_mut() { - browser_context.select_last_background_target_async().await + let selected = self + .browser_context_by_browser_id(handle.context()) + .and_then(|browser_context| { + let target_id = browser_context.last_selectable_background_target_id()?; + let handle = browser_context.web_contents_handle_for_target(&target_id)?; + Some((target_id, handle)) + }); + let selected_target_id = if let Some((target_id, handle)) = selected { + if let Err(error) = self.select_browser_web_contents_async(handle).await { + tracing::warn!(%error, "failed to update selected WebContents surface after close"); + } + Some(target_id) } else { None }; - self.refresh_active_browser_context_loader(); + if self + .browser_context + .as_ref() + .is_some_and(|context| context.browser_context_id() == handle.context()) + { + self.refresh_active_browser_context_loader(); + } if let Some(selected_target_id) = selected_target_id { self.notify_target_host_activated(&selected_target_id); out.extend( @@ -1238,7 +1220,10 @@ impl CdpConnection { if let Some(browser_context_id) = browser_context_id { let target = self .browser_context_by_id_mut(&browser_context_id) - .and_then(|browser_context| browser_context.take_page_target_for_close(target_id)); + .and_then(|browser_context| { + let handle = browser_context.web_contents_handle_for_target(target_id)?; + browser_context.begin_web_contents_close(handle).ok() + }); if let Some((target, closing)) = target { page_session_ids.extend( target @@ -1380,11 +1365,9 @@ impl CdpConnection { owner: &CommandOwnerScope, state: WindowSurfaceState, ) -> Option<()> { - let owner = self.target_session_owner_mut_for_owner(owner)?; - owner - .browser_context - .set_target_window_surface_state(&owner.target_id, state); - Some(()) + let handle = self.browser_web_contents_for_owner(owner).ok()?; + self.update_browser_window_surface(handle, Some(state), None, None, None, None) + .ok() } pub(crate) fn set_window_surface_geometry_for_owner( @@ -1395,15 +1378,9 @@ impl CdpConnection { x: Option, y: Option, ) -> Option<()> { - let owner = self.target_session_owner_mut_for_owner(owner)?; - owner.browser_context.set_target_window_surface_geometry( - &owner.target_id, - width, - height, - x, - y, - ); - Some(()) + let handle = self.browser_web_contents_for_owner(owner).ok()?; + self.update_browser_window_surface(handle, None, width, height, x, y) + .ok() } pub(crate) fn target_owner_state_for_owner( @@ -1669,28 +1646,6 @@ impl CdpConnection { )) } - #[cfg(test)] - pub(crate) fn reserve_target_page_residence_identity_for_owner( - &mut self, - owner: &CommandOwnerScope, - renderer_page: crate::conn::RendererPageResidenceIdentity, - ) -> Option { - let (browser_context_id, routed_target_id) = self.target_owner_identity_for_owner(owner)?; - let target_id = routed_target_id.or_else(|| { - self.browser_context_by_id(&browser_context_id) - .and_then(|browser_context| browser_context.active_target_id()) - .map(str::to_owned) - }); - let document_id = self - .browser_context_by_id_mut(&browser_context_id)? - .reserve_renderer_document_for_target(target_id.as_deref()?, renderer_page); - Some(TargetPageResidenceIdentity::new( - browser_context_id, - target_id, - document_id, - )) - } - /// Checks that deferred Page-owned work still addresses the same target /// Page-slot residence from which it was captured. pub(crate) fn target_page_residence_identity_is_current( @@ -1703,16 +1658,6 @@ impl CdpConnection { }) } - pub(crate) fn capture_document_lifetime_for_owner( - &mut self, - owner: &CommandOwnerScope, - ) -> Option { - let owner = self.target_session_owner_mut_for_owner(owner)?; - owner - .browser_context - .document_lifetime_observer_for_target(&owner.target_id) - } - /// Captures the exact protocol attachment currently addressing a Page. pub(crate) fn target_page_protocol_attachment_identity_for_session( &self, @@ -2278,7 +2223,7 @@ impl CdpConnection { &self, owner: &CommandOwnerScope, destination: crate::conn::HistoryTraversalDestination, - ) -> Option> { + ) -> Option> { let owner = self.target_session_owner_ref_for_owner(owner)?; owner .browser_context @@ -2418,8 +2363,9 @@ mod tests { #[test] fn generated_session_id_skips_caller_supplied_live_session() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-session-id".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-session-id".to_owned()); browser_context.set_active_target_id("TID-session-id"); browser_context.attach_active_session("SID-1".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -2511,8 +2457,9 @@ mod tests { #[test] fn renderer_runtime_inspector_session_id_tracks_owner_session_kind() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-owner-key".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-owner-key".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active-primary".to_owned()); browser_context.set_active_document_fixture_for_test(1); @@ -2800,6 +2747,7 @@ mod tests { Some("https://referrer.example/"), false, &test_browser_identity("Moli/Test-UA"), + &Default::default(), &mut network_request_id_allocator, ) .expect("active preflight should prepare"); @@ -2845,6 +2793,7 @@ mod tests { None, true, &test_browser_identity("Moli/Test-UA"), + &Default::default(), &mut network_request_id_allocator, ) .expect("active data URL preflight should prepare"); @@ -2939,6 +2888,7 @@ mod tests { Some("https://referrer.example/"), false, &test_browser_identity("Moli/Test-UA"), + &Default::default(), &mut network_request_id_allocator, ) .expect("background preflight should prepare"); @@ -3020,6 +2970,7 @@ mod tests { None, true, &test_browser_identity("Moli/Test-UA"), + &Default::default(), &mut network_request_id_allocator, ) .expect("background data URL preflight should prepare"); @@ -3097,9 +3048,12 @@ mod tests { }], ); } + let web_contents = background + .web_contents_handle_for_target("TID-background") + .unwrap(); assert!( background - .start_target_fetch_interception_update("TID-background", true, None) + .start_web_contents_fetch_interception_update(web_contents, true, None, false) .unwrap() .is_none() ); @@ -3133,14 +3087,12 @@ mod tests { target_id: "TID-background".to_owned(), session_key: DevToolsSessionKey::Primary, }; - let inputs = owner.navigation_load_inputs(); + let inputs = owner.navigation_load_inputs(&Default::default()); assert_eq!(inputs.browser_context_id.as_deref(), Some("BID-background")); - assert!( - inputs - .renderer_runtime - .runtime() - .shares_state_with(&background.renderer_runtime()), + assert_eq!( + background.page_navigation_browser_context_runtime_id_for_test("TID-background"), + Some(background.renderer_runtime_id_for_test()), "background navigation must reuse the browser-context renderer runtime" ); assert_eq!( @@ -3230,7 +3182,8 @@ mod tests { "https://peer.example/".to_owned(), ); background.set_active_target_id("TID-peer"); - let inspection = prepared_document_inspection(&background, "TID-background"); + let inspection = + prepared_document_inspection(&background, "TID-background", &Default::default()); assert_eq!( inspection.root_frame_projection_id.as_deref(), Some("TID-background") @@ -3240,7 +3193,7 @@ mod tests { assert!(sessions[0].protocol_configuration.runtime_frontend_enabled); assert_eq!(background.active_target_id(), Some("TID-peer")); assert!( - prepared_document_inspection(&background, "TID-peer") + prepared_document_inspection(&background, "TID-peer", &Default::default()) .runtime_inspector_session_restore_snapshots .iter() .all(|session| !session.protocol_configuration.runtime_frontend_enabled), @@ -3300,43 +3253,25 @@ mod tests { #[tokio::test] async fn browser_context_commits_loaded_page_to_background_owner() { let mut ctx = TestContext::new(); - let loaded = ctx - .conn - .load_navigation_via_runtime_async("data:text/html,background commit") - .await - .expect("page should load"); let page_url = Url::parse("https://nav.example/path").unwrap(); - let artifacts = loaded.page_creation_artifacts; - let mut background = BrowserContext::new("BID-background".to_owned()); + let mut background = ctx + .conn + .new_browser_context_fixture_for_test("BID-background".to_owned()); background.register_page_target_url_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), "about:blank".to_owned(), ); let initial_attachment_id = background.target_document_id("TID-background"); - let navigation = - background.begin_target_document_navigation("TID-background", "LOADER-nav".into()); - let prepared = background - .start_loaded_document_navigation_for_target( - "TID-background", - navigation, - loaded.page, - crate::conn::DocumentNavigationDestination { - url: page_url.clone(), - security_origin: "https://nav.example".into(), - secure_context_type: "Secure".into(), - }, - &artifacts, - &Default::default(), - ) - .unwrap() - .await - .unwrap(); - - let committed = background - .commit_loaded_navigation(prepared) - .expect("background Browser Document should commit"); - assert!(committed.inspection_projection.is_ok()); + ctx.conn + .install_browser_context_fixture_for_test(background); + ctx.install_buffered_navigation_fixture_for_session_owner( + page_url.clone(), + "background commit".to_owned(), + Some("SID-background"), + ) + .await; + let background = ctx.conn.browser_context.as_ref().unwrap(); let target = background .background_target("TID-background") @@ -3358,13 +3293,13 @@ mod tests { ); assert_eq!(target.target_identity().secure_context_type(), "Secure"); assert!(!background.has_pending_document_navigation_for_target("TID-background")); - committed.previous_document_retirement.close().await; } #[test] fn target_page_residence_identity_rejects_context_target_and_attachment_collisions() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-page-residence".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-page-residence".to_owned()); browser_context.set_active_target_id("TID-page-residence"); browser_context.attach_active_session("SID-page-residence"); conn.install_browser_context_fixture_for_test(browser_context); @@ -3404,8 +3339,9 @@ mod tests { #[test] fn target_without_page_has_only_a_pending_page_residence() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-pending-residence".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-pending-residence".to_owned()); browser_context.set_active_target_id("TID-pending-residence"); browser_context.attach_active_session("SID-pending-residence".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -3439,8 +3375,9 @@ mod tests { #[test] fn implicit_active_route_freezes_the_concrete_target_in_page_residence() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-implicit-page-residence".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-implicit-page-residence".to_owned()); browser_context.set_active_target_id("TID-original"); conn.install_browser_context_fixture_for_test(browser_context); conn.set_document_fixture_for_owner_test( @@ -3470,8 +3407,8 @@ mod tests { #[test] fn connection_target_owner_reference_reads_and_mutates_active_and_background_state() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -3527,8 +3464,9 @@ mod tests { #[test] fn subscribed_page_event_sessions_require_page_domain_enable_per_session() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-page-events".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-page-events".to_owned()); browser_context.set_active_target_id("TID-page-events".to_owned()); browser_context.attach_active_session("SID-primary".to_owned()); assert!( @@ -3583,8 +3521,9 @@ mod tests { #[test] fn runtime_event_attachments_include_every_enabled_session_on_the_exact_page() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-runtime-events".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-runtime-events".to_owned()); browser_context.set_active_target_id("TID-runtime-events".to_owned()); browser_context.attach_active_session("SID-runtime-a".to_owned()); assert!( @@ -3638,8 +3577,9 @@ mod tests { #[test] fn runtime_context_identity_includes_service_worker_without_page_owner_identity() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-worker-context".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-worker-context".to_owned()); browser_context.insert_service_worker_target(ServiceWorkerTargetState::new( 41, 29, @@ -3683,8 +3623,8 @@ mod tests { #[test] fn connection_runtime_slot_reference_reads_and_mutates_active_background_and_attached_slots() { - let mut conn = CdpConnection::default(); - let mut active = BrowserContext::new_with_page_for_test("BID-active", "TID-active"); + let mut conn = crate::test_support::connection(); + let mut active = conn.new_page_target_fixture_for_test("BID-active", "TID-active"); active.set_active_target_id("TID-active".to_owned()); active.set_target_url("https://active.example/".to_owned()); active.attach_active_session("SID-active".to_owned()); @@ -3704,7 +3644,7 @@ mod tests { "SID-attached-background".to_owned() )); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.set_target_url("https://inactive.example/".to_owned()); inactive @@ -3820,8 +3760,9 @@ mod tests { #[test] fn background_dialog_scope_survives_default_session_state_folding() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-dialog-scope".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-dialog-scope".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.register_page_target_url_fixture( "TID-background-dialog".to_owned(), diff --git a/moli-protocol/src/conn/browser_document_commands.rs b/moli-protocol/src/conn/browser_document_commands.rs new file mode 100644 index 0000000000..c7a4b2f560 --- /dev/null +++ b/moli-protocol/src/conn/browser_document_commands.rs @@ -0,0 +1,705 @@ +use moli_core::{ + browser::{BrowserContextId, DocumentHandle, DocumentLifetimeObserver}, + page::{ + ChildFrameTreeSnapshot, DocumentCookieOwnerSnapshot, RendererAppManifestLoadPublication, + RendererAutofillTriggerOutcome, RendererAutofillTriggerRequest, + RendererCaptureScreencastFrameReply, RendererCaptureScreencastFrameRequest, + RendererCaptureScreenshotReply, RendererCaptureScreenshotRequest, + RendererCommandTurnOutput, RendererNetworkResourceLoadPreparation, + RendererPageDiagnosticsSnapshot, RendererResourceTextSearchOutcome, + RendererSetDocumentContentResult, SubresourceNetworkRecord, + }, +}; +use std::sync::Arc; +use url::Url; + +use super::BrowserContext; +use super::{ + BrowserAppManifestLoadPreparation, CompletedAppManifestLoadPreparation, + CompletedAppManifestPublication, CompletedCaptureDocumentImage, + CompletedCaptureDocumentScreencastFrame, CompletedCaptureDocumentSnapshot, + CompletedChildFrameLifecycleWork, CompletedChildFrameNavigation, + CompletedChildFrameTreeSnapshot, CompletedDocumentAutofillTrigger, CompletedDocumentBlobRead, + CompletedDocumentCookieOwnerSnapshot, CompletedDocumentCspBypassUpdate, + CompletedDocumentDiagnosticsSnapshot, CompletedDocumentFetchCommand, + CompletedDocumentInputCommand, CompletedDocumentLifecycleStop, CompletedDocumentPolicyBatch, + CompletedDocumentPolicyUpdate, CompletedDocumentResourceRuntimeUpdate, + CompletedDocumentResourceTextSearch, CompletedDocumentStorageKeySnapshot, + CompletedNavigationHistoryReset, CompletedNetworkResourceLoadPreparation, + CompletedSetDocumentContent, CompletedTopLevelHistoryTraversal, + CompletedTopLevelSameDocumentNavigation, DocumentFetchCommand, DocumentFetchCommandOutcome, + DocumentPolicyUpdate, DocumentRuntimePolicyReconciliation, DocumentSnapshot, PageInputCommand, + PendingAppManifestLoadPreparation, PendingAppManifestPublication, PendingCaptureDocumentImage, + PendingCaptureDocumentScreencastFrame, PendingCaptureDocumentSnapshot, + PendingChildFrameLifecycleWork, PendingChildFrameNavigation, PendingChildFrameTreeSnapshot, + PendingDocumentAutofillTrigger, PendingDocumentBlobRead, PendingDocumentCookieOwnerSnapshot, + PendingDocumentCspBypassUpdate, PendingDocumentDiagnosticsSnapshot, + PendingDocumentFetchCommand, PendingDocumentInputCommand, PendingDocumentLifecycleStop, + PendingDocumentPolicyBatch, PendingDocumentPolicyUpdate, PendingDocumentResourceTextSearch, + PendingDocumentStorageKeySnapshot, PendingNavigationHistoryReset, + PendingNetworkResourceLoadPreparation, PendingSetDocumentContent, + PendingTopLevelHistoryTraversal, PendingTopLevelSameDocumentNavigation, +}; +use crate::conn::{CdpConnection, CommandOwnerScope}; + +impl CdpConnection { + #[cfg(test)] + pub(crate) fn suspend_navigation_resource_owner_for_test( + &self, + handle: moli_core::browser::WebContentsHandle, + ) -> Result + use<>, String> { + self.browser_context_handle_by_id(handle.context()) + .ok_or("BrowserContext unavailable")? + .suspend_navigation_resource_owner_for_test(handle) + } + + pub(crate) fn browser_context_by_browser_id( + &self, + context: BrowserContextId, + ) -> Option<&BrowserContext> { + self.browser_contexts() + .find(|candidate| candidate.browser_context_id() == context) + } + + pub(in crate::conn) fn browser_context_by_browser_id_mut( + &mut self, + context: BrowserContextId, + ) -> Option<&mut BrowserContext> { + self.browser_context + .iter_mut() + .chain(&mut self.inactive_browser_contexts) + .find(|candidate| candidate.browser_context_id() == context) + } + + fn browser_context_handle_by_id( + &self, + context: BrowserContextId, + ) -> Option<&moli_core::browser::BrowserContextHandle> { + Some(&self.browser_context_by_browser_id(context)?.browser_context) + } + + fn browser_context_handle_by_id_mut( + &mut self, + context: BrowserContextId, + ) -> Option<&mut moli_core::browser::BrowserContextHandle> { + Some( + &mut self + .browser_context_by_browser_id_mut(context)? + .browser_context, + ) + } + + /// DevTools projection boundary: resolve a session/target once into an + /// exact physical Browser capability. Browser operations never receive the + /// frontend identities used to find it. + pub(crate) fn loaded_browser_document_for_owner( + &self, + owner: &CommandOwnerScope, + ) -> Result { + let (context_id, target_id) = self + .loaded_document_owner_identity_for_owner(owner) + .ok_or_else(|| "NoDocumentLoaded".to_owned())?; + self.browser_context_by_id(&context_id) + .and_then(|context| context.document_handle_for_target(&target_id)) + .ok_or_else(|| "NoDocumentLoaded".to_owned()) + } + + pub(crate) fn resolve_browser_document_for_owner( + &self, + owner: &CommandOwnerScope, + ) -> Result { + self.ensure_document_accessible_for_owner(owner)?; + self.loaded_browser_document_for_owner(owner) + } + + pub(crate) fn observe_browser_document_lifetime( + &mut self, + document: DocumentHandle, + ) -> Result { + self.browser_context_handle_by_id_mut(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .observe_document_lifetime(document) + } + + pub(crate) fn start_document_fetch_command( + &self, + document: DocumentHandle, + command: DocumentFetchCommand, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_fetch_command(document, command) + } + + pub(crate) fn finish_document_fetch_command( + &mut self, + completed: CompletedDocumentFetchCommand, + ) -> Result { + let context = completed.document().web_contents().context(); + match self.browser_context_handle_by_id_mut(context) { + Some(context) => context.finish_document_fetch_command(completed), + None if completed.is_interception_update() => { + moli_core::browser::BrowserContext::finish_unobserved_document_fetch_interception_update(completed) + } + None => Err("NoDocumentLoaded".to_owned()), + } + } + + pub(crate) fn start_document_input_command( + &self, + document: DocumentHandle, + command: PageInputCommand, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_input_command(document, command) + } + + pub(crate) fn finish_document_input_command( + &mut self, + completed: CompletedDocumentInputCommand, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_input_command(completed) + } + + pub(crate) fn start_document_autofill_trigger( + &self, + document: DocumentHandle, + request: RendererAutofillTriggerRequest, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_autofill_trigger(document, request) + } + + pub(crate) fn finish_document_autofill_trigger( + &mut self, + completed: CompletedDocumentAutofillTrigger, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_autofill_trigger(completed) + } + + pub(crate) fn start_document_lifecycle_stop( + &self, + document: DocumentHandle, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_lifecycle_stop(document) + } + + pub(crate) fn finish_document_lifecycle_stop( + &mut self, + completed: CompletedDocumentLifecycleStop, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_lifecycle_stop(completed) + } + + pub(crate) fn start_document_diagnostics_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_diagnostics_snapshot(document) + } + + pub(crate) fn finish_document_diagnostics_snapshot( + &mut self, + completed: CompletedDocumentDiagnosticsSnapshot, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_diagnostics_snapshot(completed) + } + + pub(crate) fn start_document_child_frame_lifecycle_work( + &mut self, + document: DocumentHandle, + timeout: std::time::Duration, + ) -> Result { + self.browser_context_handle_by_id_mut(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_child_frame_lifecycle_work(document, timeout) + } + + pub(crate) fn finish_document_child_frame_lifecycle_work( + &mut self, + completed: CompletedChildFrameLifecycleWork, + ) -> Result<(bool, RendererCommandTurnOutput), String> { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_child_frame_lifecycle_work(completed) + } + + pub(crate) fn finish_document_resource_runtime_update( + &mut self, + completed: CompletedDocumentResourceRuntimeUpdate, + ) -> Result<(), String> { + let context = completed.document().web_contents().context(); + match self.browser_context_handle_by_id_mut(context) { + Some(context) => context.finish_document_resource_runtime_update(completed), + None => moli_core::browser::BrowserContext::finish_unobserved_document_resource_runtime_update(completed), + } + } + + pub(crate) fn start_set_document_content( + &self, + document: DocumentHandle, + frame_id: String, + html: String, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_set_document_content(document, frame_id, html) + } + + pub(crate) fn set_document_javascript_dialog_handler_enabled( + &self, + document: DocumentHandle, + enabled: bool, + ) -> Result<(), String> { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .set_document_javascript_dialog_handler_enabled(document, enabled) + } + + pub(crate) fn start_document_policy_update( + &mut self, + document: DocumentHandle, + update: DocumentPolicyUpdate, + ) -> Result { + self.browser_context_handle_by_id_mut(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_policy_update(document, update) + } + + pub(crate) fn start_document_runtime_policy_reconciliation( + &mut self, + document: DocumentHandle, + policy: DocumentRuntimePolicyReconciliation, + ) -> Result { + Ok(self + .browser_context_handle_by_id_mut(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_runtime_policy_reconciliation(document, policy)) + } + + pub(crate) fn finish_document_policy_update( + &mut self, + completed: CompletedDocumentPolicyUpdate, + ) -> Result<(), String> { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_policy_update(completed) + } + + pub(crate) fn finish_document_policy_batch( + &mut self, + completed: CompletedDocumentPolicyBatch, + ) -> Result<(), String> { + let context = completed.context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_policy_batch(completed) + } + + pub(crate) fn finish_set_document_content( + &mut self, + completed: CompletedSetDocumentContent, + ) -> Result<(RendererSetDocumentContentResult, RendererCommandTurnOutput), String> { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_set_document_content(completed) + } + + pub(crate) fn start_top_level_same_document_navigation( + &self, + document: DocumentHandle, + url: String, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_top_level_same_document_navigation(document, url) + } + + pub(crate) fn finish_top_level_same_document_navigation( + &mut self, + completed: CompletedTopLevelSameDocumentNavigation, + ) -> Result<(bool, RendererCommandTurnOutput), String> { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_top_level_same_document_navigation(completed) + } + + pub(crate) fn start_top_level_history_traversal( + &self, + document: DocumentHandle, + delta: i64, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_top_level_history_traversal(document, delta) + } + + pub(crate) fn finish_top_level_history_traversal( + &mut self, + completed: CompletedTopLevelHistoryTraversal, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_top_level_history_traversal(completed) + } + + pub(crate) fn start_navigation_history_reset( + &self, + document: DocumentHandle, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_navigation_history_reset(document) + } + + pub(crate) fn finish_navigation_history_reset( + &mut self, + completed: CompletedNavigationHistoryReset, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_navigation_history_reset(completed) + } + + pub(crate) fn start_child_frame_navigation( + &self, + document: DocumentHandle, + frame_id: String, + url: String, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_child_frame_navigation(document, frame_id, url) + } + + pub(crate) fn finish_child_frame_navigation( + &mut self, + completed: CompletedChildFrameNavigation, + ) -> Result<(bool, RendererCommandTurnOutput), String> { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_child_frame_navigation(completed) + } + + pub(crate) fn start_capture_document_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_capture_document_snapshot(document) + } + + pub(crate) fn finish_capture_document_snapshot( + &mut self, + completed: CompletedCaptureDocumentSnapshot, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_capture_document_snapshot(completed) + } + + pub(crate) fn start_capture_document_image( + &self, + document: DocumentHandle, + request: RendererCaptureScreenshotRequest, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_capture_document_image(document, request) + } + + pub(crate) fn finish_capture_document_image( + &mut self, + completed: CompletedCaptureDocumentImage, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_capture_document_image(completed) + } + + pub(crate) fn start_capture_document_screencast_frame( + &self, + document: DocumentHandle, + request: RendererCaptureScreencastFrameRequest, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_capture_document_screencast_frame(document, request) + } + + pub(crate) fn finish_capture_document_screencast_frame( + &mut self, + completed: CompletedCaptureDocumentScreencastFrame, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_capture_document_screencast_frame(completed) + } + + pub(crate) fn start_child_frame_document_resource_text_search( + &self, + document: DocumentHandle, + frame_id: String, + url: String, + query: String, + case_sensitive: bool, + is_regex: bool, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_child_frame_document_resource_text_search( + document, + frame_id, + url, + query, + case_sensitive, + is_regex, + ) + } + + pub(crate) fn start_document_text_search( + &self, + document: DocumentHandle, + text: String, + query: String, + case_sensitive: bool, + is_regex: bool, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_text_search(document, text, query, case_sensitive, is_regex) + } + + pub(crate) fn finish_document_resource_text_search( + &mut self, + completed: CompletedDocumentResourceTextSearch, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_resource_text_search(completed) + } + + pub(crate) fn document_subresource_network_records( + &self, + document: DocumentHandle, + ) -> Result, String> { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .document_subresource_network_records(document) + } + + pub(crate) fn ensure_browser_document_current( + &self, + document: DocumentHandle, + ) -> Result<(), String> { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .ensure_document_current(document) + } + + pub(crate) fn document_response_headers( + &self, + document: DocumentHandle, + ) -> Result, String> { + Ok(self + .browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .document_response_headers(document)? + .to_vec()) + } + + pub(crate) fn start_document_csp_bypass_update( + &self, + document: DocumentHandle, + bypass: bool, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_csp_bypass_update(document, bypass) + } + + pub(crate) fn finish_document_csp_bypass_update( + &mut self, + completed: CompletedDocumentCspBypassUpdate, + ) -> Result<(), String> { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_csp_bypass_update(completed) + } + + pub(crate) fn start_document_storage_key_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_storage_key_snapshot(document) + } + + pub(crate) fn finish_document_storage_key_snapshot( + &mut self, + completed: CompletedDocumentStorageKeySnapshot, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_storage_key_snapshot(completed) + } + + pub(crate) fn start_child_frame_tree_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_child_frame_tree_snapshot(document) + } + + pub(crate) fn finish_child_frame_tree_snapshot( + &mut self, + completed: CompletedChildFrameTreeSnapshot, + ) -> Result, String> { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_child_frame_tree_snapshot(completed) + } + + pub(crate) fn start_document_cookie_owner_snapshot( + &self, + document: DocumentHandle, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_cookie_owner_snapshot(document) + } + + pub(crate) fn finish_document_cookie_owner_snapshot( + &mut self, + completed: CompletedDocumentCookieOwnerSnapshot, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_cookie_owner_snapshot(completed) + } + + pub(crate) fn start_document_blob_read( + &self, + document: DocumentHandle, + uuid: String, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_document_blob_read(document, uuid) + } + + pub(crate) fn finish_document_blob_read( + &mut self, + completed: CompletedDocumentBlobRead, + ) -> Result>, String> { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_document_blob_read(completed) + } + + pub(crate) fn start_network_resource_load_preparation( + &self, + document: DocumentHandle, + frame_id: String, + url: Url, + disable_cache: bool, + include_credentials: bool, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_network_resource_load_preparation( + document, + frame_id, + url, + disable_cache, + include_credentials, + ) + } + + pub(crate) fn finish_network_resource_load_preparation( + &mut self, + completed: CompletedNetworkResourceLoadPreparation, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_network_resource_load_preparation(completed) + } + + pub(crate) fn start_app_manifest_load_preparation( + &self, + document: DocumentHandle, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_app_manifest_load_preparation(document) + } + + pub(crate) fn finish_app_manifest_load_preparation( + &mut self, + completed: CompletedAppManifestLoadPreparation, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_app_manifest_load_preparation(completed) + } + + pub(crate) fn start_app_manifest_publication( + &self, + document: DocumentHandle, + publication: RendererAppManifestLoadPublication, + ) -> Result { + self.browser_context_handle_by_id(document.web_contents().context()) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .start_app_manifest_publication(document, publication) + } + + pub(crate) fn finish_app_manifest_publication( + &mut self, + completed: CompletedAppManifestPublication, + ) -> Result { + let context = completed.document().web_contents().context(); + self.browser_context_handle_by_id_mut(context) + .ok_or_else(|| "NoDocumentLoaded".to_owned())? + .finish_app_manifest_publication(completed) + } +} diff --git a/moli-protocol/src/conn/browser_web_contents_commands.rs b/moli-protocol/src/conn/browser_web_contents_commands.rs new file mode 100644 index 0000000000..cff260fad5 --- /dev/null +++ b/moli-protocol/src/conn/browser_web_contents_commands.rs @@ -0,0 +1,138 @@ +use moli_core::browser::WebContentsHandle; + +use crate::conn::{ + CdpConnection, CommandOwnerScope, TargetPageResidenceIdentity, WindowSurface, + WindowSurfaceState, +}; + +impl CdpConnection { + /// Resolves a frontend route once into a stable physical WebContents + /// capability. Browser operations below never receive Target or Session + /// identifiers. + pub(crate) fn browser_web_contents_for_owner( + &self, + owner: &CommandOwnerScope, + ) -> Result { + let (context_id, target_id) = self + .target_owner_identity_for_owner(owner) + .ok_or_else(|| "BrowserContextNotLoaded".to_owned())?; + let context = self + .browser_context_by_id(&context_id) + .ok_or_else(|| "BrowserContextNotLoaded".to_owned())?; + match target_id { + Some(target_id) => context + .web_contents_handle_for_target(&target_id) + .ok_or_else(|| "NoSuchTarget".to_owned()), + None => context + .selected_web_contents_handle() + .ok_or_else(|| "NoSuchTarget".to_owned()), + } + } + + pub(crate) fn browser_web_contents_for_target( + &self, + target_id: &str, + ) -> Result { + self.browser_contexts() + .find_map(|context| context.web_contents_handle_for_target(target_id)) + .ok_or_else(|| "NoSuchTarget".to_owned()) + } + + pub(crate) fn browser_web_contents_for_page_residence( + &self, + page: &TargetPageResidenceIdentity, + ) -> Result { + if !self.target_page_residence_identity_is_current(page) { + return Err("WebContents unavailable".to_owned()); + } + let target_id = page + .target_id() + .ok_or_else(|| "WebContents unavailable".to_owned())?; + self.browser_context_by_id(page.browser_context_id()) + .and_then(|context| context.web_contents_handle_for_target(target_id)) + .ok_or_else(|| "WebContents unavailable".to_owned()) + } + + pub(crate) fn browser_web_contents_for_window_id( + &self, + window_id: u64, + ) -> Result { + self.browser_contexts() + .find_map(|context| context.web_contents_handle_for_window_id(window_id)) + .ok_or_else(|| "UnknownWindowId".to_owned()) + } + + pub(crate) fn browser_window_surface( + &self, + handle: WebContentsHandle, + ) -> Result { + self.browser_context_by_browser_id(handle.context()) + .ok_or_else(|| "WebContents unavailable".to_owned())? + .web_contents_window_surface(handle) + } + + pub(crate) fn crash_browser_web_contents_renderer_from_io( + &self, + handle: WebContentsHandle, + ) -> Result<(), String> { + self.browser_context_by_browser_id(handle.context()) + .ok_or_else(|| "WebContents unavailable".to_owned())? + .browser_context + .crash_web_contents_renderer_from_io(handle) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn update_browser_window_surface( + &mut self, + handle: WebContentsHandle, + state: Option, + width: Option, + height: Option, + x: Option, + y: Option, + ) -> Result<(), String> { + self.browser_context_by_browser_id_mut(handle.context()) + .ok_or_else(|| "WebContents unavailable".to_owned())? + .update_web_contents_window_surface(handle, state, width, height, x, y) + } + + pub(crate) async fn select_browser_web_contents_async( + &mut self, + handle: WebContentsHandle, + ) -> Result<(), String> { + let browser_globals = self.browser_global_overrides.clone(); + let pending = self + .browser_context_by_browser_id_mut(handle.context()) + .ok_or_else(|| "WebContents unavailable".to_owned())? + .start_select_web_contents(handle, &browser_globals)?; + let completed = pending.wait().await; + self.browser_context_by_browser_id_mut(handle.context()) + .ok_or_else(|| "WebContents unavailable".to_owned())? + .finish_select_web_contents(completed) + } + + pub(crate) async fn apply_browser_page_surface_async( + &mut self, + handle: WebContentsHandle, + foreground: bool, + ) -> Result { + let browser_globals = self.browser_global_overrides.clone(); + let pending = { + let context = self + .browser_context_by_browser_id_mut(handle.context()) + .ok_or_else(|| "WebContents unavailable".to_owned())?; + let Some(document) = context.document_handle_for_web_contents(handle)? else { + return Ok(false); + }; + context.start_document_page_surface_update( + document, + foreground, + browser_globals.network_conditions, + browser_globals.geolocation.as_ref(), + )? + }; + let completed = pending.wait().await; + self.finish_document_policy_batch(completed)?; + Ok(true) + } +} diff --git a/moli-protocol/src/conn/browser_worker_commands.rs b/moli-protocol/src/conn/browser_worker_commands.rs new file mode 100644 index 0000000000..388487ff51 --- /dev/null +++ b/moli-protocol/src/conn/browser_worker_commands.rs @@ -0,0 +1,86 @@ +use moli_core::browser::{BrowserContextId, ServiceWorkerCommand}; +use moli_shared_worker::SharedWorkerInstanceId; + +use super::CdpConnection; + +impl CdpConnection { + pub(crate) fn browser_context_handle_for_devtools_id( + &self, + browser_context_id: &str, + ) -> Result { + self.browser_context_by_id(browser_context_id) + .map(super::BrowserContext::browser_context_id) + .ok_or_else(|| "BrowserContext unavailable".to_owned()) + } + + pub(crate) fn execute_browser_service_worker_command( + &self, + context: BrowserContextId, + command: ServiceWorkerCommand, + ) -> Result<(), String> { + self.browser_context_by_browser_id(context) + .ok_or_else(|| "BrowserContext unavailable".to_owned())? + .execute_service_worker_command(command) + } + + pub(crate) fn close_browser_shared_worker( + &self, + context: BrowserContextId, + instance_id: SharedWorkerInstanceId, + ) -> Result { + Ok(self + .browser_context_by_browser_id(context) + .ok_or_else(|| "BrowserContext unavailable".to_owned())? + .close_shared_worker(instance_id)) + } + + pub(crate) fn close_browser_dedicated_worker( + &self, + context: BrowserContextId, + instance_id: u64, + ) -> Result { + Ok(self + .browser_context_by_browser_id(context) + .ok_or_else(|| "BrowserContext unavailable".to_owned())? + .close_dedicated_worker(instance_id)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn service_worker_control_rejects_a_replaced_devtools_context_id() { + let mut conn = crate::test_support::connection(); + conn.install_browser_context_fixture_for_test( + conn.new_browser_context_fixture_for_test("BID-worker-control"), + ); + let original = conn + .browser_context_handle_for_devtools_id("BID-worker-control") + .unwrap(); + + conn.install_browser_context_fixture_for_test( + conn.new_browser_context_fixture_for_test("BID-worker-control"), + ); + let replacement = conn + .browser_context_handle_for_devtools_id("BID-worker-control") + .unwrap(); + assert_ne!(original, replacement); + + assert_eq!( + conn.execute_browser_service_worker_command( + original, + ServiceWorkerCommand::SetForceUpdateOnPageLoad(true), + ), + Err("BrowserContext unavailable".into()) + ); + assert!( + !conn + .browser_context + .as_ref() + .unwrap() + .service_worker_force_update_on_page_load() + ); + } +} diff --git a/moli-protocol/src/conn/command_owner_scope.rs b/moli-protocol/src/conn/command_owner_scope.rs index 1a99a8b8f2..401177ea0a 100644 --- a/moli-protocol/src/conn/command_owner_scope.rs +++ b/moli-protocol/src/conn/command_owner_scope.rs @@ -102,7 +102,7 @@ impl CommandOwnerScope { #[cfg(test)] mod tests { use super::*; - use crate::conn::BrowserContext; + use crate::devtools_runtime::{ DevToolsCommandContext, DevToolsProtocol, DevToolsSessionId, DevToolsTargetId, }; @@ -118,8 +118,8 @@ mod tests { #[test] fn root_page_scope_freezes_the_concrete_active_target() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-scope".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-scope".to_owned()); browser_context.set_active_target_id("TID-original"); conn.install_browser_context_fixture_for_test(browser_context); @@ -137,7 +137,7 @@ mod tests { #[test] fn root_scope_without_a_browser_context_has_explicit_browser_authority() { - let conn = CdpConnection::default(); + let conn = crate::test_support::connection(); let scope = CommandOwnerScope::capture(&conn, None); @@ -146,8 +146,9 @@ mod tests { #[test] fn root_scope_without_a_page_has_explicit_browser_context_authority() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-empty".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-empty".to_owned())); let scope = CommandOwnerScope::capture(&conn, None); @@ -161,8 +162,8 @@ mod tests { #[test] fn explicit_target_preserves_matching_attached_session_authority() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new_with_page_for_test("BID-owner", "TID-owner"); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_page_target_fixture_for_test("BID-owner", "TID-owner"); browser_context.attach_active_session("SID-primary"); assert!( browser_context @@ -180,9 +181,8 @@ mod tests { #[test] fn explicit_target_routes_protocol_neutral_session_and_rejects_wrong_cdp_attachment() { - let mut conn = CdpConnection::default(); - let mut browser_context = - BrowserContext::new_with_page_for_test("BID-owner", "TID-primary"); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_page_target_fixture_for_test("BID-owner", "TID-primary"); browser_context.attach_active_session("SID-primary"); assert!(browser_context.register_page_target_url_fixture( "TID-background".to_owned(), diff --git a/moli-protocol/src/conn/cookie_manager_surface.rs b/moli-protocol/src/conn/cookie_manager_surface.rs index b0b01bd820..4b2e7c6a06 100644 --- a/moli-protocol/src/conn/cookie_manager_surface.rs +++ b/moli-protocol/src/conn/cookie_manager_surface.rs @@ -7,8 +7,6 @@ use moli_cookie_jar::{ }; use url::Url; -#[cfg(test)] -use moli_core::page::Page; use moli_core::page::{ DocumentCookieBrowserContextSnapshot, DocumentCookieCacheLookupResult, DocumentCookieFacadeTelemetrySnapshot, DocumentCookieFirstOperation, @@ -983,8 +981,4 @@ impl BrowserContextCookieManagerSurface { pub(crate) fn clear_policy_browser_context_overrides(&mut self) -> bool { self.policy_surface.clear_browser_context_overrides() } - - pub(crate) async fn apply_to_page_async(&self, page: &mut Page) { - self.policy_surface.apply_to_page_async(page).await; - } } diff --git a/moli-protocol/src/conn/cookie_owner.rs b/moli-protocol/src/conn/cookie_owner.rs index f09775ebd7..318138d059 100644 --- a/moli-protocol/src/conn/cookie_owner.rs +++ b/moli-protocol/src/conn/cookie_owner.rs @@ -198,22 +198,14 @@ fn browser_context_cookie_manager_surface_snapshot( let snapshot = browser_context.raw_cookie_manager_surface_snapshot(); #[cfg(not(test))] let snapshot = BrowserContextCookieManagerSurfaceSnapshot::default(); - let current_document_url = browser_context - .loaded_page() - .map(|page| page.final_url().clone()); - let navigation_initiator_url = browser_context - .loaded_page() - .and_then(|page| page.navigation_initiator_url().cloned()); - let requested_document_url = browser_context - .loaded_page() - .map(|page| page.requested_url().clone()); - let navigation_was_redirected = browser_context - .loaded_page() - .is_some_and(|page| page.navigation_redirected()); - let navigation_redirect_count = browser_context - .loaded_page() - .map(|page| page.navigation_redirect_count()) - .unwrap_or(0); + let navigation = browser_context.selected_document_navigation_metadata(); + let current_document_url = navigation.as_ref().map(|state| state.current_url.clone()); + let navigation_initiator_url = navigation + .as_ref() + .and_then(|state| state.initiator_url.clone()); + let requested_document_url = navigation.as_ref().map(|state| state.requested_url.clone()); + let navigation_was_redirected = navigation.as_ref().is_some_and(|state| state.redirected); + let navigation_redirect_count = navigation.as_ref().map_or(0, |state| state.redirect_count); let (default_cookie_write_url, default_cookie_write_url_source) = browser_context.default_cookie_write_url_with_source(); let structured_write_backend_status = browser_context_structured_cookie_write_backend_status(); @@ -368,9 +360,7 @@ impl BrowserContext { return report; } - self.with_cookie_store_mut(|store| { - store.upsert_with_request_url_report(cookie, request_url.as_ref(), CookieSource::Cdp) - }) + self.store_cookie(cookie, request_url.as_ref(), CookieSource::Management) } #[cfg(test)] @@ -546,12 +536,13 @@ impl BrowserContext { &mut self, ) -> Option { let target_id = self.active_target_id_owned()?; - let pending = self - .start_document_cookie_owner_snapshot_for_target(&target_id) - .ok()?; - let completion = pending.wait().await.ok()?; - self.finish_document_cookie_owner_snapshot_for_target(&target_id, completion) - .ok() + let document = self.document_handle_for_target(&target_id)?; + let completion = self + .start_document_cookie_owner_snapshot(document) + .ok()? + .wait() + .await; + self.finish_document_cookie_owner_snapshot(completion).ok() } pub(super) fn default_cookie_write_url_with_source( @@ -562,8 +553,8 @@ impl BrowserContext { // document URL into every `set()` call. Prefer the live page URL when // one exists; otherwise fall back to the BrowserContext's current URL. if let Some(url) = self - .loaded_page() - .map(|page| page.final_url().clone()) + .selected_document_navigation_metadata() + .map(|state| state.current_url) .filter(|url| matches!(url.scheme(), "http" | "https")) { return ( diff --git a/moli-protocol/src/conn/cookie_policy_surface.rs b/moli-protocol/src/conn/cookie_policy_surface.rs index fb36cc3c36..53612454b5 100644 --- a/moli-protocol/src/conn/cookie_policy_surface.rs +++ b/moli-protocol/src/conn/cookie_policy_surface.rs @@ -1,6 +1,4 @@ use moli_cookie_jar::{BrowserCookieFacadeContextOverrides, BrowserCookieFacadeOverrides}; -#[cfg(test)] -use moli_core::page::Page; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct BrowserContextDocumentCookiePolicySurfaceSnapshot { @@ -26,11 +24,6 @@ impl BrowserContextDocumentCookiePolicySurface { } } - #[cfg(test)] - pub(crate) fn overrides(&self) -> &BrowserCookieFacadeOverrides { - &self.overrides - } - pub(crate) fn cookies_enabled_override(&self) -> Option { self.overrides.cookies_enabled } @@ -95,17 +88,4 @@ impl BrowserContextDocumentCookiePolicySurface { overrides.storage_access_status = None; self.set_overrides(&overrides) } - - #[cfg(test)] - pub(crate) async fn apply_to_page_async(&self, page: &mut Page) { - if self.overrides == BrowserCookieFacadeOverrides::default() { - // See the sync variant above for why empty policy means clearing - // the live document facade rather than applying empty overrides. - let _ = page.clear_document_cookie_facade_overrides_async().await; - } else { - let _ = page - .apply_document_cookie_facade_overrides_async(self.overrides()) - .await; - } - } } diff --git a/moli-protocol/src/conn/dispatch_tests.rs b/moli-protocol/src/conn/dispatch_tests.rs index 2e36736cc9..6e7db638a1 100644 --- a/moli-protocol/src/conn/dispatch_tests.rs +++ b/moli-protocol/src/conn/dispatch_tests.rs @@ -245,6 +245,20 @@ async fn ensure_initial_document_for_target_id_for_test( .unwrap_or_else(|message| panic!("initial document should install: {message}")); } +async fn install_navigation_fixture_for_target_for_test( + conn: &mut CdpConnection, + target_id: &str, + url: &str, +) { + let route = conn + .target_session_route_for_target_id(target_id) + .unwrap_or_else(|| panic!("target route should exist for {target_id}")); + let owner = CommandOwnerScope::for_route(route); + let _ = conn + .install_navigation_fixture_for_owner_for_test(url, &owner) + .await; +} + async fn evaluate_string_for_test( conn: &mut CdpConnection, context: DevToolsCommandContext, @@ -281,8 +295,9 @@ async fn evaluate_string_for_test( } fn connection_with_background_pending_fetch_action(request_id: &str) -> CdpConnection { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-fetch-background".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-fetch-background".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -407,7 +422,8 @@ async fn devtools_browser_context_create_uses_ephemeral_storage_partition() { vec![stored_cookie_for_dispatch_test("sid", "seeded")], &initial_storage_owner, ); - let mut conn = CdpConnection::new_with_initial_storage_partition(initial_storage_partition); + let mut conn = + crate::test_support::connection_with_config(initial_storage_partition, Default::default()); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -459,7 +475,7 @@ async fn devtools_browser_context_create_rejects_persistent_partition_id() { ("default", "DefaultPersistentBrowserContextNotAllowed"), ("tenant/a", "InvalidPersistentBrowserContextId"), ] { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -496,7 +512,7 @@ async fn devtools_browser_context_create_rejects_persistent_partition_id() { #[tokio::test] async fn devtools_command_executes_target_create_and_get_targets() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -647,11 +663,9 @@ async fn devtools_script_navigation_exact_cursor_rejects_replaced_page_owner_act #[tokio::test] async fn devtools_fetch_control_command_routes_through_fetch_owner() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-fetch-control", - "TID-fetch-control", - )); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_page_target_fixture_for_test("BID-fetch-control", "TID-fetch-control")); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -697,8 +711,9 @@ async fn devtools_fetch_control_command_routes_through_fetch_owner() { #[tokio::test] async fn bidi_fetch_control_resolves_background_request_owner() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-fetch-background".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-fetch-background".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -719,6 +734,7 @@ async fn bidi_fetch_control_resolves_background_request_owner() { redirect_headers: None, navigate_id: None, owner: CommandOwnerScope::for_session("SID-background"), + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: NavigationResultProjection::WebDriverBidi(json!({})), frame_id: "TID-background".to_owned(), session_id: Some("SID-background".to_owned()), @@ -828,7 +844,7 @@ async fn bidi_fetch_request_actions_resolve_background_request_owner() { #[tokio::test] async fn devtools_browser_context_commands_create_list_and_remove_user_context() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -961,7 +977,7 @@ async fn devtools_browser_context_commands_create_list_and_remove_user_context() #[tokio::test] async fn devtools_browser_context_create_installs_socks_proxy_for_requests() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -1017,7 +1033,7 @@ async fn devtools_browser_context_create_installs_socks_proxy_for_requests() { #[tokio::test] async fn devtools_remove_browser_context_rejects_unknown_user_context() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -1042,7 +1058,7 @@ async fn devtools_remove_browser_context_rejects_unknown_user_context() { #[tokio::test] async fn devtools_create_target_explicit_default_browser_context_materializes_default_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let user_context = conn.new_browser_context("user-context-1".to_owned()); conn.insert_browser_context(user_context); @@ -1086,10 +1102,11 @@ async fn devtools_create_target_explicit_default_browser_context_materializes_de #[tokio::test] async fn devtools_create_target_uses_reference_target_browser_context_when_unspecified() { - let mut conn = CdpConnection::new(); - let mut default_context = BrowserContext::new("BID-default".to_owned()); + let mut conn = crate::test_support::connection(); + let mut default_context = conn.new_browser_context_fixture_for_test("BID-default".to_owned()); default_context.set_active_target_id("TID-default".to_owned()); - let mut reference_context = BrowserContext::new("BID-reference".to_owned()); + let mut reference_context = + conn.new_browser_context_fixture_for_test("BID-reference".to_owned()); reference_context.set_active_target_id("TID-reference".to_owned()); conn.insert_browser_context(default_context); conn.insert_browser_context(reference_context); @@ -1141,12 +1158,13 @@ async fn devtools_create_target_uses_reference_target_browser_context_when_unspe #[tokio::test] async fn devtools_create_target_explicit_browser_context_overrides_reference_target() { - let mut conn = CdpConnection::new(); - let mut default_context = BrowserContext::new("BID-default".to_owned()); + let mut conn = crate::test_support::connection(); + let mut default_context = conn.new_browser_context_fixture_for_test("BID-default".to_owned()); default_context.set_active_target_id("TID-default".to_owned()); - let mut reference_context = BrowserContext::new("BID-reference".to_owned()); + let mut reference_context = + conn.new_browser_context_fixture_for_test("BID-reference".to_owned()); reference_context.set_active_target_id("TID-reference".to_owned()); - let mut explicit_context = BrowserContext::new("BID-explicit".to_owned()); + let mut explicit_context = conn.new_browser_context_fixture_for_test("BID-explicit".to_owned()); explicit_context.set_active_target_id("TID-explicit".to_owned()); conn.insert_browser_context(default_context); conn.insert_browser_context(reference_context); @@ -1201,23 +1219,20 @@ async fn devtools_create_target_explicit_browser_context_overrides_reference_tar #[tokio::test(flavor = "multi_thread")] async fn devtools_runtime_call_function_popup_activity_drains_from_protocol_neutral_command() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); ctx.conn.set_root_target_discovery_enabled(true); - let mut browser_context = BrowserContext::new("BID-neutral-popup".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-neutral-popup".to_owned()); browser_context.set_active_target_id("TID-neutral-popup-opener".to_owned()); browser_context.attach_active_session("SID-neutral-popup-opener"); ctx.conn .install_browser_context_fixture_for_test(browser_context); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,

neutral popup opener

") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,

neutral popup opener

", + None, + ) + .await; let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, @@ -1327,8 +1342,8 @@ async fn devtools_runtime_call_function_popup_activity_drains_from_protocol_neut #[tokio::test] async fn devtools_create_target_rejects_unknown_reference_target() { - let mut conn = CdpConnection::new(); - let mut default_context = BrowserContext::new("BID-default".to_owned()); + let mut conn = crate::test_support::connection(); + let mut default_context = conn.new_browser_context_fixture_for_test("BID-default".to_owned()); default_context.set_active_target_id("TID-default".to_owned()); conn.insert_browser_context(default_context); @@ -1352,7 +1367,7 @@ async fn devtools_create_target_rejects_unknown_reference_target() { #[tokio::test] async fn devtools_command_preserves_target_create_typed_sidecar() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.set_root_target_discovery_enabled(true); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, @@ -1383,8 +1398,9 @@ async fn devtools_command_preserves_target_create_typed_sidecar() { #[tokio::test] async fn devtools_command_preserves_target_close_detached_typed_sidecar() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-close-sidecar".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-close-sidecar".to_owned()); browser_context.set_active_target_id("TID-close-sidecar".to_owned()); browser_context.attach_active_session("SID-close-sidecar".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -1434,8 +1450,9 @@ async fn devtools_command_preserves_target_close_detached_typed_sidecar() { #[tokio::test] async fn devtools_command_preserves_remove_browser_context_detached_typed_sidecar() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("user-context-detach-sidecar".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("user-context-detach-sidecar".to_owned()); browser_context.set_active_target_id("TID-dispose-sidecar".to_owned()); browser_context.attach_active_session("SID-dispose-sidecar".to_owned()); conn.insert_browser_context(browser_context); @@ -1483,7 +1500,7 @@ async fn devtools_command_preserves_remove_browser_context_detached_typed_sideca #[tokio::test] async fn devtools_command_executes_target_pending_activate_and_close() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -1550,7 +1567,7 @@ async fn devtools_command_executes_target_pending_activate_and_close() { #[tokio::test] async fn devtools_runtime_command_uses_background_initial_document_without_resolver_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverClassic, session_id: Some(DevToolsSessionId::from("classic-session-1")), @@ -1714,7 +1731,7 @@ async fn devtools_runtime_command_uses_background_initial_document_without_resol #[tokio::test] async fn protocol_neutral_await_promise_keeps_background_owner_route_across_pending_completion() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-owner-route")), @@ -1878,7 +1895,7 @@ async fn protocol_neutral_await_promise_keeps_background_owner_route_across_pend #[tokio::test] async fn pending_runtime_binding_page_phase_keeps_background_owner_route_across_completion() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let conn = &mut ctx.conn; let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, @@ -1998,7 +2015,7 @@ async fn pending_runtime_binding_page_phase_keeps_background_owner_route_across_ #[tokio::test] async fn runtime_enable_uses_background_initial_document_through_attached_session() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-runtime-enable-normal-route")), @@ -2109,7 +2126,7 @@ async fn runtime_enable_uses_background_initial_document_through_attached_sessio #[tokio::test] async fn page_enable_uses_background_initial_document_through_attached_session() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-page-normal-route")), @@ -2206,7 +2223,7 @@ async fn page_enable_uses_background_initial_document_through_attached_session() #[tokio::test] async fn initial_document_page_ensure_completion_uses_captured_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-initial-owner")), @@ -2283,7 +2300,7 @@ async fn initial_document_page_ensure_completion_uses_captured_owner() { #[tokio::test] async fn target_lifecycle_ensure_installs_initial_about_blank_page_for_active_target() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-lifecycle-active")), @@ -2356,7 +2373,7 @@ async fn target_lifecycle_ensure_installs_initial_about_blank_page_for_active_ta #[tokio::test] async fn stale_initial_document_page_build_does_not_overwrite_committed_page() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-lifecycle-stale-initial")), @@ -2390,36 +2407,8 @@ async fn stale_initial_document_page_build_does_not_overwrite_committed_page() { .wait() .await .expect("build candidate before replacement"); - let parsed_real_page_url = url::Url::parse(real_page_url).expect("data URL should parse"); - let owner = crate::conn::CommandOwnerScope::capture(&conn, None); - let token = conn - .start_document_navigation_for_owner(&owner, "LOADER-real-page".into()) - .unwrap(); - let loaded = conn - .load_navigation_via_runtime_async(real_page_url) - .await - .expect("real navigation page should build"); - let artifacts = loaded.page_creation_artifacts; - let prepared = conn - .start_loaded_document_navigation_for_owner( - &owner, - token, - loaded.page, - crate::conn::DocumentNavigationDestination { - url: parsed_real_page_url.clone(), - security_origin: "null".into(), - secure_context_type: "InsecureScheme".into(), - }, - &artifacts, - ) - .unwrap() - .await - .unwrap(); - let committed = conn - .commit_loaded_navigation(prepared) - .expect("real Browser Document should commit"); - assert!(committed.inspection_projection.is_ok()); - committed.previous_document_retirement.close().await; + conn.install_navigation_fixture_for_session_owner_for_test(real_page_url, None) + .await; let attachment_after_real_page = conn .browser_context .as_ref() @@ -2554,11 +2543,10 @@ async fn stale_initial_document_page_build_does_not_overwrite_committed_page() { ); assert_eq!( browser_context - .loaded_page() + .loaded_document_url_for_test() .expect("real page should stay installed") - .final_url() .as_str(), - parsed_real_page_url.as_str(), + real_page_url, "current page should remain the committed navigation page" ); let initial = browser_context @@ -2576,7 +2564,7 @@ async fn stale_initial_document_page_build_does_not_overwrite_committed_page() { #[tokio::test] async fn bidi_create_target_installs_initial_about_blank_page_without_default_preload() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-lifecycle-create")), @@ -2609,7 +2597,7 @@ async fn bidi_create_target_installs_initial_about_blank_page_without_default_pr #[tokio::test] async fn target_lifecycle_ensure_installs_initial_about_blank_page_for_background_target() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-lifecycle-background")), @@ -2686,7 +2674,7 @@ async fn target_lifecycle_ensure_installs_initial_about_blank_page_for_backgroun #[tokio::test] async fn devtools_get_realms_observes_create_target_initial_about_blank_page() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -2756,19 +2744,17 @@ async fn devtools_get_realms_observes_create_target_initial_about_blank_page() { #[tokio::test] async fn devtools_get_realms_succeeds_when_page_loaded_before_session_attach() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-late-realms".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-late-realms".to_owned()); browser_context.set_active_target_id("TID-late-realms".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,late-realms") - .await - .expect("page should load before DevTools session attaches"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,late-realms", + None, + ) + .await; conn.browser_context .as_mut() .expect("browser context") @@ -2803,7 +2789,7 @@ async fn devtools_get_realms_succeeds_when_page_loaded_before_session_attach() { #[tokio::test] async fn devtools_runtime_evaluate_uses_fresh_initial_document_without_resolver_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -2856,7 +2842,7 @@ async fn devtools_runtime_evaluate_uses_fresh_initial_document_without_resolver_ #[tokio::test] async fn classic_create_target_ensures_fresh_initial_document_without_resolver_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverClassic, session_id: Some(DevToolsSessionId::from("classic-session-1")), @@ -2916,8 +2902,9 @@ async fn classic_create_target_ensures_fresh_initial_document_without_resolver_f #[tokio::test] async fn devtools_runtime_evaluate_reports_no_document_without_resolver_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-no-page-runtime".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-no-page-runtime".to_owned()); browser_context.set_active_target_id("TID-no-page-runtime".to_owned()); browser_context.attach_active_session("SID-no-page-runtime".to_owned()); browser_context.set_target_url("about:blank".to_owned()); @@ -2961,7 +2948,7 @@ async fn devtools_runtime_evaluate_reports_no_document_without_resolver_fallback #[tokio::test] async fn element_screenshot_reports_unsupported_without_placeholder_payload() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("SID-element-shot")), @@ -3056,8 +3043,9 @@ async fn element_screenshot_reports_unsupported_without_placeholder_payload() { #[tokio::test] async fn element_screenshot_reports_unsupported_without_initial_document_repair() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-no-page-element-shot".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-no-page-element-shot".to_owned()); browser_context.set_active_target_id("TID-no-page-element-shot".to_owned()); browser_context.attach_active_session("SID-no-page-element-shot".to_owned()); browser_context.set_target_url("about:blank".to_owned()); @@ -3103,7 +3091,7 @@ async fn element_screenshot_reports_unsupported_without_initial_document_repair( #[tokio::test] async fn devtools_create_target_can_activate_created_target() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -3265,7 +3253,7 @@ async fn devtools_create_target_can_activate_created_target() { #[tokio::test] async fn devtools_command_executes_page_navigation_and_reload() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -3330,7 +3318,7 @@ async fn devtools_command_executes_page_navigation_and_reload() { #[tokio::test] async fn devtools_command_executes_page_navigation_without_cdp_response_sidecar() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -3494,7 +3482,7 @@ async fn devtools_command_executes_child_frame_navigation_without_cdp_response_s #[tokio::test] async fn devtools_command_reports_invalid_navigation_without_cdp_response_parser() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -3544,7 +3532,7 @@ async fn devtools_command_reports_invalid_navigation_without_cdp_response_parser #[tokio::test] async fn devtools_command_executes_preload_without_cdp_response_sidecar() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -3629,7 +3617,7 @@ async fn devtools_command_executes_preload_without_cdp_response_sidecar() { #[tokio::test] async fn devtools_command_reports_invalid_preload_without_cdp_response_parser() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -3667,7 +3655,7 @@ async fn devtools_command_reports_invalid_preload_without_cdp_response_parser() #[tokio::test] async fn devtools_command_navigates_explicit_about_blank_without_fetch() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -3753,7 +3741,7 @@ async fn devtools_command_navigates_explicit_about_blank_without_fetch() { #[tokio::test] async fn devtools_call_function_node_shared_id_failure_precedes_handle() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -3854,7 +3842,7 @@ async fn devtools_call_function_node_shared_id_failure_precedes_handle() { #[tokio::test] async fn bidi_node_remote_value_registers_renderer_shared_node_binding() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let (_target_context, shared_id, backend_node_id) = materialize_bidi_target_input_node_for_test( &mut ctx, @@ -3884,7 +3872,7 @@ async fn bidi_node_remote_value_registers_renderer_shared_node_binding() { #[tokio::test] async fn bidi_node_remote_value_reuses_renderer_frontend_node_id() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let context = bidi_fetch_command_context(); let create_result = execute_direct_devtools_command_through_renderer_fence_for_test( &mut ctx, @@ -3987,7 +3975,7 @@ async fn bidi_node_remote_value_reuses_renderer_frontend_node_id() { #[tokio::test] async fn bidi_node_remote_value_registers_child_shared_node_bindings() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let (target_context, _shared_id, _backend_node_id) = materialize_bidi_target_node_for_test( &mut ctx, "
Inside
", @@ -4078,7 +4066,7 @@ async fn bidi_node_remote_value_registers_child_shared_node_bindings() { #[tokio::test] async fn set_file_input_files_shared_id_uses_renderer_binding_without_protocol_registry() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let (target_context, _shared_id, backend_node_id) = materialize_bidi_target_input_node_for_test(&mut ctx, "") .await; @@ -4141,7 +4129,7 @@ async fn set_file_input_files_shared_id_uses_renderer_binding_without_protocol_r #[tokio::test] async fn locate_nodes_start_node_shared_id_uses_renderer_binding_without_protocol_registry() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let (target_context, _shared_id, backend_node_id) = materialize_bidi_target_node_for_test( &mut ctx, "
Inside
Outside", @@ -4190,7 +4178,7 @@ async fn locate_nodes_start_node_shared_id_uses_renderer_binding_without_protoco #[tokio::test] async fn call_function_shared_id_uses_renderer_binding_without_protocol_registry() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let (target_context, _shared_id, backend_node_id) = materialize_bidi_target_node_for_test( &mut ctx, "
Inside
", @@ -4243,7 +4231,7 @@ async fn call_function_shared_id_uses_renderer_binding_without_protocol_registry #[tokio::test] async fn devtools_command_rejects_page_print_to_pdf_without_placeholder_payload() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -4311,7 +4299,7 @@ async fn devtools_command_rejects_page_print_to_pdf_without_placeholder_payload( #[tokio::test] async fn devtools_command_executes_context_viewport_override() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -4386,7 +4374,7 @@ async fn devtools_command_executes_context_viewport_override() { #[tokio::test] async fn devtools_command_applies_window_state_to_document_surface() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -4430,7 +4418,7 @@ async fn devtools_command_applies_window_state_to_document_surface() { { let context = &conn.browser_context.as_ref().expect("browser context"); context - .target_window_surface(context.active_target_id().unwrap()) + .web_contents_window_surface(context.selected_web_contents_handle().unwrap()) .expect("registered WebContents") } .state @@ -4465,7 +4453,7 @@ async fn devtools_command_applies_window_state_to_document_surface() { { let context = conn.browser_context.as_ref().expect("browser context"); context - .target_window_surface(context.active_target_id().unwrap()) + .web_contents_window_surface(context.selected_web_contents_handle().unwrap()) .expect("registered WebContents") } .state, @@ -4546,7 +4534,7 @@ async fn evaluate_document_surface_payload( #[tokio::test] async fn devtools_command_rejects_missing_user_context_viewport_override() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -4577,7 +4565,7 @@ async fn devtools_command_rejects_missing_user_context_viewport_override() { #[tokio::test] async fn devtools_command_applies_known_user_context_viewport_default() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let browser_context = conn.new_browser_context("custom-user-context".to_owned()); conn.insert_browser_context(browser_context); let context = DevToolsCommandContext { @@ -4614,8 +4602,8 @@ async fn devtools_command_applies_known_user_context_viewport_default() { browser_context.active_target_id().is_none(), "userContext viewport should not create a page target" ); - let default_metrics = browser_context - .emulation_defaults() + let emulation_defaults = browser_context.emulation_defaults(); + let default_metrics = emulation_defaults .device_metrics .as_ref() .expect("userContext should hold default emulated device metrics"); @@ -4657,7 +4645,7 @@ async fn devtools_command_applies_known_user_context_viewport_default() { #[tokio::test] async fn devtools_command_executes_dom_outer_html_for_document_source() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverClassic, session_id: Some(DevToolsSessionId::from("classic-session-1")), @@ -4718,7 +4706,7 @@ async fn devtools_command_executes_dom_outer_html_for_document_source() { #[tokio::test] async fn devtools_command_executes_dom_query_selector_for_document_root() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverClassic, session_id: Some(DevToolsSessionId::from("classic-session-1")), @@ -5212,7 +5200,7 @@ async fn devtools_command_executes_dom_query_selector_for_document_root() { #[tokio::test] async fn devtools_command_low_backend_node_refs_miss_without_backend_binding() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverClassic, session_id: Some(DevToolsSessionId::from("classic-session-1")), @@ -5456,7 +5444,7 @@ async fn devtools_command_dispatches_coordinate_mouse_input_for_target() { #[tokio::test] async fn devtools_command_executes_input_key_command_without_cdp_sidecar() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverClassic, session_id: Some(DevToolsSessionId::from("classic-session-1")), @@ -5561,7 +5549,7 @@ async fn devtools_command_executes_input_key_command_without_cdp_sidecar() { #[tokio::test] async fn devtools_command_executes_storage_cookie_commands_for_webdriver_context() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverClassic, session_id: Some(DevToolsSessionId::from("classic-session-1")), @@ -5691,10 +5679,10 @@ async fn devtools_command_executes_storage_cookie_commands_for_webdriver_context #[tokio::test] async fn devtools_storage_cookie_commands_scope_to_target_browser_context() { - let mut conn = CdpConnection::new(); - let mut default_context = BrowserContext::new("BID-default".to_owned()); + let mut conn = crate::test_support::connection(); + let mut default_context = conn.new_browser_context_fixture_for_test("BID-default".to_owned()); default_context.set_active_target_id("TID-default".to_owned()); - let mut custom_context = BrowserContext::new("BID-custom".to_owned()); + let mut custom_context = conn.new_browser_context_fixture_for_test("BID-custom".to_owned()); custom_context.set_active_target_id("TID-custom".to_owned()); conn.install_browser_context_fixture_for_test(default_context); conn.push_inactive_browser_context_fixture_for_test(custom_context); @@ -5782,7 +5770,7 @@ async fn devtools_storage_cookie_commands_scope_to_target_browser_context() { #[tokio::test] async fn devtools_command_executes_navigation_history_and_traverse() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverClassic, session_id: Some(DevToolsSessionId::from("classic-session-1")), @@ -5924,7 +5912,7 @@ async fn devtools_command_executes_navigation_history_and_traverse() { #[tokio::test] async fn devtools_command_executes_context_preload_add_and_remove() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -6089,7 +6077,7 @@ async fn devtools_command_executes_context_preload_add_and_remove() { #[tokio::test] async fn devtools_command_executes_default_preload_add_and_remove_without_loaded_target() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -6362,7 +6350,7 @@ async fn devtools_command_executes_user_context_preload_without_default_leakage( #[tokio::test] async fn devtools_runtime_call_function_channel_does_not_emit_direct_script_message_sidecar() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -6445,7 +6433,7 @@ async fn devtools_runtime_call_function_channel_does_not_emit_direct_script_mess #[tokio::test] async fn devtools_command_executes_script_evaluate_and_call_function() { - let mut ctx = crate::testing::TestContext::from_conn(CdpConnection::new()); + let mut ctx = crate::testing::TestContext::from_conn(crate::test_support::connection()); let context = DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, session_id: Some(DevToolsSessionId::from("bidi-session-1")), @@ -6755,7 +6743,7 @@ async fn devtools_command_executes_script_evaluate_and_call_function() { #[test] fn command_dispatch_completes_parse_errors_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let step = conn.start_command_dispatch("{"); assert_eq!( complete_messages(step), @@ -6768,7 +6756,7 @@ fn command_dispatch_completes_parse_errors_without_legacy_fallback() { #[test] fn command_dispatch_completes_invalid_methods_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let raw = serde_json::to_string(&json!({ "id": 7, "method": "MalformedMethod", @@ -6788,7 +6776,7 @@ fn command_dispatch_completes_invalid_methods_without_legacy_fallback() { #[test] fn command_dispatch_completes_startup_commands_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let raw = serde_json::to_string(&json!({ "id": 8, "method": "Page.getFrameTree", @@ -6808,8 +6796,8 @@ fn command_dispatch_completes_startup_commands_without_legacy_fallback() { #[test] fn command_dispatch_completes_unknown_domains_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); conn.install_browser_context_fixture_for_test(bc); @@ -6832,8 +6820,9 @@ fn command_dispatch_completes_unknown_domains_without_legacy_fallback() { #[test] fn command_dispatch_completes_console_log_and_inspector_owner_commands() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-dispatch".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-dispatch".to_owned())); for (id, method) in [ (10, "Console.enable"), @@ -6853,7 +6842,16 @@ fn command_dispatch_completes_console_log_and_inspector_owner_commands() { #[test] fn command_dispatch_completes_browser_sync_commands() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-browser-sync".to_owned()); + browser_context.set_active_target_id("TID-browser-sync"); + conn.install_browser_context_fixture_for_test(browser_context); + let window_id = conn + .browser_web_contents_for_target("TID-browser-sync") + .unwrap() + .id() + .get(); for (id, method) in [ (20, "Browser.getVersion"), (21, "Browser.getWindowForTarget"), @@ -6862,7 +6860,7 @@ fn command_dispatch_completes_browser_sync_commands() { ] { let params = match method { "Browser.setWindowBounds" => json!({ - "windowId": 1_923_710_101_i64, + "windowId": window_id, "bounds": { "windowState": "normal", "width": 800, "height": 600 } }), "Browser.setDownloadBehavior" => json!({ @@ -6885,9 +6883,55 @@ fn command_dispatch_completes_browser_sync_commands() { } } +#[test] +fn browser_window_bounds_are_owned_by_exact_web_contents() { + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-window-owner".to_owned()); + browser_context.set_active_target_id("TID-window-first"); + browser_context.stage_background_target( + "TID-window-second".to_owned(), + None, + "about:blank".to_owned(), + None, + None, + ); + conn.install_browser_context_fixture_for_test(browser_context); + let first = conn + .browser_web_contents_for_target("TID-window-first") + .unwrap(); + let second = conn + .browser_web_contents_for_target("TID-window-second") + .unwrap(); + assert_ne!(first, second); + + let raw = json!({ + "id": 221, + "method": "Browser.setWindowBounds", + "params": { + "windowId": second.id().get(), + "bounds": { "windowState": "maximized", "left": -12, "top": 34 } + } + }) + .to_string(); + assert_eq!( + complete_messages(conn.start_command_dispatch(&raw)), + vec![json!({ "id": 221, "result": {} })] + ); + + let first_surface = conn.browser_window_surface(first).unwrap(); + let second_surface = conn.browser_window_surface(second).unwrap(); + assert_eq!(first_surface.state, crate::conn::WindowSurfaceState::Normal); + assert_eq!( + second_surface.state, + crate::conn::WindowSurfaceState::Maximized + ); + assert_eq!((second_surface.x, second_surface.y), (-12, 34)); +} + #[test] fn command_dispatch_completes_browser_owner_commands_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); for (id, method, params, expects_result) in [ ( 24, @@ -6938,18 +6982,16 @@ fn command_dispatch_completes_browser_owner_commands_without_legacy_fallback() { #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_browser_permission_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-browser-permission-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-browser-permission-live".to_owned()); browser_context.set_active_target_id("TID-browser-permission-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

browser permission

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

browser permission

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 28, @@ -6977,7 +7019,7 @@ async fn command_dispatch_completes_live_browser_permission_without_legacy_fallb #[tokio::test] async fn command_dispatch_completes_target_startup_commands_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let create_raw = serde_json::to_string(&json!({ "id": 29, "method": "Target.createTarget", @@ -7034,11 +7076,9 @@ async fn command_dispatch_completes_target_startup_commands_without_legacy_fallb #[test] fn command_dispatch_completes_network_sync_settings() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-network", - "TID-network", - )); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_page_target_fixture_for_test("BID-network", "TID-network")); for (id, method, params) in [ (30, "Network.enable", json!({})), @@ -7067,7 +7107,7 @@ fn command_dispatch_completes_network_sync_settings() { #[test] fn command_dispatch_completes_page_dialog_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let raw = serde_json::to_string(&json!({ "id": 40, "method": "Page.handleJavaScriptDialog", @@ -7087,7 +7127,8 @@ fn command_dispatch_completes_page_dialog_without_legacy_fallback() { #[test] fn command_dispatch_reports_no_document_for_default_page_screenshot() { let mut conn = crate::testing::real_layout_test_connection(); - conn.browser_context = Some(BrowserContext::new("BID-page-shot".to_owned())); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-page-shot".to_owned())); let raw = serde_json::to_string(&json!({ "id": 41, "method": "Page.captureScreenshot" @@ -7108,7 +7149,7 @@ fn command_dispatch_reports_no_document_for_default_page_screenshot() { #[test] fn command_dispatch_preserves_page_screenshot_unsupported_for_mock_layout() { - let mut conn = CdpConnection::new_with_initial_storage_partition_and_runtime_config( + let mut conn = crate::test_support::connection_with_config( crate::CdpInitialStoragePartition::memory(), moli_core::runtime::NavigationRuntimeConfig::new( moli_fetch::FetchConfig::default(), @@ -7117,7 +7158,8 @@ fn command_dispatch_preserves_page_screenshot_unsupported_for_mock_layout() { moli_core::LayoutPolicy::Mock, ), ); - conn.browser_context = Some(BrowserContext::new("BID-page-shot-mock".to_owned())); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-page-shot-mock".to_owned())); let raw = serde_json::to_string(&json!({ "id": 42, "method": "Page.captureScreenshot" @@ -7138,8 +7180,8 @@ fn command_dispatch_preserves_page_screenshot_unsupported_for_mock_layout() { #[test] fn command_dispatch_completes_additional_page_sync_commands_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-page-sync".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-page-sync".to_owned()); browser_context.set_active_target_id("TID-page-sync"); conn.install_browser_context_fixture_for_test(browser_context); @@ -7191,7 +7233,7 @@ fn command_dispatch_completes_additional_page_sync_commands_without_legacy_fallb #[test] fn command_dispatch_migrates_page_navigation_and_termination_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let navigate_raw = serde_json::to_string(&json!({ "id": 413, "method": "Page.navigate", @@ -7221,7 +7263,7 @@ fn command_dispatch_migrates_page_navigation_and_termination_without_legacy_fall #[test] fn command_dispatch_completes_page_create_isolated_world_errors_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let raw = serde_json::to_string(&json!({ "id": 415, "method": "Page.createIsolatedWorld", @@ -7246,7 +7288,7 @@ fn command_dispatch_completes_page_create_isolated_world_errors_without_legacy_f #[test] fn command_dispatch_completes_dom_sync_and_error_commands_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let enable_raw = serde_json::to_string(&json!({ "id": 421, @@ -7288,7 +7330,8 @@ fn command_dispatch_completes_dom_sync_and_error_commands_without_legacy_fallbac vec![json!({ "id": 423, "result": {} })] ); - conn.browser_context = Some(BrowserContext::new("BID-dom-sync".to_owned())); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-dom-sync".to_owned())); let unknown_raw = serde_json::to_string(&json!({ "id": 424, "method": "DOM.noSuchMethod" @@ -7315,16 +7358,8 @@ async fn command_dispatch_completes_live_page_preload_without_legacy_fallback() browser_context.attach_active_session("SID-page-preload-live"); ctx.conn .install_browser_context_fixture_for_test(browser_context); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,

preload

") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner("data:text/html,

preload

", None) + .await; let add_raw = serde_json::to_string(&json!({ "id": 42, @@ -7444,7 +7479,7 @@ async fn command_dispatch_completes_live_page_preload_without_legacy_fallback() #[test] fn command_dispatch_completes_target_sync_commands() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); for (id, method) in [ (50, "Target.createBrowserContext"), @@ -7474,8 +7509,9 @@ fn command_dispatch_completes_target_sync_commands() { #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_target_activate_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-target-activate".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-target-activate".to_owned()); browser_context.set_active_target_id("TID-target-activate".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -7499,8 +7535,9 @@ async fn command_dispatch_completes_target_activate_without_legacy_fallback() { #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_target_set_auto_attach_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-target-auto-attach".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-target-auto-attach".to_owned()); browser_context.set_active_target_id("TID-target-auto-attach".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -7532,8 +7569,9 @@ async fn command_dispatch_completes_target_set_auto_attach_without_legacy_fallba #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_page_bring_to_front_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-page-bring".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-page-bring".to_owned()); browser_context.set_active_target_id("TID-page-bring".to_owned()); browser_context.attach_active_session("SID-page-bring".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -7562,8 +7600,9 @@ async fn command_dispatch_completes_page_bring_to_front_without_legacy_fallback( #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_target_detach_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-target-detach".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-target-detach".to_owned()); browser_context.set_active_target_id("TID-target-detach".to_owned()); browser_context.attach_active_session("SID-target-detach".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -7605,8 +7644,9 @@ async fn command_dispatch_completes_target_detach_without_legacy_fallback() { #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_target_close_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-target-close".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-target-close".to_owned()); browser_context.set_active_target_id("TID-target-close".to_owned()); browser_context.attach_active_session("SID-target-close".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -7655,8 +7695,9 @@ async fn command_dispatch_completes_target_close_without_legacy_fallback() { #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_target_dispose_browser_context_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-target-dispose".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-target-dispose".to_owned()); browser_context.set_active_target_id("TID-target-dispose".to_owned()); browser_context.attach_active_session("SID-target-dispose".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -7696,10 +7737,53 @@ async fn command_dispatch_completes_target_dispose_browser_context_without_legac ); } +#[tokio::test(flavor = "multi_thread")] +async fn pending_target_dispose_rejects_a_replaced_same_wire_browser_context() { + let mut conn = crate::test_support::connection(); + let mut original = + conn.new_browser_context_fixture_for_test("BID-target-dispose-stale".to_owned()); + original.set_active_target_id("TID-target-dispose-original".to_owned()); + conn.install_browser_context_fixture_for_test(original); + + let raw = serde_json::to_string(&json!({ + "id": 601, + "method": "Target.disposeBrowserContext", + "params": { "browserContextId": "BID-target-dispose-stale" } + })) + .unwrap(); + let pending = match conn.start_command_dispatch(&raw) { + CdpCommandTaskStep::Pending(pending) => pending, + CdpCommandTaskStep::Complete(_) => { + panic!("Target.disposeBrowserContext should freeze its Context before completion") + } + }; + + let mut replacement = + conn.new_browser_context_fixture_for_test("BID-target-dispose-stale".to_owned()); + replacement.set_active_target_id("TID-target-dispose-replacement".to_owned()); + let replacement_id = replacement.browser_context_id(); + conn.install_browser_context_fixture_for_test(replacement); + + let messages = complete_command_task_for_test(&mut conn, *pending).await; + assert_eq!(messages.len(), 1); + assert_eq!(messages[0]["id"], json!(601)); + assert!(messages[0].get("error").is_some()); + let replacement = conn + .browser_context + .as_ref() + .expect("stale disposal must preserve the replacement Context"); + assert_eq!(replacement.browser_context_id(), replacement_id); + assert_eq!( + replacement.active_target_id(), + Some("TID-target-dispose-replacement") + ); +} + #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_target_send_message_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-target-send".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-target-send".to_owned()); browser_context.set_active_target_id("TID-target-send".to_owned()); browser_context.attach_active_session("SID-target-send".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -7747,8 +7831,8 @@ async fn command_dispatch_completes_target_send_message_without_legacy_fallback( #[test] fn command_dispatch_completes_cookie_read_commands() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-cookie".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-cookie".to_owned())); for (id, method) in [ (60, "Storage.getCookies"), @@ -7793,18 +7877,16 @@ fn command_dispatch_completes_cookie_read_commands() { #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_storage_set_cookies_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-storage-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-storage-live".to_owned()); browser_context.set_active_target_id("TID-storage-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

storage

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

storage

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 67, @@ -7839,18 +7921,16 @@ async fn command_dispatch_completes_live_storage_set_cookies_without_legacy_fall #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_network_extra_headers_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-network-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-network-live".to_owned()); browser_context.set_active_target_id("TID-network-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

network

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

network

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 68, @@ -7874,18 +7954,16 @@ async fn command_dispatch_completes_live_network_extra_headers_without_legacy_fa #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_network_blocked_urls_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-network-blocked-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-network-blocked-live".to_owned()); browser_context.set_active_target_id("TID-network-blocked-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

network blocked

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

network blocked

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 681, @@ -7909,18 +7987,16 @@ async fn command_dispatch_completes_live_network_blocked_urls_without_legacy_fal #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_network_set_cookie_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-network-cookie-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-network-cookie-live".to_owned()); browser_context.set_active_target_id("TID-network-cookie-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

network cookie

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

network cookie

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 69, @@ -7952,18 +8028,16 @@ async fn command_dispatch_completes_live_network_set_cookie_without_legacy_fallb #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_network_emulation_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-network-emulated-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-network-emulated-live".to_owned()); browser_context.set_active_target_id("TID-network-emulated-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

network emulated

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

network emulated

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 682, @@ -7992,18 +8066,16 @@ async fn command_dispatch_completes_live_network_emulation_without_legacy_fallba #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_network_user_agent_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-network-ua-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-network-ua-live".to_owned()); browser_context.set_active_target_id("TID-network-ua-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

network ua

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

network ua

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 683, @@ -8027,18 +8099,16 @@ async fn command_dispatch_completes_live_network_user_agent_without_legacy_fallb #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_emulation_user_agent_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-emulation-ua-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-emulation-ua-live".to_owned()); browser_context.set_active_target_id("TID-emulation-ua-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

emulation ua

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

emulation ua

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 684, @@ -8062,19 +8132,10 @@ async fn command_dispatch_completes_live_emulation_user_agent_without_legacy_fal #[tokio::test(flavor = "multi_thread")] async fn pending_emulation_user_agent_loader_keeps_active_owner_route_across_completion() { - let mut conn = CdpConnection::new(); - let active_page = conn - .load_page_via_runtime_async("data:text/html,active emulation ua") - .await - .expect("active page should load"); - let background_page = conn - .load_page_via_runtime_async("data:text/html,background emulation ua") - .await - .expect("background page should load"); - - let mut browser_context = BrowserContext::new("BID-emulation-ua-owner-route".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-emulation-ua-owner-route"); browser_context.set_active_target_id("TID-emulation-ua-active".to_owned()); - browser_context.replace_active_page_for_test(Some(active_page)); browser_context.stage_background_target( "TID-emulation-ua-background".to_owned(), None, @@ -8082,9 +8143,19 @@ async fn pending_emulation_user_agent_loader_keeps_active_owner_route_across_com None, None, ); - browser_context - .replace_target_page_for_test("TID-emulation-ua-background", Some(background_page)); conn.install_browser_context_fixture_for_test(browser_context); + install_navigation_fixture_for_target_for_test( + &mut conn, + "TID-emulation-ua-active", + "data:text/html,active emulation ua", + ) + .await; + install_navigation_fixture_for_target_for_test( + &mut conn, + "TID-emulation-ua-background", + "data:text/html,background emulation ua", + ) + .await; let raw = serde_json::to_string(&json!({ "id": 688, @@ -8108,9 +8179,8 @@ async fn pending_emulation_user_agent_loader_keeps_active_owner_route_across_com let browser_context = conn.browser_context.as_ref().expect("browser context"); assert_eq!( browser_context - .loaded_page() - .expect("active page should remain loaded") - .document_title(), + .loaded_document_title_for_test() + .expect("active page should remain loaded"), "active emulation ua", "Emulation user-agent loader completion must stay on the captured active owner" ); @@ -8133,19 +8203,10 @@ async fn pending_emulation_user_agent_loader_keeps_active_owner_route_across_com #[tokio::test(flavor = "multi_thread")] async fn pending_emulation_viewport_keeps_original_page_when_active_target_changes() { - let mut conn = CdpConnection::new(); - let original_page = conn - .load_page_via_runtime_async("data:text/html,original viewport owner") - .await - .expect("original page should load"); - let replacement_page = conn - .load_page_via_runtime_async("data:text/html,replacement active page") - .await - .expect("replacement page should load"); - - let mut browser_context = BrowserContext::new("BID-emulation-viewport-owner".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-emulation-viewport-owner"); browser_context.set_active_target_id("TID-emulation-viewport-original".to_owned()); - browser_context.replace_active_page_for_test(Some(original_page)); browser_context.stage_background_target( "TID-emulation-viewport-replacement".to_owned(), None, @@ -8153,9 +8214,19 @@ async fn pending_emulation_viewport_keeps_original_page_when_active_target_chang None, None, ); - browser_context - .replace_target_page_for_test("TID-emulation-viewport-replacement", Some(replacement_page)); conn.install_browser_context_fixture_for_test(browser_context); + install_navigation_fixture_for_target_for_test( + &mut conn, + "TID-emulation-viewport-original", + "data:text/html,original viewport owner", + ) + .await; + install_navigation_fixture_for_target_for_test( + &mut conn, + "TID-emulation-viewport-replacement", + "data:text/html,replacement active page", + ) + .await; let raw = serde_json::to_string(&json!({ "id": 690, @@ -8194,9 +8265,8 @@ async fn pending_emulation_viewport_keeps_original_page_when_active_target_chang ); assert_eq!( browser_context - .loaded_page() - .expect("replacement page should remain loaded") - .document_title(), + .loaded_document_title_for_test() + .expect("replacement page should remain loaded"), "replacement active page", "the original Page completion must not overwrite the newly active Page state" ); @@ -8211,19 +8281,10 @@ async fn pending_emulation_viewport_keeps_original_page_when_active_target_chang #[tokio::test(flavor = "multi_thread")] async fn pending_emulation_timezone_keeps_background_owner_route_across_completion() { - let mut conn = CdpConnection::new(); - let active_page = conn - .load_page_via_runtime_async("data:text/html,active timezone") - .await - .expect("active page should load"); - let background_page = conn - .load_page_via_runtime_async("data:text/html,background timezone") - .await - .expect("background page should load"); - - let mut browser_context = BrowserContext::new("BID-emulation-timezone-owner-route".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-emulation-timezone-owner-route"); browser_context.set_active_target_id("TID-emulation-timezone-active".to_owned()); - browser_context.replace_active_page_for_test(Some(active_page)); browser_context.stage_background_target( "TID-emulation-timezone-background".to_owned(), None, @@ -8231,9 +8292,19 @@ async fn pending_emulation_timezone_keeps_background_owner_route_across_completi None, None, ); - browser_context - .replace_target_page_for_test("TID-emulation-timezone-background", Some(background_page)); conn.install_browser_context_fixture_for_test(browser_context); + install_navigation_fixture_for_target_for_test( + &mut conn, + "TID-emulation-timezone-active", + "data:text/html,active timezone", + ) + .await; + install_navigation_fixture_for_target_for_test( + &mut conn, + "TID-emulation-timezone-background", + "data:text/html,background timezone", + ) + .await; let background_session = attach_page_session_for_test(&mut conn, "TID-emulation-timezone-background").await; @@ -8257,9 +8328,8 @@ async fn pending_emulation_timezone_keeps_background_owner_route_across_completi let browser_context = conn.browser_context.as_ref().expect("browser context"); assert_eq!( browser_context - .loaded_page() - .expect("active page should remain loaded") - .document_title(), + .loaded_document_title_for_test() + .expect("active page should remain loaded"), "active timezone", "ambient active owner must not consume the background timezone completion" ); @@ -8292,18 +8362,16 @@ async fn pending_emulation_timezone_keeps_background_owner_route_across_completi #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_emulation_locale_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-emulation-locale-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-emulation-locale-live".to_owned()); browser_context.set_active_target_id("TID-emulation-locale-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

emulation locale

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

emulation locale

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 685, @@ -8319,18 +8387,16 @@ async fn command_dispatch_completes_live_emulation_locale_without_legacy_fallbac #[tokio::test(flavor = "multi_thread")] async fn command_dispatch_completes_live_security_tls_without_legacy_fallback() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-security-tls-live".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-security-tls-live".to_owned()); browser_context.set_active_target_id("TID-security-tls-live".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

security tls

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

security tls

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 686, @@ -8355,19 +8421,9 @@ async fn command_dispatch_completes_live_security_tls_without_legacy_fallback() #[tokio::test(flavor = "multi_thread")] async fn pending_security_tls_keeps_background_owner_route_across_completion() { - let mut conn = CdpConnection::new(); - let active_page = conn - .load_page_via_runtime_async("data:text/html,active tls") - .await - .expect("active page should load"); - let background_page = conn - .load_page_via_runtime_async("data:text/html,background tls") - .await - .expect("background page should load"); - - let mut browser_context = BrowserContext::new("BID-security-owner-route".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-security-owner-route"); browser_context.set_active_target_id("TID-security-active".to_owned()); - browser_context.replace_active_page_for_test(Some(active_page)); browser_context.stage_background_target( "TID-security-background".to_owned(), None, @@ -8375,8 +8431,19 @@ async fn pending_security_tls_keeps_background_owner_route_across_completion() { None, None, ); - browser_context.replace_target_page_for_test("TID-security-background", Some(background_page)); conn.install_browser_context_fixture_for_test(browser_context); + install_navigation_fixture_for_target_for_test( + &mut conn, + "TID-security-active", + "data:text/html,active tls", + ) + .await; + install_navigation_fixture_for_target_for_test( + &mut conn, + "TID-security-background", + "data:text/html,background tls", + ) + .await; let background_session = attach_page_session_for_test(&mut conn, "TID-security-background").await; @@ -8410,9 +8477,8 @@ async fn pending_security_tls_keeps_background_owner_route_across_completion() { let browser_context = conn.browser_context.as_ref().expect("browser context"); assert_eq!( browser_context - .loaded_page() - .expect("active page should remain loaded") - .document_title(), + .loaded_document_title_for_test() + .expect("active page should remain loaded"), "active tls", "background Security completion must not finish on the ambient active owner" ); @@ -8444,16 +8510,8 @@ async fn command_dispatch_completes_live_fetch_enable_without_legacy_fallback() browser_context.set_active_target_id("TID-fetch-live".to_owned()); ctx.conn .install_browser_context_fixture_for_test(browser_context); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,

fetch

") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner("data:text/html,

fetch

", None) + .await; let raw = serde_json::to_string(&json!({ "id": 69, @@ -8477,8 +8535,9 @@ async fn command_dispatch_completes_live_fetch_enable_without_legacy_fallback() #[tokio::test] async fn devtools_network_intercept_commands_route_to_fetch_owner() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-bidi-intercept".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-bidi-intercept".to_owned()); browser_context.set_active_target_id("TID-bidi-intercept".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); let context = DevToolsCommandContext { @@ -8690,16 +8749,11 @@ async fn command_dispatch_completes_live_fetch_disable_without_legacy_fallback() .configure(None, true, Vec::new()); ctx.conn .install_browser_context_fixture_for_test(browser_context); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,

fetch disable

") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,

fetch disable

", + None, + ) + .await; let raw = serde_json::to_string(&json!({ "id": 6901, @@ -8729,7 +8783,7 @@ async fn command_dispatch_completes_live_fetch_disable_without_legacy_fallback() #[test] fn command_dispatch_completes_fetch_fulfill_request_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let raw = serde_json::to_string(&json!({ "id": 6902, @@ -8755,7 +8809,7 @@ fn command_dispatch_completes_fetch_fulfill_request_without_legacy_fallback() { #[test] fn command_dispatch_completes_fetch_fail_request_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let raw = serde_json::to_string(&json!({ "id": 6903, @@ -8781,7 +8835,7 @@ fn command_dispatch_completes_fetch_fail_request_without_legacy_fallback() { #[test] fn command_dispatch_completes_fetch_websocket_commands_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); for (id, method, params) in [ ( @@ -8825,7 +8879,7 @@ fn command_dispatch_completes_fetch_websocket_commands_without_legacy_fallback() #[test] fn command_dispatch_completes_fetch_body_commands_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); for (id, method, request_id) in [ (6906, "Fetch.getResponseBody", "INT-6906"), @@ -8853,7 +8907,7 @@ fn command_dispatch_completes_fetch_body_commands_without_legacy_fallback() { #[test] fn command_dispatch_completes_fetch_continue_commands_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); for (id, method, request_id, extra_params) in [ (6908, "Fetch.continueRequest", "INT-6908", json!({})), @@ -8894,7 +8948,7 @@ fn command_dispatch_completes_fetch_continue_commands_without_legacy_fallback() #[test] fn command_dispatch_completes_fetch_unknown_method_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let raw = serde_json::to_string(&json!({ "id": 6911, @@ -8917,7 +8971,7 @@ fn command_dispatch_completes_fetch_unknown_method_without_legacy_fallback() { #[test] fn command_dispatch_completes_shim_domains_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); for (id, method) in [ (70, "Audits.enable"), @@ -8944,7 +8998,7 @@ fn command_dispatch_completes_shim_domains_without_legacy_fallback() { #[test] fn command_dispatch_completes_shim_domain_unknown_methods_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); for (id, method) in [ (80, "Audits.noSuchMethod"), @@ -8967,7 +9021,7 @@ fn command_dispatch_completes_shim_domain_unknown_methods_without_legacy_fallbac #[test] fn command_dispatch_completes_additional_sync_domains_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); for (id, method, expects_result) in [ (90, "DOMSnapshot.enable", true), @@ -9020,11 +9074,16 @@ fn command_dispatch_completes_additional_sync_domains_without_legacy_fallback() #[test] fn command_dispatch_completes_input_owner_commands_without_legacy_fallback() { - let mut conn = crate::testing::real_layout_test_connection(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-input", - "TID-input", - )); + let mut conn = crate::test_support::connection_with_config( + crate::CdpInitialStoragePartition::memory(), + moli_core::runtime::NavigationRuntimeConfig::new( + moli_fetch::FetchConfig::default(), + moli_core::OptionalResourceFetchMask::NONE, + true, + moli_core::LayoutPolicy::Mock, + ), + ); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-input", "TID-input")); let raw = serde_json::to_string(&json!({ "id": 1001, @@ -9067,7 +9126,7 @@ fn command_dispatch_completes_input_owner_commands_without_legacy_fallback() { "id": 1002, "error": { "code": -32000, - "message": "NoDocumentLoaded" + "message": crate::domains::input::DISPATCH_DRAG_EVENT_UNSUPPORTED_MESSAGE } })] ); @@ -9075,7 +9134,7 @@ fn command_dispatch_completes_input_owner_commands_without_legacy_fallback() { #[test] fn command_dispatch_completes_additional_sync_domain_unknown_methods_without_legacy_fallback() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); for (id, method) in [ (101, "Browser.noSuchMethod"), @@ -9106,18 +9165,16 @@ fn command_dispatch_completes_additional_sync_domain_unknown_methods_without_leg } async fn loaded_agent_host_dispatch_connection_for_test() -> CdpConnection { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-agent-host-dispatch".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-agent-host-dispatch".to_owned()); browser_context.set_active_target_id("TID-agent-host-dispatch".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

agent host dispatch

") - .await - .expect("agent host dispatch page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

agent host dispatch

", + None, + ) + .await; conn } diff --git a/moli-protocol/src/conn/download_policy.rs b/moli-protocol/src/conn/download_policy.rs index be4c17c06d..41b844a505 100644 --- a/moli-protocol/src/conn/download_policy.rs +++ b/moli-protocol/src/conn/download_policy.rs @@ -1,5 +1,5 @@ use super::CdpConnection; -use moli_core::browser::{DownloadBehavior, DownloadPolicy}; +use moli_core::browser::{BrowserContextId, DownloadBehavior, DownloadPolicy}; use std::collections::HashMap; pub(crate) fn parse_download_behavior(value: &str) -> Option { @@ -114,16 +114,18 @@ impl CdpConnection { Ok(()) } + #[cfg(test)] pub(crate) fn download_policy_for_browser_context( &self, context_id: Option<&str>, - ) -> &DownloadPolicy { + ) -> DownloadPolicy { context_id .and_then(|id| self.browser_context_by_id(id)) .and_then(|context| context.download_policy()) - .unwrap_or(&self.download_policy) + .unwrap_or_else(|| self.download_policy.clone()) } + #[cfg(test)] pub(crate) fn automation_download_events_enabled_for_context( &self, context_id: Option<&str>, @@ -134,6 +136,21 @@ impl CdpConnection { .unwrap_or(self.download_subscriptions.automation_events_enabled) } + pub(crate) fn download_configuration_for_browser_context( + &self, + context: BrowserContextId, + ) -> Option<(DownloadPolicy, bool)> { + let context = self.browser_context_by_browser_id(context)?; + Some(( + context + .download_policy() + .unwrap_or_else(|| self.download_policy.clone()), + context + .automation_download_events_enabled + .unwrap_or(self.download_subscriptions.automation_events_enabled), + )) + } + pub(crate) fn set_browser_download_events_enabled_for_session( &mut self, session_id: Option<&str>, diff --git a/moli-protocol/src/conn/download_policy_tests.rs b/moli-protocol/src/conn/download_policy_tests.rs index 81cc33a7d6..9bec37fad4 100644 --- a/moli-protocol/src/conn/download_policy_tests.rs +++ b/moli-protocol/src/conn/download_policy_tests.rs @@ -12,10 +12,10 @@ fn command(conn: &mut CdpConnection, method: &str, params: Value) { #[test] fn download_policy_scopes_preserve_observer_shadowing_and_reset_fallback() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("first".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("first")); conn.inactive_browser_contexts - .push(BrowserContext::new("second".into())); + .push(conn.new_browser_context_fixture_for_test("second")); conn.configure_download_policy( None, DownloadPolicy { @@ -63,7 +63,7 @@ fn download_policy_scopes_preserve_observer_shadowing_and_reset_fallback() { conn.reset_download_policy(Some("first")).unwrap(); assert_eq!( conn.download_policy_for_browser_context(Some("first")), - &second + second ); assert!(conn.automation_download_events_enabled_for_context(Some("first"))); @@ -79,7 +79,7 @@ fn download_policy_scopes_preserve_observer_shadowing_and_reset_fallback() { conn.reset_download_policy(None).unwrap(); assert_eq!( conn.download_policy_for_browser_context(Some("first")), - &DownloadPolicy::default() + DownloadPolicy::default() ); assert!(!conn.automation_download_events_enabled_for_context(Some("first"))); assert_eq!( @@ -91,7 +91,7 @@ fn download_policy_scopes_preserve_observer_shadowing_and_reset_fallback() { #[test] fn download_configuration_is_lazy_and_missing_allow_path_remains_explicit() { - let mut conn = CdpConnection::new_with_deferred_navigation_runtime( + let mut conn = crate::test_support::connection_with_config( CdpInitialStoragePartition::memory(), NavigationRuntimeConfig::default(), ); @@ -101,7 +101,10 @@ fn download_configuration_is_lazy_and_missing_allow_path_remains_explicit() { json!({"behavior": "allow"}), ); assert!(conn.browser_context.is_none()); - assert!(!conn.standalone_navigation_engine.is_materialized()); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); assert_eq!( conn.download_policy_for_browser_context(None).behavior, DownloadBehavior::Allow @@ -111,7 +114,7 @@ fn download_configuration_is_lazy_and_missing_allow_path_remains_explicit() { .download_path .is_none() ); - conn.browser_context = Some(BrowserContext::new("later".into())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("later")); assert_eq!( conn.download_policy_for_browser_context(Some("later")) .behavior, @@ -121,8 +124,8 @@ fn download_configuration_is_lazy_and_missing_allow_path_remains_explicit() { #[test] fn download_policy_leaves_with_its_context_without_disposal_cleanup() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("same-context".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("same-context")); let policy = DownloadPolicy { behavior: DownloadBehavior::AllowAndName, download_path: Some("/owned".into()), @@ -133,13 +136,13 @@ fn download_policy_leaves_with_its_context_without_disposal_cleanup() { let removed = conn .browser_context - .replace(BrowserContext::new("same-context".into())) + .replace(conn.new_browser_context_fixture_for_test("same-context")) .unwrap(); - assert_eq!(removed.download_policy(), Some(&policy)); + assert_eq!(removed.download_policy(), Some(policy)); assert_eq!(removed.automation_download_events_enabled, Some(true)); assert_eq!( conn.download_policy_for_browser_context(Some("same-context")), - &DownloadPolicy::default() + DownloadPolicy::default() ); assert!(!conn.automation_download_events_enabled_for_context(Some("same-context"))); assert_eq!( @@ -182,7 +185,7 @@ async fn detaching_a_download_observer_preserves_browser_context_policy() { assert_eq!( ctx.conn .download_policy_for_browser_context(Some("CTX-download")), - &policy + policy ); assert!( !ctx.conn diff --git a/moli-protocol/src/conn/downloads.rs b/moli-protocol/src/conn/downloads.rs index e47d1d2140..7147a00a09 100644 --- a/moli-protocol/src/conn/downloads.rs +++ b/moli-protocol/src/conn/downloads.rs @@ -1,5 +1,6 @@ use moli_core::browser::{ - DownloadAccessError, DownloadBody, DownloadObservation, DownloadSnapshot, DownloadState, + DownloadAccessError, DownloadBody, DownloadObservation, DownloadPolicy, DownloadSnapshot, + DownloadState, WebContentsHandle, }; use moli_core::page::RendererPendingDownloadActivation; use moli_fetch::Request; @@ -14,27 +15,31 @@ use super::{ #[path = "downloads/lifecycle_tests.rs"] mod lifecycle_tests; -struct PendingDownloadOwnerContext { - browser_context_id: String, +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct PreparedDownloadActivation { + web_contents: WebContentsHandle, frame_id: String, request_headers: moli_fetch::RequestHeaders, initiator_url: Option, + policy: DownloadPolicy, + event_route: DownloadEventRoute, + activation: RendererPendingDownloadActivation, } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] struct DownloadEventRoute { browser_observers: Vec, automation_events_enabled: bool, page_observers: Vec, } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] struct BrowserDownloadObserver { session_id: Option, subscription_generation: u64, } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] struct PageDownloadObserver { session_id: Option, subscription_generation: u64, @@ -49,56 +54,89 @@ impl DownloadEventRoute { } impl CdpConnection { - pub(crate) async fn handle_pending_download_activation_background_events_async( - &mut self, - out: &mut Vec, + pub(crate) fn prepare_download_activation_for_owner( + &self, owner: &CommandOwnerScope, activation: RendererPendingDownloadActivation, - command_context: &mut CommandDispatchContext, - ) -> Result<(), String> { - self.handle_pending_download_activation_with_event_route_async( - out, - owner, - activation, - true, - command_context, - ) - .await + ) -> Option { + let web_contents = self.browser_web_contents_for_owner(owner).ok()?; + self.prepare_download_activation(owner, web_contents, activation) } - pub(crate) async fn handle_pending_download_activation_inline_async( - &mut self, - out: &mut Vec, + pub(crate) fn prepare_download_activation( + &self, owner: &CommandOwnerScope, + web_contents: WebContentsHandle, activation: RendererPendingDownloadActivation, - command_context: &mut CommandDispatchContext, - ) -> Result<(), String> { - self.handle_pending_download_activation_with_event_route_async( - out, - owner, + ) -> Option { + let context = self.browser_context_by_browser_id(web_contents.context())?; + let frame_id = context + .download_frame_id_for_web_contents(web_contents)? + .to_owned(); + let request_headers = context.effective_extra_headers_for_target( + &frame_id, + &self.browser_global_overrides.extra_headers, + ); + let initiator_url = context.target_document_url(&frame_id); + let (policy, automation_events_enabled) = + self.download_configuration_for_browser_context(web_contents.context())?; + let event_route = self.download_event_route(owner, automation_events_enabled); + Some(PreparedDownloadActivation { + web_contents, + frame_id, + request_headers, + initiator_url, + policy, + event_route, activation, - false, - command_context, - ) - .await + }) } - async fn handle_pending_download_activation_with_event_route_async( + pub(crate) async fn handle_prepared_download_activation_background_events_async( &mut self, out: &mut Vec, - owner: &CommandOwnerScope, - activation: RendererPendingDownloadActivation, + activation: PreparedDownloadActivation, + command_context: &mut CommandDispatchContext, + ) -> Result<(), String> { + self.handle_prepared_download_activation_async(out, activation, true, command_context) + .await + } + + pub(crate) async fn handle_prepared_download_activation_inline_async( + &mut self, + out: &mut Vec, + activation: PreparedDownloadActivation, + command_context: &mut CommandDispatchContext, + ) -> Result<(), String> { + self.handle_prepared_download_activation_async(out, activation, false, command_context) + .await + } + + async fn handle_prepared_download_activation_async( + &mut self, + out: &mut Vec, + prepared: PreparedDownloadActivation, allow_background_events: bool, command_context: &mut CommandDispatchContext, ) -> Result<(), String> { - let Some(context) = self.pending_download_owner_context(owner) else { + let PreparedDownloadActivation { + web_contents, + frame_id, + request_headers, + initiator_url, + policy, + event_route, + activation, + } = prepared; + let Some(context) = self.browser_context_by_browser_id(web_contents.context()) else { return Ok(()); }; - let event_route = self.download_event_route( - owner, - self.automation_download_events_enabled_for_context(Some(&context.browser_context_id)), - ); - let policy = self.download_policy_for_browser_context(Some(&context.browser_context_id)); + if context + .download_frame_id_for_web_contents(web_contents) + .is_none() + { + return Ok(()); + } let observation = if policy.behavior.is_canceled_without_download() { if !event_route.has_observers() { return Ok(()); @@ -117,15 +155,15 @@ impl CdpConnection { if policy.download_path.is_none() { return Ok(()); } - let default_policy = self.download_policy.clone(); if let Some(response) = activation.response { let url = Url::parse(&response.final_url) .or_else(|_| Url::parse(&activation.url)) .map_err(|error| format!("invalid download url: {error}"))?; - self.browser_context_by_id_mut(&context.browser_context_id) - .expect("download owner was resolved without yielding") + self.browser_context_by_browser_id_mut(web_contents.context()) + .expect("exact download Context was resolved without yielding") .start_download_response( - &default_policy, + web_contents, + &policy, url, response.headers, DownloadBody::Buffered(response.body), @@ -133,41 +171,36 @@ impl CdpConnection { } else { let mut request = Request::get(&activation.url) .map_err(|error| format!("invalid download url: {error}"))?; - request.request_headers = context.request_headers; + request.request_headers = request_headers; request = request .with_top_level_navigation_cookie_context() .with_page_network_policy() .with_request_origin( - context - .initiator_url + initiator_url .as_ref() .map_or(moli_url::WebOrigin::Opaque, moli_url::WebOrigin::from_url), ); - if let Some(initiator_url) = &context.initiator_url { + if let Some(initiator_url) = &initiator_url { request = request.with_initiator_url(initiator_url); } - #[cfg(test)] - self.ensure_page_navigation_engine_for_target( - &context.browser_context_id, - &context.frame_id, - ) - .ok_or("navigation WebContents engine unavailable")?; let fetch_defaults = self.document_fetch_defaults(); - self.browser_context_by_id_mut(&context.browser_context_id) - .expect("download owner was resolved without yielding") + let browser_globals = self.browser_global_overrides.clone(); + self.browser_context_by_browser_id_mut(web_contents.context()) + .expect("exact download Context was resolved without yielding") .start_download_request( - &context.frame_id, + web_contents, fetch_defaults, - &default_policy, + &policy, request, activation.suggested_filename, + &browser_globals, )? } }; if let Some(observation) = observation { self.observe_download( DownloadProjection { - frame_id: context.frame_id, + frame_id, event_route, observation, started: false, @@ -189,24 +222,27 @@ impl CdpConnection { body_artifact: CompletedDownloadBodyArtifact, command_context: &mut CommandDispatchContext, ) -> Result<(), String> { - // The frozen initiating frame, not a subsequently selected Context or a - // still-attached session, identifies the navigation's Browser residence. - let Some(context_id) = self - .browser_context_id_for_target(&state.frame_id) - .map(str::to_owned) + let web_contents = state.web_contents; + let Some(context) = self.browser_context_by_browser_id(web_contents.context()) else { + return Ok(()); + }; + if context + .download_frame_id_for_web_contents(web_contents) + .is_none() + { + return Ok(()); + } + let Some((policy, automation_events_enabled)) = + self.download_configuration_for_browser_context(web_contents.context()) else { return Ok(()); }; - let event_route = self.download_event_route( - &state.owner, - self.automation_download_events_enabled_for_context(Some(&context_id)), - ); - let default_policy = self.download_policy.clone(); + let event_route = self.download_event_route(&state.owner, automation_events_enabled); let (body, headers) = body_artifact.into_parts(); let observation = self - .browser_context_by_id_mut(&context_id) - .expect("download owner was resolved without yielding") - .start_download_response(&default_policy, final_url, headers, body)?; + .browser_context_by_browser_id_mut(web_contents.context()) + .expect("exact navigation download Context was resolved without yielding") + .start_download_response(web_contents, &policy, final_url, headers, body)?; if let Some(observation) = observation { self.observe_download( DownloadProjection { @@ -292,31 +328,6 @@ impl CdpConnection { self.open_global_io_stream(bytes) } - fn pending_download_owner_context( - &self, - owner: &CommandOwnerScope, - ) -> Option { - let (browser_context_id, target_id) = self.target_owner_identity_for_owner(owner)?; - let browser_context = self.browser_context_by_id(&browser_context_id)?; - let frame_id = target_id - .clone() - .or_else(|| browser_context.active_target_id_owned()) - .unwrap_or_else(|| "FRAME-0".to_owned()); - let request_headers = target_id - .as_deref() - .map(|target_id| browser_context.effective_extra_headers_for_target(target_id)) - .unwrap_or_else(|| browser_context.effective_extra_headers()); - let initiator_url = self - .resolved_page_owner_identity_for_owner(owner) - .and_then(|(_, target_id)| browser_context.target_document_url(&target_id).cloned()); - Some(PendingDownloadOwnerContext { - browser_context_id, - frame_id, - request_headers, - initiator_url, - }) - } - fn download_event_route( &self, owner: &CommandOwnerScope, @@ -588,9 +599,10 @@ fn download_progress_event( #[cfg(test)] mod tests { use moli_core::browser::DownloadBehavior; + use moli_core::page::RendererPendingDownloadActivation; use crate::{ - conn::{BackgroundProtocolEvent, BrowserContext, CdpConnection, CommandOwnerScope}, + conn::{BackgroundProtocolEvent, BrowserContext, CommandOwnerScope}, devtools_runtime::AutomationEvent, }; @@ -624,14 +636,15 @@ mod tests { #[test] fn background_download_uses_its_target_and_context_header_layers() { - let mut connection = CdpConnection::new(); + let mut connection = crate::test_support::connection(); let mut browser_context = BrowserContext::new("BID-download".to_owned()); browser_context.set_active_target_id("TID-active"); browser_context.set_default_extra_headers( vec![("X-Context-Default".to_owned(), "default".to_owned())].into(), ); - browser_context.global_extra_headers = - vec![("X-Context-Global".to_owned(), "global".to_owned())].into(); + connection.set_global_extra_headers( + vec![("X-Context-Global".to_owned(), "global".to_owned())].into(), + ); assert!(browser_context.register_page_target_url_fixture( "TID-background".to_owned(), @@ -645,7 +658,14 @@ mod tests { connection.install_browser_context_fixture_for_test(browser_context); let owner = connection - .pending_download_owner_context(&CommandOwnerScope::for_session("SID-background")) + .prepare_download_activation_for_owner( + &CommandOwnerScope::for_session("SID-background"), + RendererPendingDownloadActivation { + url: "https://background.test/report.txt".to_owned(), + suggested_filename: None, + response: None, + }, + ) .expect("background session must resolve its Page target"); assert_eq!(owner.frame_id, "TID-background"); assert_eq!( @@ -795,7 +815,7 @@ mod tests { #[test] fn stale_browser_observer_does_not_suppress_automation_download_event() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.set_browser_download_events_enabled_for_session(Some("SID-browser"), true); let generation = conn.download_subscriptions.browser_event_observers()[0].1; let route = DownloadEventRoute { diff --git a/moli-protocol/src/conn/downloads/lifecycle_tests.rs b/moli-protocol/src/conn/downloads/lifecycle_tests.rs index 66688deccb..5da20c8875 100644 --- a/moli-protocol/src/conn/downloads/lifecycle_tests.rs +++ b/moli-protocol/src/conn/downloads/lifecycle_tests.rs @@ -25,8 +25,8 @@ impl Drop for TestDirectory { fn fixture() -> (TestDirectory, CdpConnection) { let directory = TestDirectory::new(); - let mut conn = CdpConnection::new(); - let mut source = BrowserContext::new("CTX-source".into()); + let mut conn = crate::test_support::connection(); + let mut source = conn.new_browser_context_fixture_for_test("CTX-source"); source.set_active_target_id("TID-source"); source.attach_active_session("SID-source"); conn.install_browser_context_fixture_for_test(source); @@ -45,16 +45,22 @@ fn fixture() -> (TestDirectory, CdpConnection) { fn projection(conn: &mut CdpConnection, body: DownloadBody) -> DownloadProjection { let owner = CommandOwnerScope::for_session("SID-source"); - let context = conn.pending_download_owner_context(&owner).unwrap(); - let event_route = conn.download_event_route( - &owner, - conn.automation_download_events_enabled_for_context(Some(&context.browser_context_id)), - ); - let policy = conn.download_policy.clone(); + let web_contents = conn.browser_web_contents_for_owner(&owner).unwrap(); + let (policy, automation_events_enabled) = conn + .download_configuration_for_browser_context(web_contents.context()) + .unwrap(); + let frame_id = conn + .browser_context_by_browser_id(web_contents.context()) + .unwrap() + .download_frame_id_for_web_contents(web_contents) + .unwrap() + .to_owned(); + let event_route = conn.download_event_route(&owner, automation_events_enabled); let observation = conn - .browser_context_by_id_mut(&context.browser_context_id) + .browser_context_by_browser_id_mut(web_contents.context()) .unwrap() .start_download_response( + web_contents, &policy, Url::parse("https://source.test/report.txt").unwrap(), Vec::new(), @@ -63,7 +69,7 @@ fn projection(conn: &mut CdpConnection, body: DownloadBody) -> DownloadProjectio .unwrap() .unwrap(); DownloadProjection { - frame_id: context.frame_id, + frame_id, event_route, observation, started: false, @@ -271,15 +277,16 @@ async fn retiring_context_cancels_download_and_new_same_wire_context_cannot_read chunks.send(b"partial".to_vec()).unwrap(); wait_for(&mut monitor, |snapshot| snapshot.received_bytes == 7).await; assert_eq!(std::fs::read_dir(&directory.0).unwrap().count(), 1); - drop( - conn.remove_browser_context_by_id_restoring_active_async("CTX-source", None) - .await - .unwrap(), - ); + let removed = conn + .remove_browser_context_by_id_restoring_active_async("CTX-source", None) + .await + .unwrap(); + assert!(removed.remove_from_browser().unwrap()); + drop(removed); assert_eq!(terminal(&mut monitor).await.state, DownloadState::Canceled); assert!(cancel.is_cancelled()); assert_eq!(std::fs::read_dir(&directory.0).unwrap().count(), 0); - conn.insert_browser_context(BrowserContext::new("CTX-source".into())); + conn.insert_browser_context(conn.new_browser_context_fixture_for_test("CTX-source")); assert_eq!( conn.cancel_download(monitor.guid()), Err("No download item found for the given GUID".into()) @@ -289,17 +296,17 @@ async fn retiring_context_cancels_download_and_new_same_wire_context_cannot_read ); } -#[tokio::test] -async fn navigation_download_uses_its_frozen_frame_after_session_detach_and_selection_change() { +fn navigation_download_state(conn: &CdpConnection) -> NavigationDispatchState { use crate::conn::{ NavigationRequestLoadPolicy, NavigationResultProjection, NavigationSourceDocumentSecurityContext, }; - let (directory, mut conn) = fixture(); - let state = NavigationDispatchState { + let owner = CommandOwnerScope::for_session("SID-source"); + NavigationDispatchState { navigate_id: None, - owner: CommandOwnerScope::for_session("SID-source"), + web_contents: conn.browser_web_contents_for_owner(&owner).unwrap(), + owner, result_projection: NavigationResultProjection::Cdp(serde_json::json!({})), frame_id: "TID-source".into(), session_id: Some("SID-source".into()), @@ -319,7 +326,34 @@ async fn navigation_download_uses_its_frozen_frame_after_session_detach_and_sele "https://source.test".into(), "Secure".into(), ), - }; + } +} + +async fn replace_source_download_context(conn: &mut CdpConnection, directory: &TestDirectory) { + drop( + conn.remove_browser_context_by_id_restoring_active_async("CTX-source", None) + .await + .unwrap(), + ); + let mut replacement = conn.new_browser_context_fixture_for_test("CTX-source"); + replacement.set_active_target_id("TID-source"); + replacement.attach_active_session("SID-source"); + conn.install_browser_context_fixture_for_test(replacement); + conn.configure_download_policy( + Some("CTX-source"), + DownloadPolicy { + behavior: DownloadBehavior::AllowAndName, + download_path: Some(directory.0.to_str().unwrap().to_owned()), + }, + Some(true), + ) + .unwrap(); +} + +#[tokio::test] +async fn navigation_download_uses_its_frozen_frame_after_session_detach_and_selection_change() { + let (directory, mut conn) = fixture(); + let state = navigation_download_state(&conn); conn.detach_known_session_event_plan("TID-source", "SID-source", None, None); assert!(conn.target_owner_identity_for_owner(&state.owner).is_none()); let source = conn @@ -363,3 +397,61 @@ async fn navigation_download_uses_its_frozen_frame_after_session_detach_and_sele ); assert_eq!(conn.browser_context.as_ref().unwrap().id, "CTX-foreground"); } + +#[tokio::test] +async fn navigation_download_does_not_enter_a_replacement_with_the_same_wire_identity() { + let (directory, mut conn) = fixture(); + let state = navigation_download_state(&conn); + replace_source_download_context(&mut conn, &directory).await; + + let mut out = Vec::new(); + conn.handle_navigation_download_response_async( + &mut out, + &state, + state.requested_url.clone(), + CompletedDownloadBodyArtifact::from_body( + DownloadBody::Buffered(b"stale navigation".to_vec()), + Vec::new(), + ), + &mut CommandDispatchContext::default(), + ) + .await + .unwrap(); + + assert!(out.is_empty()); + assert_eq!(std::fs::read_dir(&directory.0).unwrap().count(), 0); +} + +#[tokio::test] +async fn prepared_renderer_download_does_not_enter_a_replacement_web_contents() { + let (directory, mut conn) = fixture(); + let owner = CommandOwnerScope::for_session("SID-source"); + let prepared = conn + .prepare_download_activation_for_owner( + &owner, + RendererPendingDownloadActivation { + url: "https://source.test/report.txt".to_owned(), + suggested_filename: Some("report.txt".to_owned()), + response: Some(moli_core::page::RendererPendingDownloadResponse { + final_url: "https://source.test/report.txt".to_owned(), + status: 200, + headers: Vec::new(), + body: b"stale renderer".to_vec(), + }), + }, + ) + .unwrap(); + replace_source_download_context(&mut conn, &directory).await; + + let mut out = Vec::new(); + conn.handle_prepared_download_activation_background_events_async( + &mut out, + prepared, + &mut CommandDispatchContext::default(), + ) + .await + .unwrap(); + + assert!(out.is_empty()); + assert_eq!(std::fs::read_dir(&directory.0).unwrap().count(), 0); +} diff --git a/moli-protocol/src/conn/fetch_support.rs b/moli-protocol/src/conn/fetch_support.rs index 830c5e9632..6f5670b597 100644 --- a/moli-protocol/src/conn/fetch_support.rs +++ b/moli-protocol/src/conn/fetch_support.rs @@ -1,24 +1,28 @@ -use anyhow::Context; use std::str::FromStr; use url::Url; -use super::body_spool::ensure_materialize_limit; use super::{ - CapturedBody, CapturedBodyWriter, CdpConnection, ClaimedNavigationRequest, CommandOwnerScope, - NavigationDispatchState, NavigationId, NavigationLoadOutcome, PausedResponsePreparedDocument, + CapturedBody, CdpConnection, ClaimedNavigationRequest, CommandOwnerScope, DocumentFetchCommand, + DocumentFetchCommandOutcome, NavigationDispatchState, NavigationId, NavigationLoadOutcome, + PausedResponsePreparedDocument, }; use crate::devtools_runtime::{DevToolsNetworkInterceptId, DevToolsNetworkResourceType}; use crate::domains::network::MainDocumentBodyProgressSource; use moli_cookie_jar::StoredCookieQueryReport; -use moli_core::browser::NavigationRequestLoadPolicy; +use moli_core::browser::{ + NavigationRequestLoadPolicy, + web_contents::{ + DocumentBodySource, PausedDocumentTransfer, + PausedResponsePreparedDocument as BrowserPausedResponsePreparedDocument, + }, +}; use moli_core::page::{ PendingSubresourceContinueOutcome, SubresourceAuthCredentials, SubresourceNetworkRequestHandle, SubresourceResourceType, }; use moli_core::runtime::DetachedParserScriptFetchContinuation; use moli_fetch::{ - NetworkFetchResult, NetworkObservationJournal, RawResponse, ResponseHead, StreamingRawResponse, - url_pattern_matches, + NetworkFetchResult, NetworkObservationJournal, StreamingRawResponse, url_pattern_matches, }; #[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumString, strum::IntoStaticStr)] @@ -579,9 +583,12 @@ pub struct PendingFetchResponseNavigation { pub navigation: NavigationDispatchState, pub(crate) permit: super::state::NavigationInterceptionPermit, active_body_stream_handle: Option, + body_progress_source: MainDocumentBodyProgressSource, + prepared_document: Option>, } impl PendingFetchResponseNavigation { + #[cfg(test)] pub(crate) fn new( navigation: NavigationDispatchState, permit: super::state::NavigationInterceptionPermit, @@ -590,6 +597,23 @@ impl PendingFetchResponseNavigation { navigation, permit, active_body_stream_handle: None, + body_progress_source: MainDocumentBodyProgressSource::default(), + prepared_document: None, + } + } + + pub(crate) fn new_with_response_projection( + navigation: NavigationDispatchState, + permit: super::state::NavigationInterceptionPermit, + body_progress_source: MainDocumentBodyProgressSource, + prepared_document: Option>, + ) -> Self { + Self { + navigation, + permit, + active_body_stream_handle: None, + body_progress_source, + prepared_document, } } @@ -642,56 +666,33 @@ impl ClaimedFetchResponseNavigation { } pub(crate) fn into_pending_streaming_document_response_navigation( - self, + mut self, ) -> Result> { - let Self { - request_id, - pending, - transfer, - } = self; - let Some(PausedDocumentTransfer { - request_load_policy, - state, - }) = transfer - else { - return Err(Box::new(Self { - request_id, - pending, - transfer: None, - })); + let Some(transfer) = self.transfer.take() else { + return Err(Box::new(self)); }; - match state { - PausedDocumentTransferState::Pending { - body: - DocumentBodySource::StreamingRaw { - response, - network_observation_journal, - body_progress_source, - prepared_document, - .. - }, - } => Ok(PendingStreamingDocumentResponseNavigation { - permit: pending.permit, - request_load_policy, - navigation: pending.navigation, - response, - network_observation_journal, - body_progress_source, - prepared_document, - }), - state => Err(Box::new(Self { - request_id, - pending, - transfer: Some(PausedDocumentTransfer { - request_load_policy, - state, - }), - })), - } + let streaming = match transfer.into_streaming_response() { + Ok(streaming) => streaming, + Err(transfer) => { + self.transfer = Some(*transfer); + return Err(Box::new(self)); + } + }; + let pending = self.pending; + Ok(PendingStreamingDocumentResponseNavigation { + permit: pending.permit, + request_load_policy: streaming.request_load_policy, + navigation: pending.navigation, + response: streaming.response, + network_observation_journal: streaming.network_observation_journal, + body_progress_source: pending.body_progress_source, + prepared_document: streaming.prepared_document, + prepared_document_projection: pending.prepared_document, + }) } pub(crate) async fn continue_response_async( - self, + mut self, conn: &mut CdpConnection, response_code: Option, response_headers: Vec<(String, String)>, @@ -703,41 +704,40 @@ impl ClaimedFetchResponseNavigation { ), Self, > { - let Self { - request_id, - pending, - transfer, - } = self; - let Some(transfer) = transfer else { + let Some(transfer) = self.transfer.take() else { return Ok(( - Some(pending.permit.navigation()), - pending.navigation, + Some(self.pending.permit.navigation()), + self.pending.navigation, Err(anyhow::anyhow!( "renderer channel navigation was superseded by a newer navigation" )), )); }; - match transfer - .continue_response_async( - conn, - pending.permit, - &pending.navigation, - response_code, - response_headers, - ) - .await - { - Ok(completed) => Ok(( - Some(pending.permit.navigation()), - pending.navigation, - completed, - )), - Err(transfer) => Err(Self { - request_id, - pending, - transfer: Some(transfer), - }), - } + let (request_load_policy, body) = match transfer.into_pending() { + Ok(parts) => parts, + Err(transfer) => { + self.transfer = Some(*transfer); + return Err(self); + } + }; + let pending = self.pending; + let navigation = continue_document_body_source_async( + conn, + pending.permit, + request_load_policy, + &pending.navigation, + body, + pending.body_progress_source, + pending.prepared_document, + response_code, + response_headers, + ) + .await; + Ok(( + Some(pending.permit.navigation()), + pending.navigation, + navigation, + )) } pub(crate) async fn fulfill_synthetic_async( @@ -763,16 +763,18 @@ impl ClaimedFetchResponseNavigation { )), ); }; - let navigation = transfer - .fulfill_synthetic_async( - conn, - pending.permit, - &pending.navigation, - response_code, - response_headers, - synthetic_body, - ) - .await; + let context = transfer.into_synthetic_response_context(); + let navigation = fulfill_synthetic_document_response_async( + conn, + pending.permit, + &pending.navigation, + context, + pending.body_progress_source, + response_code, + response_headers, + synthetic_body, + ) + .await; ( Some(pending.permit.navigation()), pending.navigation, @@ -780,6 +782,47 @@ impl ClaimedFetchResponseNavigation { ) } + pub(crate) async fn continue_response_neutrally_async( + self, + conn: &mut CdpConnection, + ) -> ( + Option, + NavigationDispatchState, + anyhow::Result, + ) { + let Self { + pending, transfer, .. + } = self; + let navigation_token = Some(pending.permit.navigation()); + let Some(transfer) = transfer else { + return ( + navigation_token, + pending.navigation, + Err(anyhow::anyhow!( + "renderer channel navigation was superseded by a newer navigation" + )), + ); + }; + let navigation = match transfer.finish_body_stream_async().await { + Ok((request_load_policy, body)) => { + continue_document_body_source_async( + conn, + pending.permit, + request_load_policy, + &pending.navigation, + body, + pending.body_progress_source, + pending.prepared_document, + None, + Vec::new(), + ) + .await + } + Err((_, message)) => Err(message), + }; + (navigation_token, pending.navigation, navigation) + } + pub(crate) fn fail( self, error_text: String, @@ -800,43 +843,6 @@ impl ClaimedFetchResponseNavigation { } } -#[derive(Debug)] -pub struct PausedDocumentTransfer { - request_load_policy: NavigationRequestLoadPolicy, - state: PausedDocumentTransferState, -} - -#[derive(Debug)] -enum PausedDocumentTransferState { - Pending { - body: DocumentBodySource, - }, - ActiveBodyStream { - stream: ActiveDocumentBodyStreamState, - }, -} - -#[derive(Debug)] -struct ActiveDocumentBodyStreamState { - requested_url: Url, - request_method: String, - request_headers: moli_fetch::RequestHeaders, - response: StreamingRawResponse, - network_observation_journal: NetworkObservationJournal, - body_progress_source: MainDocumentBodyProgressSource, - captured_body: CapturedBodyWriter, - unread_body: Vec, - offset: usize, - finished: bool, -} - -#[derive(Debug)] -pub struct PendingFetchResponseOpenedBodyStream { - pub handle: String, - pub buffered_bytes: Option>, - pub transfer: PausedDocumentTransfer, -} - #[derive(Debug)] pub(crate) enum PendingFetchResponseBodyStreamRead { NotFound, @@ -919,15 +925,6 @@ impl CompletedFetchResponseBodyStreamReadDispatch { } } -#[derive(Debug)] -pub(crate) enum OpenBodyStreamError { - NotOpenable(Box), - Failed { - transfer: Box, - error: anyhow::Error, - }, -} - pub(crate) struct PendingStreamingDocumentResponseNavigation { pub(crate) permit: super::state::NavigationInterceptionPermit, pub(crate) request_load_policy: NavigationRequestLoadPolicy, @@ -935,925 +932,188 @@ pub(crate) struct PendingStreamingDocumentResponseNavigation { pub(crate) response: StreamingRawResponse, pub(crate) network_observation_journal: NetworkObservationJournal, pub(crate) body_progress_source: MainDocumentBodyProgressSource, - pub(crate) prepared_document: Option>, + pub(crate) prepared_document: Option>, + pub(crate) prepared_document_projection: Option>, } -impl PausedDocumentTransfer { - pub(crate) fn pending( - request_load_policy: NavigationRequestLoadPolicy, - body: DocumentBodySource, - ) -> Self { - Self { - request_load_policy, - state: PausedDocumentTransferState::Pending { body }, - } - } - - #[cfg(test)] - pub(crate) fn prepared_renderer_agent_token( - &self, - ) -> Option { - match &self.state { - PausedDocumentTransferState::Pending { - body: - DocumentBodySource::StreamingRaw { - prepared_document: Some(prepared_document), - .. - }, - .. - } => Some(prepared_document.renderer_devtools_agent_token()), - _ => None, - } - } - - pub(crate) fn open_body_stream( - self, - handle: String, - ) -> Result { - let Self { - request_load_policy, - state, - } = self; - let PausedDocumentTransferState::Pending { body } = state else { - return Err(OpenBodyStreamError::NotOpenable(Box::new(Self { - request_load_policy, - state, - }))); - }; - match body { - DocumentBodySource::StreamingRaw { - requested_url, - request_method, - request_headers, - response, - network_observation_journal, - body_progress_source, - prepared_document: _, - } => Ok(PendingFetchResponseOpenedBodyStream { - handle, - buffered_bytes: None, - transfer: PausedDocumentTransfer { - request_load_policy, - state: PausedDocumentTransferState::ActiveBodyStream { - stream: ActiveDocumentBodyStreamState::new( - requested_url, - request_method, - request_headers, - response, - network_observation_journal, - body_progress_source, - ), - }, - }, - }), - DocumentBodySource::BufferedRaw { - requested_url, - request_method, - request_headers, - response, - network_observation_journal, - } => { - let bytes = clone_buffered_raw_body_for_paused_reuse(&response); - Ok(PendingFetchResponseOpenedBodyStream { - handle, - buffered_bytes: Some(bytes), - transfer: PausedDocumentTransfer::pending( - request_load_policy, - DocumentBodySource::BufferedRaw { - requested_url, - request_method, - request_headers, - response, - network_observation_journal, - }, - ), - }) - } - DocumentBodySource::CapturedRaw { - requested_url, - request_method, - request_headers, - head, - body, - network_observation_journal, - body_progress_source, - } => { - let bytes = match body.materialize_bytes() { - Ok(bytes) => bytes, - Err(error) => { - return Err(OpenBodyStreamError::Failed { - transfer: Box::new(PausedDocumentTransfer::pending( - request_load_policy, - DocumentBodySource::CapturedRaw { - requested_url, - request_method, - request_headers, - head, - body, - network_observation_journal, - body_progress_source, - }, - )), - error: error.context("failed to materialize captured response body"), - }); - } - }; - Ok(PendingFetchResponseOpenedBodyStream { - handle, - buffered_bytes: Some(bytes), - transfer: PausedDocumentTransfer::pending( - request_load_policy, - DocumentBodySource::CapturedRaw { - requested_url, - request_method, - request_headers, - head, - body, - network_observation_journal, - body_progress_source, - }, - ), - }) - } - } - } - - pub(crate) fn body_stream_offset(&self) -> Option { - match &self.state { - PausedDocumentTransferState::ActiveBodyStream { stream, .. } => Some(stream.offset()), - PausedDocumentTransferState::Pending { .. } => None, - } - } - - pub(crate) async fn read_body_stream_async( - self, - size: Option, - ) -> Result<(Vec, bool, Self), (Self, anyhow::Error)> { - let Self { - request_load_policy, - state, - } = self; - let PausedDocumentTransferState::ActiveBodyStream { mut stream } = state else { - return Err(( - Self { - request_load_policy, - state, - }, - anyhow::anyhow!("StreamHandleNotFound"), - )); - }; - match stream.read_async(size).await { - Ok((bytes, eof)) => { - let state = if eof { - let body = match stream.finish_pending_body_source() { - Ok(body) => body, - Err(message) => { - return Err(( - Self { - request_load_policy, - state: PausedDocumentTransferState::ActiveBodyStream { stream }, - }, - message, - )); - } - }; - PausedDocumentTransferState::Pending { body } - } else { - PausedDocumentTransferState::ActiveBodyStream { stream } - }; - Ok(( - bytes, - eof, - Self { - request_load_policy, - state, - }, - )) - } - Err(message) => Err(( - Self { - request_load_policy, - state: PausedDocumentTransferState::ActiveBodyStream { stream }, - }, - message, - )), - } - } - - pub(crate) async fn materialize_body_limited_async( - self, - limit: usize, - ) -> Result<(Option>, Self), (anyhow::Error, Self)> { - let Self { - request_load_policy, - state, - } = self; - let PausedDocumentTransferState::Pending { body } = state else { - return Ok(( - None, - Self { - request_load_policy, - state, - }, - )); - }; - match body.materialize_body_limited_async(limit).await { - Ok((bytes, body)) => Ok(( - Some(bytes), - PausedDocumentTransfer::pending(request_load_policy, body), - )), - Err((message, body)) => Err(( - message, - PausedDocumentTransfer::pending(request_load_policy, body), - )), - } - } - - pub(crate) async fn continue_response_async( - self, - conn: &mut CdpConnection, - permit: super::state::NavigationInterceptionPermit, - navigation: &NavigationDispatchState, - response_code: Option, - response_headers: Vec<(String, Vec)>, - ) -> Result, Self> { - let Self { - request_load_policy, - state, - } = self; - match state { - PausedDocumentTransferState::Pending { body } => Ok(body - .continue_navigation_async( - conn, - permit, - request_load_policy, - navigation, - response_code, - response_headers, - ) - .await), - PausedDocumentTransferState::ActiveBodyStream { stream } => Err(Self { - request_load_policy, - state: PausedDocumentTransferState::ActiveBodyStream { stream }, - }), - } - } - - /// Releases a response pause after its decision provider disappears. - /// - /// An active DevTools body stream has already consumed part of the wire - /// response, so it cannot use the ordinary continue path. Finish capturing - /// that stream and resume from the complete body instead of turning a - /// session lifecycle event into a Browser navigation failure. - pub(crate) async fn continue_response_neutrally_async( - self, - conn: &mut CdpConnection, - permit: super::state::NavigationInterceptionPermit, - navigation: &NavigationDispatchState, - ) -> anyhow::Result { - let Self { - request_load_policy, - state, - } = self; - let body = match state { - PausedDocumentTransferState::Pending { body } => body, - PausedDocumentTransferState::ActiveBodyStream { mut stream } => { - stream.read_async(None).await?; - stream.finish_pending_body_source()? - } - }; - body.continue_navigation_async( - conn, - permit, - request_load_policy, - navigation, - None, - Vec::new(), - ) - .await - } - - pub(crate) async fn fulfill_synthetic_async( - self, - conn: &mut CdpConnection, - permit: super::state::NavigationInterceptionPermit, - navigation: &NavigationDispatchState, - response_code: u16, - response_headers: Vec<(String, Vec)>, - synthetic_body: CapturedBody, - ) -> anyhow::Result { - let Self { - request_load_policy, - state, - } = self; - match state { - PausedDocumentTransferState::Pending { body } => { - body.fulfill_synthetic_navigation_async( - conn, - permit, - request_load_policy, - navigation, - response_code, - response_headers, - synthetic_body, - ) - .await - } - PausedDocumentTransferState::ActiveBodyStream { stream } => { - stream - .fulfill_synthetic_async( - conn, - permit, - request_load_policy, - navigation, - response_code, - response_headers, - synthetic_body, - ) - .await - } - } - } -} - -impl ActiveDocumentBodyStreamState { - fn new( - requested_url: Url, - request_method: String, - request_headers: moli_fetch::RequestHeaders, - response: StreamingRawResponse, - network_observation_journal: NetworkObservationJournal, - body_progress_source: MainDocumentBodyProgressSource, - ) -> Self { - Self { +#[allow(clippy::too_many_arguments)] +async fn continue_document_body_source_async( + conn: &mut CdpConnection, + permit: super::state::NavigationInterceptionPermit, + request_load_policy: NavigationRequestLoadPolicy, + navigation: &NavigationDispatchState, + body: DocumentBodySource, + body_progress_source: MainDocumentBodyProgressSource, + prepared_document_projection: Option>, + response_code: Option, + response_headers: Vec<(String, Vec)>, +) -> anyhow::Result { + let has_response_override = response_code.is_some() || !response_headers.is_empty(); + match body { + DocumentBodySource::BufferedRaw { requested_url, request_method, request_headers, response, network_observation_journal, - body_progress_source, - captured_body: CapturedBodyWriter::default(), - unread_body: Vec::new(), - offset: 0, - finished: false, - } - } - - fn offset(&self) -> usize { - self.offset - } - - async fn read_async(&mut self, size: Option) -> anyhow::Result<(Vec, bool)> { - read_active_body_stream_async( - &mut self.response, - &mut self.captured_body, - &mut self.unread_body, - &mut self.offset, - &mut self.finished, - size, - ) - .await - } - - fn finish_pending_body_source(&mut self) -> anyhow::Result { - let head = ResponseHead { - final_url: self.response.final_url.clone(), - status: self.response.status, - headers: self.response.headers.clone(), - request_cookie_report: self.response.request_cookie_report.clone(), - cookie_set_reports: self.response.cookie_set_reports.clone(), - redirected: self.response.redirected, - redirect_chain: self.response.redirect_chain.clone(), - from_cache: self.response.from_cache, - negotiated_http_version: self.response.negotiated_http_version, - }; - let body = self - .captured_body - .finish_in_place() - .context("failed to finish captured response body")?; - Ok(DocumentBodySource::CapturedRaw { - requested_url: self.requested_url.clone(), - request_method: self.request_method.clone(), - request_headers: self.request_headers.clone(), - head, - body, - network_observation_journal: self.network_observation_journal.clone(), - body_progress_source: self.body_progress_source.clone(), - }) - } - - async fn fulfill_synthetic_async( - self, - conn: &mut CdpConnection, - permit: super::state::NavigationInterceptionPermit, - request_load_policy: NavigationRequestLoadPolicy, - navigation: &NavigationDispatchState, - response_code: u16, - response_headers: Vec<(String, Vec)>, - synthetic_body: CapturedBody, - ) -> anyhow::Result { - let final_url = self.response.final_url.clone(); - let request_cookie_report = self.response.request_cookie_report.clone(); - let work = conn.start_intercepted_navigation_load_for_response( - permit, - request_load_policy, - self.requested_url, - self.request_method, - self.request_headers, - )?; - conn.build_navigation_from_buffered_body_source_for_intercepted_request_async( - navigation, - work, - final_url, - response_code, - response_headers, - synthetic_body, - request_cookie_report, - NetworkObservationJournal::default(), - self.body_progress_source, - ) - .await - } -} - -#[derive(Debug)] -pub enum DocumentBodySource { - BufferedRaw { - requested_url: Url, - request_method: String, - request_headers: moli_fetch::RequestHeaders, - response: RawResponse, - network_observation_journal: NetworkObservationJournal, - }, - StreamingRaw { - requested_url: Url, - request_method: String, - request_headers: moli_fetch::RequestHeaders, - response: StreamingRawResponse, - network_observation_journal: NetworkObservationJournal, - body_progress_source: MainDocumentBodyProgressSource, - prepared_document: Option>, - }, - CapturedRaw { - requested_url: Url, - request_method: String, - request_headers: moli_fetch::RequestHeaders, - head: ResponseHead, - body: CapturedBody, - network_observation_journal: NetworkObservationJournal, - body_progress_source: MainDocumentBodyProgressSource, - }, -} - -async fn read_active_body_stream_async( - response: &mut StreamingRawResponse, - captured_body: &mut CapturedBodyWriter, - unread_body: &mut Vec, - offset: &mut usize, - finished: &mut bool, - size: Option, -) -> anyhow::Result<(Vec, bool)> { - let mut bytes = Vec::new(); - match size { - Some(limit) => { - while bytes.len() < limit { - if unread_body.is_empty() && !*finished { - read_next_active_body_stream_chunk_async( - response, - captured_body, - unread_body, - finished, - ) - .await?; - } - if unread_body.is_empty() { - break; - } - let remaining = limit.saturating_sub(bytes.len()); - drain_unread_active_body_stream_bytes(unread_body, &mut bytes, remaining); - } - } - None => { - while !unread_body.is_empty() || !*finished { - if unread_body.is_empty() { - read_next_active_body_stream_chunk_async( - response, - captured_body, - unread_body, - finished, - ) - .await?; - } - let remaining = unread_body.len(); - drain_unread_active_body_stream_bytes(unread_body, &mut bytes, remaining); - } - } - } - *offset = offset.saturating_add(bytes.len()); - - let eof = *finished && unread_body.is_empty(); - Ok((bytes, eof)) -} - -fn drain_unread_active_body_stream_bytes( - unread_body: &mut Vec, - bytes: &mut Vec, - limit: usize, -) { - let take = limit.min(unread_body.len()); - bytes.extend(unread_body.drain(..take)); -} - -async fn read_next_active_body_stream_chunk_async( - response: &mut StreamingRawResponse, - captured_body: &mut CapturedBodyWriter, - unread_body: &mut Vec, - finished: &mut bool, -) -> anyhow::Result<()> { - if *finished { - return Ok(()); - } - if let Some(chunk) = response.next_chunk().await { - captured_body - .append(&chunk) - .context("failed to capture response body stream")?; - unread_body.extend(chunk); - return Ok(()); - } - response - .finish() - .await - .context("failed to read page body from stream")?; - *finished = true; - Ok(()) -} - -fn clone_buffered_raw_body_for_paused_reuse(response: &RawResponse) -> Vec { - // Fetch.getResponseBody and Fetch.takeResponseBodyAsStream can inspect a - // buffered response-stage body while the paused request remains resumable. - // Until paused state owns a shared/spooled body source, that requires an - // explicit clone rather than consuming the RawResponse. - response.clone_body_bytes() -} - -impl DocumentBodySource { - pub(crate) async fn continue_navigation_async( - self, - conn: &mut CdpConnection, - permit: super::state::NavigationInterceptionPermit, - request_load_policy: NavigationRequestLoadPolicy, - navigation: &NavigationDispatchState, - response_code: Option, - response_headers: Vec<(String, Vec)>, - ) -> anyhow::Result { - let has_response_override = response_code.is_some() || !response_headers.is_empty(); - match self { - Self::BufferedRaw { + } => { + let work = conn.start_intercepted_navigation_load_for_response( + permit, + request_load_policy, requested_url, request_method, request_headers, - response, - network_observation_journal, - } => { - let work = conn.start_intercepted_navigation_load_for_response( - permit, - request_load_policy, - requested_url, - request_method, - request_headers, - )?; - if !has_response_override { - conn.build_intercepted_navigation_response_async( - navigation, - work.with_response(NetworkFetchResult::with_observation_journal( - response, - network_observation_journal, - )), - ) - .await - } else { - let (head, body) = response.into_body(); - let status = head.status; - let headers = head.headers.clone(); - let final_url = head.final_url.clone(); - let request_cookie_report = head.request_cookie_report.clone(); - let body = body - .try_into_materialized_bytes() - .expect("RawResponse body should remain materialized at the response override boundary"); - let body = CapturedBody::from_bytes(body); - conn.build_navigation_from_buffered_body_source_for_intercepted_request_async( - navigation, - work, - final_url, - response_code.unwrap_or(status), - if response_headers.is_empty() { - headers - } else { - response_headers - }, - body, - request_cookie_report, - network_observation_journal, - MainDocumentBodyProgressSource::default(), - ) - .await - } - } - Self::StreamingRaw { - requested_url, - request_method, - request_headers, - response, - network_observation_journal, - body_progress_source, - prepared_document, - } => { - if !has_response_override && let Some(prepared_document) = prepared_document { - return Ok(prepared_document.resume_streaming(response, None)); - } - let work = conn.start_intercepted_navigation_load_for_response( - permit, - request_load_policy, - requested_url, - request_method, - request_headers, - )?; - conn.build_navigation_from_intercepted_streaming_response_with_override_async( + )?; + if !has_response_override { + conn.build_intercepted_navigation_response_async( navigation, work.with_response(NetworkFetchResult::with_observation_journal( response, network_observation_journal, )), - response_code, - response_headers, - body_progress_source, + ) + .await + } else { + let (head, body) = response.into_body(); + let status = head.status; + let headers = head.headers.clone(); + let final_url = head.final_url.clone(); + let request_cookie_report = head.request_cookie_report.clone(); + let body = body.try_into_materialized_bytes().expect( + "RawResponse body should remain materialized at the response override boundary", + ); + conn.build_navigation_from_buffered_body_source_for_intercepted_request_async( + navigation, + work, + final_url, + response_code.unwrap_or(status), + if response_headers.is_empty() { + headers + } else { + response_headers + }, + CapturedBody::from_bytes(body), + request_cookie_report, + network_observation_journal, + MainDocumentBodyProgressSource::default(), ) .await } - Self::CapturedRaw { - requested_url, - request_method, - request_headers, - head, - body, - network_observation_journal, - body_progress_source, - } => { - let work = conn.start_intercepted_navigation_load_for_response( - permit, - request_load_policy, - requested_url, - request_method, - request_headers, - )?; - if !has_response_override { - conn.build_navigation_from_captured_raw_response_for_intercepted_request_async( - navigation, - work, - head, - body, - network_observation_journal, - body_progress_source, - ) - .await - } else { - let status = head.status; - let headers = head.headers.clone(); - let final_url = head.final_url.clone(); - let request_cookie_report = head.request_cookie_report.clone(); - conn.build_navigation_from_buffered_body_source_for_intercepted_request_async( - navigation, - work, - final_url, - response_code.unwrap_or(status), - if response_headers.is_empty() { - headers - } else { - response_headers - }, - body, - request_cookie_report, - network_observation_journal, - body_progress_source, - ) - .await - } - } } - } - - #[allow(clippy::too_many_arguments)] - async fn fulfill_synthetic_navigation_async( - self, - conn: &mut CdpConnection, - permit: super::state::NavigationInterceptionPermit, - request_load_policy: NavigationRequestLoadPolicy, - navigation: &NavigationDispatchState, - response_code: u16, - response_headers: Vec<(String, String)>, - synthetic_body: CapturedBody, - ) -> anyhow::Result { - let ( + DocumentBodySource::StreamingRaw { requested_url, request_method, request_headers, - final_url, - request_cookie_report, - body_progress_source, - ) = match self { - Self::BufferedRaw { + response, + network_observation_journal, + prepared_document, + } => { + if !has_response_override + && let (Some(prepared_document), Some(projection)) = + (prepared_document, prepared_document_projection) + { + return Ok(projection.resume_streaming(*prepared_document, response, None)); + } + let work = conn.start_intercepted_navigation_load_for_response( + permit, + request_load_policy, requested_url, request_method, request_headers, - response, - .. - } => ( - requested_url, - request_method, - request_headers, - response.final_url.clone(), - response.request_cookie_report.clone(), - MainDocumentBodyProgressSource::default(), - ), - Self::StreamingRaw { - requested_url, - request_method, - request_headers, - response, + )?; + conn.build_navigation_from_intercepted_streaming_response_with_override_async( + navigation, + work.with_response(NetworkFetchResult::with_observation_journal( + response, + network_observation_journal, + )), + response_code, + response_headers, body_progress_source, - .. - } => ( - requested_url, - request_method, - request_headers, - response.final_url.clone(), - response.request_cookie_report.clone(), - body_progress_source, - ), - Self::CapturedRaw { - requested_url, - request_method, - request_headers, - head, - body_progress_source, - .. - } => ( - requested_url, - request_method, - request_headers, - head.final_url.clone(), - head.request_cookie_report.clone(), - body_progress_source, - ), - }; - let work = conn.start_intercepted_navigation_load_for_response( - permit, - request_load_policy, + ) + .await + } + DocumentBodySource::CapturedRaw { requested_url, request_method, request_headers, - )?; - conn.build_navigation_from_buffered_body_source_for_intercepted_request_async( - navigation, - work, - final_url, - response_code, - response_headers, - synthetic_body, - request_cookie_report, - NetworkObservationJournal::default(), - body_progress_source, - ) - .await - } - - pub(crate) async fn materialize_body_limited_async( - self, - limit: usize, - ) -> Result<(Vec, Self), (anyhow::Error, Self)> { - match self { - Self::BufferedRaw { + head, + body, + network_observation_journal, + } => { + let work = conn.start_intercepted_navigation_load_for_response( + permit, + request_load_policy, requested_url, request_method, request_headers, - response, - network_observation_journal, - } => { - if let Err(error) = ensure_materialize_limit(response.body_bytes().len(), limit) { - return Err(( - error, - Self::BufferedRaw { - requested_url, - request_method, - request_headers, - response, - network_observation_journal, - }, - )); - } - let bytes = clone_buffered_raw_body_for_paused_reuse(&response); - Ok(( - bytes, - Self::BufferedRaw { - requested_url, - request_method, - request_headers, - response, - network_observation_journal, + )?; + if !has_response_override { + conn.build_navigation_from_captured_raw_response_for_intercepted_request_async( + navigation, + work, + head, + body, + network_observation_journal, + body_progress_source, + ) + .await + } else { + let status = head.status; + let headers = head.headers.clone(); + let final_url = head.final_url.clone(); + let request_cookie_report = head.request_cookie_report.clone(); + conn.build_navigation_from_buffered_body_source_for_intercepted_request_async( + navigation, + work, + final_url, + response_code.unwrap_or(status), + if response_headers.is_empty() { + headers + } else { + response_headers }, - )) - } - Self::StreamingRaw { - requested_url, - request_method, - request_headers, - response, - network_observation_journal, - body_progress_source, - prepared_document: _, - } => { - let preserved_head = response.head(); - let (head, body) = match capture_streaming_raw_response(response).await { - Ok(captured) => captured, - Err(message) => { - return Err(( - message, - Self::CapturedRaw { - requested_url, - request_method, - request_headers, - head: preserved_head, - body: CapturedBody::from_bytes(Vec::new()), - network_observation_journal, - body_progress_source, - }, - )); - } - }; - let result = body - .materialize_bytes_limited(limit) - .context("failed to materialize captured response body"); - let source = Self::CapturedRaw { - requested_url, - request_method, - request_headers, - head, body, + request_cookie_report, network_observation_journal, body_progress_source, - }; - match result { - Ok(bytes) => Ok((bytes, source)), - Err(message) => Err((message, source)), - } - } - Self::CapturedRaw { - requested_url, - request_method, - request_headers, - head, - body, - network_observation_journal, - body_progress_source, - } => { - let result = body - .materialize_bytes_limited(limit) - .context("failed to materialize captured response body"); - let source = Self::CapturedRaw { - requested_url, - request_method, - request_headers, - head, - body, - network_observation_journal, - body_progress_source, - }; - match result { - Ok(bytes) => Ok((bytes, source)), - Err(message) => Err((message, source)), - } + ) + .await } } } } -async fn capture_streaming_raw_response( - mut response: StreamingRawResponse, -) -> anyhow::Result<(ResponseHead, CapturedBody)> { - let head = response.head(); - let mut body = CapturedBodyWriter::default(); - while let Some(chunk) = response.next_chunk().await { - body.append(&chunk) - .context("failed to capture response body stream")?; - } - response - .finish() - .await - .context("failed to read page body from stream")?; - let body = body - .finish() - .context("failed to finish captured response body")?; - Ok((head, body)) +#[allow(clippy::too_many_arguments)] +async fn fulfill_synthetic_document_response_async( + conn: &mut CdpConnection, + permit: super::state::NavigationInterceptionPermit, + navigation: &NavigationDispatchState, + context: moli_core::browser::web_contents::SyntheticDocumentResponseContext, + body_progress_source: MainDocumentBodyProgressSource, + response_code: u16, + response_headers: Vec<(String, String)>, + synthetic_body: CapturedBody, +) -> anyhow::Result { + let work = conn.start_intercepted_navigation_load_for_response( + permit, + context.request_load_policy, + context.requested_url, + context.request_method, + context.request_headers, + )?; + conn.build_navigation_from_buffered_body_source_for_intercepted_request_async( + navigation, + work, + context.final_url, + response_code, + response_headers, + synthetic_body, + context.request_cookie_report, + NetworkObservationJournal::default(), + body_progress_source, + ) + .await } /// Residence that owns a request-stage subresource Fetch pause. @@ -2132,13 +1392,32 @@ pub struct FetchAuthChallenge { } impl CdpConnection { + async fn execute_document_fetch_command_for_owner( + &mut self, + owner: &CommandOwnerScope, + command: DocumentFetchCommand, + ) -> Result< + ( + DocumentFetchCommandOutcome, + Option, + ), + String, + > { + let document = self.loaded_browser_document_for_owner(owner)?; + let pending = self.start_document_fetch_command(document, command)?; + let completed = pending.wait().await; + let predecessor = completed.renderer_output_predecessor(); + let outcome = self.finish_document_fetch_command(completed)?; + Ok((outcome, predecessor)) + } + pub async fn continue_pending_subresource_fetch_async( &mut self, internal_id: u64, url: Option, method: Option, body: Option>, - headers: Option, + headers: Option, intercept_response: bool, handle_auth_requests: bool, ) -> Result { @@ -2162,7 +1441,7 @@ impl CdpConnection { url: Option, method: Option, body: Option>, - headers: Option, + headers: Option, intercept_response: bool, handle_auth_requests: bool, ) -> Result { @@ -2187,17 +1466,13 @@ impl CdpConnection { url: Option, method: Option, body: Option>, - headers: Option, + headers: Option, intercept_response: bool, handle_auth_requests: bool, ) -> Result { - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .continue_pending_subresource_fetch_for_target_async( - &target_id, + self.execute_document_fetch_command_for_owner( + owner, + DocumentFetchCommand::ContinueRequest { internal_id, url, method, @@ -2205,8 +1480,11 @@ impl CdpConnection { headers, intercept_response, handle_auth_requests, - ) - .await + }, + ) + .await? + .0 + .into_continue_outcome() } pub async fn continue_pending_subresource_auth_async( @@ -2235,13 +1513,13 @@ impl CdpConnection { internal_id: u64, auth: SubresourceAuthCredentials, ) -> Result { - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .continue_pending_subresource_auth_for_target_async(&target_id, internal_id, auth) - .await + self.execute_document_fetch_command_for_owner( + owner, + DocumentFetchCommand::ContinueAuth { internal_id, auth }, + ) + .await? + .0 + .into_continue_outcome() } pub async fn fail_pending_subresource_auth_async( @@ -2270,13 +1548,15 @@ impl CdpConnection { internal_id: u64, error_text: String, ) -> Result, String> { - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .fail_pending_subresource_auth_for_target_async(&target_id, internal_id, error_text) - .await + self.execute_document_fetch_command_for_owner( + owner, + DocumentFetchCommand::FailAuth { + internal_id, + error_text, + }, + ) + .await + .map(|(_, predecessor)| predecessor) } pub async fn fail_pending_subresource_fetch_async( @@ -2305,13 +1585,15 @@ impl CdpConnection { internal_id: u64, error_text: String, ) -> Result, String> { - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .fail_pending_subresource_fetch_for_target_async(&target_id, internal_id, error_text) - .await + self.execute_document_fetch_command_for_owner( + owner, + DocumentFetchCommand::FailRequest { + internal_id, + error_text, + }, + ) + .await + .map(|(_, predecessor)| predecessor) } pub async fn fulfill_pending_subresource_fetch_async( @@ -2340,19 +1622,17 @@ impl CdpConnection { response_body: moli_core::page::RendererSyntheticResponseBody, ) -> Result<(), String> { let owner = CommandOwnerScope::capture(self, session_id); - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(&owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .fulfill_pending_subresource_fetch_for_target_async( - &target_id, + self.execute_document_fetch_command_for_owner( + &owner, + DocumentFetchCommand::FulfillRequest { internal_id, response_code, response_headers, response_body, - ) - .await + }, + ) + .await + .map(|_| ()) } pub async fn continue_pending_subresource_response_async( @@ -2394,18 +1674,16 @@ impl CdpConnection { response_code: Option, response_headers: Option)>>, ) -> Result<(), String> { - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .continue_pending_subresource_response_for_target_async( - &target_id, + self.execute_document_fetch_command_for_owner( + owner, + DocumentFetchCommand::ContinueResponse { internal_id, response_code, response_headers, - ) - .await + }, + ) + .await + .map(|_| ()) } pub async fn fail_pending_subresource_response_async( @@ -2438,13 +1716,15 @@ impl CdpConnection { internal_id: u64, error_text: String, ) -> Result, String> { - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .fail_pending_subresource_response_for_target_async(&target_id, internal_id, error_text) - .await + self.execute_document_fetch_command_for_owner( + owner, + DocumentFetchCommand::FailResponse { + internal_id, + error_text, + }, + ) + .await + .map(|(_, predecessor)| predecessor) } pub async fn fulfill_pending_subresource_response_async( @@ -2473,19 +1753,17 @@ impl CdpConnection { response_body: moli_core::page::RendererSyntheticResponseBody, ) -> Result<(), String> { let owner = CommandOwnerScope::capture(self, session_id); - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(&owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .fulfill_pending_subresource_response_for_target_async( - &target_id, + self.execute_document_fetch_command_for_owner( + &owner, + DocumentFetchCommand::FulfillResponse { internal_id, response_code, response_headers, response_body, - ) - .await + }, + ) + .await + .map(|_| ()) } pub async fn receive_synthetic_websocket_text_async( @@ -2504,13 +1782,12 @@ impl CdpConnection { data: String, ) -> Result<(), String> { let owner = CommandOwnerScope::capture(self, session_id); - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(&owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .receive_synthetic_websocket_text_for_target_async(&target_id, socket_id, data) - .await + self.execute_document_fetch_command_for_owner( + &owner, + DocumentFetchCommand::DispatchWebSocketText { socket_id, data }, + ) + .await + .map(|_| ()) } pub async fn receive_synthetic_websocket_binary_async( @@ -2529,13 +1806,12 @@ impl CdpConnection { data: Vec, ) -> Result<(), String> { let owner = CommandOwnerScope::capture(self, session_id); - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(&owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .receive_synthetic_websocket_binary_for_target_async(&target_id, socket_id, data) - .await + self.execute_document_fetch_command_for_owner( + &owner, + DocumentFetchCommand::DispatchWebSocketBinary { socket_id, data }, + ) + .await + .map(|_| ()) } pub async fn close_synthetic_websocket_from_server_async( @@ -2558,14 +1834,15 @@ impl CdpConnection { reason: String, ) -> Result<(), String> { let owner = CommandOwnerScope::capture(self, session_id); - let (context_id, target_id) = self - .resolved_page_owner_identity_for_owner(&owner) - .ok_or("NoDocumentLoaded")?; - self.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .close_synthetic_websocket_from_server_for_target_async( - &target_id, socket_id, code, reason, - ) - .await + self.execute_document_fetch_command_for_owner( + &owner, + DocumentFetchCommand::CloseWebSocket { + socket_id, + code, + reason, + }, + ) + .await + .map(|_| ()) } } diff --git a/moli-protocol/src/conn/inspection_binding_tests.rs b/moli-protocol/src/conn/inspection_binding_tests.rs index e8acd457eb..6f3f51c788 100644 --- a/moli-protocol/src/conn/inspection_binding_tests.rs +++ b/moli-protocol/src/conn/inspection_binding_tests.rs @@ -1,6 +1,6 @@ use serde_json::{Value, json}; -use super::{BrowserContext, CdpCommandTaskStep, CommandOwnerScope}; +use super::{CdpCommandTaskStep, CommandOwnerScope}; use crate::testing::TestContext; mod document_agents; @@ -9,15 +9,62 @@ mod lifecycle; mod native_commands; mod runtime_agents; -pub(super) fn take_inspection_document( - conn: &mut super::CdpConnection, +pub(super) struct InspectionDocumentHandle { + context: moli_core::browser::BrowserContextHandle, + document: moli_core::browser::DocumentHandle, +} + +impl InspectionDocumentHandle { + pub(super) async fn runtime_heap_usage_for_test( + &self, + ) -> Result { + self.context + .document_runtime_heap_usage_for_test(self.document) + .await + } + + pub(super) async fn evaluate_runtime_expression_for_test( + &self, + expression: &str, + await_promise: bool, + ) -> Result { + self.context + .evaluate_document_expression_for_test(self.document, expression, await_promise) + .await + } + + pub(super) fn document_title_for_test(&self) -> String { + self.context + .document_title(self.document) + .expect("inspection document must remain current") + } + + pub(super) fn start_blob_bytes_for_uuid_for_test( + &self, + uuid: String, + ) -> Result { + self.context.start_document_blob_read(self.document, uuid) + } + + pub(super) fn finish_blob_bytes_for_uuid_for_test( + &self, + completed: moli_core::browser::CompletedDocumentBlobRead, + ) -> Result>, String> { + self.context.finish_document_blob_read(completed) + } +} + +pub(super) fn inspection_document_handle( + conn: &super::CdpConnection, owner: &CommandOwnerScope, -) -> super::state::DocumentHost { +) -> InspectionDocumentHandle { let (context_id, target_id) = conn.resolved_page_owner_identity_for_owner(owner).unwrap(); - conn.browser_context_by_id_mut(&context_id) - .unwrap() - .take_document_host_for_inspection_test(&target_id) + let (context, document) = conn + .browser_context_by_id(&context_id) .unwrap() + .inspection_document_handle_for_test(&target_id) + .unwrap(); + InspectionDocumentHandle { context, document } } #[tokio::test(flavor = "multi_thread")] @@ -68,7 +115,7 @@ async fn dom_inspection_rejects_frozen_replies_and_follow_ups_after_rebind() { }; let completed = pending.wait().await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let old_document = take_inspection_document(&mut ctx.conn, &owner); + let old_document = inspection_document_handle(&ctx.conn, &owner); ctx.install_navigation_fixture_for_session_owner( "data:text/html,
new
", None, @@ -118,7 +165,9 @@ async fn dom_inspection_rejects_frozen_replies_and_follow_ups_after_rebind() { async fn dom_context() -> TestContext { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-dom-inspection".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-dom-inspection"); context.set_active_target_id("TID-dom-inspection"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -131,8 +180,8 @@ async fn dom_context() -> TestContext { async fn dom_inspection_round_trip(start_before_move: bool) { let mut ctx = dom_context().await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let take_document = |ctx: &mut TestContext| take_inspection_document(&mut ctx.conn, &owner); - let mut document = (!start_before_move).then(|| take_document(&mut ctx)); + let document_handle = |ctx: &TestContext| inspection_document_handle(&ctx.conn, &owner); + let mut document = (!start_before_move).then(|| document_handle(&ctx)); let raw = json!({"id": 1, "method": "DOM.getDocument", "params": {"depth": -1}}).to_string(); let step = ctx.conn.start_command_dispatch(&raw); assert!( @@ -140,9 +189,9 @@ async fn dom_inspection_round_trip(start_before_move: bool) { "DOM inspection must start on the live binding without a Protocol Document" ); if start_before_move { - document = Some(take_document(&mut ctx)); + document = Some(document_handle(&ctx)); } - let mut document = document.unwrap(); + let document = document.unwrap(); let (messages, _) = ctx.complete_command_task_step_for_test(step).await; let response = messages .iter() @@ -227,8 +276,7 @@ async fn dom_inspection_round_trip(start_before_move: bool) { ) .await; let actual = document - .page - .evaluate_runtime_expression_without_navigation_follow_with_await_async( + .evaluate_runtime_expression_for_test( "document.getElementById('inspected').getAttribute('data-phase')", false, ) @@ -239,7 +287,7 @@ async fn dom_inspection_round_trip(start_before_move: bool) { json!("after"), "the DOM mutation must reach the actual renderer" ); - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); } async fn dom_command(ctx: &mut TestContext, id: u64, method: &str, params: Value) -> Value { diff --git a/moli-protocol/src/conn/inspection_binding_tests/document_agents.rs b/moli-protocol/src/conn/inspection_binding_tests/document_agents.rs index 36f2ac8035..aab2d047e6 100644 --- a/moli-protocol/src/conn/inspection_binding_tests/document_agents.rs +++ b/moli-protocol/src/conn/inspection_binding_tests/document_agents.rs @@ -32,7 +32,9 @@ async fn dom_snapshot_completes_without_protocol_document() { async fn document_agent_context() -> TestContext { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-document-agents".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-document-agents"); context.set_active_target_id("TID-document-agents"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -45,8 +47,8 @@ async fn document_agent_context() -> TestContext { async fn document_agent_round_trip(method: &str, start_before_move: bool) { let mut ctx = document_agent_context().await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let take_document = |ctx: &mut TestContext| take_inspection_document(&mut ctx.conn, &owner); - let mut document = (!start_before_move).then(|| take_document(&mut ctx)); + let document_handle = |ctx: &mut TestContext| inspection_document_handle(&ctx.conn, &owner); + let mut document = (!start_before_move).then(|| document_handle(&mut ctx)); let step = ctx.conn.start_command_dispatch( &json!({"id": 1, "method": method, "params": {"computedStyles": ["color"]}}).to_string(), ); @@ -55,9 +57,9 @@ async fn document_agent_round_trip(method: &str, start_before_move: bool) { "{method} must start on its live binding" ); if start_before_move { - document = Some(take_document(&mut ctx)); + document = Some(document_handle(&mut ctx)); } - let mut document = document.unwrap(); + let document = document.unwrap(); let (messages, _) = ctx.complete_command_task_step_for_test(step).await; let response = messages .iter() @@ -118,8 +120,7 @@ async fn document_agent_round_trip(method: &str, start_before_move: bool) { json!("button { color: rgb(4, 5, 6); }") ); let actual = document - .page - .evaluate_runtime_expression_without_navigation_follow_with_await_async( + .evaluate_runtime_expression_for_test( "getComputedStyle(document.getElementById('inspected')).color", false, ) @@ -188,7 +189,7 @@ async fn document_agent_round_trip(method: &str, start_before_move: bool) { } _ => unreachable!(), } - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); } #[tokio::test(flavor = "multi_thread")] @@ -216,7 +217,7 @@ async fn document_agents_reject_old_completion_and_follow_up_after_rebind() { }; let completed = pending.wait().await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let old_document = take_inspection_document(&mut ctx.conn, &owner); + let old_document = inspection_document_handle(&ctx.conn, &owner); ctx.install_navigation_fixture_for_session_owner( "data:text/html,", None, diff --git a/moli-protocol/src/conn/inspection_binding_tests/io.rs b/moli-protocol/src/conn/inspection_binding_tests/io.rs index a6e193abcd..69fb06b0f2 100644 --- a/moli-protocol/src/conn/inspection_binding_tests/io.rs +++ b/moli-protocol/src/conn/inspection_binding_tests/io.rs @@ -37,8 +37,8 @@ async fn blob_inspection_round_trip(start_before_move: bool) { ctx.conn.commit_declared_session_fixtures_for_test(); let object_id = blob_object(&mut ctx, Some(session)).await; let owner = CommandOwnerScope::capture(&ctx.conn, Some(session)); - let take_document = |ctx: &mut TestContext| take_inspection_document(&mut ctx.conn, &owner); - let mut document = (!start_before_move).then(|| take_document(&mut ctx)); + let document_handle = |ctx: &mut TestContext| inspection_document_handle(&ctx.conn, &owner); + let mut document = (!start_before_move).then(|| document_handle(&mut ctx)); let raw = json!({"id": 2, "sessionId": session, "method": "IO.resolveBlob", "params": { "objectId": object_id, }}) @@ -49,9 +49,9 @@ async fn blob_inspection_round_trip(start_before_move: bool) { "Blob inspection must start through the live session binding without a Protocol Document" ); if start_before_move { - document = Some(take_document(&mut ctx)); + document = Some(document_handle(&mut ctx)); } - let mut document = document.unwrap(); + let document = document.unwrap(); let (messages, _) = ctx.complete_command_task_step_for_test(step).await; let response = messages .iter() @@ -63,22 +63,19 @@ async fn blob_inspection_round_trip(start_before_move: bool) { ); let uuid = response["result"]["uuid"].as_str().unwrap(); let completed = document - .page - .start_blob_bytes_for_uuid(uuid.to_owned()) + .start_blob_bytes_for_uuid_for_test(uuid.to_owned()) .unwrap() .wait() - .await - .unwrap(); + .await; let bytes = document - .page - .finish_blob_bytes_for_uuid(completed) + .finish_blob_bytes_for_uuid_for_test(completed) .unwrap() .unwrap(); assert_eq!( &*bytes, b"bound blob", "the reply must resolve the actual session object" ); - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); } #[tokio::test(flavor = "multi_thread")] @@ -100,7 +97,7 @@ async fn blob_inspection_rejects_frozen_reply_after_rebind() { }; let completed = pending.wait().await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let old_document = take_inspection_document(&mut ctx.conn, &owner); + let old_document = inspection_document_handle(&ctx.conn, &owner); ctx.install_navigation_fixture_for_session_owner( "data:text/html,replacement", None, diff --git a/moli-protocol/src/conn/inspection_binding_tests/lifecycle.rs b/moli-protocol/src/conn/inspection_binding_tests/lifecycle.rs index 0868e42877..67891659b6 100644 --- a/moli-protocol/src/conn/inspection_binding_tests/lifecycle.rs +++ b/moli-protocol/src/conn/inspection_binding_tests/lifecycle.rs @@ -21,10 +21,9 @@ async fn session_detach_reaches_renderer_without_protocol_document() { let owner = CommandOwnerScope::capture(&ctx.conn, None); // Both child sessions have already used the binding. Browser-owned work // must not inherit either frontend or resurrect its V8 session on detach. - let mut document = take_inspection_document(&mut ctx.conn, &owner); + let document = inspection_document_handle(&ctx.conn, &owner); let before = document - .page - .runtime_heap_usage_async() + .runtime_heap_usage_for_test() .await .unwrap() .moli @@ -47,8 +46,7 @@ async fn session_detach_reaches_renderer_without_protocol_document() { // This real owner command is sequenced after detach finalization. The // frontend acknowledgement alone cannot prove that V8 cleanup happened. let after = document - .page - .runtime_heap_usage_async() + .runtime_heap_usage_for_test() .await .unwrap() .moli @@ -66,6 +64,6 @@ async fn session_detach_reaches_renderer_without_protocol_document() { .await; let surviving = ctx.take_response_by_id(4); assert_eq!(surviving["result"]["result"]["value"], json!("value")); - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); drop(document); } diff --git a/moli-protocol/src/conn/inspection_binding_tests/native_commands.rs b/moli-protocol/src/conn/inspection_binding_tests/native_commands.rs index a12d77ab4b..d67ef2fe6a 100644 --- a/moli-protocol/src/conn/inspection_binding_tests/native_commands.rs +++ b/moli-protocol/src/conn/inspection_binding_tests/native_commands.rs @@ -55,15 +55,16 @@ async fn native_history_reset_completion_survives_selection_and_session_detach() panic!("history reset must execute on the Browser Document"); }; let completed = pending.wait().await; - assert!( + assert!({ + let handle = ctx + .conn + .browser_web_contents_for_target("TID-native-background") + .unwrap(); ctx.conn - .browser_context - .as_mut() - .unwrap() - .select_page_target_async("TID-native-background") + .select_browser_web_contents_async(handle) .await - .unwrap() - ); + .is_ok() + }); let peer = ctx .conn .browser_context @@ -119,15 +120,14 @@ async fn native_input_completion_keeps_its_document_after_selection_and_session_ panic!("input must be admitted to the original Browser document"); }; let completed = pending.wait().await; - assert!( - ctx.conn - .browser_context - .as_mut() - .unwrap() - .select_page_target_async("TID-native-background") - .await - .unwrap() - ); + let handle = ctx + .conn + .browser_web_contents_for_target("TID-native-background") + .unwrap(); + ctx.conn + .select_browser_web_contents_async(handle) + .await + .unwrap(); ctx.process_async( json!({"id": 23, "method": "Target.detachFromTarget", "params": { "targetId": "TID-dom-inspection", "sessionId": "SID-native-original", @@ -177,15 +177,14 @@ async fn native_diagnostics_completion_keeps_exact_documents_after_selection_and .wait() .await .unwrap(); - assert!( - ctx.conn - .browser_context - .as_mut() - .unwrap() - .select_page_target_async("TID-native-background") - .await - .unwrap() - ); + let handle = ctx + .conn + .browser_web_contents_for_target("TID-native-background") + .unwrap(); + ctx.conn + .select_browser_web_contents_async(handle) + .await + .unwrap(); ctx.process_async( json!({"id": 31, "method": "Target.detachFromTarget", "params": { "targetId": "TID-dom-inspection", "sessionId": "SID-native-original", @@ -257,14 +256,19 @@ async fn browser_native_commands_do_not_require_a_live_inspector_session() { }})).await; assert!(ctx.take_response_by_id(1).get("error").is_none()); let owner = CommandOwnerScope::capture(&ctx.conn, None); - let (context_id, target_id) = ctx.conn.resolve_document_command_owner(&owner).unwrap(); + let (context_id, target_id) = ctx + .conn + .loaded_document_owner_identity_for_owner(&owner) + .unwrap(); let context = ctx.conn.browser_context_by_id(&context_id).unwrap(); let residence = context .target_renderer_page_residence_identity(&target_id) .unwrap(); - let pending_capture = context - .start_capture_screenshot_with_request_for_target( - &target_id, + let document = ctx.conn.resolve_browser_document_for_owner(&owner).unwrap(); + let pending_capture = ctx + .conn + .start_capture_document_image( + document, moli_core::page::RendererCaptureScreenshotRequest::viewport_png(), ) .unwrap(); @@ -279,15 +283,18 @@ async fn browser_native_commands_do_not_require_a_live_inspector_session() { .await .unwrap(); - let completion = pending_capture.wait().await.unwrap(); - let context = ctx.conn.browser_context_by_id_mut(&context_id).unwrap(); - assert!(residence.matches_residence(completion.page_state().renderer_residence())); + let completion = pending_capture.wait().await; assert!( - context - .finish_capture_screenshot_for_target(&target_id, completion) - .is_ok(), + ctx.conn.finish_capture_document_image(completion).is_ok(), "session detach must not cancel already admitted Browser capture" ); + assert_eq!( + ctx.conn + .browser_context_by_id(&context_id) + .unwrap() + .target_renderer_page_residence_identity(&target_id), + Some(residence) + ); ctx.process_async(json!({"id": 2, "method": "Input.insertText", "params": {"text": "native"}})) .await; @@ -357,3 +364,93 @@ async fn native_capture_rejects_a_foreign_page_without_devtools_attachments() { .unwrap(); assert!(first.finish_capture_screenshot(completion).is_ok()); } + +#[tokio::test(flavor = "multi_thread")] +async fn completed_browser_document_command_cannot_retarget_a_replacement_document() { + let mut ctx = dom_context().await; + let owner = CommandOwnerScope::capture(&ctx.conn, None); + let document = ctx.conn.resolve_browser_document_for_owner(&owner).unwrap(); + let completed = ctx + .conn + .start_capture_document_snapshot(document) + .unwrap() + .wait() + .await; + + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,replacement-document", + None, + ) + .await; + + assert!(matches!( + ctx.conn.finish_capture_document_snapshot(completed), + Err(error) if error == "Document changed" + )); + assert_eq!( + ctx.conn + .browser_context + .as_ref() + .unwrap() + .target_document_title("TID-dom-inspection") + .as_deref(), + Some("replacement-document") + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn app_manifest_fetch_cannot_publish_into_a_replacement_document() { + let mut ctx = dom_context().await; + ctx.install_buffered_navigation_fixture_for_session_owner( + url::Url::parse("https://manifest.example/page").unwrap(), + r#"manifest-owner"# + .into(), + None, + ) + .await; + + let raw = json!({"id": 801, "method": "Page.getAppManifest"}).to_string(); + let CdpCommandTaskStep::Pending(prepare) = ctx.conn.start_command_dispatch(&raw) else { + panic!("app manifest inspection must start on the original document"); + }; + let CdpCommandTaskStep::Pending(fetch) = ctx + .conn + .complete_pending_command_dispatch(prepare.wait().await) + .await + else { + panic!("an external app manifest must enter the browser fetch stage"); + }; + let fetched = fetch.wait().await; + + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,replacement-manifest-owner", + None, + ) + .await; + + let CdpCommandTaskStep::Complete(outcome) = + ctx.conn.complete_pending_command_dispatch(fetched).await + else { + panic!("a stale app manifest fetch must not publish into the replacement document"); + }; + let (messages, _) = ctx.route_completed_command_outcome_for_test(outcome).await; + let response = messages + .iter() + .find(|message| message["id"] == json!(801)) + .expect("app manifest response"); + assert_eq!(response["error"]["code"], json!(-32000), "{response}"); + assert_eq!( + response["error"]["message"], + json!("Failed to publish app manifest result: Document changed"), + "the fetched result must retain its originating Browser Document: {response}" + ); + assert_eq!( + ctx.conn + .browser_context + .as_ref() + .unwrap() + .target_document_title("TID-dom-inspection") + .as_deref(), + Some("replacement-manifest-owner") + ); +} diff --git a/moli-protocol/src/conn/inspection_binding_tests/runtime_agents.rs b/moli-protocol/src/conn/inspection_binding_tests/runtime_agents.rs index 6200f68f6d..2d4e61b363 100644 --- a/moli-protocol/src/conn/inspection_binding_tests/runtime_agents.rs +++ b/moli-protocol/src/conn/inspection_binding_tests/runtime_agents.rs @@ -4,7 +4,7 @@ use super::*; async fn runtime_controls_start_without_protocol_document() { let mut ctx = dom_context().await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let document = take_inspection_document(&mut ctx.conn, &owner); + let document = inspection_document_handle(&ctx.conn, &owner); for (id, method) in (1..).zip([ "Runtime.enable", "Runtime.disable", @@ -37,7 +37,7 @@ async fn runtime_controls_start_without_protocol_document() { }; assert!(response.get("error").is_none(), "{method}: {response}"); } - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); drop(document); } @@ -46,7 +46,7 @@ async fn preloads_register_run_remove_and_replay_without_protocol_document() { let mut ctx = dom_context().await; dom_command(&mut ctx, 1, "Runtime.enable", json!({})).await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let document = take_inspection_document(&mut ctx.conn, &owner); + let document = inspection_document_handle(&ctx.conn, &owner); dom_command( &mut ctx, 2, @@ -113,7 +113,7 @@ async fn preloads_register_run_remove_and_replay_without_protocol_document() { "realm creation must consume the renderer's updated preload registry" ); } - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); drop(document); } @@ -128,13 +128,13 @@ async fn isolated_world_restarts_on_replacement_binding_without_protocol_documen ); assert!(matches!(&step, CdpCommandTaskStep::Pending(_))); let owner = CommandOwnerScope::capture(&ctx.conn, None); - let old_document = take_inspection_document(&mut ctx.conn, &owner); + let old_document = inspection_document_handle(&ctx.conn, &owner); ctx.install_navigation_fixture_for_session_owner( "data:text/html,
replacement
", None, ) .await; - let replacement = take_inspection_document(&mut ctx.conn, &owner); + let replacement = inspection_document_handle(&ctx.conn, &owner); let (messages, _) = ctx.complete_command_task_step_for_test(step).await; let response = messages .iter() @@ -169,7 +169,7 @@ async fn session_preload_cleanup_updates_renderer_without_protocol_document() { }})).await; ctx.expect_result(1, json!({"identifier": "1"}), Some("SID-preload-cleanup")); let owner = CommandOwnerScope::capture(&ctx.conn, Some("SID-preload-cleanup")); - let document = take_inspection_document(&mut ctx.conn, &owner); + let document = inspection_document_handle(&ctx.conn, &owner); ctx.process_async( json!({"id": 10, "method": "Target.detachFromTarget", "params": { "targetId": "TID-dom-inspection", "sessionId": "SID-preload-cleanup", @@ -315,8 +315,8 @@ async fn runtime_agent_round_trip(method: &str, start_before_move: bool) { })).await["result"]["objectId"].clone(); assert!(object.is_string()); let owner = CommandOwnerScope::capture(&ctx.conn, None); - let take_document = |ctx: &mut TestContext| take_inspection_document(&mut ctx.conn, &owner); - let mut document = (!start_before_move).then(|| take_document(&mut ctx)); + let document_handle = |ctx: &mut TestContext| inspection_document_handle(&ctx.conn, &owner); + let mut document = (!start_before_move).then(|| document_handle(&mut ctx)); let params = match method { "Runtime.addBinding" => json!({"name": "inspectBinding"}), "Page.createIsolatedWorld" => { @@ -333,7 +333,7 @@ async fn runtime_agent_round_trip(method: &str, start_before_move: bool) { "{method} must dispatch to the live binding" ); if start_before_move { - document = Some(take_document(&mut ctx)); + document = Some(document_handle(&mut ctx)); } let document = document.unwrap(); let (messages, _) = ctx.complete_command_task_step_for_test(step).await; @@ -451,6 +451,6 @@ async fn runtime_agent_round_trip(method: &str, start_before_move: bool) { } _ => unreachable!(), } - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); drop(document); } diff --git a/moli-protocol/src/conn/inspector_route.rs b/moli-protocol/src/conn/inspector_route.rs index 6b77130171..1428615d3b 100644 --- a/moli-protocol/src/conn/inspector_route.rs +++ b/moli-protocol/src/conn/inspector_route.rs @@ -259,7 +259,7 @@ mod tests { false, crate::conn::CapturedBody::from_string("candidate".to_owned()), ); - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.install_browser_context_fixture_for_test(context); let owner = CommandOwnerScope::for_route(crate::conn::CdpSessionRoute::PageTarget { browser_context_id: "BID-rollback".to_owned(), @@ -289,7 +289,7 @@ mod tests { let mut context = BrowserContext::new("BID-cleanup".to_owned()); context.set_active_target_id("TID-cleanup"); context.begin_active_target_initial_empty_document("about:blank".to_owned()); - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.install_browser_context_fixture_for_test(context); let owner = CommandOwnerScope::for_route(crate::conn::CdpSessionRoute::PageTarget { browser_context_id: "BID-cleanup".to_owned(), @@ -421,7 +421,7 @@ mod tests { let second = context .start_document_navigation_for_target("TID-second", "LOADER-shared".to_owned()) .unwrap(); - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.install_browser_context_fixture_for_test(context); let owner = CommandOwnerScope::for_route(crate::conn::CdpSessionRoute::PageTarget { browser_context_id: "BID-navigation-route".to_owned(), @@ -471,7 +471,7 @@ mod tests { "TID-background", "SID-background-attached".to_owned(), )); - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.install_browser_context_fixture_for_test(browser_context); let filtered = conn.filter_renderer_inspector_batches_for_target_owner( @@ -499,14 +499,9 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn inspector_state_updates_require_the_current_attachment_and_agent() { let mut ctx = TestContext::new(); - let page = ctx + let mut browser_context = ctx .conn - .load_page_via_runtime_async( - "data:text/html,inspector-state-source-validation", - ) - .await - .expect("state source validation page should load"); - let mut browser_context = BrowserContext::new("BID-state-route".to_owned()); + .new_browser_context_fixture_for_test("BID-state-route"); browser_context.set_active_target_id("TID-state-route".to_owned()); browser_context.attach_active_session("SID-state-primary".to_owned()); assert!( @@ -515,14 +510,21 @@ mod tests { "SID-state-attached".to_owned(), ) ); - browser_context.set_loaded_page_async(page).await; + ctx.conn + .install_browser_context_fixture_for_test(browser_context); + let _ = ctx + .conn + .install_navigation_fixture_for_session_owner_for_test( + "data:text/html,inspector-state-source-validation", + None, + ) + .await; + let browser_context = ctx.conn.browser_context.as_ref().expect("browser context"); let current = browser_context .active_page_target() .runtime_slot .current_renderer_attachment() .expect("installed page should have a renderer attachment"); - ctx.conn - .install_browser_context_fixture_for_test(browser_context); let accepted_state = V8InspectorSessionState::from_bytes(vec![1, 2, 3]); let mut accepted = batch(DevToolsSessionKey::Primary); diff --git a/moli-protocol/src/conn/navigation_error.rs b/moli-protocol/src/conn/navigation_error.rs index 5d5ee17017..37de3f97ac 100644 --- a/moli-protocol/src/conn/navigation_error.rs +++ b/moli-protocol/src/conn/navigation_error.rs @@ -1,57 +1,4 @@ -use std::{error::Error, fmt}; +pub(crate) use moli_core::browser::{NavigationNetworkError, NavigationRequestBlocked}; -use url::Url; - -/// Network policy failures produced by the navigation request loaders. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum NavigationNetworkErrorKind { - InternetDisconnected, -} - -impl NavigationNetworkErrorKind { - pub(crate) fn error_text(self) -> &'static str { - match self { - Self::InternetDisconnected => "net::ERR_INTERNET_DISCONNECTED", - } - } -} - -/// A navigation request rejected by emulated offline network policy. -/// -/// Transport failures retain `moli_fetch::NetworkFetchFailureContext` and use -/// their existing handling paths; this cause currently represents offline -/// enforcement only. -/// -/// This is an error cause rather than display context, so callers can recover -/// its request identity through `anyhow::Error::downcast_ref` after adding -/// diagnostic context along the navigation pipeline. -#[derive(Debug)] -pub(crate) struct NavigationNetworkError { - pub(crate) kind: NavigationNetworkErrorKind, - pub(crate) unreachable_url: Url, - pub(crate) request_method: String, - pub(crate) request_headers: moli_fetch::RequestHeaders, -} - -impl fmt::Display for NavigationNetworkError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(self.kind.error_text()) - } -} - -impl Error for NavigationNetworkError {} - -/// A navigation request rejected by `Network.setBlockedURLs`. -/// -/// The protocol boundary preserves its canonical error text independently of -/// diagnostic context on other navigation failures. -#[derive(Debug)] -pub(crate) struct NavigationRequestBlocked; - -impl fmt::Display for NavigationRequestBlocked { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("net::ERR_BLOCKED_BY_CLIENT") - } -} - -impl Error for NavigationRequestBlocked {} +#[cfg(test)] +pub(crate) use moli_core::browser::NavigationNetworkErrorKind; diff --git a/moli-protocol/src/conn/output.rs b/moli-protocol/src/conn/output.rs index e4a6295c19..8430276bcf 100644 --- a/moli-protocol/src/conn/output.rs +++ b/moli-protocol/src/conn/output.rs @@ -4381,7 +4381,7 @@ mod tests { BackgroundServiceWorkerVersion, NavigationBackgroundEvent, PageScreencastFrameMetadata, RuntimeInspectorResponseReady, build_event, }; - use crate::conn::{BrowserContext, CdpConnection, DevToolsDocumentLifecycleWaitKey}; + use crate::conn::{BrowserContext, DevToolsDocumentLifecycleWaitKey}; use moli_core::{ PageId, RendererRuntimeInspectorAsyncCompletion, page::{ @@ -4429,7 +4429,7 @@ mod tests { #[test] fn page_download_envelope_rejects_disabled_and_reenabled_subscription() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.install_default_browser_target(); assert!(conn.set_page_domain_enabled_for_session_owner(None, true)); let first_generation = conn @@ -4474,7 +4474,7 @@ mod tests { #[test] fn browser_download_route_guard_rejects_detached_and_reenabled_subscription() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.set_browser_download_events_enabled_for_session(Some("SID-browser"), true); let first_generation = conn.download_subscriptions.browser_event_observers()[0].1; let event = BackgroundProtocolEvent::browser_download_progress( diff --git a/moli-protocol/src/conn/permission_tests.rs b/moli-protocol/src/conn/permission_tests.rs index 85a258a62a..9702c41252 100644 --- a/moli-protocol/src/conn/permission_tests.rs +++ b/moli-protocol/src/conn/permission_tests.rs @@ -27,10 +27,11 @@ fn permission_settings(conn: &CdpConnection, context: &str) -> Vec { #[test] fn permission_scope_order_tracks_writes_instead_of_fixed_scope_precedence() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-first".into())); - conn.inactive_browser_contexts - .push(BrowserContext::new("BID-second".into())); + let mut conn = crate::test_support::connection(); + let first = conn.new_browser_context_fixture_for_test("BID-first"); + let second = conn.new_browser_context_fixture_for_test("BID-second"); + conn.install_browser_context_fixture_for_test(first); + conn.push_inactive_browser_context_fixture_for_test(second); set_permission(&mut conn, None, "denied"); set_permission(&mut conn, Some("BID-first"), "granted"); @@ -52,23 +53,27 @@ fn permission_scope_order_tracks_writes_instead_of_fixed_scope_precedence() { permission_settings(&conn, "BID-first"), ["prompt", "denied"] ); - conn.inactive_browser_contexts - .push(BrowserContext::new("BID-later".into())); + let later = conn.new_browser_context_fixture_for_test("BID-later"); + conn.push_inactive_browser_context_fixture_for_test(later); assert_eq!(permission_settings(&conn, "BID-later"), ["prompt"]); } #[test] fn permission_resets_preserve_other_scopes_and_do_not_materialize_pages() { - let mut conn = CdpConnection::new_with_deferred_navigation_runtime( + let mut conn = crate::test_support::connection_with_config( CdpInitialStoragePartition::memory(), NavigationRuntimeConfig::default(), ); set_permission(&mut conn, None, "denied"); assert!(conn.browser_context.is_none()); - assert!(!conn.standalone_navigation_engine.is_materialized()); - conn.browser_context = Some(BrowserContext::new("BID-first".into())); - conn.inactive_browser_contexts - .push(BrowserContext::new("BID-second".into())); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); + let first = conn.new_browser_context_fixture_for_test("BID-first"); + let second = conn.new_browser_context_fixture_for_test("BID-second"); + conn.install_browser_context_fixture_for_test(first); + conn.push_inactive_browser_context_fixture_for_test(second); set_permission(&mut conn, Some("BID-first"), "granted"); set_permission(&mut conn, Some("BID-second"), "prompt"); @@ -89,22 +94,25 @@ fn permission_resets_preserve_other_scopes_and_do_not_materialize_pages() { conn.browser_contexts() .all(|context| context.page_targets.is_empty()) ); - assert!(!conn.standalone_navigation_engine.is_materialized()); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); } #[test] fn permission_rules_leave_the_connection_with_their_context() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("same-context".into())); + let mut conn = crate::test_support::connection(); + let original = conn.new_browser_context_fixture_for_test("same-context"); + conn.install_browser_context_fixture_for_test(original); set_permission(&mut conn, None, "denied"); set_permission(&mut conn, Some("same-context"), "granted"); assert_eq!(conn.permission_override_count(), 2); // No disposal handler or ID-keyed cleanup is allowed to be necessary. - let removed = conn - .browser_context - .replace(BrowserContext::new("same-context".into())) - .unwrap(); + let removed = conn.browser_context.take().unwrap(); + let replacement = conn.new_browser_context_fixture_for_test("same-context"); + conn.install_browser_context_fixture_for_test(replacement); assert_ne!( removed.browser_context_id(), conn.browser_context.as_ref().unwrap().browser_context_id() @@ -113,7 +121,7 @@ fn permission_rules_leave_the_connection_with_their_context() { assert_eq!(conn.permission_override_count(), 1); assert_eq!( removed - .permission_snapshot(&conn.permission_defaults) + .permission_snapshot() .iter() .map(|entry| entry.setting.as_str()) .collect::>(), @@ -140,7 +148,9 @@ async fn current_permission(ctx: &mut TestContext) -> String { #[tokio::test(flavor = "multi_thread")] async fn permission_update_order_and_context_reset_reach_live_and_replacement_documents() { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-permission-live".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-permission-live"); context.set_active_target_id("TID-permission-live"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner("data:text/html,first", None) @@ -171,7 +181,9 @@ async fn permission_update_order_and_context_reset_reach_live_and_replacement_do } async fn loaded_permission_context(ctx: &mut TestContext, title: &str) { - let mut context = BrowserContext::new("BID-permission-live".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-permission-live"); context.set_active_target_id("TID-permission-live"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -226,9 +238,8 @@ async fn permission_completion_preserves_its_ack_without_observing_a_replacement .browser_context .as_ref() .unwrap() - .loaded_page() - .unwrap() - .document_title(), + .loaded_document_title_for_test() + .unwrap(), "replacement" ); assert_eq!(current_permission(&mut ctx).await, "denied"); diff --git a/moli-protocol/src/conn/permissions.rs b/moli-protocol/src/conn/permissions.rs index 3ab5c5df51..c218d8eaf0 100644 --- a/moli-protocol/src/conn/permissions.rs +++ b/moli-protocol/src/conn/permissions.rs @@ -7,8 +7,8 @@ impl CdpConnection { browser_context_id: &str, ) -> Vec { self.browser_context_by_id(browser_context_id) - .map(|context| context.permission_snapshot(&self.permission_defaults)) - .unwrap_or_else(|| self.permission_defaults.snapshot()) + .map(|context| context.permission_snapshot()) + .unwrap_or_else(|| self.browser.permission_defaults_snapshot()) } pub(crate) fn set_permission_override( @@ -23,9 +23,9 @@ impl CdpConnection { .chain(&mut self.inactive_browser_contexts) .find(|context| context.id == id) .ok_or("UnknownBrowserContextId")?; - context.set_permission_override(&mut self.permission_defaults, registration); + context.set_permission_override(registration)?; } else { - self.permission_defaults.set(registration); + self.browser.set_permission_default(registration); } Ok(()) } @@ -39,7 +39,7 @@ impl CdpConnection { .ok_or("UnknownBrowserContextId")? .clear_permission_overrides(); } else { - self.permission_defaults.clear(); + self.browser.clear_permission_defaults(); for context in self .browser_context .iter_mut() @@ -52,7 +52,7 @@ impl CdpConnection { } pub(crate) fn permission_override_count(&self) -> usize { - self.permission_defaults.override_count() + self.browser.permission_default_count() + self .browser_contexts() .map(|context| context.permission_override_count()) @@ -64,7 +64,7 @@ impl CdpConnection { ) -> Result, String> { let mut pending = Vec::new(); for context in self.browser_contexts() { - if let Some(update) = context.start_permission_update(&self.permission_defaults)? { + if let Some(update) = context.start_permission_update()? { pending.push(update); } } diff --git a/moli-protocol/src/conn/renderer_command_turn.rs b/moli-protocol/src/conn/renderer_command_turn.rs index 8a668fd13d..bc820461e0 100644 --- a/moli-protocol/src/conn/renderer_command_turn.rs +++ b/moli-protocol/src/conn/renderer_command_turn.rs @@ -1,38 +1,6 @@ -use moli_core::page::{ - CompletedPageCommand, RendererCommandTurnCompletion, RendererCommandTurnOutput, -}; +use moli_core::page::{RendererCommandTurnCompletion, RendererCommandTurnOutput}; -use super::{CdpConnection, CommandDispatchContext, TargetPageResidenceIdentity}; - -impl CdpConnection { - /// Settles a renderer command turn against the Page residence that admitted it. - /// - /// A completed renderer turn is immutable even if navigation replaces its - /// originating Page before protocol delivery resumes. Refresh the Page cache - /// only while that exact residence is still current; otherwise preserve the - /// output for domain-specific reply handling without installing stale state - /// into the replacement Page. - pub(crate) fn settle_page_command_turn_for_owner( - &mut self, - owner: &TargetPageResidenceIdentity, - completion: CompletedPageCommand, - ) -> RendererCommandTurnOutput { - if let Some(target_id) = owner.target_id() - && let Some(context) = self.browser_context_by_id_mut(owner.browser_context_id()) - { - return context.settle_target_page_command_turn( - target_id, - owner.document_id(), - completion, - ); - } - - // The renderer has already settled this immutable result. Losing the - // protocol-side Page cache must not turn its acknowledgement into an - // error or apply its state to a future replacement Page. - completion.into_output() - } -} +use super::CommandDispatchContext; impl CommandDispatchContext { /// Captures the exact concrete-stream predecessor and consumes the unique diff --git a/moli-protocol/src/conn/resource_runtime_support.rs b/moli-protocol/src/conn/resource_runtime_support.rs index ea4996f2d3..80cfd00ab8 100644 --- a/moli-protocol/src/conn/resource_runtime_support.rs +++ b/moli-protocol/src/conn/resource_runtime_support.rs @@ -2,9 +2,10 @@ use anyhow::Context; #[cfg(test)] use moli_core::network::ResourceRequestClient; -use moli_core::page::{CompletedPageCommand, PendingPageCommand}; -use super::{BrowserContext, CdpConnection}; +#[cfg(test)] +use super::BrowserContext; +use super::{CdpConnection, PendingDocumentResourceRuntimeUpdate}; #[cfg(test)] use super::{TargetNavigationLoadInputs, state::BrowserContextResourceStorageHandles}; @@ -12,8 +13,6 @@ impl CdpConnection { pub(crate) fn invalidate_resource_runtime(&mut self) { if let Some(context) = self.browser_context.as_mut() { context.invalidate_selected_resource_runtime(); - } else if let Some(engine) = self.standalone_navigation_engine.engine.get_mut() { - engine.reset_resource_runtime_without_loaded_page(); } } @@ -29,15 +28,8 @@ impl CdpConnection { pub(crate) fn ensure_resource_request_client( &mut self, ) -> Result { - self.apply_active_engine_fetch_overrides(); - let storage = self.resource_storage_handles(); - let engine = self.active_navigation_engine_mut(); - engine - .ensure_resource_runtime_ready_for_navigation_storage(storage.into_navigation_storage()) - .map_err(|error| format!("failed to initialize resource runtime: {error}"))?; - engine - .resource_request_client() - .ok_or_else(|| "resource request client unavailable".to_owned()) + let owner = super::CommandOwnerScope::capture(self, None); + self.resource_request_client_for_owner(&owner) } #[cfg(test)] @@ -48,25 +40,17 @@ impl CdpConnection { if let (Some(context_id), Some(target_id)) = (&inputs.browser_context_id, &inputs.root_frame_id) { - self.ensure_page_navigation_engine_for_target(context_id, target_id) - .context("navigation WebContents engine unavailable")?; let defaults = self.document_fetch_defaults(); + let browser_globals = self.browser_global_overrides.clone(); return self .browser_context_by_id_mut(context_id) .context("BrowserContext unavailable")? - .resource_request_client_for_test(target_id, defaults) + .resource_request_client_for_test(target_id, defaults, &browser_globals) .map_err(anyhow::Error::msg); } - let storage = inputs.resource_storage_handles(); - let engine = self - .configured_navigation_engine_for_load_inputs_mut(inputs) - .context("navigation Page engine unavailable")?; - engine - .ensure_resource_runtime_ready_for_navigation_storage(storage.into_navigation_storage()) - .context("failed to initialize resource runtime")?; - engine - .resource_request_client() - .context("resource request client unavailable") + Err(anyhow::anyhow!( + "resource request fixture requires an installed WebContents" + )) } #[cfg(test)] @@ -77,77 +61,11 @@ impl CdpConnection { let (context_id, target_id) = self .resolved_page_owner_identity_for_owner(owner) .ok_or("NoDocumentLoaded")?; - self.ensure_page_navigation_engine_for_target(&context_id, &target_id) - .ok_or("navigation WebContents engine unavailable")?; let defaults = self.document_fetch_defaults(); + let browser_globals = self.browser_global_overrides.clone(); self.browser_context_by_id_mut(&context_id) .ok_or("BrowserContext unavailable")? - .resource_request_client_for_test(&target_id, defaults) - } - - #[cfg(test)] - pub(super) fn configured_navigation_engine_for_load_inputs_mut( - &mut self, - load_inputs: &TargetNavigationLoadInputs, - ) -> Option<&mut moli_core::runtime::NavigationEngine> { - let browser_identity = load_inputs - .browser_identity_override - .clone() - .or_else(|| self.global_browser_identity_override.clone()) - .unwrap_or_else(|| self.base_browser_identity.clone()); - let http_proxy = load_inputs - .http_proxy_override - .clone() - .or_else(|| self.base_http_proxy.clone()); - let http_no_proxy = load_inputs - .http_no_proxy_override - .clone() - .or_else(|| self.base_http_no_proxy.clone()); - let tls_verify_host = load_inputs - .tls_verify_host_override - .unwrap_or(self.base_tls_verify_host); - let engine = self.navigation_engine_for_load_inputs_mut(load_inputs)?; - engine.set_browser_identity_override(browser_identity); - engine.set_http_proxy_override(http_proxy); - engine.set_http_no_proxy_override(http_no_proxy); - engine.set_tls_verify_host(tls_verify_host); - engine.set_extra_http_headers(&load_inputs.extra_http_headers); - engine.set_network_offline(load_inputs.network_offline); - engine.set_blocked_url_patterns(&load_inputs.blocked_url_patterns); - engine.set_bypass_service_worker(load_inputs.bypass_service_worker); - engine.set_cache_disabled(load_inputs.cache_disabled); - Some(engine) - } - - #[cfg(test)] - pub(super) fn navigation_engine_for_load_inputs_mut( - &mut self, - load_inputs: &TargetNavigationLoadInputs, - ) -> Option<&mut moli_core::runtime::NavigationEngine> { - let mut engine = match ( - load_inputs.browser_context_id.as_deref(), - load_inputs.root_frame_id.as_deref(), - ) { - (Some(browser_context_id), Some(target_id)) => { - self.ensure_page_navigation_engine_for_target(browser_context_id, target_id) - } - (Some(browser_context_id), None) => { - let active_target_id = self - .browser_context_by_id(browser_context_id) - .and_then(|context| context.active_target_id()) - .map(str::to_owned); - if let Some(target_id) = active_target_id { - self.ensure_page_navigation_engine_for_target(browser_context_id, &target_id) - } else { - Some(self.standalone_navigation_engine.ensure_mut()) - } - } - (None, _) => Some(self.standalone_navigation_engine.ensure_mut()), - }; - if let Some(engine) = engine.as_mut() { - engine.set_document_activity(load_inputs.document_activity); - } - engine + .resource_request_client_for_test(&target_id, defaults, &browser_globals) } #[cfg(test)] @@ -155,31 +73,22 @@ impl CdpConnection { &mut self, ) -> Result { self.apply_active_engine_fetch_overrides(); - let storage = self.resource_storage_handles(); - self.active_navigation_engine_mut() - .ensure_cookie_store_for_navigation_storage(storage.into_navigation_storage()) - .map_err(|error| format!("failed to initialize loader: {error}")) + Ok(self.resource_storage_handles().cookie_store) } #[cfg(test)] pub(crate) async fn reset_resource_runtime_async(&mut self) { - if let Some(context) = self.browser_context.as_mut() - && context.reset_selected_resource_runtime_async().await - { - return; + if let Some(context) = self.browser_context.as_mut() { + context.reset_selected_resource_runtime_async().await; } - self.standalone_navigation_engine - .ensure_mut() - .reset_resource_runtime_async(None) - .await; } pub(crate) async fn rebuild_resource_runtime_for_loaded_page_async(&mut self) { let owner = super::CommandOwnerScope::capture(self, None); let result = async { if let Some(pending) = self.start_rebuild_resource_runtime_for_owner(&owner)? { - let completion = pending.wait().await.map_err(|error| error.to_string())?; - self.finish_rebuild_resource_runtime_for_owner(&owner, completion)?; + let completed = pending.wait().await; + self.finish_document_resource_runtime_update(completed)?; } Ok::<(), String>(()) } @@ -192,7 +101,7 @@ impl CdpConnection { pub(crate) fn start_rebuild_resource_runtime_for_session_owner( &mut self, session_id: Option<&str>, - ) -> Result, String> { + ) -> Result, String> { let owner = super::CommandOwnerScope::capture(self, session_id); self.start_rebuild_resource_runtime_for_owner(&owner) } @@ -200,49 +109,32 @@ impl CdpConnection { pub(crate) fn start_rebuild_resource_runtime_for_owner( &mut self, owner: &super::CommandOwnerScope, - ) -> Result, String> { - let Some((context_id, target_id)) = self.resolved_page_owner_identity_for_owner(owner) - else { - // Only live Browser/context defaults may exist without a page. - // An expired session or Page route must never fall back to a peer. - return match owner.resolve_route(self) { - Some(super::CdpSessionRoute::Browser) => Ok(None), - Some(super::CdpSessionRoute::BrowserContext { browser_context_id }) - if self.browser_context_by_id(&browser_context_id).is_some() => - { - Ok(None) - } - _ => Err("NoDocumentLoaded".to_owned()), - }; + ) -> Result, String> { + let web_contents = match self.browser_web_contents_for_owner(owner) { + Ok(web_contents) => web_contents, + Err(_) => { + // Only live Browser/context defaults may exist without a page. + // An expired session or Page route must never fall back to a peer. + return match owner.resolve_route(self) { + Some(super::CdpSessionRoute::Browser) => Ok(None), + Some(super::CdpSessionRoute::BrowserContext { browser_context_id }) + if self.browser_context_by_id(&browser_context_id).is_some() => + { + Ok(None) + } + _ => Err("NoDocumentLoaded".to_owned()), + }; + } }; - #[cfg(test)] - self.ensure_page_navigation_engine_for_target(&context_id, &target_id) - .ok_or("navigation WebContents engine unavailable")?; let defaults = self.document_fetch_defaults(); - self.browser_context_by_id_mut(&context_id) + let browser_globals = self.browser_global_overrides.clone(); + self.browser_context_by_browser_id_mut(web_contents.context()) .ok_or("BrowserContext unavailable")? - .start_target_resource_runtime_rebuild(&target_id, defaults) - } - - pub(crate) fn finish_rebuild_resource_runtime_for_session_owner( - &mut self, - session_id: Option<&str>, - completion: CompletedPageCommand, - ) -> Result<(), String> { - let owner = super::CommandOwnerScope::capture(self, session_id); - self.finish_rebuild_resource_runtime_for_owner(&owner, completion) - } - - pub(crate) fn finish_rebuild_resource_runtime_for_owner( - &mut self, - owner: &super::CommandOwnerScope, - completion: CompletedPageCommand, - ) -> Result<(), String> { - if let Some((context_id, target_id)) = self.resolved_page_owner_identity_for_owner(owner) - && let Some(context) = self.browser_context_by_id_mut(&context_id) - { - return context.finish_target_resource_runtime_update(&target_id, completion); - } - BrowserContext::finish_unobserved_resource_runtime_update(completion) + .start_web_contents_resource_runtime_rebuild( + web_contents, + defaults, + &browser_globals.extra_headers, + browser_globals.network_conditions, + ) } } diff --git a/moli-protocol/src/conn/runtime_eval.rs b/moli-protocol/src/conn/runtime_eval.rs index 7395af90bd..f5eac457b3 100644 --- a/moli-protocol/src/conn/runtime_eval.rs +++ b/moli-protocol/src/conn/runtime_eval.rs @@ -12,6 +12,7 @@ use crate::devtools_runtime::{ use moli_core::{ RendererOutputFence, RendererRuntimeCommandCausalIdentity, RendererRuntimeInspectorResponseSender, + browser::BrowserContextId, page::{ DocumentNodeObjectSnapshot, DocumentNodeRuntimeObjectResolution, MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH, RendererAgentAttachmentId, RendererCommandTurnOutput, @@ -207,7 +208,7 @@ enum RuntimeRemoteObjectOwnerIdentity { #[derive(Clone, Debug, Eq, PartialEq)] struct SharedWorkerRuntimeTargetRoute { - browser_context_id: String, + browser_context: BrowserContextId, worker: WorkerRuntimeTarget, } @@ -219,7 +220,7 @@ enum WorkerRuntimeTarget { #[derive(Clone, Debug, Eq, PartialEq)] struct ServiceWorkerRuntimeTargetRoute { - browser_context_id: String, + browser_context: BrowserContextId, version_id: u64, } @@ -360,8 +361,7 @@ pub struct PendingMoliDiagnosticsDispatch { } struct PendingMoliDiagnosticsPageSnapshot { - owner: crate::conn::TargetPageResidenceIdentity, - pending: moli_core::page::PendingPageCommand, + pending: PendingDocumentDiagnosticsSnapshot, } pub struct PendingRuntimeEnableEventsDispatch { @@ -445,8 +445,7 @@ pub struct CompletedMoliDiagnosticsDispatch { } struct CompletedMoliDiagnosticsPageSnapshot { - owner: crate::conn::TargetPageResidenceIdentity, - completion: Result, + completed: CompletedDocumentDiagnosticsSnapshot, } pub struct CompletedRuntimeEnableEventsDispatch { @@ -529,19 +528,11 @@ fn collect_moli_diagnostics_pending_snapshots( .into_iter() .chain(browser_context.background_targets()) { - let Some(document_id) = browser_context.target_document_id(target.target_id()) else { + let Some(document) = browser_context.document_handle_for_target(target.target_id()) else { continue; }; - if !browser_context.target_has_loaded_page(target.target_id()) { - continue; - } pending.push(PendingMoliDiagnosticsPageSnapshot { - owner: crate::conn::TargetPageResidenceIdentity::new( - browser_context.id.clone(), - Some(target.target_id().to_owned()), - document_id, - ), - pending: browser_context.start_target_page_diagnostics_snapshot(target.target_id())?, + pending: browser_context.start_document_diagnostics_snapshot(document)?, }); } Ok(()) @@ -696,12 +687,7 @@ impl PendingMoliDiagnosticsDispatch { let mut completed = Vec::with_capacity(self.pending.len()); for pending in self.pending { completed.push(CompletedMoliDiagnosticsPageSnapshot { - owner: pending.owner, - completion: pending - .pending - .wait() - .await - .map_err(|error| format!("moli diagnostics snapshot failed: {error}")), + completed: pending.pending.wait().await, }); } Ok(CompletedMoliDiagnosticsDispatch { completed }) @@ -4112,7 +4098,10 @@ impl CdpConnection { await_promise: bool, ) -> Result { let owner = CommandOwnerScope::capture(self, session_id); - let (context_id, target_id) = self.resolve_document_command_owner(&owner)?; + self.ensure_document_accessible_for_owner(&owner)?; + let (context_id, target_id) = self + .loaded_document_owner_identity_for_owner(&owner) + .ok_or_else(|| "NoDocumentLoaded".to_owned())?; let payload = self .browser_context_by_id_mut(&context_id) .ok_or("NoDocumentLoaded")? @@ -4315,12 +4304,14 @@ impl CdpConnection { browser_context_id, target_id, } => { - let target = self + let context = self .browser_context_by_id(&browser_context_id) - .and_then(|context| context.shared_worker_target(&target_id)) + .ok_or_else(|| "UnknownSession".to_owned())?; + let target = context + .shared_worker_target(&target_id) .ok_or_else(|| "UnknownSession".to_owned())?; Ok(SharedWorkerRuntimeTargetRoute { - browser_context_id, + browser_context: context.browser_context_id(), worker: WorkerRuntimeTarget::Shared(target.renderer_instance_id), }) } @@ -4328,12 +4319,14 @@ impl CdpConnection { browser_context_id, target_id, } => { - let target = self + let context = self .browser_context_by_id(&browser_context_id) - .and_then(|context| context.dedicated_worker_target(&target_id)) + .ok_or_else(|| "UnknownSession".to_owned())?; + let target = context + .dedicated_worker_target(&target_id) .ok_or_else(|| "UnknownSession".to_owned())?; Ok(SharedWorkerRuntimeTargetRoute { - browser_context_id, + browser_context: context.browser_context_id(), worker: WorkerRuntimeTarget::Dedicated(target.renderer_instance_id), }) } @@ -4353,11 +4346,10 @@ impl CdpConnection { if let Some(target) = self.dedicated_worker_target_for_session_mut(Some(session_id)) { target.discard_main_script_network_replay_for(session_id); } - let renderer_runtime = self - .browser_context_by_id(&route.browser_context_id) - .map(|context| context.renderer_runtime()) + let browser_context = self + .browser_context_by_browser_id(route.browser_context) .ok_or_else(|| "UnknownSession".to_owned())?; - Ok(renderer_runtime.run_dedicated_worker_if_waiting_for_debugger_for_devtools(instance_id)) + Ok(browser_context.run_dedicated_worker_if_waiting_for_debugger(instance_id)) } fn service_worker_runtime_target_for_session( @@ -4374,12 +4366,14 @@ impl CdpConnection { else { return Err("UnknownSession".to_owned()); }; - let target = self + let context = self .browser_context_by_id(&browser_context_id) - .and_then(|context| context.service_worker_target(&target_id)) + .ok_or_else(|| "UnknownSession".to_owned())?; + let target = context + .service_worker_target(&target_id) .ok_or_else(|| "UnknownSession".to_owned())?; Ok(ServiceWorkerRuntimeTargetRoute { - browser_context_id, + browser_context: context.browser_context_id(), version_id: target.renderer_version_id, }) } @@ -4425,8 +4419,8 @@ impl CdpConnection { ) -> Result { let route = self.shared_worker_runtime_target_for_session(session_id)?; let renderer_runtime = self - .browser_context_by_id(&route.browser_context_id) - .map(|context| context.renderer_runtime()) + .browser_context_by_browser_id(route.browser_context) + .map(BrowserContext::worker_runtime_inspection_endpoint) .ok_or_else(|| "UnknownSession".to_owned())?; let worker = route.worker; let inspector_session_id = session_id.map(str::to_owned); @@ -4660,8 +4654,8 @@ impl CdpConnection { ) -> Result { let route = self.service_worker_runtime_target_for_session(session_id)?; let renderer_runtime = self - .browser_context_by_id(&route.browser_context_id) - .map(|context| context.renderer_runtime()) + .browser_context_by_browser_id(route.browser_context) + .map(BrowserContext::worker_runtime_inspection_endpoint) .ok_or_else(|| "UnknownSession".to_owned())?; let version_id = route.version_id; let inspector_session_id = session_id.map(str::to_owned); @@ -4751,18 +4745,7 @@ impl CdpConnection { let mut failed_page_snapshot_count = 0; for completed in completed.completed { - if !self.target_page_residence_identity_is_current(&completed.owner) { - failed_page_snapshot_count += 1; - continue; - } - let snapshot = completed.completion.and_then(|completion| { - self.browser_context_by_id_mut(completed.owner.browser_context_id()) - .ok_or_else(|| "NoDocumentLoaded".to_owned())? - .finish_target_page_diagnostics_snapshot( - completed.owner.target_id().ok_or("NoDocumentLoaded")?, - completion, - ) - }); + let snapshot = self.finish_document_diagnostics_snapshot(completed.completed); let Ok(snapshot) = snapshot else { failed_page_snapshot_count += 1; continue; @@ -6699,8 +6682,8 @@ mod tests { } fn connection_with_bidi_page_session() -> CdpConnection { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-owner".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-owner".to_owned()); browser_context.set_active_target_id("TID-active"); browser_context.attach_active_session("SID-active".to_owned()); browser_context.set_active_document_fixture_for_test(1); @@ -6710,8 +6693,8 @@ mod tests { #[test] fn runtime_remote_object_validation_allows_session_local_id_collisions() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-owner".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-owner".to_owned()); browser_context.set_active_target_id("TID-active"); browser_context.attach_active_session("SID-active".to_owned()); assert!( @@ -6758,7 +6741,9 @@ mod tests { #[test] fn runtime_remote_object_validation_tolerates_an_empty_browser_context() { let mut conn = connection_with_bidi_page_session(); - conn.insert_browser_context(BrowserContext::new("BID-empty".to_owned())); + conn.insert_browser_context( + conn.new_browser_context_fixture_for_test("BID-empty".to_owned()), + ); assert!( conn.validate_runtime_remote_object_ids_for_session_owner( @@ -6828,7 +6813,9 @@ mod tests { async fn frozen_inspector_completion_fixture() -> (TestContext, CompletedRuntimeProtocolMessageDispatch) { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-inspection-output".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-inspection-output"); context.set_active_target_id("TID-inspection-output"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -7021,13 +7008,21 @@ mod tests { ctx.conn .runtime_protocol_message_started_slot_mut(&route) .unwrap(); - let pending = ctx + let context = ctx .conn .browser_context_by_id(&route.browser_context_id) - .unwrap() - .start_target_page_diagnostics_snapshot(&route.target_id) .unwrap(); - let completion = pending.wait().await.unwrap(); + let document = context + .document_handle_for_target(&route.target_id) + .expect("route should address an exact Document"); + let pending = context + .start_document_diagnostics_snapshot(document) + .unwrap(); + let completion = pending + .wait() + .await + .into_page_completion_for_test() + .unwrap(); assert_eq!(completion.page_state().document_title(), "after-inspection"); let error = ctx .conn @@ -7046,7 +7041,9 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn inspector_binding_applies_io_script_policy_without_protocol_page_ownership() { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-inspection-io-policy".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-inspection-io-policy"); context.set_active_target_id("TID-inspection-io-policy"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -7055,8 +7052,8 @@ mod tests { ) .await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let mut document = - crate::conn::inspection_binding_tests::take_inspection_document(&mut ctx.conn, &owner); + let document = + crate::conn::inspection_binding_tests::inspection_document_handle(&ctx.conn, &owner); for (id, disabled) in [(41, true), (42, false)] { let raw = json!({ "id": id, "method": "Emulation.setScriptExecutionDisabled", "params": { "value": disabled } }) @@ -7082,13 +7079,12 @@ mod tests { .any(|message| message["id"] == json!(id) && message["result"] == json!({})), "IO policy response: {messages:?}", ); - document.page.evaluate_runtime_expression_without_navigation_follow_with_await_async( + document.evaluate_runtime_expression_for_test( "(() => { const script = document.createElement('script'); script.textContent = \"document.documentElement.setAttribute('data-inspection-io', 'ran')\"; document.body.appendChild(script); })()", false, ).await.unwrap(); let actual = document - .page - .evaluate_runtime_expression_without_navigation_follow_with_await_async( + .evaluate_runtime_expression_for_test( "document.documentElement.getAttribute('data-inspection-io')", false, ) @@ -7099,7 +7095,7 @@ mod tests { if disabled { Value::Null } else { json!("ran") } ); } - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); } #[tokio::test(flavor = "multi_thread")] @@ -7109,7 +7105,9 @@ mod tests { try_start_performance_command_dispatch, }; let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-inspection-io-metrics".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-inspection-io-metrics"); context.set_active_target_id("TID-inspection-io-metrics"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -7140,7 +7138,7 @@ mod tests { completions.push(pending.wait().await); } let document = - crate::conn::inspection_binding_tests::take_inspection_document(&mut ctx.conn, &owner); + crate::conn::inspection_binding_tests::inspection_document_handle(&ctx.conn, &owner); for (index, completion) in completions.into_iter().enumerate() { if index == 1 { ctx.install_navigation_fixture_for_session_owner( @@ -7166,7 +7164,7 @@ mod tests { documents >= 1.0, "the exact live binding retains its frozen Browser snapshot" ); - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); } else { assert_eq!( documents, 0.0, @@ -7194,7 +7192,9 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn inspector_binding_observes_native_surface_without_protocol_page_ownership() { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-inspection-emulation".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-inspection-emulation"); context.set_active_target_id("TID-inspection-emulation"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -7203,14 +7203,22 @@ mod tests { ) .await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let mut document = - crate::conn::inspection_binding_tests::take_inspection_document(&mut ctx.conn, &owner); - // Browser configures its physical Document independently of the inspection binding. - document - .page - .set_document_activity_async(moli_page_types::DocumentActivity::new(false, false)) - .await + let document = + crate::conn::inspection_binding_tests::inspection_document_handle(&ctx.conn, &owner); + let context = ctx.conn.browser_context.as_mut().unwrap(); + let native_document = context + .document_handle_for_target("TID-inspection-emulation") .unwrap(); + let pending = context + .start_document_policy_update( + native_document, + crate::conn::DocumentPolicyUpdate::DocumentActivity( + moli_page_types::DocumentActivity::new(false, false), + ), + ) + .unwrap(); + let completed = pending.wait().await; + context.finish_document_policy_update(completed).unwrap(); let source = "JSON.stringify([document.hasFocus(), document.hidden, document.visibilityState])"; let raw = json!({ @@ -7250,14 +7258,14 @@ mod tests { "{response}" ); let actual = document - .page - .evaluate_runtime_expression_async( + .evaluate_runtime_expression_for_test( "JSON.stringify([document.hasFocus(), document.hidden, document.visibilityState])", + false, ) .await .unwrap(); assert_eq!(actual["value"], json!("[false,true,\"hidden\"]")); - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); } #[tokio::test(flavor = "multi_thread")] @@ -7314,7 +7322,9 @@ mod tests { start_before_move: bool, ) { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-inspection-enable".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-inspection-enable"); context.set_active_target_id("TID-inspection-enable"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -7329,7 +7339,7 @@ mod tests { .unwrap() }); let document = - crate::conn::inspection_binding_tests::take_inspection_document(&mut ctx.conn, &owner); + crate::conn::inspection_binding_tests::inspection_document_handle(&ctx.conn, &owner); let pending = pending.unwrap_or_else(|| { ctx.conn .start_runtime_enable_events_for_owner(&owner) @@ -7356,14 +7366,14 @@ mod tests { .conn .runtime_session_owner_slot_for_owner(&owner) .unwrap(); - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); assert_eq!( slot.observable_output_queue_snapshot() .unwrap() .observable_output_items, items ); - assert_eq!(document.page.document_title(), "inspection enable"); + assert_eq!(document.document_title_for_test(), "inspection enable"); assert!( ctx.conn .target_devtools_session_state_for_owner(&owner) @@ -7409,7 +7419,9 @@ mod tests { deferred_response: bool, ) { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-inspection-binding".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-inspection-binding"); context.set_active_target_id("TID-inspection-binding"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -7426,7 +7438,7 @@ mod tests { // Simulate the Browser aggregate moving out of the Protocol residence. // The physical Document stays alive; no detach/replacement has occurred. let document = - crate::conn::inspection_binding_tests::take_inspection_document(&mut ctx.conn, &owner); + crate::conn::inspection_binding_tests::inspection_document_handle(&ctx.conn, &owner); let (method, params) = match lane { RendererInspectorCommandRoute::MainThread => { ("Runtime.evaluate", json!({"expression": "42"})) @@ -7484,8 +7496,8 @@ mod tests { json!(42) ); } - assert_eq!(document.page.document_title(), "inspection binding"); - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert_eq!(document.document_title_for_test(), "inspection binding"); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); } #[tokio::test(flavor = "multi_thread")] @@ -7521,7 +7533,9 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn retired_inspection_endpoint_discards_prepared_renderer_calls() { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-inspection-retired".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-inspection-retired"); context.set_active_target_id("TID-inspection-retired"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -7530,13 +7544,13 @@ mod tests { ) .await; let owner = CommandOwnerScope::capture(&ctx.conn, None); - let mut root = ctx - .conn - .browser_context_by_id_mut("BID-inspection-retired") - .unwrap() - .take_renderer_runtime_owner_for_teardown() - .unwrap(); - root.shutdown_and_join(); + assert!( + ctx.conn + .browser_context_by_id("BID-inspection-retired") + .unwrap() + .remove_from_browser() + .expect("physical BrowserContext should be removable") + ); for (lane, action) in [ (RendererInspectorCommandRoute::MainThread, None), @@ -7561,7 +7575,10 @@ mod tests { ctx.conn .start_renderer_inspection_for_owner(&owner, descriptor, 41, lane, None) }; - assert!(matches!(result, Err(error) if error.contains("Inspector Page is retired"))); + assert!( + result.is_err(), + "retired BrowserContext must reject inspection" + ); assert_eq!( ctx.conn .take_renderer_call_for_frontend_for_owner(&owner, 41), @@ -7597,7 +7614,9 @@ mod tests { async fn inspector_binding_replays_io_agent(method: &str) { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-inspection-io-replay".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-inspection-io-replay"); context.set_active_target_id("TID-inspection-io-replay"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -7659,7 +7678,7 @@ mod tests { // Metrics is layered: Browser snapshot read plus renderer IO dispatch. // Script inspection itself must work with only the binding in Protocol. let mut document = (method == "Emulation.setScriptExecutionDisabled").then(|| { - crate::conn::inspection_binding_tests::take_inspection_document(&mut ctx.conn, &owner) + crate::conn::inspection_binding_tests::inspection_document_handle(&ctx.conn, &owner) }); let response_start = ctx.sent.len(); let events = ctx @@ -7680,12 +7699,11 @@ mod tests { ); if let Some(document) = document.as_mut() { assert_eq!(response["result"], json!({})); - document.page.evaluate_runtime_expression_without_navigation_follow_with_await_async( + document.evaluate_runtime_expression_for_test( "(() => { const s = document.createElement('script'); s.textContent = \"document.body.setAttribute('data-io-replay', 'ran')\"; document.body.appendChild(s); })()", false, ).await.unwrap(); let actual = document - .page - .evaluate_runtime_expression_without_navigation_follow_with_await_async( + .evaluate_runtime_expression_for_test( "document.body.getAttribute('data-io-replay')", false, ) @@ -7696,7 +7714,7 @@ mod tests { Value::Null, "replayed IO policy must actually disable scripts" ); - assert!(!ctx.conn.has_loaded_page_for_owner(&owner)); + assert!(ctx.conn.has_loaded_page_for_owner(&owner)); } else { assert!( response["result"]["metrics"] @@ -7724,7 +7742,9 @@ mod tests { params: Value, ) { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-inspection-replay".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-inspection-replay"); context.set_active_target_id("TID-inspection-replay"); ctx.conn.install_browser_context_fixture_for_test(context); ctx.install_navigation_fixture_for_session_owner( @@ -7762,7 +7782,7 @@ mod tests { assert!(terminations.is_empty()); assert_eq!(replays.len(), 1); let document = - crate::conn::inspection_binding_tests::take_inspection_document(&mut ctx.conn, &owner); + crate::conn::inspection_binding_tests::inspection_document_handle(&ctx.conn, &owner); ctx.conn .replay_prepared_renderer_calls_after_navigation_async(replays, current) .await @@ -7781,7 +7801,7 @@ mod tests { assert_eq!(response["id"], json!(41)); assert!(response.get("error").is_none(), "replay failed: {response}"); assert!(response.get("result").is_some()); - assert_eq!(document.page.document_title(), "replay"); + assert_eq!(document.document_title_for_test(), "replay"); } fn devtools_session_renderer_command_descriptor_for_test( @@ -7834,8 +7854,9 @@ mod tests { #[test] fn navigation_termination_consumes_a_devtools_session_frontend_call() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-navigation-termination".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-navigation-termination".to_owned()); browser_context.set_active_target_id("TID-navigation-termination".to_owned()); browser_context.attach_active_session("SID-navigation-termination".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -7923,7 +7944,9 @@ mod tests { let sessions = [primary_session, Some("SID-replay-a"), Some("SID-replay-z")]; for failed_session in sessions { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-replay-exhaustion".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-replay-exhaustion"); context.set_active_target_id("TID-replay-exhaustion"); if let Some(session) = primary_session { context.attach_active_session(session); @@ -8131,8 +8154,9 @@ mod tests { #[test] fn navigation_termination_isolated_same_frontend_id_by_devtools_session() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-navigation-sessions".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-navigation-sessions".to_owned()); browser_context.set_active_target_id("TID-navigation-sessions".to_owned()); browser_context.attach_active_session("SID-navigation-primary".to_owned()); assert!(browser_context.assign_attached_session_to_target( @@ -8243,8 +8267,9 @@ mod tests { #[test] fn navigation_termination_preserves_sessionless_page_response_shape() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-navigation-sessionless".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-navigation-sessionless".to_owned()); browser_context.set_active_target_id("TID-navigation-sessionless".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -8300,8 +8325,9 @@ mod tests { #[test] fn devtools_session_output_wrong_attachment_does_not_consume_live_correlation() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-attachment-race".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-attachment-race".to_owned()); browser_context.set_active_target_id("TID-attachment-race".to_owned()); browser_context.attach_active_session("SID-attachment-race".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -8359,8 +8385,9 @@ mod tests { #[test] fn devtools_session_output_keeps_first_of_duplicate_terminal_responses() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-duplicate-response".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-duplicate-response".to_owned()); browser_context.set_active_target_id("TID-duplicate-response".to_owned()); browser_context.attach_active_session("SID-duplicate-response".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -8400,8 +8427,9 @@ mod tests { #[test] fn devtools_session_output_restores_interleaved_calls_without_reordering() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-interleaved-response".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-interleaved-response".to_owned()); browser_context.set_active_target_id("TID-interleaved-response".to_owned()); browser_context.attach_active_session("SID-interleaved-response".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -8463,8 +8491,9 @@ mod tests { #[tokio::test] async fn devtools_session_output_restores_only_the_exact_registered_frontend_response() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-session-output".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-session-output".to_owned()); browser_context.set_active_target_id("TID-session-output".to_owned()); browser_context.attach_active_session("SID-session-output".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -8545,8 +8574,9 @@ mod tests { #[test] fn devtools_session_output_preserves_remote_object_group_projection() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-session-projection".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-session-projection".to_owned()); browser_context.set_active_target_id("TID-session-projection".to_owned()); browser_context.attach_active_session("SID-session-projection".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -8748,7 +8778,9 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn document_node_snapshot_for_backend_node_id_reads_live_renderer_snapshot() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-runtime-node-snapshot".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-runtime-node-snapshot".to_owned()); browser_context.set_active_target_id("TID-runtime-node-snapshot".to_owned()); ctx.conn .install_browser_context_fixture_for_test(browser_context); @@ -8887,7 +8919,7 @@ mod tests { #[test] fn route_inspector_notifications_strip_stale_session_id_without_current_session() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); let mut response_events = Vec::new(); let mut background_events = Vec::new(); @@ -8941,7 +8973,7 @@ mod tests { #[test] fn route_inspector_runtime_context_notifications_use_current_session() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); let mut response_events = Vec::new(); let mut background_events = Vec::new(); @@ -9008,8 +9040,8 @@ mod tests { #[test] fn pending_inspector_await_registry_scopes_entries_to_devtools_session() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-owner".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-owner".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -9088,8 +9120,9 @@ mod tests { #[test] fn same_pending_inspector_await_id_is_isolated_by_devtools_session() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-same-id".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-same-id".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -9171,8 +9204,9 @@ mod tests { #[test] fn session_detach_settles_claimed_await_before_late_scheduler_completion() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-claimed-detach".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-claimed-detach".to_owned()); browser_context.set_active_target_id("TID-claimed-detach".to_owned()); browser_context.attach_active_session("SID-claimed-detach".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -9218,8 +9252,9 @@ mod tests { #[test] fn failed_await_registration_does_not_discard_existing_renderer_owner() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-duplicate-owner".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-duplicate-owner".to_owned()); browser_context.set_active_target_id("TID-duplicate-owner".to_owned()); browser_context.attach_active_session("SID-duplicate-owner".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -9264,8 +9299,9 @@ mod tests { #[test] fn bidi_listener_cancellation_discards_correlation_registered_first() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-listener-cancel".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-listener-cancel".to_owned()); browser_context.set_active_target_id("TID-listener-cancel".to_owned()); browser_context.attach_active_session("SID-listener-cancel".to_owned()); browser_context.set_active_document_fixture_for_test(1); @@ -9307,8 +9343,9 @@ mod tests { #[test] fn non_await_cancellation_releases_frontend_command_id() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-command-cancel".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-command-cancel".to_owned()); browser_context.set_active_target_id("TID-command-cancel".to_owned()); browser_context.attach_active_session("SID-command-cancel".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -9337,8 +9374,9 @@ mod tests { #[tokio::test] async fn terminal_session_cleanup_completes_non_await_once_and_releases_frontend_id() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-terminal".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-terminal".to_owned()); browser_context.set_active_target_id("TID-terminal".to_owned()); browser_context.attach_active_session("SID-terminal".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -9415,8 +9453,9 @@ mod tests { #[test] fn pending_inspector_await_response_routes_through_owner_runtime_response() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-owner-output".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-owner-output".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( diff --git a/moli-protocol/src/conn/runtime_load.rs b/moli-protocol/src/conn/runtime_load.rs index 76d59c692c..d3f148a2f8 100644 --- a/moli-protocol/src/conn/runtime_load.rs +++ b/moli-protocol/src/conn/runtime_load.rs @@ -2,14 +2,12 @@ use anyhow::Context; use data_url::DataUrl; use moli_core::{ RendererOutputFence, + browser::web_contents::PausedResponsePreparedDocument as BrowserPausedResponsePreparedDocument, page::{RendererMainDocumentCommit, RendererPageCreationDiagnostics, RendererRuntimeRealmInfo}, runtime::{CommittedDocumentResourceSource, PageVmInitStage, RendererReplyBoundary}, }; #[cfg(test)] -use moli_core::{ - page::{NavigationResponse, Page}, - runtime::PreparedDocumentPagePolicy, -}; +use moli_core::{page::NavigationResponse, runtime::PreparedDocumentPagePolicy}; use moli_fetch::{ BrowserNavigationRequestKind, FetchConfig, NetworkFetchFailureContext, NetworkFetchResult, NetworkObservationJournal, RawResponse, ResponseHead, StreamingRawResponse, @@ -93,6 +91,7 @@ async fn prepare_browser_owned_error_page_navigation_with_load_async( error_text: String, body: CapturedBody, reply_boundary: RendererReplyBoundary, + page_vm_init_stage: PageVmInitStage, ) -> anyhow::Result { let error_page_url = Url::parse(NETWORK_ERROR_PAGE_URL) .expect("the browser-owned network error page URL must be valid"); @@ -124,6 +123,7 @@ async fn prepare_browser_owned_error_page_navigation_with_load_async( Some(error_page), true, reply_boundary, + page_vm_init_stage, ) .await } @@ -137,6 +137,7 @@ async fn prepare_network_error_page_navigation_with_load_async( request_headers: moli_fetch::RequestHeaders, error_text: String, reply_boundary: RendererReplyBoundary, + page_vm_init_stage: PageVmInitStage, ) -> anyhow::Result { let body = CapturedBody::from_string(network_error_page_html(&unreachable_url, &error_text)); prepare_browser_owned_error_page_navigation_with_load_async( @@ -148,6 +149,7 @@ async fn prepare_network_error_page_navigation_with_load_async( error_text, body, reply_boundary, + page_vm_init_stage, ) .await .map(NavigationLoadOutcome::response_commit_ready) @@ -290,34 +292,6 @@ impl std::fmt::Debug for ResponseCommitReady { } impl ResponseCommitReady { - #[cfg(test)] - pub(crate) fn final_url(&self) -> &Url { - &self.final_url - } - - #[cfg(test)] - pub(crate) async fn materialize( - mut self, - policy: Option, - mut inspection: moli_renderer_v8::RendererPreparedDocumentInspectionConfiguration, - ) -> anyhow::Result { - let prepared_page = self - .prepared_page - .take() - .expect("response commit-ready value must retain its prepared Page"); - // Admit the service-owned bootstrap update without waiting on it to - // authorize the Browser operation. Both use this exact renderer owner. - inspection.main_document_commit = self.main_document_commit.as_deref().cloned(); - let inspection_ack = prepared_page - .inspection_configuration_endpoint() - .start_configure(inspection); - let built = prepared_page.materialize(policy).await; - if let Err(error) = inspection_ack.await { - tracing::warn!(%error, "prepared document inspection configuration failed"); - } - self.finish_materialization(built).await - } - async fn finish_materialization

( mut self, built: anyhow::Result>, @@ -401,8 +375,6 @@ impl ResponseCommitReady { response_from_cache: self.response_from_cache, initial_runtime_realms: diagnostics.initial_runtime_realms, renderer_output_predecessor: diagnostics.renderer_output_predecessor, - #[cfg(test)] - main_document_commit: self.main_document_commit.take(), document_progress_transfer, network_error_page: self.network_error_page.take(), }) @@ -418,9 +390,6 @@ impl Drop for ResponseCommitReady { } pub struct PausedResponsePreparedDocument { - prepared_page: PreparedNavigationResponse, - renderer_body_tx: mpsc::Sender>, - renderer_completion_tx: oneshot::Sender>, body_progress_source: MainDocumentBodyProgressSource, body_network_progress_state: MainDocumentBodyNetworkProgress, requested_url: Url, @@ -442,27 +411,19 @@ impl std::fmt::Debug for PausedResponsePreparedDocument { .debug_struct("PausedResponsePreparedDocument") .field("requested_url", &self.requested_url) .field("final_url", &self.final_url) - .field( - "renderer_devtools_agent_token", - &self.prepared_page.renderer_devtools_agent_token(), - ) .finish_non_exhaustive() } } impl PausedResponsePreparedDocument { - #[cfg(test)] - pub(crate) fn renderer_devtools_agent_token( - &self, - ) -> moli_core::page::RendererDevToolsAgentToken { - self.prepared_page.renderer_devtools_agent_token() - } - pub(crate) fn resume_streaming( self, + prepared_document: BrowserPausedResponsePreparedDocument, response: StreamingRawResponse, body_completion_sink: Option, ) -> NavigationLoadOutcome { + let (prepared_page, renderer_body_tx, renderer_completion_tx) = + prepared_document.into_parts(); let network_extra_info_available = !self.network_observation_journal.is_empty(); self.body_progress_source.emit_response_metadata( &self.request_method, @@ -478,14 +439,10 @@ impl PausedResponsePreparedDocument { self.response_from_cache, self.negotiated_http_version, ); - let body_capture_task = spawn_streaming_body_capture( - response, - None, - self.renderer_body_tx, - self.renderer_completion_tx, - ); + let body_capture_task = + spawn_streaming_body_capture(response, None, renderer_body_tx, renderer_completion_tx); let ready = ResponseCommitReady { - prepared_page: Some(self.prepared_page), + prepared_page: Some(prepared_page), body_capture: Some(ResponseCommitBodyCapture::Pending(body_capture_task)), body_completion_sink, body_progress_source: self.body_progress_source, @@ -628,6 +585,7 @@ fn complete_body_capture( pub(crate) struct BackgroundNavigationLoadJob { load: AdmittedNavigationLoad, reply_boundary: RendererReplyBoundary, + page_vm_init_stage: PageVmInitStage, early_result: Option, load_inputs: TargetNavigationLoadInputs, method: String, @@ -765,6 +723,7 @@ impl BackgroundNavigationLoadJob { &self.raw_url, self.request_headers.clone(), self.reply_boundary, + self.page_vm_init_stage, ) .await { @@ -780,6 +739,7 @@ impl BackgroundNavigationLoadJob { &self.raw_url, self.request_headers.clone(), self.reply_boundary, + self.page_vm_init_stage, ) .await { @@ -811,6 +771,7 @@ impl BackgroundNavigationLoadJob { network_error.request_headers.clone(), network_error.kind.error_text().to_owned(), self.reply_boundary, + self.page_vm_init_stage, ) .await; } @@ -849,6 +810,7 @@ impl BackgroundNavigationLoadJob { self.request_headers, failure.network_error_text().to_owned(), self.reply_boundary, + self.page_vm_init_stage, ) .await; } @@ -883,6 +845,7 @@ impl BackgroundNavigationLoadJob { reserved_service_worker_client, CommittedDocumentResourceSource::Navigation(Box::new(document_fetch_context_seed)), self.reply_boundary, + self.page_vm_init_stage, ) .await; if defer_early_result_for_http_error_body { @@ -925,6 +888,7 @@ impl BackgroundStreamingResponseNavigationLoadJob { None, CommittedDocumentResourceSource::Synthetic, RendererReplyBoundary::DocumentCommit, + PageVmInitStage::DomContentLoaded, ) .await } @@ -1060,6 +1024,7 @@ async fn load_inline_html_navigation_with_load_async( raw_url: &str, request_headers: moli_fetch::RequestHeaders, reply_boundary: RendererReplyBoundary, + page_vm_init_stage: PageVmInitStage, ) -> Option> { let source = inline_html_navigation_source(raw_url)?; Some( @@ -1093,6 +1058,7 @@ async fn load_inline_html_navigation_with_load_async( None, true, reply_boundary, + page_vm_init_stage, ) .await .map(NavigationLoadOutcome::response_commit_ready) @@ -1108,6 +1074,7 @@ async fn load_data_url_navigation_with_load_async( raw_url: &str, request_headers: moli_fetch::RequestHeaders, reply_boundary: RendererReplyBoundary, + page_vm_init_stage: PageVmInitStage, ) -> Option> { let source = decoded_data_url_navigation_response(raw_url)?; Some( @@ -1131,6 +1098,7 @@ async fn load_data_url_navigation_with_load_async( None, false, reply_boundary, + page_vm_init_stage, ) .await .map(NavigationLoadOutcome::response_commit_ready) @@ -1154,6 +1122,7 @@ async fn build_navigation_from_streaming_raw_response_with_load_async( reserved_service_worker_client: Option, resource_source: CommittedDocumentResourceSource, reply_boundary: RendererReplyBoundary, + page_vm_init_stage: PageVmInitStage, ) -> anyhow::Result { let timing_enabled = moli_trace::cdp_nav_timing_enabled(); let timing_started = std::time::Instant::now(); @@ -1244,6 +1213,7 @@ async fn build_navigation_from_streaming_raw_response_with_load_async( HTTP_RESPONSE_CODE_FAILURE_ERROR_TEXT.to_owned(), body, reply_boundary, + page_vm_init_stage, ) .await .map(NavigationLoadOutcome::response_commit_ready); @@ -1291,7 +1261,7 @@ async fn build_navigation_from_streaming_raw_response_with_load_async( moli_core::runtime::ExternalRawDocumentBodyStream::from_bytes( response_text.into_bytes(), ), - PageVmInitStage::DomContentLoaded, + page_vm_init_stage, RendererReplyBoundary::Stage, resource_source, None, @@ -1342,7 +1312,7 @@ async fn build_navigation_from_streaming_raw_response_with_load_async( response_status, response_headers.clone(), raw_body, - PageVmInitStage::DomContentLoaded, + page_vm_init_stage, reply_boundary, resource_source, reserved_service_worker_client, @@ -1401,6 +1371,7 @@ impl CdpConnection { ) -> Result { let web_contents = permit.web_contents(); let defaults = self.document_fetch_defaults(); + let browser_globals = self.browser_global_overrides.clone(); let (context_id, target_id, load) = { let context = self .browser_context @@ -1413,7 +1384,7 @@ impl CdpConnection { permit, policy, defaults, - &self.permission_defaults, + &browser_globals, )?; (context_id, target_id, load) }; @@ -1450,6 +1421,7 @@ impl CdpConnection { ) -> Result { let web_contents = request.permit().web_contents(); let defaults = self.document_fetch_defaults(); + let browser_globals = self.browser_global_overrides.clone(); let (context_id, target_id, load) = { let context = self .browser_context @@ -1458,11 +1430,8 @@ impl CdpConnection { .find(|context| context.owns_web_contents(web_contents)) .ok_or("navigation BrowserContext unavailable")?; let context_id = context.id.clone(); - let (target_id, load) = context.start_claimed_navigation_request( - request, - defaults, - &self.permission_defaults, - )?; + let (target_id, load) = + context.start_claimed_navigation_request(request, defaults, &browser_globals)?; (context_id, target_id, load) }; self.bind_renderer_page_output_owner( @@ -1493,10 +1462,8 @@ impl CdpConnection { navigation: &NavigationDispatchState, ) -> Result { let (context_id, target_id, token) = self.navigation_admission_identity(navigation)?; - #[cfg(test)] - self.ensure_page_navigation_engine_for_target(&context_id, &target_id) - .ok_or("navigation WebContents engine unavailable")?; let defaults = self.document_fetch_defaults(); + let browser_globals = self.browser_global_overrides.clone(); let context = self .browser_context .iter_mut() @@ -1508,7 +1475,7 @@ impl CdpConnection { token, navigation.request_load_policy, defaults, - &self.permission_defaults, + &browser_globals, )?; // Projection binds the native reservation before prepare can publish. // It does not allocate or select a Browser Document identity. @@ -1537,7 +1504,7 @@ impl CdpConnection { } pub(super) fn document_fetch_defaults(&self) -> FetchConfig { - let mut config = FetchConfig::default(); + let mut config = self.navigation_runtime_config.fetch_config().clone(); config.set_browser_identity( self.global_browser_identity_override .clone() @@ -1561,20 +1528,14 @@ impl CdpConnection { // refresh. Keep the native policy supplied when it was prepared. return Ok(None); }; - self.ensure_page_navigation_engine_for_target(&context_id, &target_id) - .ok_or("navigation WebContents engine unavailable")?; let defaults = self.document_fetch_defaults(); + let browser_globals = self.browser_global_overrides.clone(); self.browser_context .iter_mut() .chain(self.inactive_browser_contexts.iter_mut()) .find(|context| context.id == context_id) .ok_or("navigation BrowserContext unavailable")? - .capture_document_policy_for_target( - &target_id, - final_url, - defaults, - &self.permission_defaults, - ) + .capture_document_policy_for_target(&target_id, final_url, defaults, &browser_globals) .map(Some) } @@ -1610,6 +1571,7 @@ impl CdpConnection { .expect("response must retain its prepared Document"); let endpoint = page.inspection_configuration_endpoint(); let defaults = self.document_fetch_defaults(); + let browser_globals = self.browser_global_overrides.clone(); let materialization = self .browser_context .iter_mut() @@ -1622,7 +1584,7 @@ impl CdpConnection { page, destination, defaults, - &self.permission_defaults, + &browser_globals, ) .map_err(|error| match error.as_str() { "stale navigation document candidate" @@ -1652,7 +1614,13 @@ impl CdpConnection { response: &StreamingRawResponse, network_observation_journal: &NetworkObservationJournal, body_progress_source: MainDocumentBodyProgressSource, - ) -> anyhow::Result> { + ) -> Result< + Option<( + BrowserPausedResponsePreparedDocument, + PausedResponsePreparedDocument, + )>, + anyhow::Error, + > { if super::downloads::response_headers_indicate_download(&response.headers) || response_headers_indicate_xml_document(&response.headers) || response_status_may_use_http_error_page(response.status) @@ -1733,24 +1701,28 @@ impl CdpConnection { elapsed_ms = started.elapsed().as_millis(), ); } - Ok(Some(PausedResponsePreparedDocument { - prepared_page, - renderer_body_tx, - renderer_completion_tx, - body_progress_source, - body_network_progress_state, - requested_url, - final_url, - request_method, - request_headers, - response_status, - response_headers, - response_from_cache, - negotiated_http_version, - network_observation_journal: network_observation_journal.clone(), - timing_started, - main_document_commit, - })) + Ok(Some(( + BrowserPausedResponsePreparedDocument::new( + prepared_page, + renderer_body_tx, + renderer_completion_tx, + ), + PausedResponsePreparedDocument { + body_progress_source, + body_network_progress_state, + requested_url, + final_url, + request_method, + request_headers, + response_status, + response_headers, + response_from_cache, + negotiated_http_version, + network_observation_journal: network_observation_journal.clone(), + timing_started, + main_document_commit, + }, + ))) } pub(crate) fn start_initial_document_page_ensure_for_owner( @@ -1886,13 +1858,14 @@ impl CdpConnection { return Ok(None); }; let defaults = self.document_fetch_defaults(); + let browser_globals = self.browser_global_overrides.clone(); let admission = self .browser_context .iter_mut() .chain(self.inactive_browser_contexts.iter_mut()) .find(|context| context.id == context_id) .ok_or("TargetNotLoaded")? - .start_initial_document_for_target(&target_id, defaults, &self.permission_defaults)?; + .start_initial_document_for_target(&target_id, defaults, &browser_globals)?; let kind = match admission { InitialDocumentAdmission::Present => return Ok(None), InitialDocumentAdmission::Join(waiter) => { @@ -1998,10 +1971,9 @@ impl CdpConnection { pub(crate) async fn load_navigation_via_runtime_async( &mut self, raw_url: &str, - ) -> anyhow::Result { + ) -> anyhow::Result> { let owner = CommandOwnerScope::capture(self, None); - let load_inputs = self.navigation_load_inputs_for_owner(&owner); - self.load_navigation_via_runtime_with_load_inputs_async(&owner, raw_url, load_inputs) + self.load_navigation_via_runtime_for_owner_async(&owner, raw_url) .await } @@ -2013,14 +1985,13 @@ impl CdpConnection { /// the Page stream is opened while the page is built and must already be /// bound to its target before any concrete publication is consumed. #[cfg(test)] - pub(crate) async fn load_navigation_via_runtime_for_session_owner_async( + pub(crate) async fn load_navigation_via_runtime_for_owner_async( &mut self, - session_id: Option<&str>, + owner: &CommandOwnerScope, raw_url: &str, - ) -> anyhow::Result { - let owner = CommandOwnerScope::capture(self, session_id); - let load_inputs = self.navigation_fixture_load_inputs_for_session_owner(session_id)?; - self.load_navigation_via_runtime_with_load_inputs_async(&owner, raw_url, load_inputs) + ) -> anyhow::Result> { + let load_inputs = self.navigation_fixture_load_inputs_for_owner(owner)?; + self.load_navigation_via_runtime_with_load_inputs_async(owner, raw_url, load_inputs) .await } @@ -2030,7 +2001,7 @@ impl CdpConnection { owner: &CommandOwnerScope, raw_url: &str, load_inputs: TargetNavigationLoadInputs, - ) -> anyhow::Result { + ) -> anyhow::Result> { let request_headers = load_inputs.extra_http_headers.clone(); let navigation = self .load_navigation_request_via_runtime_with_network_events_and_load_inputs_async( @@ -2048,19 +2019,25 @@ impl CdpConnection { } #[cfg(test)] - pub(super) async fn commit_navigation_load_outcome_for_owner_async( + pub(crate) async fn commit_navigation_load_outcome_for_owner_async( &mut self, owner: &CommandOwnerScope, navigation: NavigationLoadOutcome, - ) -> anyhow::Result { + ) -> anyhow::Result> { match navigation { NavigationLoadOutcome::ResponseCommitReady(navigation) => { - let navigation = *navigation; - let policy = self - .capture_document_policy_for_owner(owner, navigation.final_url()) - .map_err(anyhow::Error::msg)?; - let inspection = self.prepared_document_inspection_for_owner(owner); - navigation.materialize(policy, inspection).await + let (context_id, target_id) = self + .resolved_page_owner_identity_for_owner(owner) + .ok_or_else(|| anyhow::anyhow!("navigation WebContents unavailable"))?; + let token = self + .browser_context_by_id(&context_id) + .and_then(|context| { + context.pending_navigation_id_for_loader(&target_id, DEFAULT_LOADER_ID) + }) + .ok_or_else(|| anyhow::anyhow!("stale navigation document candidate"))?; + self.start_response_document_materialization_for_owner(owner, token, *navigation) + .map_err(anyhow::Error::msg)? + .await } NavigationLoadOutcome::Download(_) => { Err(anyhow::anyhow!("navigation resolved to a download")) @@ -2104,7 +2081,8 @@ impl CdpConnection { ) -> anyhow::Result { let owner = CommandOwnerScope::capture(self, session_id); let load_inputs = apply_navigation_request_load_policy( - self.navigation_load_inputs_for_owner(&owner), + self.navigation_fixture_load_inputs_for_owner(&owner) + .map_err(anyhow::Error::msg)?, request_load_policy, ); self.load_navigation_request_via_runtime_with_network_events_and_load_inputs_async( @@ -2129,6 +2107,7 @@ impl CdpConnection { BackgroundNavigationLoadJob { load, reply_boundary: RendererReplyBoundary::Stage, + page_vm_init_stage: PageVmInitStage::DomContentLoaded, early_result: None, load_inputs: self.navigation_load_inputs_for_navigation(navigation), method, @@ -2160,6 +2139,7 @@ impl CdpConnection { BackgroundNavigationLoadJob { load, reply_boundary: RendererReplyBoundary::Stage, + page_vm_init_stage: PageVmInitStage::DomContentLoaded, early_result: None, load_inputs, method: method.to_owned(), @@ -2193,6 +2173,7 @@ impl CdpConnection { network_error.request_headers.clone(), network_error.kind.error_text().to_owned(), RendererReplyBoundary::Stage, + PageVmInitStage::DomContentLoaded, ) .await .context("failed to prepare network error document") @@ -2216,6 +2197,7 @@ impl CdpConnection { Some(BackgroundNavigationLoadJob { load, reply_boundary: RendererReplyBoundary::DocumentCommit, + page_vm_init_stage: PageVmInitStage::DomContentLoaded, early_result, load_inputs: self.navigation_load_inputs_for_navigation(navigation), method: navigation.request_method.clone(), @@ -2276,7 +2258,28 @@ impl CdpConnection { owner: &CommandOwnerScope, inputs: &TargetNavigationLoadInputs, ) -> Result { - let engine = self.navigation_engine_handle_for_load_inputs(inputs); + let (context_id, target_id) = self + .resolved_page_owner_identity_for_owner(owner) + .ok_or("navigation fixture requires an installed target")?; + let token = self + .browser_context_by_id(&context_id) + .and_then(|context| { + context.pending_navigation_id_for_loader(&target_id, DEFAULT_LOADER_ID) + }) + .unwrap_or_else(|| { + self.start_document_navigation_for_owner(owner, DEFAULT_LOADER_ID.to_owned()) + .expect("resolved fixture target must start navigation") + }); + self.navigation_load_fixture_for_token(owner, inputs, token) + } + + #[cfg(test)] + fn navigation_load_fixture_for_token( + &mut self, + owner: &CommandOwnerScope, + inputs: &TargetNavigationLoadInputs, + token: NavigationId, + ) -> Result { let policy = if inputs.browser_navigation_kind == BrowserNavigationRequestKind::Reload { NavigationRequestLoadPolicy::Reload } else if inputs.infer_navigation_referrer { @@ -2284,51 +2287,85 @@ impl CdpConnection { } else { NavigationRequestLoadPolicy::BrowserInitiated }; - let load = AdmittedNavigationLoad::for_fixture( - engine, - inputs.resource_storage_handles().into_navigation_storage(), - inputs.page_storage_handles().into_navigation_storage(), - inputs.navigation_initiator_url.clone(), + let (context_id, target_id) = self + .resolved_page_owner_identity_for_owner(owner) + .ok_or("navigation fixture requires an installed target")?; + let defaults = self.document_fetch_defaults(); + let browser_globals = self.browser_global_overrides.clone(); + let context = self + .browser_context_by_id_mut(&context_id) + .ok_or("navigation BrowserContext unavailable")?; + let load = context.start_navigation_load_for_target( + &target_id, + token, policy, - inputs.network_offline, - inputs.blocked_url_patterns.clone(), + defaults, + &browser_globals, + )?; + context.project_navigation_load_for_target(&target_id, &load)?; + self.bind_renderer_page_output_owner( + load.renderer_page(), + TargetPageResidenceIdentity::new(context_id, Some(target_id), load.document_id()), ); - if inputs.browser_context_id.is_some() - && self.resolved_page_owner_identity_for_owner(owner).is_some() - { - let projected = self - .reserve_target_page_residence_identity_for_owner(owner, load.renderer_page()) - .ok_or("navigation fixture owner unavailable")?; - self.bind_renderer_page_output_owner(load.renderer_page(), projected); - } Ok(load) } - /// Returns a task-local handle to the NavigationEngine retained by the - /// target owner. `NavigationEngine::clone` shares the target's Page policy - /// and renderer owner, so a background load never has to return or replace - /// the resident engine when it completes. #[cfg(test)] - pub(super) fn navigation_engine_handle_for_load_inputs( + pub(crate) async fn prepare_navigation_fixture_for_owner_and_token_async( &mut self, - load_inputs: &TargetNavigationLoadInputs, - ) -> NavigationEngine { - let mut engine = self - .configured_navigation_engine_for_load_inputs_mut(load_inputs) - .expect("navigation load target must retain its resident NavigationEngine") - .clone(); - engine.set_document_activity(load_inputs.document_activity); - // The handle may publish lifecycle or resource activity before the - // DCL-bound navigation result is committed into a target slot. - self.apply_scheduler_senders_to_navigation_engine(&engine); - engine + owner: &CommandOwnerScope, + token: NavigationId, + loader_id: &str, + raw_url: &str, + ) -> anyhow::Result> { + let load_inputs = self.navigation_load_inputs_for_owner(owner); + let frame_id = load_inputs.root_frame_id.clone().ok_or_else(|| { + anyhow::anyhow!("navigation fixture requires an installed target root frame") + })?; + let load_inputs = load_inputs.with_main_document_commit_seed( + RendererMainDocumentCommitSeed::from_navigation_fixture( + frame_id, + loader_id.to_owned(), + monotonic_timestamp_seconds(), + ), + ); + let request_headers = load_inputs.extra_http_headers.clone(); + let load = self + .navigation_load_fixture_for_token(owner, &load_inputs, token) + .map_err(anyhow::Error::msg)?; + let navigation = BackgroundNavigationLoadJob { + load, + reply_boundary: RendererReplyBoundary::Stage, + page_vm_init_stage: PageVmInitStage::Load, + early_result: None, + load_inputs, + method: "GET".to_owned(), + raw_url: raw_url.to_owned(), + body: None, + request_headers, + body_progress_source: MainDocumentBodyProgressSource::default(), + } + .run(None) + .await + .0?; + match navigation { + NavigationLoadOutcome::ResponseCommitReady(navigation) => { + self.start_response_document_materialization_for_owner(owner, token, *navigation) + .map_err(anyhow::Error::msg)? + .await + } + NavigationLoadOutcome::Download(_) => { + Err(anyhow::anyhow!("navigation resolved to a download")) + } + NavigationLoadOutcome::NetworkFailure(error) => Err(anyhow::Error::msg(error)), + } } #[cfg(test)] pub(crate) async fn load_page_via_runtime_async( &mut self, raw_url: &str, - ) -> anyhow::Result { + ) -> anyhow::Result { let navigation = self.load_navigation_via_runtime_async(raw_url).await?; Ok(navigation.page) } @@ -2348,20 +2385,15 @@ impl CdpConnection { response_status: u16, response_headers: Vec<(String, Vec)>, response_body: String, - ) -> anyhow::Result { - let load_inputs = self.navigation_load_inputs_for_session_owner(None); - let initial_request_cookie_report = - load_inputs.request_cookie_report_for_navigation(&requested_url, &request_method, true); - self.build_loaded_navigation_from_buffered_response_with_request_cookie_report_async( - &load_inputs, + ) -> anyhow::Result> { + self.build_loaded_navigation_from_buffered_response_for_session_owner_async( + None, requested_url, request_method, request_headers, response_status, response_headers, response_body, - None, - initial_request_cookie_report, ) .await } @@ -2376,9 +2408,11 @@ impl CdpConnection { response_status: u16, response_headers: Vec<(String, Vec)>, response_body: String, - ) -> anyhow::Result { + ) -> anyhow::Result> { let owner = CommandOwnerScope::capture(self, session_id); - let load_inputs = self.navigation_fixture_load_inputs_for_session_owner(session_id)?; + let load_inputs = self + .navigation_fixture_load_inputs_for_owner(&owner) + .map_err(anyhow::Error::msg)?; let initial_request_cookie_report = load_inputs.request_cookie_report_for_navigation(&requested_url, &request_method, true); let mut load = self @@ -2405,15 +2439,14 @@ impl CdpConnection { } #[cfg(test)] - fn navigation_fixture_load_inputs_for_session_owner( + fn navigation_fixture_load_inputs_for_owner( &self, - session_id: Option<&str>, + owner: &CommandOwnerScope, ) -> anyhow::Result { - let load_inputs = self.navigation_load_inputs_for_session_owner(session_id); - let frame_id = load_inputs - .root_frame_id - .clone() - .context("navigation fixture requires an installed target root frame")?; + let load_inputs = self.navigation_load_inputs_for_owner(owner); + let frame_id = load_inputs.root_frame_id.clone().ok_or_else(|| { + anyhow::anyhow!("navigation fixture requires an installed target root frame") + })?; Ok(load_inputs.with_main_document_commit_seed( RendererMainDocumentCommitSeed::from_navigation_fixture( frame_id, @@ -2440,20 +2473,32 @@ impl CdpConnection { response_headers: Vec<(String, Vec)>, response_body: String, initial_request_cookie_report: Option, - ) -> anyhow::Result { - let load_inputs = self.navigation_load_inputs_for_session_owner(None); - self.build_loaded_navigation_from_buffered_response_with_request_cookie_report_async( - &load_inputs, - requested_url, - request_method, - request_headers, - response_status, - response_headers, - response_body, - None, - initial_request_cookie_report, - ) - .await + ) -> anyhow::Result> { + let owner = CommandOwnerScope::capture(self, None); + let load_inputs = self + .navigation_fixture_load_inputs_for_owner(&owner) + .map_err(anyhow::Error::msg)?; + let mut load = self + .navigation_load_fixture(&owner, &load_inputs) + .map_err(anyhow::Error::msg)?; + let navigation = self + .build_navigation_from_buffered_body_source_with_load_inputs_async( + &mut load, + &load_inputs, + requested_url.clone(), + requested_url, + request_method, + request_headers, + response_status, + response_headers, + CapturedBody::from_string(response_body), + initial_request_cookie_report, + NetworkObservationJournal::default(), + MainDocumentBodyProgressSource::default(), + ) + .await?; + self.commit_navigation_load_outcome_for_owner_async(&owner, navigation) + .await } #[cfg(test)] @@ -2565,107 +2610,6 @@ impl CdpConnection { .await } - #[cfg(test)] - async fn build_loaded_navigation_from_buffered_response_with_request_cookie_report_async( - &mut self, - load_inputs: &TargetNavigationLoadInputs, - requested_url: Url, - request_method: String, - request_headers: moli_fetch::RequestHeaders, - response_status: u16, - response_headers: Vec<(String, Vec)>, - response_body: String, - captured_response_body: Option, - initial_request_cookie_report: Option, - ) -> anyhow::Result { - let response_cookie_reports = - load_inputs.store_response_cookie_reports(&requested_url, &response_headers); - let (fetch_subresource_interception_enabled, fetch_subresource_interception_resource_type) = - load_inputs.fetch_subresource_interception; - let page_storage = load_inputs.page_storage_handles(); - let main_document_commit = load_inputs - .main_document_commit_for_final_url(&requested_url, None) - .map(Arc::new); - let built = self - .navigation_engine_for_load_inputs_mut(load_inputs) - .context("navigation Page engine unavailable")? - .build_html_page_from_response_with_storage_and_inspector_session_restores_async( - page_storage.into_navigation_storage(), - requested_url.clone(), - requested_url.clone(), - load_inputs.navigation_initiator_url.clone(), - false, - 0, - response_status, - response_headers.clone(), - response_body.clone(), - load_inputs.document_start_scripts.clone(), - load_inputs.runtime_bindings.clone(), - load_inputs - .runtime_inspector_session_restore_snapshots - .clone(), - load_inputs.extra_http_headers.clone(), - load_inputs.script_execution_disabled, - load_inputs.bypass_content_security_policy, - load_inputs.emulated_media.clone(), - load_inputs.viewport_surface, - load_inputs.network_offline, - load_inputs.blocked_url_patterns.clone(), - fetch_subresource_interception_enabled, - fetch_subresource_interception_resource_type, - load_inputs.root_frame_id.clone(), - main_document_commit.as_deref().cloned(), - ) - .await - .with_context(|| { - format!( - "failed to execute scripts for synthetic response `{}`", - requested_url - ) - })?; - let diagnostics = loaded_page_creation_diagnostics_parts(built.page_creation_diagnostics); - let mut page = built.page; - apply_fixture_permission_overrides(&mut page, &load_inputs.permission_overrides) - .await - .map_err(anyhow::Error::msg)?; - let redirect_chain = Vec::new(); - let network_progress = MainDocumentBodyNetworkProgress::CompletedBody(Box::new( - CompletedMainDocumentNetworkEvents::new( - request_method.clone(), - request_headers.to_byte_strings(), - initial_request_cookie_report.clone(), - response_status, - response_headers.clone(), - response_cookie_reports.clone(), - redirect_chain.clone(), - false, - false, - ), - )); - - Ok(LoadedNavigation { - page, - pending_download: built.pending_download, - page_creation_artifacts: built.page_creation_artifacts, - requested_url: requested_url.clone(), - final_url: requested_url, - request_method, - request_headers, - response_status, - response_headers, - response_from_cache: false, - initial_runtime_realms: diagnostics.initial_runtime_realms, - renderer_output_predecessor: diagnostics.renderer_output_predecessor, - main_document_commit, - document_progress_transfer: CompletedDocumentProgressTransfer::new_captured( - captured_response_body.unwrap_or_else(|| CapturedBody::from_string(response_body)), - false, - network_progress, - ), - network_error_page: None, - }) - } - #[cfg(test)] pub async fn build_navigation_from_network_response_async( &mut self, @@ -2673,7 +2617,7 @@ impl CdpConnection { request_method: String, request_headers: moli_fetch::RequestHeaders, response: NetworkFetchResult, - ) -> anyhow::Result { + ) -> anyhow::Result> { self.build_navigation_from_network_response_for_session_owner_async( None, requested_url, @@ -2692,107 +2636,33 @@ impl CdpConnection { request_method: String, request_headers: moli_fetch::RequestHeaders, response: NetworkFetchResult, - ) -> anyhow::Result { - let load_inputs = self.navigation_load_inputs_for_session_owner(session_id); + ) -> anyhow::Result> { + let owner = CommandOwnerScope::capture(self, session_id); + let load_inputs = self + .navigation_fixture_load_inputs_for_owner(&owner) + .map_err(anyhow::Error::msg)?; + let mut load = self + .navigation_load_fixture(&owner, &load_inputs) + .map_err(anyhow::Error::msg)?; let (response, network_observation_journal) = response.into_parts_with_observation_journal(); - let network_extra_info_available = !network_observation_journal.is_empty(); - let (fetch_subresource_interception_enabled, fetch_subresource_interception_resource_type) = - load_inputs.fetch_subresource_interception; - let (response_head, response_body, response_body_bytes) = response.into_parts(); - let final_url = response_head.final_url.clone(); - let response_status = response_head.status; - let response_headers = response_head.headers.clone(); + let (response_head, _response_body, response_body_bytes) = response.into_parts(); let captured_response_body = CapturedBody::from_bytes(response_body_bytes); - let initial_request_cookie_report = response_head.request_cookie_report.clone(); - let response_cookie_reports = response_head.cookie_set_reports.clone(); - let response_from_cache = response_head.from_cache; - let negotiated_http_version = response_head.negotiated_http_version; - let redirected = response_head.redirected; - let redirect_chain: Vec<_> = response_head - .redirect_chain - .clone() - .into_iter() - .map(Into::into) - .collect(); - let page_storage = load_inputs.page_storage_handles(); - let main_document_commit = load_inputs - .main_document_commit_for_final_url(&final_url, None) - .map(Arc::new); - let built = self - .navigation_engine_for_load_inputs_mut(&load_inputs) - .context("navigation Page engine unavailable")? - .build_html_page_from_response_with_storage_and_inspector_session_restores_async( - page_storage.into_navigation_storage(), - requested_url.clone(), - final_url.clone(), - load_inputs.navigation_initiator_url.clone(), - redirected, - redirect_chain.len(), - response_status, - response_headers.clone(), - response_body, - load_inputs.document_start_scripts.clone(), - load_inputs.runtime_bindings.clone(), - load_inputs - .runtime_inspector_session_restore_snapshots - .clone(), - load_inputs.extra_http_headers.clone(), - load_inputs.script_execution_disabled, - load_inputs.bypass_content_security_policy, - load_inputs.emulated_media.clone(), - load_inputs.viewport_surface, - load_inputs.network_offline, - load_inputs.blocked_url_patterns.clone(), - fetch_subresource_interception_enabled, - fetch_subresource_interception_resource_type, - load_inputs.root_frame_id.clone(), - main_document_commit.as_deref().cloned(), - ) - .await - .with_context(|| format!("failed to execute scripts for page `{}`", requested_url))?; - let diagnostics = loaded_page_creation_diagnostics_parts(built.page_creation_diagnostics); - let mut page = built.page; - apply_fixture_permission_overrides(&mut page, &load_inputs.permission_overrides) - .await - .map_err(anyhow::Error::msg)?; - let network_progress = MainDocumentBodyNetworkProgress::CompletedBody(Box::new( - CompletedMainDocumentNetworkEvents::new( - request_method.clone(), - request_headers.to_byte_strings(), - initial_request_cookie_report.clone(), - response_status, - response_headers.clone(), - response_cookie_reports.clone(), - redirect_chain.clone(), - network_extra_info_available, - response_from_cache, - ) - .with_negotiated_http_version(negotiated_http_version) - .with_network_observation_journal(network_observation_journal), - )); - - Ok(LoadedNavigation { - page, - pending_download: built.pending_download, - page_creation_artifacts: built.page_creation_artifacts, - requested_url, - final_url, - request_method, - request_headers, - response_status, - response_headers, - response_from_cache, - initial_runtime_realms: diagnostics.initial_runtime_realms, - renderer_output_predecessor: diagnostics.renderer_output_predecessor, - main_document_commit, - document_progress_transfer: CompletedDocumentProgressTransfer::new_captured( + let navigation = self + .build_navigation_from_captured_raw_response_with_load_inputs_async( + &mut load, + &load_inputs, + requested_url, + request_method, + request_headers, + response_head, captured_response_body, - false, - network_progress, - ), - network_error_page: None, - }) + network_observation_journal, + MainDocumentBodyProgressSource::default(), + ) + .await?; + self.commit_navigation_load_outcome_for_owner_async(&owner, navigation) + .await } pub(crate) async fn build_intercepted_navigation_response_async( @@ -2917,6 +2787,7 @@ impl CdpConnection { None, false, RendererReplyBoundary::Stage, + PageVmInitStage::DomContentLoaded, ) .await .map(NavigationLoadOutcome::response_commit_ready) @@ -2992,6 +2863,7 @@ impl CdpConnection { None, CommittedDocumentResourceSource::Synthetic, RendererReplyBoundary::Stage, + PageVmInitStage::DomContentLoaded, ) .await } @@ -3034,11 +2906,10 @@ impl CdpConnection { pub(crate) fn current_navigation_initiator_url(&self) -> Option { let browser_context = self.browser_context.as_ref()?; - if let Some(loaded_page) = browser_context.loaded_page() { - let url = loaded_page.final_url().clone(); - if url.host_str().is_some() { - return Some(url); - } + if let Some(url) = browser_context.loaded_document_url_for_test() + && url.host_str().is_some() + { + return Some(url); } let url = Url::parse(browser_context.target_url()).ok()?; @@ -3059,11 +2930,12 @@ async fn prepare_navigation_from_captured_raw_response_with_load_async( network_error_page: Option, synthetic_body: bool, reply_boundary: RendererReplyBoundary, + page_vm_init_stage: PageVmInitStage, ) -> anyhow::Result { let network_extra_info_available = !network_observation_journal.is_empty(); if network_error_page.is_none() && response_status_may_use_http_error_page(head.status) - && body.len() == 0 + && body.is_empty() { body_progress_source.emit_response_metadata( &request_method, @@ -3091,6 +2963,7 @@ async fn prepare_navigation_from_captured_raw_response_with_load_async( HTTP_RESPONSE_CODE_FAILURE_ERROR_TEXT.to_owned(), body, reply_boundary, + page_vm_init_stage, ) .await; } @@ -3107,6 +2980,7 @@ async fn prepare_navigation_from_captured_raw_response_with_load_async( network_error_page, synthetic_body, reply_boundary, + page_vm_init_stage, ) .await } @@ -3125,6 +2999,7 @@ async fn prepare_captured_document_response_with_load_async( network_error_page: Option, synthetic_body: bool, reply_boundary: RendererReplyBoundary, + page_vm_init_stage: PageVmInitStage, ) -> anyhow::Result { let network_extra_info_available = !network_observation_journal.is_empty(); body_progress_source.emit_response_metadata( @@ -3185,7 +3060,7 @@ async fn prepare_captured_document_response_with_load_async( response_status, response_headers.clone(), raw_body, - PageVmInitStage::DomContentLoaded, + page_vm_init_stage, reply_boundary, CommittedDocumentResourceSource::Synthetic, None, @@ -3222,16 +3097,6 @@ async fn prepare_captured_document_response_with_load_async( }) } -#[cfg(test)] -async fn apply_fixture_permission_overrides( - page: &mut moli_core::page::Page, - permissions: &[moli_core::page::PermissionOverrideRegistration], -) -> Result<(), String> { - page.set_permission_overrides_async(permissions) - .await - .map_err(|error| format!("failed to apply page permission overrides: {error}")) -} - #[cfg(test)] mod tests { use super::{ diff --git a/moli-protocol/src/conn/settings/engine.rs b/moli-protocol/src/conn/settings/engine.rs index ff2379722f..62244ca91a 100644 --- a/moli-protocol/src/conn/settings/engine.rs +++ b/moli-protocol/src/conn/settings/engine.rs @@ -3,22 +3,16 @@ use crate::conn::{EmulatedGeolocationOverrideState, EmulatedNetworkConditions}; impl CdpConnection { pub(crate) fn apply_active_engine_fetch_overrides(&mut self) { - #[cfg(test)] - if let Some((context_id, target_id)) = self - .browser_context - .as_ref() - .and_then(|context| Some((context.id.clone(), context.active_target_id()?.to_owned()))) - { - self.ensure_page_navigation_engine_for_target(&context_id, &target_id); - } let defaults = self.document_fetch_defaults(); - if let Some(context) = self.browser_context.as_mut() { - if let Err(error) = context.configure_selected_navigation_policy(defaults) { - tracing::warn!(%error, "Browser navigation policy configuration failed"); - } - } else { - self.standalone_navigation_engine - .apply_fetch_defaults(defaults); + let browser_globals = self.browser_global_overrides.clone(); + if let Some(context) = self.browser_context.as_mut() + && let Err(error) = context.configure_selected_navigation_policy( + defaults, + &browser_globals.extra_headers, + browser_globals.network_conditions, + ) + { + tracing::warn!(%error, "Browser navigation policy configuration failed"); } } @@ -39,16 +33,16 @@ impl CdpConnection { .unwrap_or(self.base_tls_verify_host) } - pub fn user_agent(&self) -> &str { + pub fn user_agent(&self) -> String { self.browser_context .as_ref() .and_then(|browser_context| browser_context.reported_active_user_agent_override()) .or_else(|| { self.global_browser_identity_override .as_ref() - .map(moli_browser_profile::BrowserIdentityProfile::user_agent) + .map(|identity| identity.user_agent().to_owned()) }) - .unwrap_or_else(|| self.base_browser_identity.user_agent()) + .unwrap_or_else(|| self.base_browser_identity.user_agent().to_owned()) } pub async fn set_user_agent_override_async(&mut self, user_agent: impl Into) { @@ -87,26 +81,14 @@ impl CdpConnection { &mut self, conditions: Option, ) { - self.global_network_conditions = conditions; - if let Some(browser_context) = self.browser_context.as_mut() { - browser_context.global_network_conditions = conditions; - } - for browser_context in &mut self.inactive_browser_contexts { - browser_context.global_network_conditions = conditions; - } + self.browser_global_overrides.network_conditions = conditions; } pub(crate) fn set_global_geolocation_override( &mut self, override_state: Option, ) { - self.global_geolocation_override = override_state.clone(); - if let Some(browser_context) = self.browser_context.as_mut() { - browser_context.global_geolocation_override = override_state.clone(); - } - for browser_context in &mut self.inactive_browser_contexts { - browser_context.global_geolocation_override = override_state.clone(); - } + self.browser_global_overrides.geolocation = override_state; } #[cfg(test)] @@ -120,11 +102,11 @@ impl CdpConnection { self.rebuild_resource_runtime_for_loaded_page_async().await; } - pub fn http_proxy(&self) -> Option<&str> { + pub fn http_proxy(&self) -> Option { self.browser_context .as_ref() - .and_then(|bc| bc.network_policy().http_proxy.as_deref()) - .or(self.base_http_proxy.as_deref()) + .and_then(|bc| bc.network_policy().http_proxy) + .or_else(|| self.base_http_proxy.clone()) } pub(crate) fn http_proxy_for_session_owner_owned( @@ -136,18 +118,18 @@ impl CdpConnection { .or_else(|| self.base_http_proxy.clone()) } - pub fn http_no_proxy(&self) -> Option<&str> { + pub fn http_no_proxy(&self) -> Option { self.browser_context .as_ref() - .and_then(|bc| bc.network_policy().http_no_proxy.as_deref()) - .or(self.base_http_no_proxy.as_deref()) + .and_then(|bc| bc.network_policy().http_no_proxy) + .or_else(|| self.base_http_no_proxy.clone()) } - pub(crate) fn fetch_config(&self) -> &moli_fetch::FetchConfig { + pub(crate) fn fetch_config(&self) -> moli_fetch::FetchConfig { self.browser_context .as_ref() .and_then(|context| context.page_navigation_fetch_config(context.active_target_id()?)) - .unwrap_or_else(|| self.standalone_navigation_engine.fetch_config()) + .unwrap_or_else(|| self.document_fetch_defaults()) } pub(crate) fn base_browser_identity(&self) -> &moli_browser_profile::BrowserIdentityProfile { diff --git a/moli-protocol/src/conn/state/bounds.rs b/moli-protocol/src/conn/state/bounds.rs deleted file mode 100644 index e4ed56d899..0000000000 --- a/moli-protocol/src/conn/state/bounds.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BrowserWindowBounds { - pub left: Option, - pub top: Option, - pub width: Option, - pub height: Option, - pub window_state: String, -} - -impl Default for BrowserWindowBounds { - fn default() -> Self { - Self { - left: None, - top: None, - width: None, - height: None, - window_state: "normal".to_owned(), - } - } -} diff --git a/moli-protocol/src/conn/state/browser_context.rs b/moli-protocol/src/conn/state/browser_context.rs index 2af69d3d31..38d0abfab8 100644 --- a/moli-protocol/src/conn/state/browser_context.rs +++ b/moli-protocol/src/conn/state/browser_context.rs @@ -3,21 +3,26 @@ use std::{ path::PathBuf, }; +#[cfg(test)] +use moli_cookie_jar::{BrowserCookieStore, SharedBrowserCookieStore}; use moli_cookie_jar::{ - BrowserCookieStore, CookieSource, NetworkCookieRequestContext, SharedBrowserCookieStore, - StoredCookie, StoredCookieQueryReport, new_shared_browser_cookie_store, + CookieSource, NetworkCookieRequestContext, StoredCookie, StoredCookieQueryReport, }; -use moli_core::browser::{BrowserContextId, NavigationId}; -use moli_core::network::{SharedWebStorageStore, new_shared_web_storage_store}; -use moli_core::runtime::{ - NavigationEngine, NavigationPageStorageHandles, NavigationResourceStorageHandles, - NavigationRuntimeConfig, RendererBrowserContextRuntime, RendererBrowserContextRuntimeOwner, - RendererBrowserContextRuntimeOwnerAccess, RendererSharedWorkerRuntimeDiagnostics, - storage_partition::StoragePartitionState, +#[cfg(test)] +use moli_core::browser::BrowserContextResourceStorageHandles; +#[cfg(test)] +use moli_core::browser::BrowserService; +use moli_core::browser::{ + BrowserContextHandle, BrowserContextId, BrowserContextPageStorageHandles, + BrowserContextStoragePartitionHandles, BrowserHandle, ContextEmulationDefaults, + ContextNetworkPolicy, EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, + EmulatedNetworkConditions, NavigationId, StoragePartitionKind, }; -use moli_core::storage::{ - SharedIndexedDbManager, SharedStorageBucketStore, WeakIndexedDbManager, - new_shared_storage_bucket_store_with_indexed_db_manager, +use moli_core::runtime::{NavigationRuntimeConfig, RendererSharedWorkerRuntimeDiagnostics}; +#[cfg(test)] +use moli_core::{ + network::SharedWebStorageStore, + storage::{SharedIndexedDbManager, SharedStorageBucketStore}, }; use moli_shared_worker::SharedWorkerInstanceId; use serde_json::{Value, json}; @@ -29,43 +34,63 @@ use super::{ DevToolsSessionState, browser_identity::BrowserIdentityOverrideInputs, dedicated_worker_target::DedicatedWorkerTargetState, - emulation::{ - EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, EmulatedNetworkConditions, - }, javascript_dialog::TargetPreparedJavaScriptDialog, page_agent_host::{PageAgentHost, PageAgentHostRegistry}, page_slot::DocumentStartScript, service_worker_target::ServiceWorkerTargetState, shared_worker_target::SharedWorkerTargetState, - web_contents::WebContents, }; +#[path = "../browser_document_commands.rs"] +mod browser_document_commands; +#[path = "../browser_web_contents_commands.rs"] +mod browser_web_contents_commands; mod collection; mod downloads; +#[cfg(test)] +mod initial_document_tests; pub(in crate::conn) mod javascript_dialog; mod navigation; mod page_runtime; -mod resource_runtime; -pub(crate) use page_runtime::{NetworkPolicyUpdateKind, PageInputCommand, PagePolicyUpdateKind}; +pub(crate) use page_runtime::{ + BrowserAppManifestLoadPreparation, CompletedAppManifestLoadPreparation, + CompletedAppManifestPublication, CompletedCaptureDocumentImage, + CompletedCaptureDocumentScreencastFrame, CompletedCaptureDocumentSnapshot, + CompletedChildFrameLifecycleWork, CompletedChildFrameNavigation, + CompletedChildFrameTreeSnapshot, CompletedDocumentAutofillTrigger, CompletedDocumentBlobRead, + CompletedDocumentCookieOwnerSnapshot, CompletedDocumentCspBypassUpdate, + CompletedDocumentDiagnosticsSnapshot, CompletedDocumentFetchCommand, + CompletedDocumentInputCommand, CompletedDocumentLifecycleStop, CompletedDocumentPolicyBatch, + CompletedDocumentPolicyUpdate, CompletedDocumentResourceRuntimeUpdate, + CompletedDocumentResourceTextSearch, CompletedDocumentStorageKeySnapshot, + CompletedNavigationHistoryReset, CompletedNetworkResourceLoadPreparation, + CompletedSetDocumentContent, CompletedTopLevelHistoryTraversal, + CompletedTopLevelSameDocumentNavigation, DocumentFetchCommand, DocumentFetchCommandOutcome, + DocumentPolicyUpdate, DocumentRuntimePolicyReconciliation, DocumentSnapshot, PageInputCommand, + PendingAppManifestLoadPreparation, PendingAppManifestPublication, PendingCaptureDocumentImage, + PendingCaptureDocumentScreencastFrame, PendingCaptureDocumentSnapshot, + PendingChildFrameLifecycleWork, PendingChildFrameNavigation, PendingChildFrameTreeSnapshot, + PendingDocumentAutofillTrigger, PendingDocumentBlobRead, PendingDocumentCookieOwnerSnapshot, + PendingDocumentCspBypassUpdate, PendingDocumentDiagnosticsSnapshot, + PendingDocumentFetchCommand, PendingDocumentInputCommand, PendingDocumentLifecycleStop, + PendingDocumentPolicyBatch, PendingDocumentPolicyUpdate, PendingDocumentResourceRuntimeUpdate, + PendingDocumentResourceTextSearch, PendingDocumentStorageKeySnapshot, + PendingNavigationHistoryReset, PendingNetworkResourceLoadPreparation, + PendingSetDocumentContent, PendingTopLevelHistoryTraversal, + PendingTopLevelSameDocumentNavigation, +}; pub(in crate::conn) mod page_slot; mod page_state; mod permissions; -pub(crate) use permissions::{CompletedContextPermissionUpdate, PendingContextPermissionUpdate}; -mod physical; pub(in crate::conn) mod runtime_slot; pub(in crate::conn) mod session; -mod storage_partition; #[cfg(test)] mod tests; +mod workers; +pub(crate) use moli_core::browser::{OriginStorageUsage, SiteDataClearOptions}; pub(crate) use page_state::LoadedNavigationPageCommit; -use physical::BrowserContext as PhysicalBrowserContext; -pub(crate) use physical::{ContextEmulationDefaults, ContextNetworkPolicy}; -use storage_partition::StoragePartitionKind; -pub(crate) use storage_partition::{OriginStorageUsage, SiteDataClearOptions}; -/// DevTools context projection with a privately embedded Browser context. -/// Storage/runtime and the page collection/selection live in the Browser owner. -/// This migration wrapper is removed at Commits 24b/30. +/// DevTools projection for one Browser-owned Context. pub struct BrowserContext { pub id: String, pub(crate) page_targets: PageAgentHostRegistry, @@ -79,162 +104,21 @@ pub struct BrowserContext { pub(crate) dedicated_worker_targets: BTreeMap, pub(crate) service_worker_targets: BTreeMap, pub(crate) service_worker_domain_sessions: BTreeSet>, - pub(crate) global_extra_headers: moli_fetch::RequestHeaders, browser_identity_inputs: BrowserIdentityOverrideInputs, - pub(crate) global_network_conditions: Option, - pub(crate) global_geolocation_override: Option, - pub(crate) global_cache_disabled: bool, pub(crate) next_default_document_start_script_id: u32, pub(crate) default_document_start_scripts: Vec<(String, DocumentStartScript)>, - renderer_output_transport_sender: Option, - // Embedded page/engine residents must be dropped before the context root. - physical: PhysicalBrowserContext, -} - -#[derive(Clone)] -pub(crate) struct BrowserContextStoragePartitionHandles { - cookie_store: SharedBrowserCookieStore, - web_storage_store: SharedWebStorageStore, - indexed_db_manager: SharedIndexedDbManager, - storage_bucket_store: SharedStorageBucketStore, -} - -#[derive(Clone)] -pub(crate) struct BrowserContextResourceStorageHandles { - pub(crate) cookie_store: SharedBrowserCookieStore, - pub(crate) web_storage_store: SharedWebStorageStore, - pub(crate) session_storage_store: SharedWebStorageStore, -} - -#[derive(Clone)] -pub(crate) struct BrowserContextPageStorageHandles { - pub(crate) cookie_store: SharedBrowserCookieStore, - pub(crate) web_storage_store: SharedWebStorageStore, - pub(crate) session_storage_store: SharedWebStorageStore, - pub(crate) indexed_db_manager: Option, - pub(crate) storage_bucket_store: Option, -} - -impl BrowserContextStoragePartitionHandles { - fn from_stores( - cookie_store: SharedBrowserCookieStore, - web_storage_store: SharedWebStorageStore, - indexed_db_manager: SharedIndexedDbManager, - storage_bucket_store: SharedStorageBucketStore, - ) -> Self { - Self { - cookie_store, - web_storage_store, - indexed_db_manager, - storage_bucket_store, - } - } - - pub(crate) fn memory() -> Self { - Self::with_initial_cookies(Vec::new()) - } - - pub(crate) fn with_initial_cookies( - initial_cookies: impl IntoIterator, - ) -> Self { - let cookie_store = new_shared_browser_cookie_store(); - seed_initial_cookies(&cookie_store, initial_cookies); - let indexed_db_manager = moli_core::storage::new_indexed_db_manager(None) - .expect("in-memory IndexedDB manager should initialize"); - let storage_bucket_store = - new_shared_storage_bucket_store_with_indexed_db_manager(&indexed_db_manager); - Self::from_stores( - cookie_store, - new_shared_web_storage_store(), - indexed_db_manager, - storage_bucket_store, - ) - } - - fn from_initial_storage_partition( - cookie_store: SharedBrowserCookieStore, - local_storage_store: SharedWebStorageStore, - indexed_db_manager: SharedIndexedDbManager, - storage_bucket_store: SharedStorageBucketStore, - ) -> Self { - Self::from_stores( - cookie_store, - local_storage_store, - indexed_db_manager, - storage_bucket_store, - ) - } - - pub(crate) fn from_storage_partition( - initial_cookies: impl IntoIterator, - storage_partition: &StoragePartitionState, - ) -> Self { - let cookie_store = new_shared_browser_cookie_store(); - seed_initial_cookies(&cookie_store, initial_cookies); - let shared_storage = storage_partition.shared_storage_handles(); - Self::from_initial_storage_partition( - cookie_store, - shared_storage.web_storage_store(), - shared_storage.indexed_db_manager(), - shared_storage.storage_bucket_store(), - ) - } - - pub(crate) fn resource_storage_handles( - &self, - session_storage_store: SharedWebStorageStore, - ) -> BrowserContextResourceStorageHandles { - BrowserContextResourceStorageHandles { - cookie_store: self.cookie_store.clone(), - web_storage_store: self.web_storage_store.clone(), - session_storage_store, - } - } - - pub(crate) fn page_storage_handles( - &self, - session_storage_store: SharedWebStorageStore, - ) -> BrowserContextPageStorageHandles { - BrowserContextPageStorageHandles { - cookie_store: self.cookie_store.clone(), - web_storage_store: self.web_storage_store.clone(), - session_storage_store, - indexed_db_manager: Some(moli_core::storage::downgrade_indexed_db_manager( - &self.indexed_db_manager, - )), - storage_bucket_store: Some(self.storage_bucket_store.clone()), - } - } -} - -impl BrowserContextResourceStorageHandles { - pub(crate) fn into_navigation_storage(self) -> NavigationResourceStorageHandles { - NavigationResourceStorageHandles::new( - self.cookie_store, - self.web_storage_store, - self.session_storage_store, - ) - } -} - -impl BrowserContextPageStorageHandles { - pub(crate) fn into_navigation_storage(self) -> NavigationPageStorageHandles { - NavigationPageStorageHandles::new( - self.cookie_store, - self.web_storage_store, - self.session_storage_store, - self.indexed_db_manager, - self.storage_bucket_store, - ) - } + browser_context: BrowserContextHandle, } impl std::fmt::Debug for BrowserContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BrowserContext") - .field("browser_context_id", &self.physical.id) + .field("browser_context_id", &self.browser_context.id()) .field("id", &self.id) - .field("storage_partition", &self.physical.storage_partition) + .field( + "storage_partition_kind", + &self.browser_context.storage_partition_kind_label(), + ) .field("active_target_id", &self.active_target_id()) .field("active_session_id", &self.active_session_id()) .field("has_loaded_page", &self.has_loaded_page()) @@ -243,27 +127,15 @@ impl std::fmt::Debug for BrowserContext { } impl BrowserContext { - pub(crate) fn download_policy(&self) -> Option<&moli_core::browser::DownloadPolicy> { - self.physical.download_policy.as_ref() + pub(crate) fn download_policy(&self) -> Option { + self.browser_context.download_policy() } pub(in crate::conn) fn set_download_policy( &mut self, policy: Option, ) { - self.physical.download_policy = policy; - } - - // Internal owner lookup only. These references never leave this private - // Context module; protocol callers use concrete operations and values. - fn web_contents_for_target(&self, target_id: &str) -> Option<&WebContents> { - let id = self.page_targets.get(target_id)?.web_contents_id(); - self.physical.web_contents.get(&id) - } - - fn web_contents_for_target_mut(&mut self, target_id: &str) -> Option<&mut WebContents> { - let id = self.page_targets.get(target_id)?.web_contents_id(); - self.physical.web_contents.get_mut(&id) + self.browser_context.set_download_policy(policy); } fn page_slot_for_target(&self, target_id: &str) -> Option<&super::page_slot::TargetPageSlot> { @@ -283,18 +155,25 @@ impl BrowserContext { } pub fn browser_context_id(&self) -> BrowserContextId { - self.physical.id + self.browser_context.id() + } + + #[cfg(test)] + pub(crate) fn renderer_runtime_id_for_test( + &self, + ) -> moli_core::RendererBrowserContextRuntimeId { + self.browser_context.renderer_runtime_id_for_test() } pub(crate) fn active_page_target(&self) -> &PageAgentHost { self.page_targets - .active(self.physical.selected_web_contents_id()) + .active(self.browser_context.selected_web_contents_id()) .expect("BrowserContext has no active page target") } pub(crate) fn active_page_target_mut(&mut self) -> &mut PageAgentHost { self.page_targets - .active_mut(self.physical.selected_web_contents_id()) + .active_mut(self.browser_context.selected_web_contents_id()) .expect("BrowserContext has no active page target") } @@ -330,8 +209,21 @@ impl BrowserContext { drop(self.take_pending_popup_javascript_dialogs(popup_id)); } - pub fn new(id: String) -> Self { - Self::new_with_initial_cookies(id, Vec::new()) + /// Isolated projection-unit fixture. Connection integration tests use its + /// fixture factory so every Context belongs to that connection's Browser. + #[cfg(test)] + pub(crate) fn new(id: String) -> Self { + let browser = BrowserService::start() + .expect("standalone Browser owner should start") + .handle(); + Self::new_with_browser_and_storage_partition_kind( + &browser, + id, + BrowserContextStoragePartitionHandles::memory(), + None, + None, + StoragePartitionKind::ProfileBacked, + ) } #[cfg(test)] @@ -340,57 +232,54 @@ impl BrowserContext { target_id: impl Into, ) -> Self { let mut context = Self::new(id.into()); + context.bind_page_navigation_engines(Default::default(), None); context.set_active_target_id(target_id); context } - pub(crate) fn new_with_initial_cookies( - id: String, - initial_cookies: impl IntoIterator, + #[cfg(test)] + pub(crate) fn new_with_browser_for_test( + browser: &BrowserHandle, + id: impl Into, ) -> Self { - Self::new_with_storage_partition_and_http_cache( - id, - BrowserContextStoragePartitionHandles::with_initial_cookies(initial_cookies), + Self::new_with_browser_and_storage_partition_kind( + browser, + id.into(), + BrowserContextStoragePartitionHandles::memory(), None, None, + StoragePartitionKind::ProfileBacked, ) } - pub(crate) fn new_ephemeral_with_storage_partition_handles( + pub(crate) fn new_ephemeral_with_http_cache( + browser: &BrowserHandle, id: String, - partition: BrowserContextStoragePartitionHandles, http_cache_root: Option, http_cache_max_bytes: Option, ) -> Self { - Self::new_with_storage_partition_kind( + Self::new_with_browser_and_storage_partition_kind( + browser, id, - partition, + BrowserContextStoragePartitionHandles::memory(), http_cache_root, http_cache_max_bytes, StoragePartitionKind::Ephemeral, ) } - pub(crate) fn new_ephemeral_with_http_cache( - id: String, - http_cache_root: Option, - http_cache_max_bytes: Option, - ) -> Self { - Self::new_ephemeral_with_storage_partition_handles( - id, - BrowserContextStoragePartitionHandles::memory(), - http_cache_root, - http_cache_max_bytes, - ) - } - + #[cfg(test)] pub(crate) fn new_with_storage_partition_and_http_cache( id: String, partition: BrowserContextStoragePartitionHandles, http_cache_root: Option, http_cache_max_bytes: Option, ) -> Self { - Self::new_with_storage_partition_kind( + let browser = BrowserService::start() + .expect("standalone Browser owner should start") + .handle(); + Self::new_with_browser_and_storage_partition_kind( + &browser, id, partition, http_cache_root, @@ -400,28 +289,33 @@ impl BrowserContext { } pub(crate) fn new_with_storage_partition_handles_and_http_cache( + browser: &BrowserHandle, id: String, partition: BrowserContextStoragePartitionHandles, http_cache_root: Option, http_cache_max_bytes: Option, ) -> Self { - Self::new_with_storage_partition_and_http_cache( + Self::new_with_browser_and_storage_partition_kind( + browser, id, partition, http_cache_root, http_cache_max_bytes, + StoragePartitionKind::ProfileBacked, ) } - fn new_with_storage_partition_kind( + fn new_with_browser_and_storage_partition_kind( + browser: &BrowserHandle, id: String, partition: BrowserContextStoragePartitionHandles, http_cache_root: Option, http_cache_max_bytes: Option, kind: StoragePartitionKind, ) -> Self { - let physical = - PhysicalBrowserContext::new(partition, kind, http_cache_root, http_cache_max_bytes); + let browser_context = browser + .create_context(partition, kind, http_cache_root, http_cache_max_bytes) + .expect("BrowserContext creation should succeed"); Self { id, @@ -435,33 +329,15 @@ impl BrowserContext { dedicated_worker_targets: BTreeMap::new(), service_worker_targets: BTreeMap::new(), service_worker_domain_sessions: BTreeSet::new(), - global_extra_headers: Default::default(), browser_identity_inputs: BrowserIdentityOverrideInputs::default(), - global_network_conditions: None, - global_geolocation_override: None, - global_cache_disabled: false, next_default_document_start_script_id: 0, default_document_start_scripts: Vec::new(), - renderer_output_transport_sender: None, - physical, + browser_context, } } - pub(crate) fn new_page_navigation_engine( - &self, - config: NavigationRuntimeConfig, - ) -> NavigationEngine { - let engine = self.physical.new_page_navigation_engine(config); - if let Some(sender) = self.renderer_output_transport_sender.clone() { - engine.set_renderer_output_transport_sender(sender); - } - engine - } - - pub(in crate::conn) fn page_navigation_runtime_config( - &self, - ) -> Option { - self.physical.page_navigation_runtime_config.clone() + pub(crate) fn remove_from_browser(&self) -> Result { + self.browser_context.remove() } pub(crate) fn bind_page_navigation_engines( @@ -469,119 +345,65 @@ impl BrowserContext { config: NavigationRuntimeConfig, renderer_output_transport_sender: Option, ) { - self.physical.page_navigation_runtime_config = Some(config.clone()); - self.renderer_output_transport_sender = renderer_output_transport_sender; - - let sender = self.renderer_output_transport_sender.clone(); - let runtime = self.physical.renderer_runtime_owner_access(); - for contents in self.physical.web_contents.values_mut() { - if contents.has_navigation_engine() { - continue; - } - let engine = NavigationEngine::new_with_runtime_config_and_browser_context_access( - config.clone(), - runtime.clone(), - ) - .expect("live BrowserContext owner must accept a page engine"); - if let Some(sender) = sender.clone() { - engine.set_renderer_output_transport_sender(sender); - } - contents.install_navigation_engine(engine); - } + self.browser_context + .bind_page_navigation_engines(config, renderer_output_transport_sender); } pub(crate) fn set_renderer_output_transport_sender( &mut self, sender: moli_core::RendererOutputTransportSender, ) { - self.renderer_output_transport_sender = Some(sender.clone()); - for contents in self.physical.web_contents.values() { - contents.set_renderer_output_transport_sender(sender.clone()); - } - } - - #[cfg(test)] - pub(crate) fn page_navigation_engine(&self, target_id: &str) -> Option<&NavigationEngine> { - self.web_contents_for_target(target_id)? - .navigation_engine_for_test() - } - - #[cfg(test)] - pub(crate) fn page_navigation_engine_mut( - &mut self, - target_id: &str, - ) -> Option<&mut NavigationEngine> { - self.web_contents_for_target_mut(target_id)? - .navigation_engine_for_test_mut() + self.browser_context + .set_renderer_output_transport_sender(sender); } pub(crate) fn is_profile_backed_storage_partition(&self) -> bool { - self.physical.storage_partition.kind == StoragePartitionKind::ProfileBacked + self.browser_context.storage_partition_kind() == StoragePartitionKind::ProfileBacked } pub(crate) fn storage_partition_id(&self) -> &str { - match self.physical.storage_partition.kind { + match self.browser_context.storage_partition_kind() { StoragePartitionKind::ProfileBacked => "default", StoragePartitionKind::Ephemeral => &self.id, } } pub(crate) fn storage_partition_kind_label(&self) -> &'static str { - self.physical.storage_partition.kind_label() + self.browser_context.storage_partition_kind_label() } #[cfg(test)] pub(crate) fn resource_storage_handles(&self) -> BrowserContextResourceStorageHandles { - let session_storage_store = self - .physical - .selected_web_contents_id() - .and_then(|id| self.physical.web_contents.get(&id)) - .map(|contents| contents.session_storage.store().clone()) - .unwrap_or_else(new_shared_web_storage_store); - self.physical - .storage_partition - .handles - .resource_storage_handles(session_storage_store) + self.browser_context.resource_storage_handles_for_test() } pub(crate) fn page_storage_handles(&self) -> BrowserContextPageStorageHandles { - let session_storage_store = self - .physical - .selected_web_contents_id() - .and_then(|id| self.physical.web_contents.get(&id)) - .map(|contents| contents.session_storage.store().clone()) - .unwrap_or_else(new_shared_web_storage_store); - self.physical - .storage_partition - .handles - .page_storage_handles(session_storage_store) + self.browser_context + .page_storage_handles(None) + .expect("selected WebContents belongs to its BrowserContext") } pub(crate) fn page_storage_handles_for_target( &self, target_id: &str, ) -> Option { - let contents = self.web_contents_for_target(target_id)?; - Some( - self.physical - .storage_partition - .handles - .page_storage_handles(contents.session_storage.store().clone()), - ) + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context.page_storage_handles(Some(handle)).ok() } #[cfg(test)] pub(crate) fn with_cookie_store(&self, f: impl FnOnce(&BrowserCookieStore) -> R) -> R { - let cookie_store = self.physical.storage_partition.cookie_store().lock(); - f(&cookie_store) + let store = self.browser_context.cookie_store_for_test(); + f(&store.lock()) } + #[cfg(test)] pub(crate) fn with_cookie_store_mut( &self, f: impl FnOnce(&mut BrowserCookieStore) -> R, ) -> R { - let mut cookie_store = self.physical.storage_partition.cookie_store().lock(); - f(&mut cookie_store) + let store = self.browser_context.cookie_store_for_test(); + f(&mut store.lock()) } #[cfg(test)] @@ -594,76 +416,64 @@ impl BrowserContext { request_url: &url::Url, request_context: NetworkCookieRequestContext, ) -> Option { - let mut cookie_store = self.physical.storage_partition.cookie_store().lock(); - let report = - cookie_store.observe_cookie_access_report_for_request(request_url, request_context); - (!report.included_cookies.is_empty() || !report.excluded_cookies.is_empty()) - .then_some(report) + self.browser_context + .observe_request_cookie_access_report(request_url, request_context) } pub(crate) fn storage_quota_for_origin(&self, origin: &str) -> (f64, bool) { - self.physical - .storage_partition - .storage_quota_for_origin(origin) + self.browser_context.storage_quota_for_origin(origin) } pub(crate) fn set_storage_quota_override(&mut self, origin: String, quota: f64) { - self.physical - .storage_partition + self.browser_context .set_storage_quota_override(origin, quota); } pub(crate) fn clear_storage_quota_override(&mut self, origin: &str) { - self.physical - .storage_partition - .clear_storage_quota_override(origin); + self.browser_context.clear_storage_quota_override(origin); } pub(crate) fn storage_usage_for_origin( &self, serialized_origin: &str, ) -> Result { - self.physical - .storage_partition - .usage_for_origin(serialized_origin) + self.browser_context + .storage_usage_for_origin(serialized_origin) } - pub(crate) fn renderer_runtime(&self) -> RendererBrowserContextRuntime { - self.physical.renderer_runtime() + pub(crate) fn selected_document_navigation_metadata( + &self, + ) -> Option { + self.browser_context.selected_document_navigation_metadata() } - pub(crate) fn renderer_runtime_owner_access(&self) -> RendererBrowserContextRuntimeOwnerAccess { - self.physical.renderer_runtime_owner_access() + pub(crate) fn set_javascript_dialog_handler_enabled(&self, enabled: bool) { + self.browser_context + .set_javascript_dialog_handler_enabled(enabled); } - pub(crate) fn take_renderer_runtime_owner_for_teardown( - &mut self, - ) -> Option { - self.physical.take_renderer_runtime_owner_for_teardown() + pub(crate) fn javascript_dialog_handler_enabled(&self) -> bool { + self.browser_context.javascript_dialog_handler_enabled() } pub(crate) fn routes_renderer_browser_context_runtime( &self, runtime_id: moli_core::RendererBrowserContextRuntimeId, ) -> bool { - self.renderer_runtime().id() == runtime_id + self.browser_context + .routes_renderer_browser_context_runtime(runtime_id) } pub(crate) fn target_id_for_renderer_owner_local_host_id( &self, owner_local_host_id: moli_core::RendererOwnerLocalHostId, ) -> Option { - if self - .loaded_page() - .is_some_and(|page| page.renderer_owner_local_host_id() == owner_local_host_id) - { - return self.active_target_id_owned(); - } - self.background_targets().find_map(|target| { - self.loaded_page_for_target(target.target_id()) - .is_some_and(|page| page.renderer_owner_local_host_id() == owner_local_host_id) - .then(|| target.target_id().to_owned()) - }) + let handle = self + .browser_context + .web_contents_for_renderer_owner(owner_local_host_id)?; + self.page_targets + .get_for_web_contents(handle.id()) + .map(|target| target.target_id().to_owned()) } pub(crate) fn moli_memory_diagnostics(&self) -> Value { @@ -683,7 +493,7 @@ impl BrowserContext { self.service_worker_target_pending_inspector_await_count_for_diagnostics(); let active_target = self .page_targets - .active(self.physical.selected_web_contents_id()); + .active(self.browser_context.selected_web_contents_id()); let runtime_session_diagnostics = active_target .map(|target| { let primary = target.devtools_sessions.primary(); @@ -773,14 +583,11 @@ impl BrowserContext { .iter() .map(|target| target.devtools_sessions.attached_len()) .sum::(), - "targetOpenerCount": self.page_targets.iter() - .filter(|target| self.web_contents_for_target(target.target_id()).is_some_and(|contents| contents.window.opener.is_some())).count(), + "targetOpenerCount": self.browser_context.web_contents_window_counts().0, "targetOpenerFrameCount": self.page_targets.iter() .filter(|target| target.opener_frame_id.is_some()).count(), - "targetCanAccessOpenerCount": self.page_targets.iter() - .filter(|target| self.web_contents_for_target(target.target_id()).is_some_and(|contents| contents.window.opener.is_some_and(|opener| opener.can_access))).count(), - "targetWindowNameCount": self.page_targets.iter() - .filter(|target| self.web_contents_for_target(target.target_id()).is_some_and(|contents| contents.window.name.is_some())).count(), + "targetCanAccessOpenerCount": self.browser_context.web_contents_window_counts().1, + "targetWindowNameCount": self.browser_context.web_contents_window_counts().2, "defaultDocumentStartScriptCount": self.default_document_start_scripts.len(), "domRemoteObjectNodeCacheCount": active_target .map_or(0, |target| target.dom_remote_object_node_cache.len()), @@ -806,7 +613,7 @@ impl BrowserContext { "estimatedDocumentIsolateCount": estimated_document_isolate_count, "sharedWorkerTargetCount": self.shared_worker_targets.len(), "serviceWorkerTargetCount": self.service_worker_targets.len(), - "browserContextRuntime": self.renderer_runtime().moli_memory_diagnostics(), + "browserContextRuntime": self.browser_context.renderer_memory_diagnostics(), }, "runtimeSession": runtime_session_diagnostics, "pageSession": page_session_diagnostics, @@ -821,55 +628,57 @@ impl BrowserContext { } fn target_owner_diagnostics(&self, target: &PageAgentHost) -> Value { - let navigation = self - .web_contents_for_target(target.target_id()) - .expect("live WebContents") - .navigation(); - let initial = navigation.initial_empty_document_state().map(|document| { - let creator = document.creator().map(|creator| json!({ + let handle = self + .web_contents_handle_for_target(target.target_id()) + .expect("live WebContents"); + let initial = + self.browser_context + .web_contents_initial_document_state(handle) + .expect("live WebContents") + .map(|document| { + let creator = document.creator().map(|creator| json!({ "targetId": self.page_targets.iter() .find(|target| target.web_contents_id() == creator.web_contents_id()) .map(PageAgentHost::target_id), "securityOrigin": creator.security_origin(), "secureContextType": creator.secure_context_type(), })); - json!({ - "targetId": target.target_id(), - "initialUrl": document.initial_url(), - "creator": creator, - "materialized": document.materialized(), - "exited": document.exited(), - "pendingCrossDocumentNavigation": navigation.initial_empty_document_pending_cross_document_navigation(), - "isOnInitialEmptyDocument": document.is_on_initial_empty_document(), - }) - }); + json!({ + "targetId": target.target_id(), + "initialUrl": document.initial_url(), + "creator": creator, + "materialized": document.materialized(), + "exited": document.exited(), + "pendingCrossDocumentNavigation": self.browser_context + .initial_document_has_pending_navigation(handle) + .unwrap_or(false), + "isOnInitialEmptyDocument": document.is_on_initial_empty_document(), + }) + }); let mut diagnostics = target.owner_state.moli_memory_diagnostics(); + let window_surface = self + .web_contents_window_surface(moli_core::browser::WebContentsHandle::new( + self.browser_context.id(), + target.web_contents_id(), + )) + .expect("live WebContents"); diagnostics["initialEmptyDocument"] = json!(initial); - diagnostics["windowSurfaceState"] = json!( - self.target_window_surface(target.target_id()) - .expect("live WebContents") - .state - .label() - ); + diagnostics["windowSurfaceState"] = json!(window_surface.state.label()); diagnostics["targetCrashed"] = json!(self.target_is_crashed(target.target_id())); diagnostics["isDefault"] = json!( target.owner_state.is_default() - && navigation.is_default() - && !self.target_is_crashed(target.target_id()) && self - .target_window_surface(target.target_id()) - .expect("live WebContents") - == super::WindowSurface::default() + .browser_context + .navigation_is_default(handle) + .unwrap_or(false) + && !self.target_is_crashed(target.target_id()) + && window_surface == super::WindowSurface::default() ); diagnostics } pub(crate) fn loaded_document_page_count(&self) -> usize { - self.physical - .web_contents - .values() - .filter(|contents| contents.main_frame.current_document.is_some()) - .count() + self.browser_context.loaded_document_count() } pub(crate) fn pending_document_page_build_count(&self) -> usize { @@ -886,13 +695,14 @@ impl BrowserContext { &self, target_id: &str, ) -> Result<(), String> { - let Some(contents) = self.web_contents_for_target(target_id) else { + let Some(handle) = self.web_contents_handle_for_target(target_id) else { return Ok(()); }; - if contents - .navigation() - .has_materialized_current_initial_empty_document() - && contents.main_frame.current_document.is_none() + if self + .browser_context + .web_contents_initial_document_state(handle)? + .is_some_and(|initial| initial.is_on_initial_empty_document() && initial.materialized()) + && !self.browser_context.has_loaded_document(handle) { return Err(format!( "TargetInitialEmptyDocumentMissingPage: target {target_id} has materialized current initial empty document without loaded Page" @@ -903,17 +713,20 @@ impl BrowserContext { #[cfg(test)] pub(crate) fn can_install_current_initial_empty_document_page(&self, target_id: &str) -> bool { - let Some(target) = self.page_target(target_id) else { + let Some(handle) = self.web_contents_handle_for_target(target_id) else { return false; }; - !self.target_has_loaded_page(target.target_id()) + !self.target_has_loaded_page(target_id) + && !self + .browser_context + .has_pending_document_navigation(handle) + .unwrap_or(true) && self - .web_contents_for_target(target_id) - .is_some_and(|contents| { - contents - .navigation() - .can_install_current_initial_empty_document_page() - }) + .browser_context + .is_on_initial_document(handle) + .ok() + .flatten() + .unwrap_or(true) } /// Reports whether one exact Page target is still on its materialized @@ -926,24 +739,26 @@ impl BrowserContext { let Some(target) = self.page_target(target_id) else { return false; }; - let navigation = &self - .web_contents_for_target(target.target_id()) - .expect("live WebContents") - .navigation(); - let Some(initial_url) = navigation.initial_empty_document_url_if_current() else { + let handle = self + .web_contents_handle_for_target(target.target_id()) + .expect("live WebContents"); + let Some(initial_url) = self + .browser_context + .initial_document_url(handle) + .ok() + .flatten() + else { return false; }; target.target_url() != initial_url - && !navigation.initial_empty_document_pending_cross_document_navigation() + && !self + .browser_context + .initial_document_has_pending_navigation(handle) + .unwrap_or(false) } pub(crate) fn loaded_document_renderer_owner_ids_for_diagnostics(&self) -> HashSet { - self.physical - .web_contents - .values() - .filter_map(|contents| contents.main_frame.current_document.as_ref()) - .map(|document| document.page.renderer_owner_local_host_id().as_u64()) - .collect() + self.browser_context.loaded_document_renderer_owner_ids() } pub(crate) fn pending_document_renderer_owner_ids_for_diagnostics(&self) -> HashSet { @@ -957,18 +772,8 @@ impl BrowserContext { } pub(crate) fn dedicated_worker_running_worker_isolate_count_for_diagnostics(&self) -> usize { - self.loaded_pages_for_diagnostics() - .map( - moli_core::page::Page::dedicated_worker_running_worker_isolate_count_for_diagnostics, - ) - .sum() - } - - fn loaded_pages_for_diagnostics(&self) -> impl Iterator { - self.loaded_page().into_iter().chain( - self.background_targets() - .filter_map(|target| self.loaded_page_for_target(target.target_id())), - ) + self.browser_context + .dedicated_worker_running_isolate_count() } pub(crate) fn page_target_pending_inspector_await_count_for_diagnostics(&self) -> usize { @@ -979,10 +784,7 @@ impl BrowserContext { } pub(crate) fn has_pending_javascript_dialog(&self) -> bool { - self.physical - .web_contents - .values() - .any(|contents| !contents.javascript_dialogs.is_empty()) + self.browser_context.has_pending_javascript_dialog() } pub(crate) fn page_target_with_pending_inspector_await_count_for_diagnostics(&self) -> usize { @@ -1031,8 +833,7 @@ impl BrowserContext { pub(crate) fn shared_worker_runtime_diagnostics_for_diagnostics( &self, ) -> RendererSharedWorkerRuntimeDiagnostics { - self.renderer_runtime() - .shared_worker_runtime_diagnostics_for_diagnostics() + self.browser_context.shared_worker_runtime_diagnostics() } #[cfg(test)] @@ -1065,11 +866,8 @@ impl BrowserContext { } pub(crate) fn accepts_pending_document_navigation_event(&self, token: &NavigationId) -> bool { - self.physical.web_contents.values().any(|contents| { - contents - .navigation() - .accepts_pending_document_navigation_event(token) - }) + self.browser_context + .accepts_any_pending_navigation_event(token) } #[cfg(any(test, feature = "test-support"))] @@ -1077,11 +875,8 @@ impl BrowserContext { &self, token: &NavigationId, ) -> Option { - self.physical.web_contents.values().find_map(|contents| { - contents - .navigation() - .document_navigation_cancellation_handle(token) - }) + self.browser_context + .document_navigation_cancellation_handle_for_test(token) } pub(crate) fn arm_background_navigation_completion( @@ -1089,40 +884,23 @@ impl BrowserContext { token: &NavigationId, additional_cancellation: Option, ) -> bool { - let Some(contents) = self.physical.web_contents.values_mut().find(|contents| { - contents - .navigation() - .accepts_pending_document_navigation_event(token) - }) else { - if let Some(cancellation) = additional_cancellation { - cancellation.cancel(); - } - return false; - }; - contents.arm_background_navigation_completion(token, additional_cancellation) + self.browser_context + .arm_background_navigation_completion(token, additional_cancellation) } pub(crate) fn settle_background_navigation_completion(&mut self, token: &NavigationId) -> bool { - self.physical - .web_contents - .values_mut() - .any(|contents| contents.settle_background_navigation_completion(token)) + self.browser_context + .settle_background_navigation_completion(token) } pub(crate) fn has_inflight_background_navigation(&self) -> bool { - self.physical - .web_contents - .values() - .any(|contents| contents.navigation().has_inflight_background_navigation()) + self.browser_context.has_inflight_background_navigation() } #[cfg(test)] pub(crate) fn accepts_document_body_completion_event(&self, token: &NavigationId) -> bool { - self.physical.web_contents.values().any(|contents| { - contents - .navigation() - .accepts_document_body_completion_event(token) - }) + self.browser_context + .accepts_any_document_body_completion_for_test(token) } pub(crate) fn clear_pending_document_navigation_for_target_if_matches( @@ -1178,8 +956,7 @@ impl BrowserContext { origin: &url::Url, options: SiteDataClearOptions, ) -> Result<(), String> { - self.physical - .storage_partition + self.browser_context .clear_site_data_for_origin(origin, options) } @@ -1188,28 +965,31 @@ impl BrowserContext { storage_key: &moli_storage_key::MoliStorageKey, options: SiteDataClearOptions, ) -> Result<(), String> { - self.physical - .storage_partition + self.browser_context .clear_site_data_for_storage_key(storage_key, options) } pub(crate) fn clear_http_cache(&self) -> Result<(), String> { - self.physical.storage_partition.clear_http_cache() + self.browser_context.clear_http_cache() } #[cfg(test)] - pub(crate) fn http_cache_configuration_for_test( - &self, - ) -> (Option<&std::path::Path>, Option) { - self.physical.storage_partition.http_cache_configuration() + pub(crate) fn http_cache_configuration_for_test(&self) -> (Option, Option) { + self.browser_context.http_cache_configuration_for_test() } pub(crate) fn snapshot_cookies(&self) -> Vec { - self.physical - .storage_partition - .cookie_store() - .lock() - .cookies() + self.browser_context.snapshot_cookies() + } + + pub(crate) fn store_cookie( + &self, + cookie: StoredCookie, + request_url: Option<&url::Url>, + source: CookieSource, + ) -> moli_cookie_jar::StoredCookieSetReport { + self.browser_context + .store_cookie(cookie, request_url, source) } #[cfg(test)] @@ -1224,38 +1004,30 @@ impl BrowserContext { } #[cfg(test)] - pub(crate) fn cookie_store_for_test(&self) -> &SharedBrowserCookieStore { - self.physical.storage_partition.cookie_store() + pub(crate) fn cookie_store_for_test(&self) -> SharedBrowserCookieStore { + self.browser_context.cookie_store_for_test() } #[cfg(test)] - pub(crate) fn web_storage_store_for_test(&self) -> &SharedWebStorageStore { - self.physical.storage_partition.web_storage_store() + pub(crate) fn web_storage_store_for_test(&self) -> SharedWebStorageStore { + self.browser_context.web_storage_store_for_test() } #[cfg(test)] - pub(crate) fn session_storage_store_for_test(&self) -> &SharedWebStorageStore { - self.physical - .web_contents - .get( - &self - .physical - .selected_web_contents_id() - .expect("active WebContents"), - ) - .expect("selected WebContents") - .session_storage - .store() + pub(crate) fn session_storage_store_for_test(&self) -> SharedWebStorageStore { + self.browser_context + .selected_session_storage_store_for_test() + .expect("active WebContents") } #[cfg(test)] - pub(crate) fn indexed_db_manager_for_test(&self) -> &SharedIndexedDbManager { - self.physical.storage_partition.indexed_db_manager() + pub(crate) fn indexed_db_manager_for_test(&self) -> SharedIndexedDbManager { + self.browser_context.indexed_db_manager_for_test() } #[cfg(test)] - pub(crate) fn storage_bucket_store_for_test(&self) -> &SharedStorageBucketStore { - self.physical.storage_partition.storage_bucket_store() + pub(crate) fn storage_bucket_store_for_test(&self) -> SharedStorageBucketStore { + self.browser_context.storage_bucket_store_for_test() } #[cfg(test)] @@ -1263,9 +1035,8 @@ impl BrowserContext { &mut self, storage_bucket_store: SharedStorageBucketStore, ) { - self.physical - .storage_partition - .replace_storage_bucket_store(storage_bucket_store); + self.browser_context + .replace_storage_bucket_store_for_test(storage_bucket_store); } #[cfg(test)] @@ -1273,9 +1044,7 @@ impl BrowserContext { &self, cookie: StoredCookie, ) -> moli_cookie_jar::StoredCookieSetReport { - self.with_cookie_store_mut(|store| { - store.upsert_with_request_url_report(cookie, None, CookieSource::Cdp) - }) + self.store_cookie(cookie, None, CookieSource::Management) } #[cfg(test)] @@ -1307,13 +1076,18 @@ impl BrowserContext { url_host: Option<&str>, partition_key: Option<&moli_cookie_jar::StoredCookiePartitionKey>, ) { - let mut cookie_store = self.physical.storage_partition.cookie_store().lock(); - cookie_store.delete_cookies_with_partition_key(name, domain, path, url_host, partition_key); + self.browser_context.delete_cookies_with_partition_key( + name, + domain, + path, + url_host, + partition_key, + ); } pub(crate) fn active_target_id(&self) -> Option<&str> { self.page_targets - .get_for_web_contents(self.physical.selected_web_contents_id()?) + .get_for_web_contents(self.browser_context.selected_web_contents_id()?) .map(PageAgentHost::target_id) } @@ -1325,34 +1099,32 @@ impl BrowserContext { &self, ) -> Option { self.page_targets - .active(self.physical.selected_web_contents_id()) - .and_then(|host| { - self.browser_identity_override_for_target(host.target_id()) - .cloned() - }) + .active(self.browser_context.selected_web_contents_id()) + .and_then(|host| self.browser_identity_override_for_target(host.target_id())) .or_else(|| self.default_browser_identity_override_owned()) } - pub(crate) fn reported_active_user_agent_override(&self) -> Option<&str> { + pub(crate) fn reported_active_user_agent_override(&self) -> Option { self.page_targets - .active(self.physical.selected_web_contents_id()) + .active(self.browser_context.selected_web_contents_id()) .and_then(PageAgentHost::reported_user_agent_override) + .map(str::to_owned) .or_else(|| { self.default_browser_identity_override() - .map(moli_browser_profile::BrowserIdentityProfile::user_agent) + .map(|identity| identity.user_agent().to_owned()) }) } pub(crate) fn default_browser_identity_override( &self, - ) -> Option<&moli_browser_profile::BrowserIdentityProfile> { - self.physical.browser_identity_override.as_ref() + ) -> Option { + self.browser_context.browser_identity_override() } pub(crate) fn default_browser_identity_override_owned( &self, ) -> Option { - self.default_browser_identity_override().cloned() + self.default_browser_identity_override() } pub(crate) fn set_default_user_agent_override( @@ -1361,31 +1133,28 @@ impl BrowserContext { fallback: &moli_browser_profile::BrowserIdentityProfile, ) { self.browser_identity_inputs.user_agent = user_agent; - self.physical.browser_identity_override = - self.browser_identity_inputs.materialize(fallback); + self.browser_context + .set_browser_identity_override(self.browser_identity_inputs.materialize(fallback)); } pub(crate) fn set_default_locale_override( &mut self, locale: Option, fallback: &moli_browser_profile::BrowserIdentityProfile, - ) -> Result<(), &'static str> { - self.physical - .environment_owner - .set_locale(locale.as_deref())?; + ) -> Result<(), String> { + self.browser_context + .set_default_locale_override(locale.clone())?; self.browser_identity_inputs.accept_language = locale; - self.physical.browser_identity_override = - self.browser_identity_inputs.materialize(fallback); + self.browser_context + .set_browser_identity_override(self.browser_identity_inputs.materialize(fallback)); Ok(()) } pub(crate) fn set_default_timezone_override( &mut self, timezone: Option, - ) -> Result<(), &'static str> { - self.physical - .environment_owner - .set_timezone(timezone.as_deref()) + ) -> Result<(), String> { + self.browser_context.set_default_timezone_override(timezone) } #[cfg(test)] @@ -1394,86 +1163,84 @@ impl BrowserContext { identity: moli_browser_profile::BrowserIdentityProfile, ) { self.browser_identity_inputs = BrowserIdentityOverrideInputs::from_profile(&identity); - self.physical.browser_identity_override = Some(identity); - } - - pub(crate) fn effective_active_network_conditions(&self) -> Option { - self.page_targets - .active(self.physical.selected_web_contents_id()) - .and_then(|host| { - self.target_emulation_policy(host.target_id()) - .expect("live WebContents") - .network_conditions - }) - .or(self.emulation_defaults().network_conditions) - .or(self.global_network_conditions) + self.browser_context + .set_browser_identity_override(Some(identity)); } pub(crate) fn effective_active_tls_verify_host_override(&self) -> Option { self.page_targets - .active(self.physical.selected_web_contents_id()) + .active(self.browser_context.selected_web_contents_id()) .and_then(|host| self.tls_verify_host_override_for_target(host.target_id())) - .or(self.physical.network_policy.tls_verify_host) + .or(self.browser_context.network_policy().tls_verify_host) } - // Value-only migration boundary, replaced by typed Browser operations at - // Commit 22. Never expose the physical Context for mutation through it. - pub(crate) fn emulation_defaults(&self) -> &ContextEmulationDefaults { - &self.physical.emulation_defaults + pub(crate) fn emulation_defaults(&self) -> ContextEmulationDefaults { + self.browser_context.emulation_defaults() } pub(crate) fn set_default_network_conditions( &mut self, conditions: Option, ) { - self.physical.emulation_defaults.network_conditions = conditions; + self.browser_context + .set_default_network_conditions(conditions); } pub(crate) fn set_default_geolocation_override( &mut self, geolocation: Option, ) { - self.physical.emulation_defaults.geolocation = geolocation; + self.browser_context + .set_default_geolocation_override(geolocation); } pub(crate) fn set_default_device_metrics(&mut self, metrics: EmulatedDeviceMetrics) -> bool { - self.physical - .emulation_defaults - .device_metrics - .replace(metrics) - .is_some() + self.browser_context.set_default_device_metrics(metrics) } - pub(crate) fn network_policy(&self) -> &ContextNetworkPolicy { - &self.physical.network_policy + pub(crate) fn network_policy(&self) -> ContextNetworkPolicy { + self.browser_context.network_policy() } pub(crate) fn set_default_extra_headers(&mut self, headers: moli_fetch::RequestHeaders) { - self.physical.network_policy.extra_headers = headers; + self.browser_context.set_default_extra_headers(headers); } pub(crate) fn set_network_policy(&mut self, policy: ContextNetworkPolicy) { - self.physical.network_policy = policy; + self.browser_context.set_network_policy(policy); } pub(crate) fn set_tls_verify_host_override(&mut self, enabled: bool) { - self.physical.network_policy.tls_verify_host = Some(enabled); + self.browser_context + .set_context_tls_verify_host_override(enabled); } #[cfg(test)] pub(crate) fn set_http_proxy_override_for_test(&mut self, proxy: Option) { - self.physical.network_policy.http_proxy = proxy; + self.browser_context.set_http_proxy_override_for_test(proxy); } - pub(crate) fn effective_active_network_offline(&self) -> bool { - self.effective_active_network_conditions() + pub(crate) fn effective_active_network_offline( + &self, + global_network_conditions: Option, + ) -> bool { + self.page_targets + .active(self.browser_context.selected_web_contents_id()) + .and_then(|host| { + self.target_emulation_policy(host.target_id()) + .expect("live WebContents") + .network_conditions + }) + .or(self.emulation_defaults().network_conditions) + .or(global_network_conditions) .is_some_and(|conditions| !conditions.navigator_online()) } - pub(crate) fn effective_network_conditions_for_target( + pub(crate) fn effective_network_offline_for_target( &self, target_id: &str, - ) -> Option { + global_network_conditions: Option, + ) -> bool { self.page_target(target_id) .and_then(|state| { self.target_emulation_policy(state.target_id()) @@ -1481,17 +1248,13 @@ impl BrowserContext { .network_conditions }) .or(self.emulation_defaults().network_conditions) - .or(self.global_network_conditions) - } - - pub(crate) fn effective_network_offline_for_target(&self, target_id: &str) -> bool { - self.effective_network_conditions_for_target(target_id) + .or(global_network_conditions) .is_some_and(|conditions| !conditions.navigator_online()) } pub(crate) fn has_active_target(&self) -> bool { self.page_targets - .active(self.physical.selected_web_contents_id()) + .active(self.browser_context.selected_web_contents_id()) .is_some() } @@ -1506,13 +1269,16 @@ impl BrowserContext { target_id.clone(), None, super::identity::TargetIdentityState::about_blank(), - WebContents::default(), + moli_core::browser::WebContentsCreation::default(), super::page_slot::TargetPageSlot::default(), ); debug_assert!(inserted, "new page target id must be unique"); } - let selected = self.select_registered_page_target(&target_id); - debug_assert!(selected, "registered page target must be selectable"); + let handle = self + .web_contents_handle_for_target(&target_id) + .expect("registered page target must have WebContents"); + self.select_registered_web_contents(handle) + .expect("registered WebContents must remain selectable"); } pub(crate) fn rekey_active_target(&mut self, target_id: impl Into) -> bool { @@ -1524,7 +1290,7 @@ impl BrowserContext { pub(crate) fn active_session_id(&self) -> Option<&str> { self.page_targets - .active(self.physical.selected_web_contents_id())? + .active(self.browser_context.selected_web_contents_id())? .session_id() } @@ -1558,7 +1324,7 @@ impl BrowserContext { #[cfg(test)] pub(crate) fn detach_active_session(&mut self) -> Option { self.page_targets - .active_mut(self.physical.selected_web_contents_id())? + .active_mut(self.browser_context.selected_web_contents_id())? .detach_session() } @@ -1584,13 +1350,3 @@ impl BrowserContext { .set_secure_context_type(secure_context_type); } } - -pub(crate) fn seed_initial_cookies( - cookie_store: &SharedBrowserCookieStore, - initial_cookies: impl IntoIterator, -) { - let mut store = cookie_store.lock(); - for cookie in initial_cookies { - let _ = store.upsert_with_request_url_report(cookie, None, CookieSource::Cdp); - } -} diff --git a/moli-protocol/src/conn/state/browser_context/collection.rs b/moli-protocol/src/conn/state/browser_context/collection.rs index ab2161c798..1a86506a30 100644 --- a/moli-protocol/src/conn/state/browser_context/collection.rs +++ b/moli-protocol/src/conn/state/browser_context/collection.rs @@ -1,12 +1,9 @@ use super::BrowserContext; -use crate::conn::state::{ - PageAgentHost, TargetIdentityState, - page_slot::TargetPageSlot, - web_contents::{ - EmulationPolicy, EmulationPolicyChange, WebContents, WindowSurface, WindowSurfaceState, - }, +use crate::conn::state::{PageAgentHost, TargetIdentityState, page_slot::TargetPageSlot}; +use moli_core::browser::web_contents::{ + EmulationPolicy, EmulationPolicyChange, WindowSurface, WindowSurfaceState, }; -use moli_core::browser::WebContentsId; +use moli_core::browser::{WebContentsCreation, WebContentsHandle, WebContentsId}; #[cfg(test)] mod tests; @@ -23,18 +20,20 @@ impl BrowserContext { self.set_document_id_for_test_for_target(&target_id, raw) } - /// Move the physical Document out only in capability-independence tests. - /// Deliberately leave its inspection binding installed: tests must prove - /// that dispatch can start and finish without a borrowed Browser owner. + /// Capture only the exact Browser document capability used by inspection + /// independence tests. The physical Document remains on its Browser owner. #[cfg(test)] - pub(in crate::conn) fn take_document_host_for_inspection_test( - &mut self, + pub(in crate::conn) fn inspection_document_handle_for_test( + &self, target_id: &str, - ) -> Option { - self.web_contents_for_target_mut(target_id)? - .main_frame - .current_document - .take() + ) -> Option<( + moli_core::browser::BrowserContextHandle, + moli_core::browser::DocumentHandle, + )> { + Some(( + self.browser_context.clone(), + self.document_handle_for_target(target_id)?, + )) } #[cfg(test)] @@ -49,7 +48,7 @@ impl BrowserContext { target_id, primary_session_id, identity, - WebContents::default(), + WebContentsCreation::default(), page_projection, ) } @@ -74,38 +73,32 @@ impl BrowserContext { target_id: String, primary_session_id: Option, identity: TargetIdentityState, - mut contents: WebContents, + creation: WebContentsCreation, page_projection: TargetPageSlot, ) -> bool { if self.page_targets.get(&target_id).is_some() { return false; } - let id = contents.id(); - let mut projection = PageAgentHost::new( + let (handle, main_frame) = self + .browser_context + .create_web_contents(creation) + .expect("new WebContents identity must be unique"); + let id = handle.id(); + let projection = PageAgentHost::new( target_id, primary_session_id, identity, id, - contents.main_frame.id(), + main_frame, page_projection, ); #[cfg(test)] + let mut projection = projection; + #[cfg(test)] if self.page_targets.is_empty() { projection.document_cookie_manager_surface = self.default_document_cookie_manager_surface.clone(); } - projection.base_network_request_policy = - crate::conn::state::session::BaseNetworkRequestPolicy::with_cache_disabled( - self.global_cache_disabled, - ); - contents.network_request_policy.cache_disabled = self.global_cache_disabled; - if let Some(config) = self.page_navigation_runtime_config() { - contents.install_navigation_engine(self.new_page_navigation_engine(config)); - } - assert!( - self.physical.web_contents.insert(id, contents).is_none(), - "WebContents identity must be unique" - ); let inserted = self.page_targets.insert(projection); debug_assert!( inserted, @@ -114,33 +107,61 @@ impl BrowserContext { true } - pub(super) fn select_registered_page_target(&mut self, target_id: &str) -> bool { - let Some(id) = self - .page_targets - .get(target_id) - .map(PageAgentHost::web_contents_id) - else { - return false; - }; - self.physical.select_web_contents(id) + pub(super) fn select_registered_web_contents( + &mut self, + handle: WebContentsHandle, + ) -> Result<(), String> { + if !self.browser_context.contains_web_contents(handle) { + return Err("WebContents unavailable".into()); + } + let selected = self.browser_context.select_web_contents(handle.id()); + debug_assert!(selected, "registered WebContents must be selectable"); + Ok(()) } pub(crate) fn selected_web_contents_id(&self) -> Option { - self.physical.selected_web_contents_id() + self.browser_context.selected_web_contents_id() + } + + pub(crate) fn selected_web_contents_handle(&self) -> Option { + self.browser_context.selected_web_contents_handle() + } + + pub(crate) fn web_contents_handle_for_target( + &self, + target_id: &str, + ) -> Option { + Some(WebContentsHandle::new( + self.browser_context.id(), + self.page_targets.get(target_id)?.web_contents_id(), + )) + } + + pub(crate) fn web_contents_handle_for_window_id( + &self, + window_id: u64, + ) -> Option { + self.browser_context + .web_contents_handle_for_window_id(window_id) } pub(crate) fn target_is_crashed(&self, target_id: &str) -> bool { - self.web_contents_for_target(target_id) - .is_some_and(|contents| contents.crashed) + self.web_contents_handle_for_target(target_id) + .is_some_and(|handle| { + self.browser_context + .web_contents_is_crashed(handle) + .unwrap_or(false) + }) } pub(crate) fn target_initial_empty_document_state( &self, target_id: &str, - ) -> Option<&crate::conn::state::InitialDocument> { - self.web_contents_for_target(target_id)? - .navigation() - .initial_empty_document_state() + ) -> Option { + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context + .web_contents_initial_document_state(handle) + .ok()? } pub(crate) fn target_initial_empty_document_loader_id_if_current( @@ -157,9 +178,11 @@ impl BrowserContext { target_id: &str, change: &moli_core::RendererDocumentTitleChanged, ) -> Option { + let handle = self.web_contents_handle_for_target(target_id)?; let changed = self - .web_contents_for_target_mut(target_id)? - .commit_document_title(change)?; + .browser_context + .commit_document_title(handle, change) + .ok()??; self.page_targets .get_mut(target_id)? .owner_state @@ -168,40 +191,66 @@ impl BrowserContext { } pub(crate) fn set_target_crash_state(&mut self, target_id: &str, crashed: bool) { - if let Some(contents) = self.web_contents_for_target_mut(target_id) { - contents.crashed = crashed; + if let Some(handle) = self.web_contents_handle_for_target(target_id) { + let _ = self + .browser_context + .set_web_contents_crashed(handle, crashed); } } - pub(crate) fn target_window_surface(&self, target_id: &str) -> Option { - Some(self.web_contents_for_target(target_id)?.window.surface) + pub(crate) fn web_contents_window_surface( + &self, + handle: WebContentsHandle, + ) -> Result { + self.browser_context.web_contents_window_surface(handle) } - pub(crate) fn set_target_window_surface_state( + pub(crate) fn update_web_contents_window_surface( &mut self, - target_id: &str, - state: WindowSurfaceState, - ) { - if let Some(contents) = self.web_contents_for_target_mut(target_id) { - contents.window.surface.state = state; - } - } - - pub(crate) fn set_target_window_surface_geometry( - &mut self, - target_id: &str, + handle: WebContentsHandle, + state: Option, width: Option, height: Option, x: Option, y: Option, - ) { - if let Some(contents) = self.web_contents_for_target_mut(target_id) { - contents.window.surface.set_geometry(width, height, x, y); - } + ) -> Result<(), String> { + self.browser_context + .update_web_contents_window_surface(handle, state, width, height, x, y) } - pub(crate) fn target_emulation_policy(&self, target_id: &str) -> Option<&EmulationPolicy> { - Some(&self.web_contents_for_target(target_id)?.emulation_policy) + pub(crate) fn set_web_contents_window_name( + &mut self, + handle: WebContentsHandle, + name: Option, + ) -> Result<(), String> { + self.browser_context + .set_web_contents_window_name(handle, name) + } + + pub(crate) fn set_web_contents_opener( + &mut self, + handle: WebContentsHandle, + opener: Option, + can_access: bool, + ) -> Result<(), String> { + self.browser_context + .set_web_contents_opener(handle, opener, can_access) + } + + pub(crate) fn set_web_contents_network_offline( + &mut self, + handle: WebContentsHandle, + offline: bool, + ) -> Result<(), String> { + self.browser_context + .set_web_contents_network_offline(handle, offline) + } + + pub(crate) fn target_emulation_policy(&self, target_id: &str) -> Option { + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context + .web_contents_emulation_policy(handle) + .ok() } pub(crate) fn apply_target_emulation_policy_change( @@ -209,8 +258,10 @@ impl BrowserContext { target_id: &str, change: EmulationPolicyChange, ) { - if let Some(contents) = self.web_contents_for_target_mut(target_id) { - contents.emulation_policy.apply(change); + if let Some(handle) = self.web_contents_handle_for_target(target_id) { + let _ = self + .browser_context + .apply_web_contents_emulation_policy_change(handle, change); } } @@ -219,8 +270,10 @@ impl BrowserContext { target_id: &str, changes: Vec, ) { - if let Some(contents) = self.web_contents_for_target_mut(target_id) { - contents.emulation_policy.apply_changes(changes); + if let Some(handle) = self.web_contents_handle_for_target(target_id) { + let _ = self + .browser_context + .apply_web_contents_emulation_policy_changes(handle, changes); } } } diff --git a/moli-protocol/src/conn/state/browser_context/collection/tests.rs b/moli-protocol/src/conn/state/browser_context/collection/tests.rs index aacc8ffe28..deb1a70e6c 100644 --- a/moli-protocol/src/conn/state/browser_context/collection/tests.rs +++ b/moli-protocol/src/conn/state/browser_context/collection/tests.rs @@ -1,9 +1,6 @@ use super::*; -use crate::conn::state::{DevToolsRendererChannelError, WindowOpener}; -use moli_core::{ - browser::{DocumentRetirement, NavigationId, RendererPageResidenceIdentity}, - runtime::{Browser, BrowserConfig, NavigationEngine}, -}; +use crate::conn::state::DevToolsRendererChannelError; +use moli_core::browser::{DocumentRetirement, NavigationId}; use std::{ future::Future, task::{Context, Poll, Waker}, @@ -14,69 +11,108 @@ fn window_and_crash_state_outlive_the_devtools_projection() { let mut context = BrowserContext::new("BID-window".into()); context.set_active_target_id("TID-window"); context.attach_active_session("SID-window"); - context.set_target_window_surface_state("TID-window", WindowSurfaceState::Minimized); - context.set_target_window_surface_geometry( - "TID-window", - Some(800), - Some(600), - Some(-10), - Some(20), - ); + let handle = context + .web_contents_handle_for_target("TID-window") + .unwrap(); + context + .update_web_contents_window_surface( + handle, + Some(WindowSurfaceState::Minimized), + Some(800), + Some(600), + Some(-10), + Some(20), + ) + .unwrap(); context.set_target_crash_state("TID-window", true); let id = context.selected_web_contents_id().unwrap(); - let surface = context.target_window_surface("TID-window").unwrap(); + let surface = context.web_contents_window_surface(handle).unwrap(); let opener = WebContentsId::allocate(); - let window = &mut context.physical.web_contents.get_mut(&id).unwrap().window; - window.name = Some("report".into()); - window.opener = Some(WindowOpener { - web_contents_id: opener, - can_access: true, - }); + context + .set_web_contents_window_name(handle, Some("report".into())) + .unwrap(); + context + .browser_context + .set_web_contents_opener_for_test(handle, opener, true) + .unwrap(); let target = context.page_targets.get_mut("TID-window").unwrap(); target.opener_frame_id = Some("FRAME-opener".into()); assert_eq!(target.detach_session().as_deref(), Some("SID-window")); assert!(context.target_is_crashed("TID-window")); - assert_eq!(context.target_window_surface("TID-window"), Some(surface)); + assert_eq!(context.web_contents_window_surface(handle), Ok(surface)); drop(context.page_targets.remove("TID-window").unwrap()); assert_eq!(context.selected_web_contents_id(), Some(id)); - let contents = context.physical.web_contents.get(&id).unwrap(); - assert_eq!(contents.id(), id); - assert!(contents.crashed); - assert_eq!(contents.window.surface, surface); - assert_eq!(contents.window.name.as_deref(), Some("report")); - let relationship = contents.window.opener.unwrap(); - assert_eq!(relationship.web_contents_id, opener); - assert!(relationship.can_access); + assert!( + context + .browser_context + .web_contents_is_crashed(handle) + .unwrap() + ); + assert_eq!( + context.browser_context.web_contents_window_surface(handle), + Ok(surface) + ); + assert_eq!( + context.browser_context.web_contents_window_name(handle), + Ok(Some("report".to_owned())) + ); + assert_eq!( + context.browser_context.web_contents_opener(handle), + Ok(Some((opener, true))) + ); context.set_active_target_id("TID-window"); let replacement_id = context.selected_web_contents_id().unwrap(); assert_ne!(replacement_id, id); - let replacement = context.physical.web_contents.get(&replacement_id).unwrap(); - assert!(!replacement.crashed); - assert_eq!(replacement.window.surface, WindowSurface::default()); - assert!(replacement.window.name.is_none()); - assert!(replacement.window.opener.is_none()); - assert!(context.physical.web_contents.contains_key(&id)); + let replacement = context.selected_web_contents_handle().unwrap(); + assert!( + !context + .browser_context + .web_contents_is_crashed(replacement) + .unwrap() + ); + assert_eq!( + context + .browser_context + .web_contents_window_surface(replacement), + Ok(WindowSurface::default()) + ); + assert_eq!( + context + .browser_context + .web_contents_window_name(replacement), + Ok(None) + ); + assert_eq!( + context.browser_context.web_contents_opener(replacement), + Ok(None) + ); + assert!(context.browser_context.contains_web_contents(handle)); } #[tokio::test] async fn projection_drop_preserves_the_contexts_page_engine_selection_and_document_lifetime() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let page = browser - .fetch("data:text/html,Browser owned") - .await - .unwrap(); - let residence = RendererPageResidenceIdentity::from_page(&page); - let mut context = BrowserContext::new("BID-physical-owner".into()); + let mut conn = crate::test_support::connection(); + let mut context = conn.new_browser_context_fixture_for_test("BID-physical-owner"); context.set_active_target_id("TID-physical-owner"); context.bind_page_navigation_engines(Default::default(), None); - assert!(context.replace_loaded_page(Some(page)).is_none()); + conn.install_browser_context_fixture_for_test(context); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,Browser owned", + None, + ) + .await; + let mut context = conn.browser_context.take().unwrap(); + let residence = context + .target_renderer_page_residence_identity("TID-physical-owner") + .unwrap(); let id = context.selected_web_contents_id().unwrap(); + let handle = context.selected_web_contents_handle().unwrap(); let document_id = context.target_document_id("TID-physical-owner").unwrap(); let engine = context - .page_navigation_engine("TID-physical-owner") - .unwrap() as *const NavigationEngine; + .page_navigation_renderer_owner_id("TID-physical-owner") + .unwrap(); let observer = context .document_lifetime_observer_for_target("TID-physical-owner") .unwrap(); @@ -96,19 +132,29 @@ async fn projection_drop_preserves_the_contexts_page_engine_selection_and_docume drop(context.page_targets.remove("TID-physical-owner").unwrap()); assert_eq!(context.selected_web_contents_id(), Some(id)); - let contents = context.physical.web_contents.get(&id).unwrap(); - assert_eq!(contents.navigation_history_snapshot(), history); assert_eq!( - contents.navigation_engine_for_test().unwrap() as *const NavigationEngine, + context.browser_context.navigation_history_snapshot(handle), + Ok(history) + ); + assert_eq!( + context + .browser_context + .web_contents_navigation_renderer_owner_id(handle) + .unwrap(), engine ); - let document = contents.main_frame.current_document.as_ref().unwrap(); - assert_eq!(document.id, document_id); + let document = moli_core::browser::DocumentHandle::new(handle, document_id); assert_eq!( - RendererPageResidenceIdentity::from_page(&document.page), + context + .browser_context + .document_renderer_residence(document) + .unwrap(), residence ); - assert_eq!(document.page.document_title(), "Browser owned"); + assert_eq!( + context.browser_context.document_title(document).unwrap(), + "Browser owned" + ); let mut retired = Box::pin(observer.wait()); assert_eq!( retired @@ -119,7 +165,7 @@ async fn projection_drop_preserves_the_contexts_page_engine_selection_and_docume context.close_all_pages_async().await; assert_eq!(retired.await, DocumentRetirement::Superseded); - assert!(context.physical.web_contents.is_empty()); + assert_eq!(context.browser_context.web_contents_count(), 0); assert!(context.page_targets.is_empty()); assert_eq!(context.selected_web_contents_id(), None); } @@ -130,7 +176,7 @@ fn emulation_policy_survives_projection_drop_and_updates_without_sessions() { context.attach_active_session("SID-primary"); assert!(context.assign_attached_session_to_target("TID-policy", "SID-attached".into())); let id = context.active_page_target().web_contents_id(); - let mut conn = crate::conn::CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.install_browser_context_fixture_for_test(context); for (session, change) in [ ("SID-primary", EmulationPolicyChange::MaxTouchPoints(4)), @@ -148,16 +194,29 @@ fn emulation_policy_survives_projection_drop_and_updates_without_sessions() { let mut context = conn.browser_context.take().unwrap(); drop(context.page_targets.remove("TID-policy").unwrap()); drop(conn); - let contents = context.physical.web_contents.get_mut(&id).unwrap(); - assert_eq!(contents.id(), id); - assert_eq!(contents.emulation_policy.max_touch_points, 2); - assert!(contents.emulation_policy.focus_emulation_enabled); - let snapshot = contents.emulation_policy.clone(); - contents - .emulation_policy - .apply(EmulationPolicyChange::ScriptExecutionDisabled(true)); + let handle = WebContentsHandle::new(context.browser_context_id(), id); + let policy = context + .browser_context + .web_contents_emulation_policy(handle) + .unwrap(); + assert_eq!(policy.max_touch_points, 2); + assert!(policy.focus_emulation_enabled); + let snapshot = policy; + context + .browser_context + .apply_web_contents_emulation_policy_change( + handle, + EmulationPolicyChange::ScriptExecutionDisabled(true), + ) + .unwrap(); assert!(!snapshot.script_execution_disabled); - assert!(contents.emulation_policy.script_execution_disabled); + assert!( + context + .browser_context + .web_contents_emulation_policy(handle) + .unwrap() + .script_execution_disabled + ); } #[tokio::test] @@ -182,10 +241,10 @@ async fn close_retires_projection_waiters_and_channel_before_the_owned_page_tear }; let slot = &context.page_targets.get("TID-close").unwrap().runtime_slot; let dialog_scope = slot.javascript_dialog_scope_observer(); - let id = context.selected_web_contents_id().unwrap(); + let handle = context.web_contents_handle_for_target("TID-close").unwrap(); - let (mut projection, closing) = context.take_page_target_for_close("TID-close").unwrap(); - assert!(!context.physical.web_contents.contains_key(&id)); + let (mut projection, closing) = context.begin_web_contents_close(handle).unwrap(); + assert!(!context.browser_context.contains_web_contents(handle)); assert!(context.page_targets.is_empty()); assert_eq!(context.selected_web_contents_id(), None); assert!(context.target_popup_ids.is_empty()); @@ -208,7 +267,7 @@ async fn close_retires_projection_waiters_and_channel_before_the_owned_page_tear // Cancellation of the teardown future must not resurrect either authority. drop(closing); assert!(build.materialize().await.is_err()); - assert!(context.take_page_target_for_close("TID-close").is_none()); + assert!(context.begin_web_contents_close(handle).is_err()); drop(projection); } @@ -243,7 +302,7 @@ async fn close_all_retires_background_builds_and_removes_every_projection() { } context.set_active_target_id("TID-first"); context.close_all_pages_async().await; - assert!(context.physical.web_contents.is_empty()); + assert_eq!(context.browser_context.web_contents_count(), 0); assert!(context.page_targets.is_empty()); assert_eq!(context.selected_web_contents_id(), None); for waiter in waiters { diff --git a/moli-protocol/src/conn/state/browser_context/downloads.rs b/moli-protocol/src/conn/state/browser_context/downloads.rs index e6e5c10972..edac6395d1 100644 --- a/moli-protocol/src/conn/state/browser_context/downloads.rs +++ b/moli-protocol/src/conn/state/browser_context/downloads.rs @@ -1,59 +1,66 @@ -use moli_core::browser::{DownloadAccessError, DownloadBody, DownloadObservation, DownloadPolicy}; +use moli_core::browser::{ + DownloadAccessError, DownloadBody, DownloadObservation, DownloadPolicy, WebContentsHandle, +}; use moli_fetch::{FetchConfig, Request}; use url::Url; use super::BrowserContext; impl BrowserContext { - // Value-only migration bridge; admission and execution belong to the physical - // Context. Removed with the wrapper at the typed Browser API cutover (24b/30). + pub(in crate::conn) fn download_frame_id_for_web_contents( + &self, + web_contents: WebContentsHandle, + ) -> Option<&str> { + if !self.browser_context.contains_web_contents(web_contents) { + return None; + } + self.page_targets + .get_for_web_contents(web_contents.id()) + .map(crate::conn::PageAgentHost::target_id) + } + pub(in crate::conn) fn start_download_request( &mut self, - target: &str, + web_contents: WebContentsHandle, fetch_defaults: FetchConfig, - default_policy: &DownloadPolicy, + policy: &DownloadPolicy, request: Request, suggested_filename: Option, + browser_globals: &crate::conn::BrowserGlobalOverrides, ) -> Result, String> { - let client = self.ensure_target_resource_request_client(target, fetch_defaults)?; - let policy = self - .physical - .download_policy - .as_ref() - .unwrap_or(default_policy); - self.physical - .downloads - .start_request(policy, client, request, suggested_filename) + let client = self.ensure_web_contents_resource_request_client( + web_contents, + fetch_defaults, + &browser_globals.extra_headers, + browser_globals.network_conditions, + )?; + self.browser_context + .start_download_request(policy, client, request, suggested_filename) } pub(in crate::conn) fn start_download_response( &mut self, - default_policy: &DownloadPolicy, + web_contents: WebContentsHandle, + policy: &DownloadPolicy, url: Url, headers: Vec<(String, String)>, body: DownloadBody, ) -> Result, String> { - let policy = self - .physical - .download_policy - .as_ref() - .unwrap_or(default_policy); - self.physical - .downloads - .start_response(policy, url, headers, body) + self.browser_context + .start_download_response(web_contents, policy, url, headers, body) } pub(in crate::conn) fn cancel_download( &self, guid: &str, ) -> Option> { - self.physical.downloads.cancel(guid) + self.browser_context.cancel_download(guid) } pub(in crate::conn) fn read_download_artifact( &self, guid: &str, ) -> Option, String>>, DownloadAccessError>> { - self.physical.downloads.read_artifact(guid) + self.browser_context.read_download_artifact(guid) } } diff --git a/moli-protocol/src/conn/state/web_contents/initial_document/tests.rs b/moli-protocol/src/conn/state/browser_context/initial_document_tests.rs similarity index 62% rename from moli-protocol/src/conn/state/web_contents/initial_document/tests.rs rename to moli-protocol/src/conn/state/browser_context/initial_document_tests.rs index 8d23c21dbf..fb6ebde87c 100644 --- a/moli-protocol/src/conn/state/web_contents/initial_document/tests.rs +++ b/moli-protocol/src/conn/state/browser_context/initial_document_tests.rs @@ -1,8 +1,10 @@ -use super::*; -use crate::conn::BrowserContext; -use moli_core::page::{ - RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind, - RendererDocumentLifecycleMilestone, RendererLifecycleEpoch, RendererLifecycleStartReason, +use super::BrowserContext; +use moli_core::{ + browser::{ + BrowserInitialDocumentAdmission, BrowserInitialDocumentBuild, DocumentHandle, + web_contents::InitialDocumentPageBuildWaiter, + }, + page::{DocumentStartScript, PermissionOverrideRegistration}, }; use std::{ future::Future, @@ -19,8 +21,8 @@ fn context() -> BrowserContext { context } -fn build(context: &mut BrowserContext) -> AdmittedInitialDocumentBuild { - let InitialDocumentAdmission::Build(build) = context +fn build(context: &mut BrowserContext) -> BrowserInitialDocumentBuild { + let BrowserInitialDocumentAdmission::Build(build) = context .start_initial_document_for_target(TARGET, Default::default(), &Default::default()) .unwrap() else { @@ -30,7 +32,7 @@ fn build(context: &mut BrowserContext) -> AdmittedInitialDocumentBuild { } fn join(context: &mut BrowserContext) -> InitialDocumentPageBuildWaiter { - let InitialDocumentAdmission::Join(waiter) = context + let BrowserInitialDocumentAdmission::Join(waiter) = context .start_initial_document_for_target(TARGET, Default::default(), &Default::default()) .unwrap() else { @@ -119,15 +121,22 @@ async fn native_initial_commit_survives_projection_loss_and_has_complete_lifecyc let key = work.key(); let waiter = join(&mut context); let candidate = work.materialize().await.unwrap(); - assert!(candidate.lifecycle.snapshot().unwrap().load.is_some()); let projection = context.page_targets.remove(TARGET).unwrap(); context .commit_initial_document(candidate) .unwrap_or_else(|_| panic!("projection vetoed Browser commit")); assert_eq!(waiter.wait().await, Ok(())); assert!(context.owns_web_contents(key.web_contents())); + let web_contents = context + .browser_context + .selected_web_contents_handle() + .expect("committed native WebContents"); + assert_eq!(web_contents.id(), key.web_contents()); assert_eq!( - RendererPageResidenceIdentity::from_page(context.loaded_page().unwrap()), + context + .browser_context + .document_renderer_residence(DocumentHandle::new(web_contents, key.document())) + .unwrap(), key.renderer() ); // Reintroduce only the projection to inspect the already committed native @@ -160,91 +169,32 @@ async fn initial_candidate_cannot_be_committed_into_another_web_contents() { assert_eq!(waiter.wait().await, Ok(())); } -#[tokio::test] -async fn initial_creation_lifecycle_exits_empty_document_before_projection_can_drive_it() { - let mut owner = context(); - let mut candidate = build(&mut owner).materialize().await.unwrap(); - let mut artifacts = candidate.built.page_creation_artifacts.clone(); - // Feed a complete, consistent creation occurrence into the installation - // boundary. A parser script's document.open() is not a way to construct - // this fixture: the parser may ignore that call. - let snapshot = artifacts.lifecycle_snapshot; - let sequence = artifacts.initial_lifecycle_events.last().unwrap().sequence + 1; - let terminated = RendererDocumentLifecycleEvent { - frame: snapshot.frame, - document: snapshot.document, - epoch: snapshot.epoch, - sequence, - timestamp_micros: snapshot.load.unwrap().timestamp_micros + 1, - kind: RendererDocumentLifecycleEventKind::Terminated { - last_reached: Some(RendererDocumentLifecycleMilestone::Load), - reason: moli_core::page::RendererDocumentTerminationReason::RestartedByDocumentOpen, - }, - }; - let restarted = RendererDocumentLifecycleEvent { - epoch: RendererLifecycleEpoch(snapshot.epoch.0 + 1), - sequence: sequence + 1, - timestamp_micros: terminated.timestamp_micros + 1, - kind: RendererDocumentLifecycleEventKind::Started { - reason: RendererLifecycleStartReason::ExplicitDocumentOpen, - }, - ..terminated - }; - let load = RendererDocumentLifecycleEvent { - sequence: sequence + 2, - timestamp_micros: restarted.timestamp_micros + 1, - kind: RendererDocumentLifecycleEventKind::Milestone( - RendererDocumentLifecycleMilestone::Load, - ), - ..restarted - }; - for event in [terminated, restarted, load] { - artifacts.lifecycle_snapshot.apply_event(event); - artifacts.initial_lifecycle_events.push(event); - } - artifacts.active_epoch = restarted.epoch; - assert!(DocumentLifecycle::from_creation_artifacts(&artifacts).is_some()); - let snapshot = artifacts.lifecycle_snapshot; - assert!(snapshot.epoch.0 > 1); - assert!(snapshot.load.is_some()); - candidate.lifecycle = DocumentLifecycle::from_creation_artifacts(&artifacts).unwrap(); - candidate.built.page_creation_artifacts = artifacts; - owner - .commit_initial_document(candidate) - .unwrap_or_else(|_| panic!("native commit rejected")); - assert_eq!( - owner.renderer_document_lifecycle_authoritative_snapshot_for_target(TARGET), - Some(snapshot) - ); - assert_eq!( - owner.target_is_on_initial_empty_document(TARGET), - Some(false) - ); -} - #[tokio::test] async fn initial_preload_observes_native_admission_policy_before_materialization() { let mut context = context(); - let mut defaults = Default::default(); - let permission = |setting: &str| moli_core::page::PermissionOverrideRegistration { + let permission = |setting: &str| PermissionOverrideRegistration { permission: serde_json::json!({"name": "geolocation"}), setting: setting.into(), origin: None, embedded_origin: None, }; - context.set_permission_override(&mut defaults, permission("granted")); - let InitialDocumentAdmission::Build(mut work) = context - .start_initial_document_for_target(TARGET, Default::default(), &defaults) + context + .set_permission_override(permission("granted")) + .unwrap(); + let BrowserInitialDocumentAdmission::Build(mut work) = context + .start_initial_document_for_target(TARGET, Default::default(), &Default::default()) .unwrap() else { panic!("expected build"); }; - context.set_permission_override(&mut defaults, permission("denied")); + context + .set_permission_override(permission("denied")) + .unwrap(); work.start_preparation().unwrap(); let configured = work.inspection_endpoint().start_configure( moli_renderer_v8::RendererPreparedDocumentInspectionConfiguration { root_frame_projection_id: Some(TARGET.into()), - document_start_scripts: vec![moli_core::page::DocumentStartScript { + document_start_scripts: vec![DocumentStartScript { registry_key: None, devtools_session: None, source: "globalThis.__initialPermission = navigator.permissions.query({name:'geolocation'}).then(permission => permission.state);".into(), world_name: None, has_bidi_channel_argument: false, bidi_channel_handoffs: Vec::new(), @@ -253,12 +203,13 @@ async fn initial_preload_observes_native_admission_policy_before_materialization }, ); configured.await.unwrap(); - let mut candidate = work.materialize().await.unwrap(); + let candidate = work.materialize().await.unwrap(); + context + .commit_initial_document(candidate) + .unwrap_or_else(|_| panic!("native commit rejected")); assert_eq!( - candidate - .built - .page - .evaluate_runtime_expression_with_await_async("globalThis.__initialPermission", true) + context + .evaluate_target_expression_for_test(TARGET, "globalThis.__initialPermission", true,) .await .unwrap()["value"], "granted", diff --git a/moli-protocol/src/conn/state/browser_context/javascript_dialog.rs b/moli-protocol/src/conn/state/browser_context/javascript_dialog.rs index 114ac651d9..fdad8da257 100644 --- a/moli-protocol/src/conn/state/browser_context/javascript_dialog.rs +++ b/moli-protocol/src/conn/state/browser_context/javascript_dialog.rs @@ -1,4 +1,5 @@ use super::BrowserContext; +use moli_core::browser::{DocumentHandle, WebContentsHandle}; use moli_core::page::{ RendererDocumentLifecycleIdentity, RendererJavaScriptDialogId, RendererJavaScriptDialogSource, RendererPendingJavaScriptDialog, @@ -8,10 +9,12 @@ use std::sync::{ atomic::{AtomicBool, Ordering}, }; -use crate::conn::state::web_contents::{ +use crate::conn::state::TargetPageProtocolAttachmentIdentity; +#[cfg(test)] +use crate::conn::state::TargetPageResidenceIdentity; +use moli_core::browser::web_contents::{ JavaScriptDialogClosed, JavaScriptDialogError, JavaScriptDialogKey, JavaScriptDialogSnapshot, }; -use crate::conn::state::{TargetPageProtocolAttachmentIdentity, TargetPageResidenceIdentity}; /// Stable lifetime of one target Page's JavaScript-dialog output. /// @@ -226,17 +229,28 @@ impl Drop for TargetPreparedJavaScriptDialog { #[derive(Debug, Clone, PartialEq)] pub(crate) struct TargetJavaScriptDialog { source_frame_id: String, + document: DocumentHandle, pub(in crate::conn::state) key: JavaScriptDialogKey, } impl TargetJavaScriptDialog { - pub(crate) fn new(source_frame_id: String, key: JavaScriptDialogKey) -> Self { + pub(crate) fn new( + source_frame_id: String, + document: DocumentHandle, + key: JavaScriptDialogKey, + ) -> Self { + debug_assert_eq!(document.id(), key.document); Self { source_frame_id, + document, key, } } + pub(crate) fn into_parts(self) -> (String, DocumentHandle, JavaScriptDialogKey) { + (self.source_frame_id, self.document, self.key) + } + #[cfg(test)] pub(crate) fn document_id(&self) -> moli_core::browser::DocumentId { self.key.document @@ -286,26 +300,37 @@ impl TargetJavaScriptDialogState { } } -// In-place AgentHost -> Browser bridge, replaced by typed commands in Commit 22. -// Session authority stays here; WebContents only receives exact neutral keys. impl BrowserContext { - pub(crate) fn install_javascript_dialog_for_target( + pub(crate) fn web_contents_has_pending_javascript_dialog( + &self, + handle: WebContentsHandle, + ) -> Result { + self.browser_context + .web_contents_has_pending_javascript_dialog(handle) + } + + pub(crate) fn install_document_javascript_dialog( + &mut self, + document: DocumentHandle, + dialog: RendererPendingJavaScriptDialog, + ) -> Result { + self.browser_context + .install_document_javascript_dialog(document, dialog) + } + + pub(crate) fn project_javascript_dialog_for_session( &mut self, target_id: &str, session: &moli_page_types::DevToolsSessionKey, - page_owner: TargetPageResidenceIdentity, source_frame_id: String, - dialog: RendererPendingJavaScriptDialog, + document: DocumentHandle, + key: JavaScriptDialogKey, ) -> bool { - if self.target_document_id(target_id) != Some(page_owner.document_id()) { - let _ = dialog.finish(false, String::new()); + if self.document_handle_for_target(target_id) != Some(document) + || key.document != document.id() + { return false; } - let key = self - .web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .javascript_dialogs - .install(page_owner.document_id(), dialog); self.page_targets .get_mut(target_id) .expect("resolved target projection must remain live") @@ -313,15 +338,15 @@ impl BrowserContext { .ensure_session(session) .page_session_state .javascript_dialog_state - .push(TargetJavaScriptDialog::new(source_frame_id, key)); + .push(TargetJavaScriptDialog::new(source_frame_id, document, key)); true } - fn javascript_dialog_key_for_target( + pub(crate) fn projected_javascript_dialog_for_session( &self, target_id: &str, session: &moli_page_types::DevToolsSessionKey, - ) -> Option { + ) -> Option<(DocumentHandle, JavaScriptDialogKey)> { let dialog = self .page_targets .get(target_id) @@ -331,91 +356,89 @@ impl BrowserContext { .page_session_state .javascript_dialog_state .peek_next()?; - (Some(dialog.key.document) == self.target_document_id(target_id)).then_some(dialog.key) + (self.document_handle_for_target(target_id) == Some(dialog.document) + && self + .browser_context + .ensure_document_current(dialog.document) + .is_ok()) + .then_some((dialog.document, dialog.key)) } - pub(crate) fn javascript_dialog_snapshot_for_target( + pub(crate) fn document_javascript_dialog_snapshot( &self, - target_id: &str, - session: &moli_page_types::DevToolsSessionKey, + document: DocumentHandle, + key: JavaScriptDialogKey, ) -> Option { - let key = self.javascript_dialog_key_for_target(target_id, session)?; - self.web_contents_for_target(target_id) - .expect("resolved WebContents must remain live") - .javascript_dialogs - .snapshot(key) + self.browser_context + .document_javascript_dialog_snapshot(document, key) } - pub(crate) fn set_javascript_dialog_prompt_text_for_target( + pub(crate) fn set_document_javascript_dialog_prompt_text( &mut self, - target_id: &str, - session: &moli_page_types::DevToolsSessionKey, + document: DocumentHandle, + key: JavaScriptDialogKey, prompt_text: String, ) -> Result<(), JavaScriptDialogError> { - let key = self - .javascript_dialog_key_for_target(target_id, session) - .ok_or(JavaScriptDialogError::NotFound)?; - self.web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .javascript_dialogs - .set_prompt_text(key, prompt_text) + self.browser_context + .set_document_javascript_dialog_prompt_text(document, key, prompt_text) } - pub(crate) fn handle_javascript_dialog_for_target( + pub(crate) fn finish_document_javascript_dialog( &mut self, - target_id: &str, - session: &moli_page_types::DevToolsSessionKey, + document: DocumentHandle, + key: JavaScriptDialogKey, accepted: bool, prompt_text: Option, - ) -> Option<(String, JavaScriptDialogClosed)> { - let Some(key) = self.javascript_dialog_key_for_target(target_id, session) else { - self.dismiss_devtools_javascript_dialogs_for_target(target_id, session); - return None; - }; - let projection = self - .page_targets - .get_mut(target_id) - .expect("resolved target projection must remain live") - .devtools_sessions - .ensure_session(session) - .page_session_state - .javascript_dialog_state - .pop_next()?; - let outcome = self - .web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .javascript_dialogs - .finish(key, accepted, prompt_text)?; - Some((projection.source_frame_id, outcome)) + ) -> Option { + self.browser_context + .finish_document_javascript_dialog(document, key, accepted, prompt_text) } - pub(crate) fn dismiss_devtools_javascript_dialogs_for_target( + pub(crate) fn dismiss_document_javascript_dialog( + &mut self, + document: DocumentHandle, + key: JavaScriptDialogKey, + ) { + self.browser_context + .dismiss_document_javascript_dialog(document, key); + } + + pub(crate) fn pop_projected_javascript_dialog_for_session( &mut self, target_id: &str, session: &moli_page_types::DevToolsSessionKey, - ) { - let projections = self - .page_targets + ) -> Option { + self.page_targets .get_mut(target_id) .expect("resolved target projection must remain live") .devtools_sessions .ensure_session(session) .page_session_state .javascript_dialog_state - .take_pending(); - self.dismiss_javascript_dialog_projections_for_target(target_id, projections); + .pop_next() } - pub(in crate::conn::state) fn dismiss_javascript_dialog_projections_for_target( + pub(crate) fn take_projected_javascript_dialogs_for_session( &mut self, target_id: &str, + session: &moli_page_types::DevToolsSessionKey, + ) -> Vec { + self.page_targets + .get_mut(target_id) + .expect("resolved target projection must remain live") + .devtools_sessions + .ensure_session(session) + .page_session_state + .javascript_dialog_state + .take_pending() + } + + pub(in crate::conn) fn dismiss_projected_javascript_dialogs( + &mut self, projections: Vec, ) { for projection in projections { - self.web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .javascript_dialogs - .dismiss(projection.key); + self.dismiss_document_javascript_dialog(projection.document, projection.key); } } } diff --git a/moli-protocol/src/conn/state/browser_context/navigation.rs b/moli-protocol/src/conn/state/browser_context/navigation.rs index 5eb9bce5d3..e7e15fae51 100644 --- a/moli-protocol/src/conn/state/browser_context/navigation.rs +++ b/moli-protocol/src/conn/state/browser_context/navigation.rs @@ -1,13 +1,16 @@ use super::BrowserContext; use crate::conn::state::{PageNavigationHistoryEntry, TargetPageAbsenceReason}; -use moli_core::page::{Page, SameDocumentHistoryUpdate}; +use moli_core::page::SameDocumentHistoryUpdate; use url::Url; impl BrowserContext { #[cfg(test)] pub(crate) fn has_paused_navigation_auth_for_test(&self, target: &str) -> bool { - self.web_contents_for_target(target) - .is_some_and(|contents| contents.navigation().has_paused_auth_for_test()) + self.web_contents_handle_for_target(target) + .is_some_and(|handle| { + self.browser_context + .has_paused_navigation_auth_for_test(handle) + }) } pub(in crate::conn) fn pause_navigation_request_for_target( @@ -16,33 +19,26 @@ impl BrowserContext { navigation: moli_core::browser::NavigationId, request: crate::conn::state::NavigationRequestInterception, ) -> Result { - self.web_contents_for_target_mut(target) - .ok_or("navigation WebContents unavailable")? - .pause_navigation_request(navigation, request) + let handle = self + .web_contents_handle_for_target(target) + .ok_or("navigation WebContents unavailable")?; + self.browser_context + .pause_navigation_request(handle, navigation, request) } pub(in crate::conn) fn take_navigation_request( &mut self, permit: crate::conn::state::NavigationInterceptionPermit, ) -> Option { - self.physical - .web_contents - .get_mut(&permit.web_contents())? - .take_navigation_request(permit) + self.browser_context.take_navigation_request(permit) } pub(in crate::conn) fn start_claimed_navigation_request( &mut self, request: crate::conn::state::ClaimedNavigationRequest, fetch_defaults: moli_fetch::FetchConfig, - permissions: &moli_core::browser::PermissionDefaults, - ) -> Result< - ( - String, - crate::conn::state::web_contents::InterceptedNavigationLoad, - ), - String, - > { + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> Result<(String, moli_core::browser::BrowserInterceptedNavigationLoad), String> { let web_contents = request.permit().web_contents(); let target_id = self .page_targets @@ -51,12 +47,9 @@ impl BrowserContext { .target_id() .to_owned(); let inherited = - self.inherited_document_policy_for_target(&target_id, fetch_defaults, permissions); + self.inherited_document_policy_for_target(&target_id, fetch_defaults, browser_globals); let load = self - .physical - .web_contents - .get_mut(&web_contents) - .ok_or("navigation WebContents unavailable")? + .browser_context .start_claimed_navigation_request(request, inherited)?; self.project_navigation_load_for_target(&target_id, &load.load)?; Ok((target_id, load)) @@ -67,7 +60,7 @@ impl BrowserContext { permit: crate::conn::state::NavigationInterceptionPermit, policy: moli_core::browser::NavigationRequestLoadPolicy, fetch_defaults: moli_fetch::FetchConfig, - permissions: &moli_core::browser::PermissionDefaults, + browser_globals: &crate::conn::BrowserGlobalOverrides, ) -> Result<(String, crate::conn::state::AdmittedNavigationLoad), String> { let web_contents = permit.web_contents(); let target_id = self @@ -77,12 +70,9 @@ impl BrowserContext { .target_id() .to_owned(); let inherited = - self.inherited_document_policy_for_target(&target_id, fetch_defaults, permissions); + self.inherited_document_policy_for_target(&target_id, fetch_defaults, browser_globals); let load = self - .physical - .web_contents - .get_mut(&web_contents) - .ok_or("navigation WebContents unavailable")? + .browser_context .start_navigation_load_for_interception(permit, policy, inherited)?; self.project_navigation_load_for_target(&target_id, &load)?; Ok((target_id, load)) @@ -90,27 +80,17 @@ impl BrowserContext { pub(in crate::conn) fn pause_navigation_auth( &mut self, - response: crate::conn::state::web_contents::InterceptedNavigationResponse< - moli_fetch::RawResponse, - >, - ) -> Result { - self.physical - .web_contents - .get_mut(&response.web_contents()) - .ok_or("navigation WebContents unavailable")? - .pause_navigation_auth(response) + response: moli_core::browser::BrowserInterceptedNavigationResponse, + ) -> Result { + self.browser_context.pause_navigation_auth(response) } pub(in crate::conn) fn take_navigation_auth( &mut self, - permit: crate::conn::state::web_contents::NavigationInterceptionPermit, - ) -> Option< - crate::conn::state::web_contents::InterceptedNavigationResponse, - > { - self.physical - .web_contents - .get_mut(&permit.web_contents())? - .take_navigation_auth(permit) + permit: moli_core::browser::web_contents::NavigationInterceptionPermit, + ) -> Option> + { + self.browser_context.take_navigation_auth(permit) } pub(in crate::conn) fn pause_navigation_response_for_target( @@ -118,51 +98,49 @@ impl BrowserContext { target: &str, navigation: moli_core::browser::NavigationId, transfer: crate::conn::PausedDocumentTransfer, - ) -> Result { - self.web_contents_for_target_mut(target) - .ok_or("navigation WebContents unavailable")? - .pause_navigation_response(navigation, transfer) + ) -> Result { + let handle = self + .web_contents_handle_for_target(target) + .ok_or("navigation WebContents unavailable")?; + self.browser_context + .pause_navigation_response(handle, navigation, transfer) } pub(in crate::conn) fn take_navigation_response( &mut self, - permit: crate::conn::state::web_contents::NavigationInterceptionPermit, + permit: moli_core::browser::web_contents::NavigationInterceptionPermit, ) -> Option { - self.physical - .web_contents - .get_mut(&permit.web_contents())? - .take_navigation_response(permit) + self.browser_context.take_navigation_response(permit) } pub(in crate::conn) fn restore_navigation_response( &mut self, - permit: crate::conn::state::web_contents::NavigationInterceptionPermit, + permit: moli_core::browser::web_contents::NavigationInterceptionPermit, transfer: crate::conn::PausedDocumentTransfer, ) -> Result<(), Box> { - let Some(contents) = self.physical.web_contents.get_mut(&permit.web_contents()) else { - return Err(Box::new(transfer)); - }; - contents.restore_navigation_response(permit, transfer) + self.browser_context + .restore_navigation_response(permit, transfer) } #[cfg(test)] - pub(crate) fn paused_navigation_response_for_target( + pub(crate) fn paused_navigation_response_renderer_agent_for_target( &self, target: &str, - ) -> Option<&crate::conn::PausedDocumentTransfer> { - self.web_contents_for_target(target)? - .navigation() - .paused_response_for_test() + ) -> Option { + let handle = self.web_contents_handle_for_target(target)?; + self.browser_context + .paused_navigation_response_renderer_agent_for_test(handle) } pub(in crate::conn) fn resolve_target_history_traversal( &self, target: &str, destination: crate::conn::HistoryTraversalDestination, - ) -> Option> { + ) -> Option> { + let handle = self.web_contents_handle_for_target(target)?; Some( - self.web_contents_for_target(target)? - .resolve_history_traversal(destination), + self.browser_context + .resolve_history_traversal(handle, destination), ) } @@ -172,38 +150,46 @@ impl BrowserContext { target_id: &str, snapshot: (String, String), ) { - self.web_contents_for_target_mut(target_id) - .expect("registered fixture WebContents") - .record_navigation_history_for_test(snapshot); + let handle = self + .web_contents_handle_for_target(target_id) + .expect("registered fixture WebContents"); + self.browser_context + .record_navigation_history_for_test(handle, snapshot) + .expect("registered fixture WebContents"); } pub(crate) fn target_service_worker_client_id(&self, target_id: &str) -> Option { - self.loaded_page_for_target(target_id) - .map(|page| page.service_worker_client_id()) + let document = self.document_handle_for_target(target_id)?; + self.browser_context + .document_service_worker_client_id(document) + .ok() } pub(crate) fn target_renderer_page_residence_identity( &self, target_id: &str, ) -> Option { - self.loaded_page_for_target(target_id) - .map(moli_core::browser::RendererPageResidenceIdentity::from_page) + let document = self.document_handle_for_target(target_id)?; + self.browser_context + .document_renderer_residence(document) + .ok() } - pub(crate) fn target_document_url(&self, target_id: &str) -> Option<&Url> { - Some(self.loaded_page_for_target(target_id)?.final_url()) + pub(crate) fn target_document_url(&self, target_id: &str) -> Option { + let document = self.document_handle_for_target(target_id)?; + self.browser_context.document_url(document).ok() } pub(crate) fn target_document_title(&self, target_id: &str) -> Option { - self.loaded_page_for_target(target_id) - .map(Page::document_title) + let document = self.document_handle_for_target(target_id)?; + self.browser_context.document_title(document).ok() } pub(crate) fn target_navigation_initiator_url(&self, target_id: &str) -> Option { if let Some(url) = self.target_document_url(target_id) && url.host_str().is_some() { - return Some(url.clone()); + return Some(url); } let url = Url::parse(self.page_targets.get(target_id)?.target_url()).ok()?; url.host_str().is_some().then_some(url) @@ -213,37 +199,34 @@ impl BrowserContext { &self, target_id: &str, ) -> Option { - self.web_contents_for_target(target_id)? - .navigation() - .initial_empty_document_url_if_current() - .map(str::to_owned) + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context.initial_document_url(handle).ok()? } pub(crate) fn target_initial_empty_document_storage_key_if_current( &self, target_id: &str, ) -> Option { - self.web_contents_for_target(target_id)? - .navigation() - .initial_empty_document_storage_key_if_current() - .cloned() + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context + .initial_document_storage_key(handle) + .ok()? } pub(crate) fn target_is_on_initial_empty_document(&self, target_id: &str) -> Option { - self.web_contents_for_target(target_id)? - .navigation() - .is_on_initial_empty_document() + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context.is_on_initial_document(handle).ok()? } pub(crate) fn target_initial_empty_document_has_pending_cross_document_navigation( &self, target_id: &str, ) -> bool { - self.web_contents_for_target(target_id) - .is_some_and(|contents| { - contents - .navigation() - .initial_empty_document_pending_cross_document_navigation() + self.web_contents_handle_for_target(target_id) + .is_some_and(|handle| { + self.browser_context + .initial_document_has_pending_navigation(handle) + .unwrap_or(false) }) } @@ -251,10 +234,10 @@ impl BrowserContext { &self, target_id: &str, ) -> Option<(usize, Vec)> { - Some( - self.web_contents_for_target(target_id)? - .navigation_history_snapshot(), - ) + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context + .navigation_history_snapshot(handle) + .ok() } pub(crate) fn target_navigation_history_entry_url( @@ -262,17 +245,20 @@ impl BrowserContext { target_id: &str, entry_id: i32, ) -> Option { - self.web_contents_for_target(target_id)? - .navigation_history_entry_url(entry_id) + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context + .navigation_history_entry_url(handle, entry_id) + .ok()? } pub(crate) fn mark_target_next_navigation_history_replace_current( &mut self, target_id: &str, ) -> Option<()> { - self.web_contents_for_target_mut(target_id)? - .mark_next_navigation_history_replace_current(); - Some(()) + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context + .mark_next_navigation_history_replace_current(handle) + .ok() } pub(crate) fn mark_target_next_navigation_history_traverse_to_entry( @@ -280,9 +266,10 @@ impl BrowserContext { target_id: &str, entry_id: i32, ) -> Option<()> { - self.web_contents_for_target_mut(target_id)? - .mark_next_navigation_history_traverse_to_entry(entry_id); - Some(()) + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context + .mark_next_navigation_history_traverse_to_entry(handle, entry_id) + .ok() } pub(in crate::conn) fn commit_target_same_document_navigation( @@ -291,10 +278,12 @@ impl BrowserContext { document: moli_core::browser::DocumentId, url: Url, history_update: SameDocumentHistoryUpdate, - ) -> Option { + ) -> Option { + let handle = self.web_contents_handle_for_target(target_id)?; let committed = self - .web_contents_for_target_mut(target_id)? - .commit_same_document_navigation(document, url, history_update)?; + .browser_context + .commit_same_document_navigation(handle, document, url, history_update) + .ok()??; if self.target_document_id(target_id) != Some(committed.document) { return None; } @@ -319,22 +308,22 @@ impl BrowserContext { } pub(crate) async fn mark_target_crashed_async(&mut self, target_id: &str) -> Option<()> { - let contents = self.web_contents_for_target_mut(target_id)?; - contents.mark_renderer_crashed(); + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context.mark_renderer_crashed(handle).ok()?; let target = self.page_targets.get_mut(target_id)?; target.owner_state.clear_loaded_document_context_state(); target.fetch_owner.clear_pending(); self.clear_target_loaded_document_session_state(target_id); self.clear_document_navigation_state_for_target(target_id); - let previous = self.clear_loaded_page_with_reason_for_target( + let previous = self.retire_loaded_document_with_reason_for_target( target_id, TargetPageAbsenceReason::TargetCrashed, ); let runtime = &mut self.page_targets.get_mut(target_id)?.runtime_slot; runtime.reset_subresource_cursor(); runtime.reset_all_target_scoped_network_artifacts(); - if let Some(page) = previous { - let _ = page.close_async().await; + if let Some(document) = previous { + document.close().await; } Some(()) } diff --git a/moli-protocol/src/conn/state/browser_context/page_runtime.rs b/moli-protocol/src/conn/state/browser_context/page_runtime.rs index 386a4c32da..ec7f0a49a1 100644 --- a/moli-protocol/src/conn/state/browser_context/page_runtime.rs +++ b/moli-protocol/src/conn/state/browser_context/page_runtime.rs @@ -1,49 +1,407 @@ use super::BrowserContext; -use moli_core::page::{CompletedPageCommand, PendingPageCommand, RendererCommandTurnOutput}; -use std::time::Duration; -mod document_commands; -mod document_queries; -mod emulation; -mod input; -pub(crate) use emulation::PagePolicyUpdateKind; -mod network_commands; -mod resource_commands; -pub(crate) use input::PageInputCommand; -pub(crate) use network_commands::NetworkPolicyUpdateKind; +pub(crate) use moli_core::browser::{ + BrowserAppManifestLoadPreparation, CompletedAppManifestLoadPreparation, + CompletedAppManifestPublication, CompletedCaptureDocumentImage, + CompletedCaptureDocumentScreencastFrame, CompletedCaptureDocumentSnapshot, + CompletedChildFrameLifecycleWork, CompletedChildFrameNavigation, + CompletedChildFrameTreeSnapshot, CompletedDocumentAutofillTrigger, CompletedDocumentBlobRead, + CompletedDocumentCookieOwnerSnapshot, CompletedDocumentCspBypassUpdate, + CompletedDocumentDiagnosticsSnapshot, CompletedDocumentFetchCommand, + CompletedDocumentInputCommand, CompletedDocumentLifecycleStop, CompletedDocumentPolicyBatch, + CompletedDocumentPolicyUpdate, CompletedDocumentResourceRuntimeUpdate, + CompletedDocumentResourceTextSearch, CompletedDocumentStorageKeySnapshot, + CompletedNavigationHistoryReset, CompletedNetworkResourceLoadPreparation, + CompletedSetDocumentContent, CompletedTopLevelHistoryTraversal, + CompletedTopLevelSameDocumentNavigation, DocumentFetchCommand, DocumentFetchCommandOutcome, + DocumentPolicyUpdate, DocumentRuntimePolicyReconciliation, DocumentSnapshot, PageInputCommand, + PendingAppManifestLoadPreparation, PendingAppManifestPublication, PendingCaptureDocumentImage, + PendingCaptureDocumentScreencastFrame, PendingCaptureDocumentSnapshot, + PendingChildFrameLifecycleWork, PendingChildFrameNavigation, PendingChildFrameTreeSnapshot, + PendingDocumentAutofillTrigger, PendingDocumentBlobRead, PendingDocumentCookieOwnerSnapshot, + PendingDocumentCspBypassUpdate, PendingDocumentDiagnosticsSnapshot, + PendingDocumentFetchCommand, PendingDocumentInputCommand, PendingDocumentLifecycleStop, + PendingDocumentPolicyBatch, PendingDocumentPolicyUpdate, PendingDocumentResourceRuntimeUpdate, + PendingDocumentResourceTextSearch, PendingDocumentStorageKeySnapshot, + PendingNavigationHistoryReset, PendingNetworkResourceLoadPreparation, + PendingSetDocumentContent, PendingTopLevelHistoryTraversal, + PendingTopLevelSameDocumentNavigation, +}; impl BrowserContext { - pub(in crate::conn) fn observe_renderer_page_state( - &mut self, - snapshot: &std::sync::Arc, - ) -> bool { - self.physical - .web_contents - .values_mut() - .any(|contents| contents.observe_renderer_page_state(snapshot)) + pub(crate) fn start_document_diagnostics_snapshot( + &self, + document: moli_core::browser::DocumentHandle, + ) -> Result { + self.browser_context + .start_document_diagnostics_snapshot(document) } - pub(crate) fn start_target_fetch_interception_update( + pub(crate) fn start_document_policy_batch_with_surface( &mut self, - target_id: &str, + document: moli_core::browser::DocumentHandle, + updates: Vec, + foreground: bool, + global_network_conditions: Option, + global_geolocation: Option<&moli_core::browser::EmulatedGeolocationOverrideState>, + ) -> PendingDocumentPolicyBatch { + self.browser_context + .start_document_policy_batch_with_surface( + document, + updates, + foreground, + self.navigator_queries_for_web_contents(document.web_contents()), + global_network_conditions, + global_geolocation, + ) + } + + pub(crate) fn start_document_policy_update( + &mut self, + document: moli_core::browser::DocumentHandle, + update: DocumentPolicyUpdate, + ) -> Result { + self.browser_context + .start_document_policy_update(document, update) + } + + pub(crate) fn start_document_page_surface_update( + &mut self, + document: moli_core::browser::DocumentHandle, + foreground: bool, + global_network_conditions: Option, + global_geolocation: Option<&moli_core::browser::EmulatedGeolocationOverrideState>, + ) -> Result { + self.browser_context.start_document_page_surface_update( + document, + foreground, + self.navigator_queries_for_web_contents(document.web_contents()), + global_network_conditions, + global_geolocation, + ) + } + + #[cfg(test)] + pub(crate) fn finish_document_policy_update( + &mut self, + completed: CompletedDocumentPolicyUpdate, + ) -> Result<(), String> { + self.browser_context + .finish_document_policy_update(completed) + } + + pub(crate) fn start_web_contents_fetch_interception_update( + &mut self, + web_contents: moli_core::browser::WebContentsHandle, enabled: bool, resource_type: Option, - ) -> Result, String> { - self.web_contents_for_target_mut(target_id) - .ok_or_else(|| "WebContents unavailable".to_owned())? - .start_fetch_interception_update(enabled, resource_type) + accept_stale_completion: bool, + ) -> Result, String> { + self.browser_context + .start_web_contents_fetch_interception_update( + web_contents, + enabled, + resource_type, + accept_stale_completion, + ) } - pub(crate) fn install_target_fetch_interception_policy( + pub(in crate::conn) fn install_web_contents_fetch_interception_policy( &mut self, - target_id: &str, + web_contents: moli_core::browser::WebContentsHandle, enabled: bool, resource_type: Option, ) -> Result<(), String> { - self.web_contents_for_target_mut(target_id) - .ok_or_else(|| "WebContents unavailable".to_owned())? - .install_fetch_interception_policy(enabled, resource_type); - Ok(()) + self.browser_context + .install_web_contents_fetch_interception_policy(web_contents, enabled, resource_type) + } + + pub(in crate::conn) fn invalidate_selected_resource_runtime(&mut self) { + self.browser_context.invalidate_selected_resource_runtime(); + } + + pub(in crate::conn) fn configure_selected_navigation_policy( + &mut self, + defaults: moli_fetch::FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> Result<(), String> { + self.browser_context.configure_selected_navigation_policy( + defaults, + global_headers, + global_network_conditions, + ) + } + + pub(in crate::conn) fn ensure_web_contents_resource_request_client( + &mut self, + web_contents: moli_core::browser::WebContentsHandle, + defaults: moli_fetch::FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> Result { + self.browser_context + .ensure_web_contents_resource_request_client( + web_contents, + defaults, + global_headers, + global_network_conditions, + ) + } + + pub(in crate::conn) fn start_web_contents_resource_runtime_rebuild( + &mut self, + web_contents: moli_core::browser::WebContentsHandle, + defaults: moli_fetch::FetchConfig, + global_headers: &moli_fetch::RequestHeaders, + global_network_conditions: Option, + ) -> Result, String> { + self.browser_context + .start_web_contents_resource_runtime_rebuild( + web_contents, + defaults, + global_headers, + global_network_conditions, + ) + } + + #[cfg(test)] + pub(crate) fn finish_document_diagnostics_snapshot( + &mut self, + completed: CompletedDocumentDiagnosticsSnapshot, + ) -> Result { + self.browser_context + .finish_document_diagnostics_snapshot(completed) + } + + #[cfg(test)] + pub(crate) fn start_document_cookie_owner_snapshot( + &self, + document: moli_core::browser::DocumentHandle, + ) -> Result { + self.browser_context + .start_document_cookie_owner_snapshot(document) + } + + #[cfg(test)] + pub(crate) fn finish_document_cookie_owner_snapshot( + &mut self, + completed: CompletedDocumentCookieOwnerSnapshot, + ) -> Result { + self.browser_context + .finish_document_cookie_owner_snapshot(completed) + } + + #[cfg(test)] + pub(crate) fn start_document_autofill_trigger( + &self, + document: moli_core::browser::DocumentHandle, + request: moli_core::page::RendererAutofillTriggerRequest, + ) -> Result { + self.browser_context + .start_document_autofill_trigger(document, request) + } + + #[cfg(test)] + pub(crate) fn finish_document_autofill_trigger( + &mut self, + completed: CompletedDocumentAutofillTrigger, + ) -> Result { + self.browser_context + .finish_document_autofill_trigger(completed) + } + + #[cfg(test)] + pub(crate) fn start_document_lifecycle_stop( + &self, + document: moli_core::browser::DocumentHandle, + ) -> Result { + self.browser_context.start_document_lifecycle_stop(document) + } + + #[cfg(test)] + pub(crate) fn finish_document_lifecycle_stop( + &mut self, + completed: CompletedDocumentLifecycleStop, + ) -> Result { + self.browser_context + .finish_document_lifecycle_stop(completed) + } + + #[cfg(test)] + pub(crate) fn crash_web_contents_renderer_from_io( + &self, + web_contents: moli_core::browser::WebContentsHandle, + ) -> Result<(), String> { + self.browser_context + .crash_web_contents_renderer_from_io(web_contents) + } + + #[cfg(test)] + pub(crate) fn document_subresource_network_records( + &self, + document: moli_core::browser::DocumentHandle, + ) -> Result, String> { + self.browser_context + .document_subresource_network_records(document) + } + + #[cfg(test)] + pub(crate) async fn target_runtime_heap_usage_for_test( + &mut self, + target_id: &str, + ) -> Result { + let document = self + .document_handle_for_target(target_id) + .ok_or("NoDocumentLoaded")?; + self.browser_context + .document_runtime_heap_usage_for_test(document) + .await + } + + #[cfg(test)] + pub(crate) fn target_idle_override_for_test( + &self, + target_id: &str, + ) -> Result, String> { + let document = self + .document_handle_for_target(target_id) + .ok_or("NoDocumentLoaded")?; + self.browser_context + .document_idle_override_for_test(document) + } + + #[cfg(test)] + pub(crate) fn target_cached_observable_output_for_test( + &self, + target_id: &str, + ) -> Option> { + self.browser_context + .document_observable_output_snapshot(self.document_handle_for_target(target_id)?) + .ok() + } + + #[cfg(test)] + pub(crate) async fn evaluate_target_expression_for_test( + &mut self, + target_id: &str, + expression: &str, + await_promise: bool, + ) -> Result { + let document = self + .document_handle_for_target(target_id) + .ok_or("NoDocumentLoaded")?; + self.browser_context + .evaluate_document_expression_for_test(document, expression, await_promise) + .await + } + + #[cfg(test)] + pub(crate) async fn serialize_target_html_for_test( + &mut self, + target_id: &str, + ) -> Result { + let document = self + .document_handle_for_target(target_id) + .ok_or("NoDocumentLoaded")?; + let completion = self + .browser_context + .start_capture_document_snapshot(document)? + .wait() + .await; + Ok(self + .browser_context + .finish_capture_document_snapshot(completion)? + .html) + } + + #[cfg(test)] + pub(in crate::conn) fn resource_request_client_for_test( + &mut self, + target_id: &str, + defaults: moli_fetch::FetchConfig, + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> Result { + let web_contents = self + .web_contents_handle_for_target(target_id) + .ok_or("WebContents unavailable")?; + self.browser_context + .ensure_web_contents_resource_request_client( + web_contents, + defaults, + &browser_globals.extra_headers, + browser_globals.network_conditions, + ) + } + + pub(crate) fn document_handle_for_target( + &self, + target_id: &str, + ) -> Option { + self.document_handle_for_web_contents(self.web_contents_handle_for_target(target_id)?) + .ok() + .flatten() + } + + pub(crate) fn document_handle_for_web_contents( + &self, + handle: moli_core::browser::WebContentsHandle, + ) -> Result, String> { + self.browser_context + .document_handle_for_web_contents(handle) + } + + pub(in crate::conn) fn target_has_navigation_engine(&self, target_id: &str) -> bool { + self.web_contents_handle_for_target(target_id) + .is_some_and(|handle| { + self.browser_context + .web_contents_has_navigation_engine(handle) + }) + } + + pub(crate) fn page_navigation_fetch_config( + &self, + target_id: &str, + ) -> Option { + self.browser_context + .web_contents_navigation_fetch_config(self.web_contents_handle_for_target(target_id)?) + } + + pub(in crate::conn) fn page_navigation_layout_policy( + &self, + target_id: &str, + ) -> Option { + self.browser_context + .web_contents_navigation_layout_policy(self.web_contents_handle_for_target(target_id)?) + } + + pub(in crate::conn) fn page_navigation_renderer_owner_id( + &self, + target_id: &str, + ) -> Option { + self.browser_context + .web_contents_navigation_renderer_owner_id( + self.web_contents_handle_for_target(target_id)?, + ) + } + + #[cfg(test)] + pub(in crate::conn) fn page_navigation_browser_context_runtime_id_for_test( + &self, + target_id: &str, + ) -> Option { + self.browser_context + .web_contents_navigation_browser_context_runtime_id_for_test( + self.web_contents_handle_for_target(target_id)?, + ) + } + + pub(in crate::conn) fn page_navigation_diagnostics( + &self, + target_id: &str, + ) -> Option { + self.browser_context + .web_contents_navigation_diagnostics(self.web_contents_handle_for_target(target_id)?) } #[cfg(test)] @@ -51,199 +409,17 @@ impl BrowserContext { &self, target_id: &str, ) -> Option<(bool, Option)> { - Some( - self.web_contents_for_target(target_id)? - .fetch_subresource_interception(), - ) - } - - pub(crate) fn finish_target_fetch_interception_update( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result<(), String> { - let Some(page) = self.loaded_page_for_target_mut(target_id) else { - return Ok(()); - }; - if !completion.is_from_page(page) { - return Err("Renderer Page changed".to_owned()); - } - page.finish_set_fetch_subresource_interception(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn settle_target_page_command_turn( - &mut self, - target_id: &str, - document_id: moli_core::browser::DocumentId, - completion: CompletedPageCommand, - ) -> RendererCommandTurnOutput { - if self.target_document_id(target_id) == Some(document_id) - && let Some(page) = self.loaded_page_for_target_mut(target_id) - { - return page.finish_page_command_turn(completion); - } - completion.into_output() - } - - pub(crate) fn start_target_page_diagnostics_snapshot( - &self, - target_id: &str, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_page_diagnostics_snapshot() - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_target_page_diagnostics_snapshot( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_page_diagnostics_snapshot(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_target_service_worker_bypass_refresh( - &mut self, - target_id: &str, - ) -> Result, String> { - let Some(contents) = self.web_contents_for_target_mut(target_id) else { - return Ok(None); - }; - let Some(document) = contents.main_frame.current_document.as_mut() else { - return Ok(None); - }; - document - .page - .start_set_bypass_service_worker(contents.network_request_policy.bypass_service_worker) - .map(Some) - .map_err(|error| format!("failed to update page service worker bypass: {error}")) - } - - pub(crate) fn start_target_blocked_urls_refresh( - &mut self, - target_id: &str, - ) -> Result, String> { - let Some(contents) = self.web_contents_for_target_mut(target_id) else { - return Ok(None); - }; - let Some(document) = contents.main_frame.current_document.as_mut() else { - return Ok(None); - }; - document - .page - .start_set_blocked_url_patterns(&contents.network_request_policy.blocked_url_patterns) - .map(Some) - .map_err(|error| format!("failed to update page blocked URLs: {error}")) - } - - pub(crate) fn start_target_extra_headers_refresh( - &mut self, - target_id: &str, - ) -> Result, String> { - let headers = self.effective_extra_headers_for_target(target_id); - let Some(page) = self.loaded_page_for_target_mut(target_id) else { - return Ok(None); - }; - page.start_set_extra_http_headers(&headers) - .map(Some) - .map_err(|error| format!("failed to update page extra HTTP headers: {error}")) - } - - pub(crate) fn start_target_network_request_policy_refresh( - &mut self, - target_id: &str, - ) -> Result, String> { - let headers = self.effective_extra_headers_for_target(target_id); - let Some(contents) = self.web_contents_for_target_mut(target_id) else { - return Ok(None); - }; - let Some(document) = contents.main_frame.current_document.as_mut() else { - return Ok(None); - }; - let policy = &contents.network_request_policy; - document - .page - .start_set_network_request_policy( - &headers, - policy.bypass_service_worker, - policy.cache_disabled, - &policy.blocked_url_patterns, + self.browser_context + .web_contents_fetch_interception_for_test( + self.web_contents_handle_for_target(target_id)?, ) - .map(Some) - .map_err(|error| format!("failed to replay page network request policy: {error}")) - } - - pub(crate) fn start_target_network_offline_refresh( - &mut self, - target_id: &str, - ) -> Result, String> { - let Some(contents) = self.web_contents_for_target_mut(target_id) else { - return Ok(None); - }; - let Some(document) = contents.main_frame.current_document.as_mut() else { - return Ok(None); - }; - document - .page - .start_set_network_offline(contents.network_offline) - .map(Some) - .map_err(|error| format!("set emulated network conditions failed: {error}")) + .ok() } #[cfg(test)] pub(crate) async fn reset_selected_resource_runtime_async(&mut self) -> bool { - let Some(contents) = self - .physical - .selected_web_contents_id() - .and_then(|id| self.physical.web_contents.get_mut(&id)) - else { - return false; - }; - if !contents.has_navigation_engine() { - return false; - } - contents.reset_resource_runtime_for_test().await; - true - } - - pub(crate) fn start_target_child_frame_lifecycle_work( - &mut self, - target_id: &str, - timeout: Duration, - ) -> Result { - let storage = self.physical.storage_partition.handles.clone(); - self.web_contents_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .start_child_frame_lifecycle_work(&storage, timeout) - } - - pub(crate) fn complete_target_child_frame_lifecycle_work( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result<(bool, RendererCommandTurnOutput), String> { - let completed = self - .web_contents_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .complete_child_frame_lifecycle_work(completion)?; - self.ingest_owner_page_observable_output_updates_for_target(target_id); - Ok(completed) - } - - pub(crate) async fn target_page_diagnostics_snapshot_async( - &mut self, - target_id: &str, - ) -> Result { - let Some(page) = self.loaded_page_for_target_mut(target_id) else { - return Ok(Default::default()); - }; - page.page_diagnostics_snapshot_async() + self.browser_context + .reset_selected_resource_runtime_for_test() .await - .map_err(|error| error.to_string()) } } diff --git a/moli-protocol/src/conn/state/browser_context/page_runtime/document_commands.rs b/moli-protocol/src/conn/state/browser_context/page_runtime/document_commands.rs deleted file mode 100644 index 9daa4ddaee..0000000000 --- a/moli-protocol/src/conn/state/browser_context/page_runtime/document_commands.rs +++ /dev/null @@ -1,365 +0,0 @@ -use super::BrowserContext; -use moli_core::page::{ - CompletedPageCommand, PendingPageCommand, RendererCaptureScreencastFrameReply, - RendererCaptureScreencastFrameRequest, RendererCaptureScreenshotReply, - RendererCaptureScreenshotRequest, RendererCommandTurnOutput, RendererResourceTextSearchOutcome, - RendererSetDocumentContentResult, SubresourceNetworkRecord, -}; - -impl BrowserContext { - #[cfg(test)] - pub(crate) async fn target_runtime_heap_usage_for_test( - &mut self, - target_id: &str, - ) -> Result { - let completion = self - .loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_runtime_heap_usage() - .map_err(|error| error.to_string())? - .wait() - .await - .map_err(|error| error.to_string())?; - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_runtime_heap_usage(completion) - .map_err(|error| error.to_string()) - } - - #[cfg(test)] - pub(crate) fn target_idle_override_for_test( - &self, - target_id: &str, - ) -> Result, String> { - Ok(self - .loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .idle_override()) - } - - #[cfg(test)] - pub(crate) fn target_cached_observable_output_for_test( - &self, - target_id: &str, - ) -> Option<&[moli_core::page::ScriptObservableOutputItem]> { - Some( - self.loaded_page_for_target(target_id)? - .script_execution() - .observable_output_items(), - ) - } - - #[cfg(test)] - pub(crate) async fn evaluate_target_expression_for_test( - &mut self, - target_id: &str, - expression: &str, - await_promise: bool, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .evaluate_runtime_expression_without_navigation_follow_with_await_async( - expression, - await_promise, - ) - .await - .map_err(|error| format!("runtime evaluation failed: {error}")) - } - - #[cfg(test)] - pub(crate) async fn serialize_target_html_for_test( - &mut self, - target_id: &str, - ) -> Result { - let completion = self - .start_serialize_html_for_target(target_id)? - .wait() - .await - .map_err(|error| error.to_string())?; - self.finish_serialize_html_for_target(target_id, completion) - } - - pub(crate) fn start_set_document_content_for_target( - &self, - target_id: &str, - frame_id: String, - html: String, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_set_document_content(frame_id, html) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_set_document_content_command_turn_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result<(RendererSetDocumentContentResult, RendererCommandTurnOutput), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_set_document_content_command_turn(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_top_level_same_document_navigation_for_target( - &self, - target_id: &str, - url: String, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_top_level_same_document_navigation(url) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_top_level_same_document_navigation_command_turn_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result<(bool, RendererCommandTurnOutput), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_top_level_same_document_navigation_command_turn(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_capture_screencast_frame_for_target( - &self, - target_id: &str, - request: RendererCaptureScreencastFrameRequest, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_capture_screencast_frame(request) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_capture_screencast_frame_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_capture_screencast_frame(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_serialize_html_for_target( - &self, - target_id: &str, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_serialize_html() - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_serialize_html_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_serialize_html(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_capture_screenshot_with_request_for_target( - &self, - target_id: &str, - request: RendererCaptureScreenshotRequest, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_capture_screenshot_with_request(request) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_capture_screenshot_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_capture_screenshot(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_top_level_history_traversal_by_delta_for_target( - &self, - target_id: &str, - delta: i64, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_top_level_history_traversal_by_delta(delta) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_top_level_history_traversal_by_delta_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_top_level_history_traversal_by_delta(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_reset_navigation_history_for_target( - &self, - target_id: &str, - ) -> Result { - self.web_contents_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_reset_navigation_history() - } - - pub(crate) fn finish_reset_navigation_history_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.web_contents_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_reset_navigation_history(completion) - } - - pub(crate) fn start_child_frame_navigation_to_url_for_target( - &self, - target_id: &str, - frame_id: &str, - url: &str, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_child_frame_navigation_to_url(frame_id, url) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_child_frame_navigation_to_url_command_turn_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result<(bool, RendererCommandTurnOutput), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_child_frame_navigation_to_url_command_turn(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_child_frame_resource_search_by_lines_for_target( - &self, - target_id: &str, - frame_id: String, - url: String, - query: String, - case_sensitive: bool, - is_regex: bool, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_child_frame_resource_search_by_lines( - frame_id, - url, - query, - case_sensitive, - is_regex, - ) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_text_search_by_lines_for_target( - &self, - target_id: &str, - text: String, - query: String, - case_sensitive: bool, - is_regex: bool, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_text_search_by_lines(text, query, case_sensitive, is_regex) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_resource_search_by_lines_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_resource_search_by_lines(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_set_javascript_dialog_handler_enabled_for_target( - &self, - target_id: &str, - enabled: bool, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_set_javascript_dialog_handler_enabled(enabled) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_set_bypass_content_security_policy_for_target( - &self, - target_id: &str, - bypass: bool, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_set_bypass_content_security_policy(bypass) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_set_bypass_content_security_policy_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result<(), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_set_bypass_content_security_policy(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn target_response_headers(&self, target_id: &str) -> Option<&[(String, String)]> { - self.loaded_page_for_target(target_id) - .map(|page| page.headers()) - } - - pub(crate) fn target_subresource_network_records( - &self, - target_id: &str, - ) -> Option<&[SubresourceNetworkRecord]> { - self.loaded_page_for_target(target_id) - .map(|page| page.subresource_network_records()) - } - - pub(crate) async fn stop_target_document_lifecycle_async( - &mut self, - target_id: &str, - ) -> Result<(), String> { - let Some(page) = self.loaded_page_for_target_mut(target_id) else { - return Ok(()); - }; - page.stop_document_lifecycle_async() - .await - .map_err(|error| error.to_string()) - } - - pub(crate) fn crash_target_renderer_from_io(&mut self, target_id: &str) { - if let Some(page) = self.loaded_page_for_target_mut(target_id) { - page.crash_devtools_target_from_io(); - } - } -} diff --git a/moli-protocol/src/conn/state/browser_context/page_runtime/document_queries.rs b/moli-protocol/src/conn/state/browser_context/page_runtime/document_queries.rs deleted file mode 100644 index 6a376696bc..0000000000 --- a/moli-protocol/src/conn/state/browser_context/page_runtime/document_queries.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::BrowserContext; -use moli_core::page::{ - ChildFrameTreeSnapshot, CompletedPageCommand, DocumentCookieOwnerSnapshot, PendingPageCommand, -}; - -impl BrowserContext { - pub(crate) fn start_document_storage_key_snapshot_for_target( - &self, - target_id: &str, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_document_storage_key_snapshot() - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_document_storage_key_snapshot_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_document_storage_key_snapshot(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_child_frame_tree_snapshot_for_target( - &self, - target_id: &str, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_child_frame_tree_snapshot() - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_child_frame_tree_snapshot_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result, String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_child_frame_tree_snapshot(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_document_cookie_owner_snapshot_for_target( - &self, - target_id: &str, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_document_cookie_owner_snapshot() - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_document_cookie_owner_snapshot_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_document_cookie_owner_snapshot(completion) - .map_err(|error| error.to_string()) - } -} diff --git a/moli-protocol/src/conn/state/browser_context/page_runtime/emulation.rs b/moli-protocol/src/conn/state/browser_context/page_runtime/emulation.rs deleted file mode 100644 index 5c2d33bb1d..0000000000 --- a/moli-protocol/src/conn/state/browser_context/page_runtime/emulation.rs +++ /dev/null @@ -1,165 +0,0 @@ -use super::BrowserContext; -use moli_core::page::{ - CompletedPageCommand, EmulatedIdleOverride, EmulatedMediaOverrides, PendingPageCommand, - ViewportSurface, -}; - -pub(crate) enum PagePolicyUpdateKind { - SetExtraHttpHeaders, - SetNetworkConditions, - SetIdleOverride, - SetNavigatorOverrides, - SetEmulatedMedia, - SetViewportSurface, - SetDocumentActivity, -} - -impl BrowserContext { - pub(crate) fn start_set_document_activity_for_target( - &self, - target_id: &str, - activity: moli_page_types::DocumentActivity, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_set_document_activity(activity) - .map_err(|error| error.to_string()) - } - - #[allow(clippy::too_many_arguments)] - pub(crate) async fn reconcile_target_runtime_policy_async( - &mut self, - target_id: &str, - script_execution_disabled: bool, - media: &EmulatedMediaOverrides, - network_offline: bool, - viewport: Option, - ) -> anyhow::Result<()> { - let Some(page) = self.loaded_page_for_target_mut(target_id) else { - return Ok(()); - }; - let mut first_error = None; - let mut record = |surface: &str, result: anyhow::Result<()>| { - if let Err(error) = result { - first_error.get_or_insert_with(|| anyhow::anyhow!("{surface}: {error}")); - } - }; - // Each effective policy receives an attempt, even after an earlier admission failure. - record( - "script execution", - page.set_script_execution_disabled_async(script_execution_disabled) - .await, - ); - record("emulated media", page.set_emulated_media_async(media).await); - record( - "network conditions", - page.set_network_offline_async(network_offline).await, - ); - record( - "device metrics viewport", - page.set_viewport_surface_async(viewport).await, - ); - first_error.map_or(Ok(()), Err) - } - - pub(crate) fn start_set_navigator_overrides_for_target( - &self, - target_id: &str, - overrides: &moli_page_types::NavigatorOverrides, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_set_navigator_overrides(overrides) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_set_idle_override_for_target( - &mut self, - target_id: &str, - idle_override: Option, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .start_set_idle_override(idle_override) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_set_emulated_media_for_target( - &self, - target_id: &str, - overrides: &EmulatedMediaOverrides, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_set_emulated_media(overrides) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_set_viewport_surface_for_target( - &self, - target_id: &str, - viewport_surface: Option, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_set_viewport_surface(viewport_surface) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_set_extra_http_headers_for_target( - &self, - target_id: &str, - headers: &moli_fetch::RequestHeaders, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_set_extra_http_headers(headers) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_target_page_policy_update( - &mut self, - target_id: &str, - kind: PagePolicyUpdateKind, - completion: CompletedPageCommand, - ) -> Result<(), String> { - if let Some(page) = self.loaded_page_for_target_mut(target_id) - && completion.is_from_page(page) - { - return match kind { - PagePolicyUpdateKind::SetExtraHttpHeaders => { - page.finish_set_extra_http_headers(completion) - } - PagePolicyUpdateKind::SetNetworkConditions => { - page.finish_set_network_offline(completion) - } - PagePolicyUpdateKind::SetIdleOverride => page.finish_set_idle_override(completion), - PagePolicyUpdateKind::SetNavigatorOverrides => { - page.finish_set_navigator_overrides(completion) - } - PagePolicyUpdateKind::SetEmulatedMedia => { - page.finish_set_emulated_media(completion) - } - PagePolicyUpdateKind::SetDocumentActivity => { - page.finish_set_document_activity(completion) - } - PagePolicyUpdateKind::SetViewportSurface => { - page.finish_set_viewport_surface(completion) - } - } - .map_err(|error| error.to_string()); - } - Self::finish_unobserved_page_policy_update(completion) - } - - pub(crate) fn finish_unobserved_page_policy_update( - completion: CompletedPageCommand, - ) -> Result<(), String> { - completion - .into_unit_page_command_turn() - .map(drop) - .map_err(|error| { - format!("stale Emulation command returned an unexpected reply: {error}") - }) - } -} diff --git a/moli-protocol/src/conn/state/browser_context/page_runtime/input.rs b/moli-protocol/src/conn/state/browser_context/page_runtime/input.rs deleted file mode 100644 index 392eabffed..0000000000 --- a/moli-protocol/src/conn/state/browser_context/page_runtime/input.rs +++ /dev/null @@ -1,136 +0,0 @@ -use super::BrowserContext; -use moli_core::page::{ - PageInputExt, PendingPageCommand, RendererDragData, RendererPointerEventProperties, - RendererTouchPoint, -}; - -/// Native input admitted to the current document, independent of inspector sessions. -pub(crate) enum PageInputCommand<'a> { - Key { - event_name: &'a str, - key: &'a str, - code: &'a str, - text: &'a str, - modifiers: u8, - auto_repeat: bool, - should_insert_text: bool, - }, - Mouse { - x: f64, - y: f64, - event_name: &'a str, - button: i32, - buttons: Option, - click_count: i32, - delta_x: f64, - delta_y: f64, - pointer: RendererPointerEventProperties, - modifiers: u8, - }, - Touch { - points: Vec, - event_name: &'a str, - activate: bool, - }, - Drag { - x: f64, - y: f64, - event_name: &'a str, - data: RendererDragData, - modifiers: u8, - }, - InsertText(&'a str), -} - -impl BrowserContext { - pub(crate) fn start_target_autofill_trigger( - &self, - target_id: &str, - request: moli_core::page::RendererAutofillTriggerRequest, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_autofill_trigger(request) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_target_autofill_trigger( - &mut self, - target_id: &str, - completion: moli_core::page::CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_autofill_trigger(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_target_input_command( - &self, - target_id: &str, - command: PageInputCommand<'_>, - ) -> Result { - let page = self - .loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")?; - match command { - PageInputCommand::Key { - event_name, - key, - code, - text, - modifiers, - auto_repeat, - should_insert_text, - } => page.start_dispatch_key_event_with_outcome( - event_name, - key, - code, - text, - modifiers, - auto_repeat, - should_insert_text, - ), - PageInputCommand::Mouse { - x, - y, - event_name, - button, - buttons, - click_count, - delta_x, - delta_y, - pointer, - modifiers, - } => page.start_dispatch_mouse_event_at_point_with_pointer_outcome( - x, - y, - event_name, - button, - buttons, - click_count, - delta_x, - delta_y, - pointer, - modifiers, - ), - PageInputCommand::Touch { - points, - event_name, - activate, - } => { - page.start_dispatch_touch_event_at_points_with_outcome(points, event_name, activate) - } - PageInputCommand::Drag { - x, - y, - event_name, - data, - modifiers, - } => page - .start_dispatch_drag_event_at_point_with_outcome(x, y, event_name, data, modifiers), - PageInputCommand::InsertText(text) => page.start_insert_text_into_active_control(text), - } - .map_err(|error| error.to_string()) - } -} diff --git a/moli-protocol/src/conn/state/browser_context/page_runtime/network_commands.rs b/moli-protocol/src/conn/state/browser_context/page_runtime/network_commands.rs deleted file mode 100644 index ffdb34b025..0000000000 --- a/moli-protocol/src/conn/state/browser_context/page_runtime/network_commands.rs +++ /dev/null @@ -1,556 +0,0 @@ -use super::BrowserContext; -use moli_core::page::{ - CompletedPageCommand, PendingPageCommand, PendingSubresourceContinueOutcome, - RendererSyntheticResponseBody, SubresourceAuthCredentials, -}; -use url::Url; -#[derive(Clone, Copy)] -pub(crate) enum NetworkPolicyUpdateKind { - RequestPolicy, - ExtraHttpHeaders, - BlockedUrls, - BypassServiceWorker, - NetworkOffline, -} - -impl BrowserContext { - pub(crate) fn finish_target_network_policy_update( - &mut self, - target_id: &str, - finish: NetworkPolicyUpdateKind, - completion: CompletedPageCommand, - ) -> Result<(), String> { - if let Some(page) = self.loaded_page_for_target_mut(target_id) - && completion.is_from_page(page) - { - let result = match finish { - NetworkPolicyUpdateKind::RequestPolicy => { - page.finish_set_network_request_policy(completion) - } - NetworkPolicyUpdateKind::ExtraHttpHeaders => { - page.finish_set_extra_http_headers(completion) - } - NetworkPolicyUpdateKind::BlockedUrls => { - page.finish_set_blocked_url_patterns(completion) - } - NetworkPolicyUpdateKind::BypassServiceWorker => { - page.finish_set_bypass_service_worker(completion) - } - NetworkPolicyUpdateKind::NetworkOffline => { - page.finish_set_network_offline(completion) - } - }; - return result.map_err(|error| error.to_string()); - } - - Self::finish_unobserved_network_policy_update(completion) - } - - pub(crate) fn finish_unobserved_network_policy_update( - completion: CompletedPageCommand, - ) -> Result<(), String> { - // Target/session policy was committed before renderer dispatch. If a - // navigation installs another attachment before this frozen unit reply is - // decoded, consume the old turn without applying its PageState snapshot - // to the replacement. Prepared-document commit configuration carries the - // authoritative policy into that replacement. - completion - .into_unit_page_command_turn() - .map(drop) - .map_err(|error| format!("stale Network command returned an unexpected reply: {error}")) - } -} - -impl BrowserContext { - pub(crate) fn start_continue_pending_subresource_fetch_for_target( - &self, - target_id: &str, - internal_id: u64, - url: Option, - method: Option, - body: Option>, - headers: Option, - intercept_response: bool, - handle_auth_requests: bool, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_continue_pending_subresource_fetch( - internal_id, - url, - method, - body, - headers, - intercept_response, - handle_auth_requests, - ) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_continue_pending_subresource_auth_for_target( - &self, - target_id: &str, - internal_id: u64, - auth: SubresourceAuthCredentials, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_continue_pending_subresource_auth(internal_id, auth) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_cancel_pending_subresource_auth_for_target( - &self, - target_id: &str, - internal_id: u64, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_cancel_pending_subresource_auth(internal_id) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_fail_pending_subresource_auth_for_target( - &self, - target_id: &str, - internal_id: u64, - error_text: String, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_fail_pending_subresource_auth(internal_id, error_text) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_fail_pending_subresource_fetch_for_target( - &self, - target_id: &str, - internal_id: u64, - error_text: String, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_fail_pending_subresource_fetch(internal_id, error_text) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_fail_pending_subresource_response_for_target( - &self, - target_id: &str, - internal_id: u64, - error_text: String, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_fail_pending_subresource_response(internal_id, error_text) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_fulfill_pending_subresource_fetch_for_target( - &self, - target_id: &str, - internal_id: u64, - response_code: u16, - response_headers: Vec<(String, String)>, - response_body: RendererSyntheticResponseBody, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_fulfill_pending_subresource_fetch( - internal_id, - response_code, - response_headers, - response_body, - ) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_fulfill_pending_subresource_response_for_target( - &self, - target_id: &str, - internal_id: u64, - response_code: u16, - response_headers: Vec<(String, String)>, - response_body: RendererSyntheticResponseBody, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_fulfill_pending_subresource_response( - internal_id, - response_code, - response_headers, - response_body, - ) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_continue_pending_subresource_response_for_target( - &self, - target_id: &str, - internal_id: u64, - response_code: Option, - response_headers: Option>, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_continue_pending_subresource_response( - internal_id, - response_code, - response_headers, - ) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_receive_synthetic_websocket_text_for_target( - &self, - target_id: &str, - socket_id: u64, - data: String, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_receive_synthetic_websocket_text(socket_id, data) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_receive_synthetic_websocket_binary_for_target( - &self, - target_id: &str, - socket_id: u64, - data: Vec, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_receive_synthetic_websocket_binary(socket_id, data) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_close_synthetic_websocket_from_server_for_target( - &self, - target_id: &str, - socket_id: u64, - code: Option, - reason: String, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_close_synthetic_websocket_from_server(socket_id, code, reason) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_continue_pending_subresource_fetch_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_continue_pending_subresource_fetch(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_continue_pending_subresource_auth_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_continue_pending_subresource_auth(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_receive_synthetic_websocket_text_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result<(), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_receive_synthetic_websocket_text(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_receive_synthetic_websocket_binary_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result<(), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_receive_synthetic_websocket_binary(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_close_synthetic_websocket_from_server_for_target( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result<(), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_close_synthetic_websocket_from_server(completion) - .map_err(|error| error.to_string()) - } - pub(crate) fn finish_target_subresource_auth_terminal( - &mut self, - target_id: &str, - completion: moli_core::page::CompletedPageCommand, - expose_challenged_response: bool, - ) -> Result<(), String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")?; - let result = if expose_challenged_response { - page.finish_cancel_pending_subresource_auth(completion) - } else { - page.finish_fail_pending_subresource_auth(completion) - }; - result.map(|_| ()).map_err(|error| error.to_string()) - } - pub(crate) fn finish_fail_pending_subresource_fetch_for_target( - &mut self, - target_id: &str, - completion: moli_core::page::CompletedPageCommand, - ) -> Result<(), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_fail_pending_subresource_fetch(completion) - .map(|_| ()) - .map_err(|error| format!("subresource fetch fail failed: {error}")) - } - - pub(crate) fn finish_fail_pending_subresource_response_for_target( - &mut self, - target_id: &str, - completion: moli_core::page::CompletedPageCommand, - ) -> Result<(), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_fail_pending_subresource_response(completion) - .map(|_| ()) - .map_err(|error| format!("subresource response fail failed: {error}")) - } - - pub(crate) fn finish_fulfill_pending_subresource_fetch_for_target( - &mut self, - target_id: &str, - completion: moli_core::page::CompletedPageCommand, - ) -> Result<(), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_fulfill_pending_subresource_fetch(completion) - .map(|_| ()) - .map_err(|error| format!("subresource fetch fulfill failed: {error}")) - } - - pub(crate) fn finish_fulfill_pending_subresource_response_for_target( - &mut self, - target_id: &str, - completion: moli_core::page::CompletedPageCommand, - ) -> Result<(), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_fulfill_pending_subresource_response(completion) - .map(|_| ()) - .map_err(|error| format!("subresource response fulfill failed: {error}")) - } - - pub(crate) fn finish_continue_pending_subresource_response_for_target( - &mut self, - target_id: &str, - completion: moli_core::page::CompletedPageCommand, - ) -> Result<(), String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_continue_pending_subresource_response(completion) - .map(|_| ()) - .map_err(|error| format!("subresource response continue failed: {error}")) - } - pub(crate) async fn continue_pending_subresource_fetch_for_target_async( - &mut self, - target_id: &str, - internal_id: u64, - url: Option, - method: Option, - body: Option>, - headers: Option, - intercept_response: bool, - handle_auth_requests: bool, - ) -> Result { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.continue_pending_subresource_fetch_async( - internal_id, - url, - method, - body, - headers.map(Into::into), - intercept_response, - handle_auth_requests, - ) - .await - .map_err(|error| format!("subresource fetch continue failed: {error}")) - } - - pub(crate) async fn continue_pending_subresource_auth_for_target_async( - &mut self, - target_id: &str, - internal_id: u64, - auth: SubresourceAuthCredentials, - ) -> Result { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.continue_pending_subresource_auth_async(internal_id, auth) - .await - .map_err(|error| format!("subresource auth continue failed: {error}")) - } - - pub(crate) async fn fail_pending_subresource_auth_for_target_async( - &mut self, - target_id: &str, - internal_id: u64, - error_text: String, - ) -> Result, String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.fail_pending_subresource_auth_async(internal_id, error_text) - .await - .map_err(|error| format!("subresource auth fail failed: {error}")) - } - - pub(crate) async fn fail_pending_subresource_fetch_for_target_async( - &mut self, - target_id: &str, - internal_id: u64, - error_text: String, - ) -> Result, String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.fail_pending_subresource_fetch_async(internal_id, error_text) - .await - .map_err(|error| format!("subresource fetch fail failed: {error}")) - } - - pub(crate) async fn fulfill_pending_subresource_fetch_for_target_async( - &mut self, - target_id: &str, - internal_id: u64, - response_code: u16, - response_headers: Vec<(String, String)>, - response_body: moli_core::page::RendererSyntheticResponseBody, - ) -> Result<(), String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.fulfill_pending_subresource_fetch_async( - internal_id, - response_code, - response_headers, - response_body, - ) - .await - .map_err(|error| format!("subresource fetch fulfill failed: {error}")) - } - - pub(crate) async fn continue_pending_subresource_response_for_target_async( - &mut self, - target_id: &str, - internal_id: u64, - response_code: Option, - response_headers: Option>, - ) -> Result<(), String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.continue_pending_subresource_response_async( - internal_id, - response_code, - response_headers, - ) - .await - .map_err(|error| format!("subresource response continue failed: {error}")) - } - - pub(crate) async fn fail_pending_subresource_response_for_target_async( - &mut self, - target_id: &str, - internal_id: u64, - error_text: String, - ) -> Result, String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.fail_pending_subresource_response_async(internal_id, error_text) - .await - .map_err(|error| format!("subresource response fail failed: {error}")) - } - - pub(crate) async fn fulfill_pending_subresource_response_for_target_async( - &mut self, - target_id: &str, - internal_id: u64, - response_code: u16, - response_headers: Vec<(String, String)>, - response_body: moli_core::page::RendererSyntheticResponseBody, - ) -> Result<(), String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.fulfill_pending_subresource_response_async( - internal_id, - response_code, - response_headers, - response_body, - ) - .await - .map_err(|error| format!("subresource response fulfill failed: {error}")) - } - - pub(crate) async fn receive_synthetic_websocket_text_for_target_async( - &mut self, - target_id: &str, - socket_id: u64, - data: String, - ) -> Result<(), String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.receive_synthetic_websocket_text_async(socket_id, data) - .await - .map_err(|error| format!("synthetic websocket text dispatch failed: {error}")) - } - - pub(crate) async fn receive_synthetic_websocket_binary_for_target_async( - &mut self, - target_id: &str, - socket_id: u64, - data: Vec, - ) -> Result<(), String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.receive_synthetic_websocket_binary_async(socket_id, data) - .await - .map_err(|error| format!("synthetic websocket binary dispatch failed: {error}")) - } - - pub(crate) async fn close_synthetic_websocket_from_server_for_target_async( - &mut self, - target_id: &str, - socket_id: u64, - code: Option, - reason: String, - ) -> Result<(), String> { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or_else(|| "NoDocumentLoaded".to_owned())?; - page.close_synthetic_websocket_from_server_async(socket_id, code, reason) - .await - .map_err(|error| format!("synthetic websocket close dispatch failed: {error}")) - } -} diff --git a/moli-protocol/src/conn/state/browser_context/page_runtime/resource_commands.rs b/moli-protocol/src/conn/state/browser_context/page_runtime/resource_commands.rs deleted file mode 100644 index 5552840ebe..0000000000 --- a/moli-protocol/src/conn/state/browser_context/page_runtime/resource_commands.rs +++ /dev/null @@ -1,104 +0,0 @@ -use super::BrowserContext; -use moli_core::page::{ - CompletedPageCommand, PendingPageCommand, RendererAppManifestLoadPreparation, - RendererAppManifestLoadPublication, RendererCommandTurnOutput, - RendererNetworkResourceLoadPreparation, -}; -use std::sync::Arc; -use url::Url; - -impl BrowserContext { - pub(crate) fn start_target_blob_read( - &self, - target_id: &str, - uuid: String, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_blob_bytes_for_uuid(uuid) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_target_blob_read( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result>, String> { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_blob_bytes_for_uuid(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_target_network_resource_load( - &self, - target_id: &str, - frame_id: String, - url: Url, - disable_cache: bool, - include_credentials: bool, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_prepare_network_resource_load(frame_id, url, disable_cache, include_credentials) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_target_network_resource_load_preparation( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - let page = self - .loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")?; - if !completion.is_from_page(page) { - return Err("Document changed while preparing the network resource load".to_owned()); - } - page.finish_prepare_network_resource_load(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_target_app_manifest_load( - &self, - target_id: &str, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_prepare_app_manifest_load() - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_target_app_manifest_load_preparation( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_prepare_app_manifest_load(completion) - .map_err(|error| error.to_string()) - } - - pub(crate) fn start_target_app_manifest_publication( - &self, - target_id: &str, - publication: RendererAppManifestLoadPublication, - ) -> Result { - self.loaded_page_for_target(target_id) - .ok_or("NoDocumentLoaded")? - .start_publish_app_manifest_load(publication) - .map_err(|error| error.to_string()) - } - - pub(crate) fn finish_target_app_manifest_publication( - &mut self, - target_id: &str, - completion: CompletedPageCommand, - ) -> Result { - self.loaded_page_for_target_mut(target_id) - .ok_or("NoDocumentLoaded")? - .finish_publish_app_manifest_load(completion) - .map_err(|error| error.to_string()) - } -} diff --git a/moli-protocol/src/conn/state/browser_context/page_slot.rs b/moli-protocol/src/conn/state/browser_context/page_slot.rs index 2dcf838f48..b050b6a0a5 100644 --- a/moli-protocol/src/conn/state/browser_context/page_slot.rs +++ b/moli-protocol/src/conn/state/browser_context/page_slot.rs @@ -1,13 +1,14 @@ use super::BrowserContext; #[cfg(test)] +use moli_core::browser::{DocumentLifetime, DocumentLifetimeObserver}; +#[cfg(test)] use moli_core::page::RendererLifecycleEventStamp; use moli_core::{ browser::{ - BrowserSequence, DocumentId, DocumentLifecycle, DocumentLifetime, DocumentLifetimeObserver, - NavigationId, RendererPageResidenceIdentity, + BrowserSequence, DocumentId, DocumentLifecycle, NavigationId, RendererPageResidenceIdentity, }, page::{ - Page, RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind, + RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind, RendererDocumentLifecycleIdentity, RendererDocumentLifecycleMilestone, RendererDocumentLifecycleSnapshot, RendererDocumentLifecycleWaitOutcome, RendererDocumentLifecycleWaiter, RendererDocumentToken, RendererFrameToken, @@ -20,8 +21,6 @@ use crate::conn::state::document_lifecycle_observer::{ RendererDocumentLifecycleObserver, }; -use crate::conn::state::web_contents::DocumentHost; - #[cfg(test)] mod document_host_tests; @@ -178,6 +177,7 @@ impl PendingRendererPageBinding { } } + #[cfg(test)] fn document_id(&self) -> DocumentId { match self { #[cfg(test)] @@ -454,39 +454,23 @@ impl TargetPageSlot { } impl BrowserContext { - pub(super) fn loaded_page_for_target(&self, target_id: &str) -> Option<&Page> { - self.web_contents_for_target(target_id)? - .main_frame - .current_document - .as_ref() - .map(|document| &document.page) - } - - pub(super) fn loaded_page_for_target_mut(&mut self, target_id: &str) -> Option<&mut Page> { - self.web_contents_for_target_mut(target_id)? - .main_frame - .current_document - .as_mut() - .map(|document| &mut document.page) - } - pub(crate) fn target_has_loaded_page(&self, target_id: &str) -> bool { - self.web_contents_for_target(target_id) - .is_some_and(|contents| contents.main_frame.current_document.is_some()) + self.web_contents_handle_for_target(target_id) + .is_some_and(|handle| self.browser_context.has_loaded_document(handle)) } pub(crate) fn loaded_page_absence_reason_for_target( &self, target_id: &str, ) -> Option { - let contents = self.web_contents_for_target(target_id)?; - if contents.main_frame.current_document.is_some() { + let handle = self.web_contents_handle_for_target(target_id)?; + if self.browser_context.has_loaded_document(handle) { return None; } - if contents - .navigation() - .initial_document_build() - .is_some_and(|build| build.completion.pending()) + if self + .browser_context + .initial_document_build_pending(handle) + .unwrap_or(false) { return Some(TargetPageAbsenceReason::InitialDocumentPageBuildInProgress); } @@ -512,11 +496,11 @@ impl BrowserContext { pub(in crate::conn) fn project_initial_document_build( &mut self, target_id: &str, - key: crate::conn::state::web_contents::InitialDocumentBuildKey, + key: moli_core::browser::web_contents::InitialDocumentBuildKey, ) { debug_assert_eq!( - self.web_contents_for_target(target_id) - .map(|contents| contents.id()), + self.web_contents_handle_for_target(target_id) + .map(|handle| handle.id()), Some(key.web_contents()) ); self.page_slot_for_target_mut(target_id) @@ -529,7 +513,7 @@ impl BrowserContext { pub(in crate::conn) fn retire_initial_document_projection( &mut self, - key: crate::conn::state::web_contents::InitialDocumentBuildKey, + key: moli_core::browser::web_contents::InitialDocumentBuildKey, ) { let Some(target_id) = self .page_targets @@ -552,42 +536,6 @@ impl BrowserContext { } } - pub(super) fn replace_loaded_page_with_reason_for_target( - &mut self, - target_id: &str, - page: Option, - absence_reason: TargetPageAbsenceReason, - ) -> Option { - let next_document = page.map(|page| { - let renderer_page = RendererPageResidenceIdentity::from_page(&page); - let id = match self - .page_slot_for_target_mut(target_id) - .expect("registered Target projection") - .pending_renderer_page - .as_ref() - { - Some(binding) => { - assert_eq!( - binding.renderer_page(), - renderer_page, - "installed Page must match its explicit renderer Page reservation" - ); - binding.document_id() - } - None => DocumentId::allocate(), - }; - DocumentHost::new(id, page) - }); - self.reset_document_projection_for_target( - target_id, - next_document.is_some(), - absence_reason, - ); - self.web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .replace_document(next_document) - } - pub(super) fn reset_document_projection_for_target( &mut self, target_id: &str, @@ -628,31 +576,14 @@ impl BrowserContext { } } - #[cfg(test)] - pub(super) fn replace_target_document( - &mut self, - target_id: &str, - page: Option, - ) -> Option { - let Some(page) = page else { - panic!( - "replace_loaded_page(None) is not a valid production transition; use clear_loaded_page_with_reason" - ); - }; - self.replace_loaded_page_with_reason_for_target( - target_id, - Some(page), - TargetPageAbsenceReason::NoTarget, - ) - } - pub(crate) fn target_document_id(&self, target_id: &str) -> Option { + let handle = self.web_contents_handle_for_target(target_id)?; let id = self - .web_contents_for_target(target_id)? - .main_frame - .current_document - .as_ref() - .map(|document| document.id); + .browser_context + .document_handle(handle) + .ok() + .flatten() + .map(|document| document.id()); #[cfg(test)] let id = id.or_else(|| { self.page_slot_for_target(target_id) @@ -664,26 +595,34 @@ impl BrowserContext { id } - pub(super) fn document_lifecycle_for_target( + #[cfg(test)] + pub(super) fn document_lifecycle_snapshot_for_target( &self, target_id: &str, - ) -> Option<&DocumentLifecycle> { - let lifecycle = self - .web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .main_frame - .current_document - .as_ref() - .map(|document| &document.lifecycle); + ) -> Option { + let snapshot = self + .web_contents_handle_for_target(target_id) + .and_then(|web_contents| { + self.browser_context + .document_handle(web_contents) + .ok() + .flatten() + .and_then(|document| { + self.browser_context + .document_lifecycle_snapshot(document) + .ok() + .flatten() + }) + }); #[cfg(test)] - let lifecycle = lifecycle.or_else(|| { + let snapshot = snapshot.or_else(|| { self.page_slot_for_target(target_id) .expect("registered Target projection") .document_fixture .as_ref() - .map(|fixture| &fixture.lifecycle) + .and_then(|fixture| fixture.lifecycle.snapshot()) }); - lifecycle + snapshot } #[cfg(test)] @@ -694,17 +633,19 @@ impl BrowserContext { ) { let snapshot = lifecycle.snapshot().expect("fixture lifecycle"); let previous = self - .document_lifecycle_for_target(target_id) - .and_then(DocumentLifecycle::snapshot) + .document_lifecycle_snapshot_for_target(target_id) .map(|snapshot| (snapshot.frame, snapshot.document, snapshot.epoch)); - if let Some(document) = self - .web_contents_for_target_mut(target_id) - .unwrap() - .main_frame - .current_document - .as_mut() - { - document.lifecycle = lifecycle; + let web_contents = self + .web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"); + let physical_document = self + .browser_context + .document_handle(web_contents) + .expect("registered Target must reference live WebContents"); + if let Some(document) = physical_document { + self.browser_context + .install_document_lifecycle_for_test(document, lifecycle) + .expect("current fixture Document"); } else { self.page_slot_for_target_mut(target_id) .unwrap() @@ -716,10 +657,10 @@ impl BrowserContext { if previous != Some((snapshot.frame, snapshot.document, snapshot.epoch)) || snapshot.terminated.is_some() { - self.web_contents_for_target_mut(target_id) - .unwrap() - .javascript_dialogs - .clear(); + let handle = self.web_contents_handle_for_target(target_id).unwrap(); + self.browser_context + .clear_web_contents_javascript_dialogs_for_test(handle) + .unwrap(); } } @@ -729,12 +670,10 @@ impl BrowserContext { target_id: &str, event: RendererDocumentLifecycleEvent, ) -> bool { - if self - .web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .main_frame - .current_document - .is_none() + let handle = self + .web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"); + if !self.browser_context.has_loaded_document(handle) && let Some(fixture) = self .page_slot_for_target_mut(target_id) .expect("registered Target projection") @@ -753,10 +692,9 @@ impl BrowserContext { RendererDocumentLifecycleEventKind::Terminated { .. } )) { - self.web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .javascript_dialogs - .clear(); + self.browser_context + .clear_web_contents_javascript_dialogs_for_test(handle) + .unwrap(); } if accepted && matches!( @@ -767,43 +705,18 @@ impl BrowserContext { } ) { - self.web_contents_for_target_mut(target_id) - .unwrap() - .mark_initial_empty_document_exited(); + self.browser_context + .mark_initial_empty_document_exited_for_test(handle) + .unwrap(); } return accepted; } - self.web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .apply_document_lifecycle(event) - .is_some() - } - - pub(super) fn document_lifetime_mut_for_target( - &mut self, - target_id: &str, - ) -> Option<&mut DocumentLifetime> { - #[cfg(test)] - if self - .web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .main_frame - .current_document - .is_none() - { - return self - .page_slot_for_target_mut(target_id) - .expect("registered Target projection") - .document_fixture - .as_mut() - .map(|fixture| &mut fixture.lifetime); - } - self.web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .main_frame - .current_document - .as_mut() - .map(|document| &mut document.lifetime) + let document = self + .document_handle_for_target(target_id) + .expect("current fixture Document"); + self.browser_context + .apply_document_lifecycle_for_test(document, event) + .unwrap_or(false) } #[cfg(test)] @@ -814,10 +727,13 @@ impl BrowserContext { .as_ref() .map(PendingRendererPageBinding::document_id) .or_else(|| { - self.web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .navigation() - .pending_document() + let handle = self + .web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"); + self.browser_context + .pending_document_for_test(handle) + .ok() + .flatten() .map(|(_, document)| document) }) } @@ -845,11 +761,13 @@ impl BrowserContext { .pending_renderer_page = None; } + let handle = self + .web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"); if let Some((navigation, document_id)) = self - .web_contents_for_target_mut(target_id) + .browser_context + .pending_document_for_test(handle) .expect("registered Target must reference live WebContents") - .navigation() - .pending_document() { self.page_slot_for_target_mut(target_id) .expect("registered Target projection") @@ -871,12 +789,28 @@ impl BrowserContext { document_id } + #[cfg(test)] pub(crate) fn document_lifetime_observer_for_target( &mut self, target_id: &str, ) -> Option { - self.document_lifetime_mut_for_target(target_id) - .map(DocumentLifetime::observe) + let web_contents = self.web_contents_handle_for_target(target_id)?; + let physical_document = self + .browser_context + .document_handle(web_contents) + .ok() + .flatten(); + if let Some(document) = physical_document { + return self + .browser_context + .observe_document_lifetime(document) + .ok(); + } + self.page_slot_for_target_mut(target_id) + .expect("registered Target projection") + .document_fixture + .as_mut() + .map(|fixture| fixture.lifetime.observe()) } #[cfg(test)] @@ -917,28 +851,34 @@ impl BrowserContext { .expect("registered Target projection") .runtime_slot .retire_javascript_dialog_scope(); - self.web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .javascript_dialogs - .clear(); + let web_contents = self + .web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"); self.page_slot_for_target_mut(target_id) .expect("registered Target projection") .finish_renderer_document_lifecycle_observers( RendererDocumentLifecycleObservation::Superseded, ); - if let Some(lifetime) = self.document_lifetime_mut_for_target(target_id) { - std::mem::take(lifetime).supersede(); - } if let Some(document) = self - .web_contents_for_target_mut(target_id) + .browser_context + .document_handle(web_contents) .expect("registered Target must reference live WebContents") - .main_frame - .current_document - .as_mut() { - document.id = document_id; - document.lifecycle = DocumentLifecycle::default(); + self.browser_context + .replace_document_identity_for_test(document, document_id) + .expect("current fixture Document"); } else { + self.browser_context + .clear_web_contents_javascript_dialogs_for_test(web_contents) + .unwrap(); + if let Some(fixture) = self + .page_slot_for_target_mut(target_id) + .expect("registered Target projection") + .document_fixture + .take() + { + fixture.lifetime.supersede(); + } self.page_slot_for_target_mut(target_id) .expect("registered Target projection") .document_fixture = Some(DocumentFixture { @@ -966,9 +906,12 @@ impl BrowserContext { RendererDocumentLifecycleObservation::Superseded, ); let token = self - .web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .start_document_navigation(); + .browser_context + .start_document_navigation( + self.web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"), + ) + .expect("registered Target must reference live WebContents"); self.page_slot_for_target_mut(target_id) .expect("registered Target projection") .pending_renderer_page = None; @@ -991,10 +934,9 @@ impl BrowserContext { target_id: &str, token: &NavigationId, ) -> Option { - self.web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .navigation() - .document_navigation_cancellation_handle(token) + let _ = self.web_contents_handle_for_target(target_id)?; + self.browser_context + .document_navigation_cancellation_handle_for_test(token) } #[cfg(test)] @@ -1004,8 +946,10 @@ impl BrowserContext { token: &NavigationId, additional_cancellation: Option, ) -> bool { - self.web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") + let Some(_handle) = self.web_contents_handle_for_target(target_id) else { + return false; + }; + self.browser_context .arm_background_navigation_completion(token, additional_cancellation) } @@ -1015,14 +959,20 @@ impl BrowserContext { target_id: &str, token: &NavigationId, ) -> bool { - self.web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") + let Some(_handle) = self.web_contents_handle_for_target(target_id) else { + return false; + }; + self.browser_context .settle_background_navigation_completion(token) } pub(crate) fn has_inflight_background_navigation_for_target(&self, target_id: &str) -> bool { - self.web_contents_for_target(target_id) - .is_some_and(|contents| contents.navigation().has_inflight_background_navigation()) + self.web_contents_handle_for_target(target_id) + .is_some_and(|handle| { + self.browser_context + .has_inflight_background_navigation_for_web_contents(handle) + .unwrap_or(false) + }) } #[cfg(test)] @@ -1032,11 +982,13 @@ impl BrowserContext { token: &NavigationId, renderer_page: RendererPageResidenceIdentity, ) -> bool { + let handle = self + .web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"); let Some((navigation, document_id)) = self - .web_contents_for_target_mut(target_id) + .browser_context + .pending_document_for_test(handle) .expect("registered Target must reference live WebContents") - .navigation() - .pending_document() .filter(|(navigation, _)| navigation == token) else { return false; @@ -1073,8 +1025,11 @@ impl BrowserContext { target_id: &str, renderer_page: RendererPageResidenceIdentity, ) -> bool { - self.loaded_page_for_target(target_id) - .is_some_and(|page| RendererPageResidenceIdentity::from_page(page) == renderer_page) + self.web_contents_handle_for_target(target_id) + .is_some_and(|handle| { + self.browser_context + .document_renderer_matches(handle, renderer_page) + }) || self .page_slot_for_target(target_id) .expect("registered Target projection") @@ -1088,10 +1043,12 @@ impl BrowserContext { target_id: &str, token: &NavigationId, ) -> bool { - self.web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .navigation() - .accepts_pending_document_navigation_event(token) + self.web_contents_handle_for_target(target_id) + .is_some_and(|handle| { + self.browser_context + .accepts_pending_navigation(handle, token) + .unwrap_or(false) + }) } pub(crate) fn accepts_document_body_completion_event_for_target( @@ -1099,15 +1056,21 @@ impl BrowserContext { target_id: &str, token: &NavigationId, ) -> bool { - self.web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .navigation() - .accepts_document_body_completion_event(token) + self.web_contents_handle_for_target(target_id) + .is_some_and(|handle| { + self.browser_context + .accepts_document_body_completion(handle, token) + .unwrap_or(false) + }) } pub(crate) fn has_pending_document_navigation_for_target(&self, target_id: &str) -> bool { - self.web_contents_for_target(target_id) - .is_some_and(|contents| contents.navigation().has_pending_document_navigation()) + self.web_contents_handle_for_target(target_id) + .is_some_and(|handle| { + self.browser_context + .has_pending_document_navigation(handle) + .unwrap_or(false) + }) } pub(in crate::conn) fn pending_navigation_id_for_loader( @@ -1115,13 +1078,16 @@ impl BrowserContext { target_id: &str, loader_id: &str, ) -> Option { - let controller = self.web_contents_for_target(target_id)?.navigation(); + let handle = self.web_contents_handle_for_target(target_id)?; self.page_slot_for_target(target_id)? .cdp_navigation_loaders .iter() .find_map(|(navigation, loader)| { (loader == loader_id - && controller.accepts_pending_document_navigation_event(navigation)) + && self + .browser_context + .accepts_pending_navigation(handle, navigation) + .unwrap_or(false)) .then_some(*navigation) }) } @@ -1129,15 +1095,17 @@ impl BrowserContext { pub(in crate::conn) fn project_navigation_load_for_target( &mut self, target_id: &str, - load: &crate::conn::state::web_contents::AdmittedNavigationLoad, + load: &moli_core::browser::BrowserNavigationLoad, ) -> Result<(), String> { - let contents = self - .web_contents_for_target(target_id) + let handle = self + .web_contents_handle_for_target(target_id) .ok_or("navigation WebContents unavailable")?; - if contents.id() != load.web_contents_id() - || !contents - .navigation() - .accepts_document_preparation(load.navigation_id(), load.renderer_page()) + if handle.id() != load.web_contents_id() + || !self.browser_context.accepts_document_preparation( + handle, + load.navigation_id(), + load.renderer_page(), + )? { return Err("stale navigation document candidate".to_owned()); } @@ -1155,10 +1123,12 @@ impl BrowserContext { self.page_slot_for_target(target_id) .expect("registered Target projection") .loader_id_for_navigation( - self.web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .navigation() - .current_document_navigation()?, + self.browser_context + .current_document_navigation( + self.web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"), + ) + .ok()??, ) } @@ -1166,10 +1136,12 @@ impl BrowserContext { self.page_slot_for_target(target_id) .expect("registered Target projection") .loader_id_for_navigation( - self.web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .navigation() - .committed_document_navigation()?, + self.browser_context + .committed_document_navigation( + self.web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"), + ) + .ok()??, ) } @@ -1178,16 +1150,19 @@ impl BrowserContext { .page_targets .get_mut(target_id) .expect("registered Target projection"); - let contents = self - .physical - .web_contents - .get(&target.web_contents_id()) - .expect("registered WebContents"); + let handle = moli_core::browser::WebContentsHandle::new( + self.browser_context.id(), + target.web_contents_id(), + ); target .runtime_slot .page_slot_mut() .cdp_navigation_loaders - .retain(|(id, _)| contents.navigation().retains_navigation(*id)); + .retain(|(id, _)| { + self.browser_context + .navigation_retains(handle, *id) + .unwrap_or(false) + }); } #[cfg(test)] @@ -1196,10 +1171,12 @@ impl BrowserContext { target_id: &str, token: &NavigationId, ) -> bool { + let handle = self + .web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"); if !self - .web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .commit_pending_document_navigation_if_matches(token) + .browser_context + .commit_pending_document_navigation_for_test(handle, token) { return false; } @@ -1212,10 +1189,13 @@ impl BrowserContext { target_id: &str, navigation: &NavigationId, ) -> bool { + let handle = self + .web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"); if self - .web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .clear_pending_document_navigation_if_matches(navigation) + .browser_context + .clear_pending_navigation_if_matches(handle, navigation) + .unwrap_or(false) { if matches!( self.page_slot_for_target_mut(target_id).expect("registered Target projection").pending_renderer_page.as_ref(), @@ -1245,9 +1225,12 @@ impl BrowserContext { .finish_renderer_document_lifecycle_observers( RendererDocumentLifecycleObservation::Unavailable, ); - self.web_contents_for_target_mut(target_id) - .expect("registered Target must reference live WebContents") - .clear_document_navigation_state(); + let handle = self + .web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"); + self.browser_context + .clear_document_navigation_state(handle) + .expect("registered Target must reference live WebContents"); self.page_slot_for_target_mut(target_id) .expect("registered Target projection") .cdp_navigation_loaders @@ -1414,10 +1397,13 @@ impl BrowserContext { .is_some_and(|binding| { Some(binding.document_id) == self.target_document_id(target_id) && binding.navigation.as_ref().is_none_or(|navigation| { - self.web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .navigation() - .committed_document_navigation() + self.browser_context + .committed_document_navigation( + self.web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"), + ) + .ok() + .flatten() .as_ref() == Some(navigation) }) @@ -1573,10 +1559,13 @@ impl BrowserContext { .filter(|binding| { Some(binding.document_id) == self.target_document_id(target_id) && binding.navigation.as_ref().is_none_or(|navigation| { - self.web_contents_for_target(target_id) - .expect("registered Target must reference live WebContents") - .navigation() - .committed_document_navigation() + self.browser_context + .committed_document_navigation( + self.web_contents_handle_for_target(target_id) + .expect("registered Target must reference live WebContents"), + ) + .ok() + .flatten() .as_ref() == Some(navigation) }) @@ -1587,7 +1576,18 @@ impl BrowserContext { &self, target_id: &str, ) -> Option { - self.document_lifecycle_for_target(target_id)?.snapshot() + #[cfg(test)] + { + self.document_lifecycle_snapshot_for_target(target_id) + } + + #[cfg(not(test))] + { + let document = self.document_handle_for_target(target_id)?; + self.browser_context + .document_lifecycle_snapshot(document) + .ok()? + } } pub(crate) fn apply_renderer_document_lifecycle( @@ -1595,16 +1595,11 @@ impl BrowserContext { renderer_page: RendererPageResidenceIdentity, event: RendererDocumentLifecycleEvent, ) -> Option { - if let Some(contents) = self.physical.web_contents.values_mut().find(|contents| { - contents - .main_frame - .current_document - .as_ref() - .is_some_and(|document| { - RendererPageResidenceIdentity::from_page(&document.page) == renderer_page - }) - }) { - return contents.apply_document_lifecycle(event); + if let Some(occurrence) = self + .browser_context + .apply_renderer_document_lifecycle(renderer_page, event) + { + return Some(occurrence); } #[cfg(test)] { @@ -1612,7 +1607,7 @@ impl BrowserContext { .page_targets .iter() .find(|target| { - self.loaded_page_for_target(target.target_id()).is_none() + !self.target_has_loaded_page(target.target_id()) && self.routes_renderer_page_for_target(target.target_id(), renderer_page) && target.runtime_slot.page_slot().document_fixture.is_some() }) @@ -1879,9 +1874,8 @@ mod page_residence_tests { ); assert!( browser_context - .replace_loaded_page_with_reason_for_target( + .retire_loaded_document_with_reason_for_target( PAGE_SLOT_TEST_TARGET, - None, TargetPageAbsenceReason::TestFixture ) .is_none() diff --git a/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs b/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs index 566a39cdd2..8b9b1cd47e 100644 --- a/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs +++ b/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs @@ -1,78 +1,112 @@ use super::*; -use moli_core::runtime::{Browser, BrowserConfig}; +use crate::conn::{CdpConnection, CommandOwnerScope}; +use moli_core::browser::{DocumentHandle, WebContentsHandle}; use std::{ future::Future, + ops::{Deref, DerefMut}, task::{Context, Poll, Waker}, }; const TARGET: &str = "TID-dialog-owner"; +struct DocumentOwnerFixture { + conn: CdpConnection, + initial_artifacts: Option, +} + +impl Deref for DocumentOwnerFixture { + type Target = BrowserContext; + + fn deref(&self) -> &Self::Target { + self.conn.browser_context.as_ref().expect("fixture context") + } +} + +impl DerefMut for DocumentOwnerFixture { + fn deref_mut(&mut self) -> &mut Self::Target { + self.conn.browser_context.as_mut().expect("fixture context") + } +} + async fn prepare_navigation( - owner: &BrowserContext, + owner: &mut DocumentOwnerFixture, navigation: NavigationId, - page: Page, - url: url::Url, - artifacts: &RendererPageCreationArtifacts, -) -> crate::conn::PreparedDocumentNavigation { + loader_id: &str, + url: &str, +) -> crate::conn::LoadedNavigation { + let command_owner = CommandOwnerScope::capture(&owner.conn, None); owner - .start_loaded_document_navigation_for_target( - TARGET, + .conn + .prepare_navigation_fixture_for_owner_and_token_async( + &command_owner, navigation, - page, - crate::conn::DocumentNavigationDestination { - url, - security_origin: "null".into(), - secure_context_type: "InsecureScheme".into(), - }, - artifacts, - &Default::default(), + loader_id, + url, ) - .unwrap() .await .unwrap() } -fn empty_document_context() -> BrowserContext { - let mut owner = BrowserContext::new("BID-dialog-owner".into()); +fn empty_document_context_with_runtime_config( + runtime_config: moli_core::runtime::NavigationRuntimeConfig, +) -> DocumentOwnerFixture { + let mut conn = crate::test_support::connection(); + let mut owner = conn.new_browser_context_fixture_for_test("BID-dialog-owner"); + owner.bind_page_navigation_engines(runtime_config, None); owner.set_active_target_id(TARGET); + conn.install_browser_context_fixture_for_test(owner); + DocumentOwnerFixture { + conn, + initial_artifacts: None, + } +} + +fn empty_document_context() -> DocumentOwnerFixture { + empty_document_context_with_runtime_config(Default::default()) +} + +async fn context_with_document_and_runtime_config( + url: &str, + runtime_config: moli_core::runtime::NavigationRuntimeConfig, +) -> DocumentOwnerFixture { + let mut owner = empty_document_context_with_runtime_config(runtime_config); + let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-fixture".into()); + let loaded = prepare_navigation(&mut owner, navigation, "LOADER-fixture", url).await; + owner.initial_artifacts = Some(loaded.page_creation_artifacts.clone()); + let committed = owner.commit_loaded_navigation(loaded.page).unwrap(); + let projection_fence = committed + .inspection_projection + .expect("initial fixture must attach its exact renderer Document"); + owner + .page_targets + .get_mut(TARGET) + .unwrap() + .runtime_slot + .publish_document_projection_fence(projection_fence) + .expect("initial fixture inspection projection must finish"); + committed.previous_document_retirement.close().await; owner } -fn context_with_document(page: Page) -> BrowserContext { - let mut owner = empty_document_context(); - assert!( - owner - .replace_target_page_for_test(TARGET, Some(page)) - .is_none() - ); - owner +async fn context_with_document(url: &str) -> DocumentOwnerFixture { + context_with_document_and_runtime_config(url, Default::default()).await } #[tokio::test] async fn loaded_navigation_commit_settles_document_history_and_navigation_together() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let first = browser - .fetch("data:text/html,first") - .await - .unwrap(); - let mut owner = context_with_document(first); + let mut owner = context_with_document("data:text/html,first").await; let old_document = owner.target_document_id(TARGET).unwrap(); let token = owner.begin_target_document_navigation(TARGET, "LOADER-atomic".into()); - let expected_document = owner - .web_contents_for_target(TARGET) - .unwrap() - .navigation() - .pending_document() - .unwrap() - .1; - let mut page = browser - .fetch("data:text/html,second") - .await - .unwrap(); - let url = page.final_url().clone(); - let artifacts = page.take_page_creation_artifacts().unwrap(); - let prepared = prepare_navigation(&owner, token, page, url.clone(), &artifacts).await; - let committed = owner.commit_loaded_navigation(prepared).unwrap(); + let expected_document = owner.target_pending_document_id(TARGET).unwrap(); + let loaded = prepare_navigation( + &mut owner, + token, + "LOADER-atomic", + "data:text/html,second", + ) + .await; + let url = loaded.final_url.clone(); + let committed = owner.commit_loaded_navigation(loaded.page).unwrap(); assert!(committed.inspection_projection.is_ok()); assert_eq!( owner.target_document_id(TARGET), @@ -97,9 +131,7 @@ async fn loaded_navigation_commit_settles_document_history_and_navigation_togeth ); assert!( owner - .document_lifecycle_for_target(TARGET) - .unwrap() - .snapshot() + .document_lifecycle_snapshot_for_target(TARGET) .is_some() ); committed.previous_document_retirement.close().await; @@ -107,54 +139,80 @@ async fn loaded_navigation_commit_settles_document_history_and_navigation_togeth #[tokio::test] async fn disappearing_agent_host_cannot_cancel_an_admitted_browser_commit() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); let mut owner = empty_document_context(); let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-native".into()); let contents_id = owner.page_targets.get(TARGET).unwrap().web_contents_id(); - let expected_document = owner.physical.web_contents[&contents_id] - .navigation() - .pending_document() + let contents = WebContentsHandle::new(owner.browser_context_id(), contents_id); + let expected_document = owner + .browser_context + .pending_document_for_test(contents) + .unwrap() .unwrap() .1; - let mut page = browser - .fetch("data:text/html,native") - .await - .unwrap(); - let artifacts = page.take_page_creation_artifacts().unwrap(); - let url = page.final_url().clone(); - let prepared = prepare_navigation(&owner, navigation, page, url.clone(), &artifacts).await; + let loaded = prepare_navigation( + &mut owner, + navigation, + "LOADER-native", + "data:text/html,native", + ) + .await; + let artifacts = loaded.page_creation_artifacts.clone(); + let url = loaded.final_url.clone(); drop(owner.page_targets.remove(TARGET).unwrap()); - let committed = owner.commit_loaded_navigation(prepared).unwrap(); + let committed = owner.commit_loaded_navigation(loaded.page).unwrap(); assert!(committed.inspection_projection.is_err()); - let contents = owner.physical.web_contents.get_mut(&contents_id).unwrap(); - let document = contents.main_frame.current_document.as_ref().unwrap(); + let document = DocumentHandle::new(contents, expected_document); assert_eq!( - document.lifecycle.snapshot(), + owner + .browser_context + .document_lifecycle_snapshot(document) + .unwrap(), Some(artifacts.lifecycle_snapshot) ); - assert!(document.lifecycle.snapshot().unwrap().load.is_some()); - let renderer_page = RendererPageResidenceIdentity::from_page(&document.page); + assert!( + owner + .browser_context + .document_lifecycle_snapshot(document) + .unwrap() + .unwrap() + .load + .is_some() + ); + let renderer_page = owner + .browser_context + .document_renderer_residence(document) + .unwrap(); assert_eq!( - contents.main_frame.current_document.as_ref().unwrap().id, + owner + .browser_context + .document_handle(contents) + .unwrap() + .unwrap() + .id(), expected_document ); - assert_eq!(contents.navigation().pending_document(), None); assert_eq!( - contents.navigation().committed_document_navigation(), + owner.browser_context.pending_document_for_test(contents), + Ok(None) + ); + assert_eq!( + owner + .browser_context + .current_document_navigation(contents) + .unwrap(), Some(navigation) ); - let (_, history) = contents.navigation().navigation_history_snapshot(); + let (_, history) = owner + .browser_context + .navigation_history_snapshot(contents) + .unwrap(); assert_eq!(history.last().unwrap().url, url.as_str()); assert_eq!(history.last().unwrap().title, "native"); assert_eq!( - contents - .main_frame - .current_document - .as_mut() - .unwrap() - .page - .evaluate_runtime_expression_async("40 + 2") + owner + .browser_context + .evaluate_document_expression_for_test(document, "40 + 2", false) .await .unwrap()["value"], 42 @@ -193,13 +251,10 @@ async fn disappearing_agent_host_cannot_cancel_an_admitted_browser_commit() { .is_none() ); assert_eq!( - owner.physical.web_contents[&contents_id] - .main_frame - .current_document - .as_ref() + owner + .browser_context + .document_lifecycle_snapshot(document) .unwrap() - .lifecycle - .snapshot() .unwrap() .terminated .unwrap() @@ -212,20 +267,20 @@ async fn disappearing_agent_host_cannot_cancel_an_admitted_browser_commit() { #[tokio::test] async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replacement() { use crate::conn::CommittedDocumentLifecycle; - let browser = Browser::new(BrowserConfig::default()).unwrap(); let mut owner = empty_document_context(); let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-native".into()); - let mut page = browser - .fetch("data:text/html,native") - .await - .unwrap(); - let renderer_page = RendererPageResidenceIdentity::from_page(&page); - let artifacts = page.take_page_creation_artifacts().unwrap(); + let loaded = prepare_navigation( + &mut owner, + navigation, + "LOADER-native", + "data:text/html,native", + ) + .await; + let renderer_page = loaded.page.renderer_residence(); + let artifacts = loaded.page_creation_artifacts.clone(); let snapshot = artifacts.lifecycle_snapshot; assert!(snapshot.load.is_some()); - let url = page.final_url().clone(); - let candidate = prepare_navigation(&owner, navigation, page, url, &artifacts).await; - let commit = owner.commit_loaded_navigation(candidate).unwrap(); + let commit = owner.commit_loaded_navigation(loaded.page).unwrap(); let document = commit.lifecycle.document; assert!( owner @@ -292,11 +347,9 @@ async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replace ); let next = owner.begin_target_document_navigation(TARGET, "LOADER-next".into()); - let mut page = browser.fetch("data:text/html,next").await.unwrap(); - let url = page.final_url().clone(); - let next_artifacts = page.take_page_creation_artifacts().unwrap(); - let candidate = prepare_navigation(&owner, next, page, url, &next_artifacts).await; - let replacement = owner.commit_loaded_navigation(candidate).unwrap(); + let loaded = prepare_navigation(&mut owner, next, "LOADER-next", "data:text/html,next").await; + let next_artifacts = loaded.page_creation_artifacts.clone(); + let replacement = owner.commit_loaded_navigation(loaded.page).unwrap(); assert_ne!( owner.target_document_id(TARGET), Some(occurrence.document()) @@ -332,21 +385,10 @@ async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replace #[tokio::test] async fn failed_inspection_projection_cannot_veto_browser_document_commit() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let first = browser - .fetch("data:text/html,first") - .await - .unwrap(); - let mut owner = context_with_document(first); + let mut owner = context_with_document("data:text/html,first").await; let previous_document = owner.target_document_id(TARGET).unwrap(); let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-projection".into()); - let expected_document = owner - .web_contents_for_target(TARGET) - .unwrap() - .navigation() - .pending_document() - .unwrap() - .1; + let expected_document = owner.target_pending_document_id(TARGET).unwrap(); // Fail only the DevTools projection. The Browser WebContents and its pending // navigation remain live and must not require that projection's approval. owner @@ -355,14 +397,15 @@ async fn failed_inspection_projection_cannot_veto_browser_document_commit() { .unwrap() .runtime_slot .retire_for_target_close(); - let mut page = browser - .fetch("data:text/html,committed") - .await - .unwrap(); - let artifacts = page.take_page_creation_artifacts().unwrap(); - let url = page.final_url().clone(); - let prepared = prepare_navigation(&owner, navigation, page, url.clone(), &artifacts).await; - let result = owner.commit_loaded_navigation(prepared); + let loaded = prepare_navigation( + &mut owner, + navigation, + "LOADER-projection", + "data:text/html,committed", + ) + .await; + let url = loaded.final_url.clone(); + let result = owner.commit_loaded_navigation(loaded.page); assert_eq!( owner.target_document_id(TARGET), @@ -376,9 +419,7 @@ async fn failed_inspection_projection_cannot_veto_browser_document_commit() { assert_eq!(history.last().unwrap().title, "committed"); assert_eq!( owner - .loaded_page_for_target_mut(TARGET) - .unwrap() - .evaluate_runtime_expression_async("40 + 2") + .evaluate_target_expression_for_test(TARGET, "40 + 2", false) .await .unwrap()["value"], 42 @@ -390,14 +431,11 @@ async fn failed_inspection_projection_cannot_veto_browser_document_commit() { #[tokio::test] async fn committed_occurrence_retains_the_previous_document_output_projection() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let mut first = browser - .fetch("data:text/html,first") - .await + let mut owner = context_with_document("data:text/html,first").await; + let artifacts = owner.initial_artifacts.take().unwrap(); + let previous_renderer = owner + .target_renderer_page_residence_identity(TARGET) .unwrap(); - let artifacts = first.take_page_creation_artifacts().unwrap(); - let previous_renderer = moli_core::browser::RendererPageResidenceIdentity::from_page(&first); - let mut owner = context_with_document(first); let previous_document = owner.target_document_id(TARGET).unwrap(); owner.bind_renderer_document_lifecycle_for_target( TARGET, @@ -412,15 +450,15 @@ async fn committed_occurrence_retains_the_previous_document_output_projection() .is_some() ); let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-current".into()); - let mut page = browser - .fetch("data:text/html,current") - .await - .unwrap(); - let current_renderer = moli_core::browser::RendererPageResidenceIdentity::from_page(&page); - let artifacts = page.take_page_creation_artifacts().unwrap(); - let url = page.final_url().clone(); - let prepared = prepare_navigation(&owner, navigation, page, url, &artifacts).await; - let committed = owner.commit_loaded_navigation(prepared).unwrap(); + let loaded = prepare_navigation( + &mut owner, + navigation, + "LOADER-current", + "data:text/html,current", + ) + .await; + let current_renderer = loaded.page.renderer_residence(); + let committed = owner.commit_loaded_navigation(loaded.page).unwrap(); let current_document = owner.target_document_id(TARGET).unwrap(); assert_ne!(current_document, previous_document); let projection = &owner.page_targets.get(TARGET).unwrap().runtime_slot; @@ -434,18 +472,17 @@ async fn committed_occurrence_retains_the_previous_document_output_projection() #[tokio::test] async fn inspection_configuration_failure_cannot_roll_back_a_committed_browser_document() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let first = browser - .fetch("data:text/html,first") - .await - .unwrap(); - let mut owner = context_with_document(first); + let mut owner = context_with_document("data:text/html,first").await; let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-restore".into()); - let mut page = browser.fetch("data:text/html,committed").await.unwrap(); - let artifacts = page.take_page_creation_artifacts().unwrap(); - let url = page.final_url().clone(); - let prepared = prepare_navigation(&owner, navigation, page, url.clone(), &artifacts).await; - let committed = owner.commit_loaded_navigation(prepared).unwrap(); + let loaded = prepare_navigation( + &mut owner, + navigation, + "LOADER-restore", + "data:text/html,committed", + ) + .await; + let url = loaded.final_url.clone(); + let committed = owner.commit_loaded_navigation(loaded.page).unwrap(); assert!(committed.inspection_projection.is_ok()); let document = owner.target_document_id(TARGET); let registration = moli_core::page::RuntimeBindingRegistration { @@ -482,9 +519,7 @@ async fn inspection_configuration_failure_cannot_roll_back_a_committed_browser_d assert_eq!(history.last().unwrap().title, "committed"); assert_eq!( owner - .loaded_page_for_target_mut(TARGET) - .unwrap() - .evaluate_runtime_expression_async("protectedBinding + 41") + .evaluate_target_expression_for_test(TARGET, "protectedBinding + 41", false) .await .unwrap()["value"], 42 @@ -494,12 +529,7 @@ async fn inspection_configuration_failure_cannot_roll_back_a_committed_browser_d #[tokio::test] async fn rejected_browser_candidate_cannot_rotate_inspection_or_document_projection() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let first = browser - .fetch("data:text/html,first") - .await - .unwrap(); - let mut owner = context_with_document(first); + let mut owner = context_with_document("data:text/html,first").await; let document = owner.target_document_id(TARGET); let attachment = owner .page_targets @@ -508,13 +538,13 @@ async fn rejected_browser_candidate_cannot_rotate_inspection_or_document_project .runtime_slot .current_renderer_attachment(); let stale = owner.begin_target_document_navigation(TARGET, "LOADER-stale".into()); - let mut page = browser - .fetch("data:text/html,stale") - .await - .unwrap(); - let artifacts = page.take_page_creation_artifacts().unwrap(); - let url = page.final_url().clone(); - let prepared = prepare_navigation(&owner, stale, page, url, &artifacts).await; + let loaded = prepare_navigation( + &mut owner, + stale, + "LOADER-stale", + "data:text/html,stale", + ) + .await; let current = owner.begin_target_document_navigation(TARGET, "LOADER-current".into()); let history = owner.target_navigation_history_snapshot(TARGET).unwrap(); let target_url = owner @@ -523,14 +553,13 @@ async fn rejected_browser_candidate_cannot_rotate_inspection_or_document_project .unwrap() .target_url() .to_owned(); - assert!(owner.commit_loaded_navigation(prepared).is_err()); + assert!(owner.commit_loaded_navigation(loaded.page).is_err()); assert_eq!(owner.target_document_id(TARGET), document); assert_eq!( owner - .web_contents_for_target(TARGET) + .browser_context + .pending_document_for_test(owner.web_contents_handle_for_target(TARGET).unwrap()) .unwrap() - .navigation() - .pending_document() .unwrap() .0, current @@ -550,37 +579,31 @@ async fn rejected_browser_candidate_cannot_rotate_inspection_or_document_project #[tokio::test] async fn document_replacement_updates_inspection_binding_with_physical_page() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let first = browser - .fetch("data:text/html,first") - .await - .unwrap(); - let second = browser - .fetch("data:text/html,second") - .await - .unwrap(); - let agent = second.renderer_devtools_agent_token(); - let mut owner = context_with_document(first); + let mut owner = context_with_document("data:text/html,first").await; let old_attachment = owner .active_page_target() .runtime_slot .current_renderer_attachment() .unwrap(); - let previous = owner - .replace_loaded_page_for_target(TARGET, Some(second)) - .unwrap(); + let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-second".into()); + let loaded = prepare_navigation( + &mut owner, + navigation, + "LOADER-second", + "data:text/html,second", + ) + .await; + let committed = owner.commit_loaded_navigation(loaded.page).unwrap(); let attachment = owner .active_page_target() .runtime_slot .current_renderer_attachment() .unwrap(); - assert_eq!(attachment.agent_token(), agent); assert_ne!(attachment.id(), old_attachment.id()); assert_eq!( owner - .loaded_page_for_target(TARGET) - .unwrap() - .renderer_devtools_agent_token(), + .target_document_renderer_agent_for_test(TARGET) + .unwrap(), attachment.agent_token() ); assert!( @@ -590,13 +613,11 @@ async fn document_replacement_updates_inspection_binding_with_physical_page() { .current_renderer_inspection_binding() .is_some() ); - drop(previous); + committed.previous_document_retirement.close().await; } -async fn page_with_installed_dialog_for_test( - browser: &Browser, -) -> ( - BrowserContext, +async fn page_with_installed_dialog_for_test() -> ( + DocumentOwnerFixture, moli_core::page::RendererJavaScriptDialogCompletion, ) { use moli_core::page::{ @@ -604,13 +625,9 @@ async fn page_with_installed_dialog_for_test( RendererJavaScriptDialogSource, RendererPendingJavaScriptDialog, }; - let mut page = browser - .fetch("data:text/html,

dialog owner

") - .await - .unwrap(); - let artifacts = page.take_page_creation_artifacts().unwrap(); + let mut owner = context_with_document("data:text/html,

dialog owner

").await; + let artifacts = owner.initial_artifacts.take().unwrap(); let source = artifacts.lifecycle_snapshot; - let mut owner = context_with_document(page); owner.bind_renderer_document_lifecycle_for_target( TARGET, artifacts, @@ -620,67 +637,89 @@ async fn page_with_installed_dialog_for_test( ); owner.attach_active_session("SID-dialog-owner"); let completion = RendererJavaScriptDialogCompletion::pending(); - assert!(owner.install_javascript_dialog_for_target( + let document = owner.document_handle_for_target(TARGET).unwrap(); + let key = owner + .install_document_javascript_dialog( + document, + RendererPendingJavaScriptDialog::new( + RendererJavaScriptDialogId::new(1), + RendererDocumentLifecycleIdentity { + frame: source.frame, + document: source.document, + epoch: source.epoch, + }, + RendererJavaScriptDialogSource::RootFrame, + "about:blank".into(), + "prompt".into(), + "owned dialog".into(), + "default".into(), + Some(completion.clone()), + ), + ) + .unwrap(); + let wrong_document = moli_core::browser::DocumentHandle::new( + moli_core::browser::WebContentsHandle::new( + document.web_contents().context(), + moli_core::browser::WebContentsId::allocate(), + ), + document.id(), + ); + assert!(!owner.project_javascript_dialog_for_session( TARGET, &moli_page_types::DevToolsSessionKey::Primary, - crate::conn::TargetPageResidenceIdentity::new( - "BID-dialog-owner".into(), - Some("TID-dialog-owner".into()), - owner.target_document_id(TARGET).unwrap(), - ), "FRAME-dialog-owner".into(), - RendererPendingJavaScriptDialog::new( - RendererJavaScriptDialogId::new(1), - RendererDocumentLifecycleIdentity { - frame: source.frame, - document: source.document, - epoch: source.epoch, - }, - RendererJavaScriptDialogSource::RootFrame, - "about:blank".into(), - "prompt".into(), - "owned dialog".into(), - "default".into(), - Some(completion.clone()), - ), + wrong_document, + key, + )); + assert!(owner.project_javascript_dialog_for_session( + TARGET, + &moli_page_types::DevToolsSessionKey::Primary, + "FRAME-dialog-owner".into(), + document, + key, )); (owner, completion) } #[tokio::test] async fn document_replacement_dismisses_dialog_without_protocol_session_cleanup() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let (mut owner, completion) = page_with_installed_dialog_for_test(&browser).await; - let page = browser - .fetch("data:text/html,

replacement

") - .await - .unwrap(); - let previous = owner - .replace_loaded_page_for_target(TARGET, Some(page)) - .unwrap(); + let (mut owner, completion) = page_with_installed_dialog_for_test().await; + let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-replacement".into()); + let loaded = prepare_navigation( + &mut owner, + navigation, + "LOADER-replacement", + "data:text/html,

replacement

", + ) + .await; + let committed = owner.commit_loaded_navigation(loaded.page).unwrap(); assert!( !completion.finish(true, "late reply".into()), "Browser Document replacement must dismiss its dialog before Protocol cleanup" ); assert!(!completion.wait().accepted); - previous.close_async().await.unwrap(); + committed.previous_document_retirement.close().await; } #[tokio::test] async fn browser_drop_dismisses_dialog_even_when_session_snapshot_survives() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let (mut owner, completion) = page_with_installed_dialog_for_test(&browser).await; + let (mut owner, completion) = page_with_installed_dialog_for_test().await; let dialog_projection_snapshot = owner.active_page_target().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .page_session_state .javascript_dialog_state .clone(); let id = owner.selected_web_contents_id().unwrap(); + let handle = WebContentsHandle::new(owner.browser_context_id(), id); drop(owner.page_targets.remove(TARGET).unwrap()); - let contents = owner.physical.web_contents.get(&id).unwrap(); - assert!(contents.main_frame.current_document.is_some()); - assert!(!contents.javascript_dialogs.is_empty()); + assert!(owner.browser_context.has_loaded_document(handle)); + assert!( + owner + .browser_context + .web_contents_has_pending_javascript_dialog(handle) + .unwrap() + ); drop(owner); assert!( @@ -693,34 +732,44 @@ async fn browser_drop_dismisses_dialog_even_when_session_snapshot_survives() { #[tokio::test] async fn browser_dialog_can_be_handled_after_protocol_projection_is_dropped() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let (mut owner, completion) = page_with_installed_dialog_for_test(&browser).await; + let (mut owner, completion) = page_with_installed_dialog_for_test().await; let key = owner.active_page_target().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .page_session_state .javascript_dialog_state .pending_dialogs()[0] .key; - let id = owner.selected_web_contents_id().unwrap(); + let document = owner.document_handle_for_target(TARGET).unwrap(); drop(owner.page_targets.remove(TARGET).unwrap()); - let contents = owner.physical.web_contents.get_mut(&id).unwrap(); assert_eq!( - contents.javascript_dialogs.snapshot(key).unwrap().message, + owner + .browser_context + .document_javascript_dialog_snapshot(document, key) + .unwrap() + .message, "owned dialog" ); - contents - .javascript_dialogs - .set_prompt_text(key, "Browser input".into()) + owner + .browser_context + .set_document_javascript_dialog_prompt_text(document, key, "Browser input".into()) + .unwrap(); + let closed = owner + .browser_context + .finish_document_javascript_dialog(document, key, true, None) .unwrap(); - let closed = contents.javascript_dialogs.finish(key, true, None).unwrap(); assert_eq!(closed.dialog_type, "prompt"); assert_eq!(closed.user_input, "Browser input"); - assert!(contents.javascript_dialogs.snapshot(key).is_none()); assert!( - contents - .javascript_dialogs - .finish(key, false, None) + owner + .browser_context + .document_javascript_dialog_snapshot(document, key) + .is_none() + ); + assert!( + owner + .browser_context + .finish_document_javascript_dialog(document, key, false, None) .is_none() ); assert!(!completion.finish(false, "late reply".into())); @@ -732,17 +781,35 @@ async fn browser_dialog_can_be_handled_after_protocol_projection_is_dropped() { #[tokio::test] async fn browser_dialog_retirement_follows_admitted_document_lifecycle_without_projection() { use moli_core::page::RendererDocumentTerminationReason; - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let (mut owner, completion) = page_with_installed_dialog_for_test(&browser).await; + let (mut owner, completion) = page_with_installed_dialog_for_test().await; let contents_id = owner.selected_web_contents_id().unwrap(); + let handle = WebContentsHandle::new(owner.browser_context_id(), contents_id); + let document = owner.document_handle_for_target(TARGET).unwrap(); drop(owner.page_targets.remove(TARGET).unwrap()); - let contents = owner.physical.web_contents.get_mut(&contents_id).unwrap(); - let document = contents.main_frame.current_document.as_ref().unwrap(); - let id = document.id; - let snapshot = document.lifecycle.snapshot().unwrap(); - contents.begin_initial_empty_document("about:blank".into(), None, None); - contents.mark_initial_empty_document_materialized(); - assert!(!contents.javascript_dialogs.is_empty()); + let id = document.id(); + let snapshot = owner + .browser_context + .document_lifecycle_snapshot(document) + .unwrap() + .unwrap(); + owner + .browser_context + .begin_initial_empty_document(handle, "about:blank".into(), None, None) + .unwrap(); + owner + .browser_context + .mark_initial_empty_document_materialized_for_test(handle) + .unwrap(); + assert!( + owner + .browser_context + .web_contents_has_pending_javascript_dialog(handle) + .unwrap() + ); + let renderer = owner + .browser_context + .document_renderer_residence(document) + .unwrap(); let terminated = RendererDocumentLifecycleEvent { frame: snapshot.frame, document: snapshot.document, @@ -755,40 +822,66 @@ async fn browser_dialog_retirement_follows_admitted_document_lifecycle_without_p }, }; assert!( - contents - .apply_document_lifecycle(RendererDocumentLifecycleEvent { - document: snapshot.document.successor_for_testing(), - ..terminated - }) + owner + .browser_context + .apply_renderer_document_lifecycle( + renderer, + RendererDocumentLifecycleEvent { + document: snapshot.document.successor_for_testing(), + ..terminated + } + ) .is_none() ); assert!( - !contents.javascript_dialogs.is_empty(), + owner + .browser_context + .web_contents_has_pending_javascript_dialog(handle) + .unwrap(), "foreign lifecycle must not dismiss current dialog" ); - assert!(contents.apply_document_lifecycle(terminated).is_some()); - assert!(contents.javascript_dialogs.is_empty()); + assert!( + owner + .browser_context + .apply_renderer_document_lifecycle(renderer, terminated) + .is_some() + ); + assert!( + !owner + .browser_context + .web_contents_has_pending_javascript_dialog(handle) + .unwrap() + ); assert!(!completion.finish(true, "late reply".into())); assert!(!completion.wait().accepted); assert!( - contents - .apply_document_lifecycle(RendererDocumentLifecycleEvent { - epoch: RendererLifecycleEpoch(snapshot.epoch.0 + 1), - sequence: u64::MAX - 1, - kind: RendererDocumentLifecycleEventKind::Started { - reason: RendererLifecycleStartReason::ExplicitDocumentOpen - }, - ..terminated - }) + owner + .browser_context + .apply_renderer_document_lifecycle( + renderer, + RendererDocumentLifecycleEvent { + epoch: RendererLifecycleEpoch(snapshot.epoch.0 + 1), + sequence: u64::MAX - 1, + kind: RendererDocumentLifecycleEventKind::Started { + reason: RendererLifecycleStartReason::ExplicitDocumentOpen + }, + ..terminated + } + ) .is_some() ); assert_eq!( - contents.main_frame.current_document.as_ref().unwrap().id, - id + owner + .browser_context + .document_handle(handle) + .unwrap() + .unwrap() + .id(), + id, ); assert_eq!( - contents.navigation().is_on_initial_empty_document(), - Some(false) + owner.browser_context.is_on_initial_document(handle), + Ok(Some(false)) ); } @@ -799,92 +892,238 @@ async fn dialog_disable_and_exact_detach_dismiss_only_their_browser_dialogs() { RendererJavaScriptDialogSource, RendererPendingJavaScriptDialog, }; use moli_page_types::DevToolsSessionKey; - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let (mut owner, primary_completion) = page_with_installed_dialog_for_test(&browser).await; + let (mut owner, primary_completion) = page_with_installed_dialog_for_test().await; let peer = DevToolsSessionKey::Attached("SID-dialog-peer".into()); let peer_completion = RendererJavaScriptDialogCompletion::pending(); let document = owner.target_document_id(TARGET).unwrap(); let snapshot = owner - .web_contents_for_target(TARGET) - .unwrap() - .main_frame - .current_document - .as_ref() - .unwrap() - .lifecycle - .snapshot() + .document_lifecycle_snapshot_for_target(TARGET) .unwrap(); - assert!(owner.install_javascript_dialog_for_target( + let document_handle = owner.document_handle_for_target(TARGET).unwrap(); + let key = owner + .install_document_javascript_dialog( + document_handle, + RendererPendingJavaScriptDialog::new( + RendererJavaScriptDialogId::new(2), + RendererDocumentLifecycleIdentity { + frame: snapshot.frame, + document: snapshot.document, + epoch: snapshot.epoch, + }, + RendererJavaScriptDialogSource::RootFrame, + "about:blank".into(), + "alert".into(), + "peer".into(), + String::new(), + Some(peer_completion.clone()), + ), + ) + .unwrap(); + assert!(owner.project_javascript_dialog_for_session( TARGET, &peer, - crate::conn::TargetPageResidenceIdentity::new( - "BID-dialog-owner".into(), - Some("TID-dialog-owner".into()), - document - ), "FRAME-dialog-owner".into(), - RendererPendingJavaScriptDialog::new( - RendererJavaScriptDialogId::new(2), - RendererDocumentLifecycleIdentity { - frame: snapshot.frame, - document: snapshot.document, - epoch: snapshot.epoch - }, - RendererJavaScriptDialogSource::RootFrame, - "about:blank".into(), - "alert".into(), - "peer".into(), - String::new(), - Some(peer_completion.clone()) - ) + document_handle, + key, )); owner.disable_devtools_page_domain_for_target(TARGET, &DevToolsSessionKey::Primary); assert!(!primary_completion.finish(true, "late primary".into())); assert!(!primary_completion.wait().accepted); - assert!(owner.has_pending_javascript_dialog_for_target(TARGET)); + assert!( + owner + .web_contents_has_pending_javascript_dialog(document_handle.web_contents()) + .unwrap() + ); + let (projected_document, key) = owner + .projected_javascript_dialog_for_session(TARGET, &peer) + .unwrap(); assert_eq!( owner - .javascript_dialog_snapshot_for_target(TARGET, &peer) + .document_javascript_dialog_snapshot(projected_document, key) .unwrap() .message, "peer" ); assert!(!owner.dispose_devtools_session_for_target(TARGET, "SID-wrong", &peer)); + let (projected_document, key) = owner + .projected_javascript_dialog_for_session(TARGET, &peer) + .unwrap(); assert!( owner - .javascript_dialog_snapshot_for_target(TARGET, &peer) + .document_javascript_dialog_snapshot(projected_document, key) .is_some() ); assert!(owner.dispose_devtools_session_for_target(TARGET, "SID-dialog-peer", &peer)); assert!(!peer_completion.finish(true, "late peer".into())); assert!(!peer_completion.wait().accepted); - assert!(!owner.has_pending_javascript_dialog_for_target(TARGET)); + assert!( + !owner + .web_contents_has_pending_javascript_dialog(document_handle.web_contents()) + .unwrap() + ); assert_eq!(owner.target_document_id(TARGET), Some(document)); } #[tokio::test] -async fn document_replacement_preserves_stable_page_engine_history_and_storage() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let first = browser - .fetch("data:text/html,first") +async fn document_policy_completion_rejects_replacement_document() { + let mut owner = context_with_document("data:text/html,

first policy owner

").await; + let document = owner.document_handle_for_target(TARGET).unwrap(); + let completed = owner + .start_document_policy_update( + document, + crate::conn::DocumentPolicyUpdate::DocumentActivity( + moli_page_types::DocumentActivity::new(false, false), + ), + ) + .unwrap() + .wait() + .await; + let navigator_completed = owner + .start_document_policy_update( + document, + crate::conn::DocumentPolicyUpdate::NavigatorOverrides( + moli_page_types::NavigatorOverrides { + online: Some(false), + max_touch_points: 2, + ..Default::default() + }, + ), + ) + .unwrap() + .wait() + .await; + let surface_completed = owner + .start_document_page_surface_update(document, false, None, None) + .unwrap() + .wait() + .await; + + let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-replacement".into()); + let loaded = prepare_navigation( + &mut owner, + navigation, + "LOADER-replacement", + "data:text/html,

replacement policy owner

", + ) + .await; + let retired = owner.commit_loaded_navigation(loaded.page).unwrap(); + let replacement = owner.document_handle_for_target(TARGET).unwrap(); + assert_ne!(replacement, document); + for completed in [completed, navigator_completed] { + assert_eq!( + owner.finish_document_policy_update(completed), + Err("Document changed".to_owned()) + ); + } + assert_eq!( + owner + .browser_context + .finish_document_policy_batch(surface_completed), + Err("Document changed".to_owned()) + ); + assert_eq!(owner.document_handle_for_target(TARGET), Some(replacement)); + + retired.previous_document_retirement.close().await; + let observed = owner + .browser_context + .evaluate_document_expression_for_test( + replacement, + "JSON.stringify([navigator.onLine, navigator.maxTouchPoints])", + false, + ) .await .unwrap(); - let mut owner = context_with_document(first); + assert_eq!(observed["value"], serde_json::json!("[true,0]")); + owner + .clear_target_page_for_test(TARGET) + .unwrap() + .close() + .await; +} + +#[tokio::test] +async fn document_native_command_completions_reject_replacement_document() { + let mut owner = context_with_document("data:text/html,").await; + let document = owner.document_handle_for_target(TARGET).unwrap(); + let autofill = owner + .start_document_autofill_trigger( + document, + moli_core::page::RendererAutofillTriggerRequest { + frame_id: None, + field_id: 1, + card: None, + address: None, + }, + ) + .unwrap() + .wait() + .await; + let stopped = owner + .start_document_lifecycle_stop(document) + .unwrap() + .wait() + .await; + + let navigation = owner.begin_target_document_navigation(TARGET, "LOADER-replacement".into()); + let loaded = prepare_navigation( + &mut owner, + navigation, + "LOADER-replacement", + "data:text/html,

replacement native command owner

", + ) + .await; + let retired = owner.commit_loaded_navigation(loaded.page).unwrap(); + let replacement = owner.document_handle_for_target(TARGET).unwrap(); + assert_ne!(replacement, document); + assert_eq!( + owner.finish_document_autofill_trigger(autofill), + Err("Document changed".to_owned()) + ); + assert!(matches!( + owner.finish_document_lifecycle_stop(stopped), + Err(error) if error == "Document changed" + )); + assert_eq!(owner.document_handle_for_target(TARGET), Some(replacement)); + + retired.previous_document_retirement.close().await; + owner + .clear_target_page_for_test(TARGET) + .unwrap() + .close() + .await; +} + +#[tokio::test] +async fn document_replacement_preserves_stable_page_engine_history_and_storage() { let mut config = moli_fetch::FetchConfig::default(); config.set_user_agent("stable-engine"); - owner - .web_contents_for_target_mut(TARGET) - .unwrap() - .install_navigation_engine(moli_core::runtime::NavigationEngine::new_with_fetch_config( + let mut owner = context_with_document_and_runtime_config( + "data:text/html,first", + moli_core::runtime::NavigationRuntimeConfig::new( config, - )); + moli_core::OptionalResourceFetchMask::NONE, + true, + Default::default(), + ), + ) + .await; let stable_ids = ( owner.selected_web_contents_id().unwrap(), owner.active_page_target().main_frame_slot_id(), ); - owner.set_target_window_surface_state(TARGET, crate::conn::WindowSurfaceState::Fullscreen); - owner.set_target_window_surface_geometry(TARGET, Some(800), Some(600), Some(10), Some(20)); - let window = owner.target_window_surface(TARGET).unwrap(); + let stable_renderer_owner = owner.page_navigation_renderer_owner_id(TARGET).unwrap(); + let web_contents = owner.web_contents_handle_for_target(TARGET).unwrap(); + owner + .update_web_contents_window_surface( + web_contents, + Some(crate::conn::WindowSurfaceState::Fullscreen), + Some(800), + Some(600), + Some(10), + Some(20), + ) + .unwrap(); + let window = owner.web_contents_window_surface(web_contents).unwrap(); owner.apply_target_emulation_policy_change( TARGET, crate::conn::EmulationPolicyChange::MaxTouchPoints(4), @@ -935,10 +1174,9 @@ async fn document_replacement_preserves_stable_page_engine_history_and_storage() ); let first_document = owner.target_document_id(TARGET).unwrap(); let storage = owner - .web_contents_for_target(TARGET) + .page_storage_handles_for_target(TARGET) .unwrap() - .session_storage - .store() + .session_storage_store .clone(); assert!( storage @@ -948,22 +1186,15 @@ async fn document_replacement_preserves_stable_page_engine_history_and_storage() let observer = owner.document_lifetime_observer_for_target(TARGET).unwrap(); let navigation = owner.begin_target_document_navigation(TARGET, "second-loader".into()); - let second = browser.fetch("data:text/html,

second

").await.unwrap(); - let reserved = owner.reserve_renderer_document_for_target( - TARGET, - RendererPageResidenceIdentity::from_page(&second), - ); - let first = owner - .replace_loaded_page_for_target(TARGET, Some(second)) - .unwrap(); - assert!(owner.commit_pending_document_navigation_if_matches_for_target(TARGET, &navigation)); - owner - .web_contents_for_target_mut(TARGET) - .unwrap() - .record_navigation_history_for_test(( - "https://example.test/second".into(), - "second".into(), - )); + let reserved = owner.target_pending_document_id(TARGET).unwrap(); + let loaded = prepare_navigation( + &mut owner, + navigation, + "second-loader", + "data:text/html,second

second

", + ) + .await; + let first = owner.commit_loaded_navigation(loaded.page).unwrap(); assert_eq!( ( @@ -973,7 +1204,10 @@ async fn document_replacement_preserves_stable_page_engine_history_and_storage() stable_ids ); assert_eq!(owner.target_document_id(TARGET), Some(reserved)); - assert_eq!(owner.target_window_surface(TARGET).unwrap(), window); + assert_eq!( + owner.web_contents_window_surface(web_contents).unwrap(), + window + ); assert_eq!( owner .target_emulation_policy(TARGET) @@ -990,30 +1224,20 @@ async fn document_replacement_preserves_stable_page_engine_history_and_storage() assert!(owner.bypass_content_security_policy_for_target(TARGET)); assert_ne!(first_document, reserved); assert_eq!( - owner - .page_navigation_engine(TARGET) - .unwrap() - .fetch_config() - .user_agent(), - "stable-engine" + owner.page_navigation_renderer_owner_id(TARGET), + Some(stable_renderer_owner), + "Document replacement must reuse the WebContents navigation engine" ); - assert!(std::sync::Arc::ptr_eq( - &storage, - owner - .web_contents_for_target(TARGET) - .unwrap() - .session_storage - .store() - )); + let current_storage = owner + .page_storage_handles_for_target(TARGET) + .unwrap() + .session_storage_store; + assert!(std::sync::Arc::ptr_eq(&storage, ¤t_storage)); assert_eq!( storage.lock().get_item("https://example.test", "key"), Some("value".into()) ); - let (index, history) = owner - .web_contents_for_target_mut(TARGET) - .unwrap() - .navigation() - .navigation_history_snapshot(); + let (index, history) = owner.target_navigation_history_snapshot(TARGET).unwrap(); assert_eq!(index, 1); assert_eq!(history.len(), 2); assert_eq!(history[0].title, "first"); @@ -1022,16 +1246,16 @@ async fn document_replacement_preserves_stable_page_engine_history_and_storage() observer.wait().await, moli_core::browser::DocumentRetirement::Superseded ); - first.close_async().await.unwrap(); + first.previous_document_retirement.close().await; } #[tokio::test] async fn web_contents_owns_live_document_and_navigation_after_protocol_residence_is_dropped() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let mut page = browser.fetch("data:text/html,

owned

").await.unwrap(); - let renderer = RendererPageResidenceIdentity::from_page(&page); - let artifacts = page.take_page_creation_artifacts().unwrap(); - let mut owner = context_with_document(page); + let mut owner = context_with_document("data:text/html,

owned

").await; + let renderer = owner + .target_renderer_page_residence_identity(TARGET) + .unwrap(); + let artifacts = owner.initial_artifacts.take().unwrap(); owner.bind_renderer_document_lifecycle_for_target( TARGET, artifacts, @@ -1041,12 +1265,8 @@ async fn web_contents_owns_live_document_and_navigation_after_protocol_residence ); let document = owner.target_document_id(TARGET).unwrap(); let observer = owner.document_lifetime_observer_for_target(TARGET).unwrap(); - let stable_id = owner.web_contents_for_target_mut(TARGET).unwrap().id(); - let frame_id = owner - .web_contents_for_target_mut(TARGET) - .unwrap() - .main_frame - .id(); + let handle = owner.web_contents_handle_for_target(TARGET).unwrap(); + let (stable_id, frame_id) = owner.browser_context.web_contents_identity(handle).unwrap(); let navigation = owner.begin_target_document_navigation(TARGET, "pending-loader".into()); let cancellation = owner .document_navigation_cancellation_handle_for_target(TARGET, &navigation) @@ -1058,20 +1278,31 @@ async fn web_contents_owns_live_document_and_navigation_after_protocol_residence // Drop only DevTools; the registered Browser subtree stays in its Context. drop(owner.page_targets.remove(TARGET).unwrap()); assert_eq!(owner.selected_web_contents_id(), Some(stable_id)); - let contents = owner.physical.web_contents.get(&stable_id).unwrap(); - assert_eq!(contents.id(), stable_id); - assert_eq!(contents.main_frame.id(), frame_id); - let current = contents.main_frame.current_document.as_ref().unwrap(); - assert_eq!(current.id, document); assert_eq!( - RendererPageResidenceIdentity::from_page(¤t.page), + owner.browser_context.web_contents_identity(handle), + Ok((stable_id, frame_id)) + ); + let current = DocumentHandle::new(handle, document); + assert_eq!( + owner.browser_context.document_handle(handle), + Ok(Some(current)) + ); + assert_eq!( + owner + .browser_context + .document_renderer_residence(current) + .unwrap(), renderer ); - assert_eq!(current.lifecycle.snapshot(), Some(snapshot)); + assert_eq!( + owner.browser_context.document_lifecycle_snapshot(current), + Ok(Some(snapshot)) + ); assert!( - contents - .navigation() - .accepts_pending_document_navigation_event(&navigation) + owner + .browser_context + .accepts_pending_navigation(handle, &navigation) + .unwrap() ); assert!(!cancellation.is_cancelled()); let mut wait = Box::pin(observer.wait()); @@ -1090,17 +1321,12 @@ async fn web_contents_owns_live_document_and_navigation_after_protocol_residence #[tokio::test] async fn replacement_retires_document_identity_lifecycle_and_lifetime_together() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let mut first = browser.fetch("data:text/html,

first

").await.unwrap(); - let first_renderer = RendererPageResidenceIdentity::from_page(&first); - let first_artifacts = first.take_page_creation_artifacts().unwrap(); - let mut owner = empty_document_context(); - let first_id = owner.reserve_renderer_document_for_target(TARGET, first_renderer); - assert!( - owner - .replace_loaded_page_for_target(TARGET, Some(first)) - .is_none() - ); + let mut owner = context_with_document("data:text/html,

first

").await; + let first_renderer = owner + .target_renderer_page_residence_identity(TARGET) + .unwrap(); + let first_artifacts = owner.initial_artifacts.take().unwrap(); + let first_id = owner.target_document_id(TARGET).unwrap(); assert_eq!(owner.target_document_id(TARGET), Some(first_id)); owner.bind_renderer_document_lifecycle_for_target( TARGET, @@ -1133,11 +1359,17 @@ async fn replacement_retires_document_identity_lifecycle_and_lifetime_together() let mut context = Context::from_waker(Waker::noop()); assert_eq!(first_wait.as_mut().poll(&mut context), Poll::Pending); - let mut second = browser.fetch("data:text/html,

second

").await.unwrap(); - let second_renderer = RendererPageResidenceIdentity::from_page(&second); - let second_artifacts = second.take_page_creation_artifacts().unwrap(); let navigation = moved.begin_target_document_navigation(TARGET, "second-loader".into()); let reserved_id = moved.target_pending_document_id(TARGET).unwrap(); + let loaded = prepare_navigation( + &mut moved, + navigation, + "second-loader", + "data:text/html,

second

", + ) + .await; + let second_renderer = loaded.page.renderer_residence(); + let second_artifacts = loaded.page_creation_artifacts.clone(); assert!( moved.bind_pending_document_navigation_renderer_page_for_target( TARGET, @@ -1145,15 +1377,9 @@ async fn replacement_retires_document_identity_lifecycle_and_lifetime_together() second_renderer ) ); - let previous_page = moved - .replace_loaded_page_for_target(TARGET, Some(second)) - .unwrap(); + let replacement = moved.commit_loaded_navigation(loaded.page).unwrap(); assert_eq!(moved.target_document_id(TARGET), Some(reserved_id)); assert_ne!(first_id, reserved_id); - assert_eq!( - RendererPageResidenceIdentity::from_page(&previous_page), - first_renderer - ); assert!(!moved.routes_renderer_page_for_target(TARGET, first_renderer)); assert!(moved.routes_renderer_page_for_target(TARGET, second_renderer)); assert_eq!( @@ -1164,11 +1390,10 @@ async fn replacement_retires_document_identity_lifecycle_and_lifetime_together() another_first_observer.wait().await, moli_core::browser::DocumentRetirement::Superseded ); - assert!( - moved - .renderer_document_lifecycle_authoritative_snapshot_for_target(TARGET) - .is_none(), - "the replacement must not retain the previous Document's lifecycle" + assert_eq!( + moved.renderer_document_lifecycle_authoritative_snapshot_for_target(TARGET), + Some(second_artifacts.lifecycle_snapshot), + "the replacement must expose its own lifecycle, not the previous Document's" ); assert!( moved @@ -1183,7 +1408,6 @@ async fn replacement_retires_document_identity_lifecycle_and_lifetime_together() .is_none() ); - assert!(moved.commit_pending_document_navigation_if_matches_for_target(TARGET, &navigation)); moved.bind_renderer_document_lifecycle_for_target( TARGET, second_artifacts, @@ -1208,9 +1432,8 @@ async fn replacement_retires_document_identity_lifecycle_and_lifetime_together() ); let second_observer = moved.document_lifetime_observer_for_target(TARGET).unwrap(); let second_page = moved - .replace_loaded_page_with_reason_for_target( + .retire_loaded_document_with_reason_for_target( TARGET, - None, TargetPageAbsenceReason::TargetClosed, ) .unwrap(); @@ -1230,39 +1453,37 @@ async fn replacement_retires_document_identity_lifecycle_and_lifetime_together() second_observer.wait().await, moli_core::browser::DocumentRetirement::Superseded ); - previous_page.close_async().await.unwrap(); - second_page.close_async().await.unwrap(); + replacement.previous_document_retirement.close().await; + second_page.close().await; } #[tokio::test] -async fn rejected_reservation_preserves_current_document_until_owner_loss() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let first = browser.fetch("data:text/html,

first

").await.unwrap(); - let first_renderer = RendererPageResidenceIdentity::from_page(&first); - let mut owner = context_with_document(first); +async fn discarded_prepared_candidate_preserves_current_document_until_owner_loss() { + let mut owner = context_with_document("data:text/html,

first

").await; + let first_renderer = owner + .target_renderer_page_residence_identity(TARGET) + .unwrap(); let first_id = owner.target_document_id(TARGET); let observer = owner.document_lifetime_observer_for_target(TARGET).unwrap(); - let candidate = browser - .fetch("data:text/html,

candidate

") + let navigation = owner.begin_target_document_navigation(TARGET, "candidate-loader".into()); + let command_owner = CommandOwnerScope::capture(&owner.conn, None); + let candidate = owner + .conn + .prepare_navigation_fixture_for_owner_and_token_async( + &command_owner, + navigation, + "candidate-loader", + "data:text/html,

candidate

", + ) .await .unwrap(); - let navigation = owner.begin_target_document_navigation(TARGET, "candidate-loader".into()); - assert!( - owner.bind_pending_document_navigation_renderer_page_for_target( - TARGET, - &navigation, - first_renderer - ) - ); - assert!( - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - owner.replace_loaded_page_for_target(TARGET, Some(candidate)); - })) - .is_err() - ); + assert_ne!(candidate.page.renderer_residence(), first_renderer); + drop(candidate); assert_eq!(owner.target_document_id(TARGET), first_id); assert_eq!( - RendererPageResidenceIdentity::from_page(owner.loaded_page_for_target(TARGET).unwrap()), + owner + .target_renderer_page_residence_identity(TARGET) + .unwrap(), first_renderer ); assert!(owner.accepts_pending_document_navigation_event_for_target(TARGET, &navigation)); diff --git a/moli-protocol/src/conn/state/browser_context/page_state/fetch_state.rs b/moli-protocol/src/conn/state/browser_context/page_state/fetch_state.rs index 695c176f0a..faf4b7fc98 100644 --- a/moli-protocol/src/conn/state/browser_context/page_state/fetch_state.rs +++ b/moli-protocol/src/conn/state/browser_context/page_state/fetch_state.rs @@ -1,5 +1,5 @@ #[cfg(test)] -use crate::conn::fetch_support::OpenBodyStreamError; +use crate::conn::OpenBodyStreamError; use crate::conn::{BrowserContext, ConnectionNetworkRequestIdAllocator, PageAgentHost}; #[cfg(test)] use crate::conn::{ @@ -36,17 +36,17 @@ impl BrowserContext { pub(crate) fn background_targets(&self) -> impl DoubleEndedIterator { self.page_targets - .background(self.physical.selected_web_contents_id()) + .background(self.browser_context.selected_web_contents_id()) } pub(crate) fn background_target_count(&self) -> usize { self.page_targets - .background_len(self.physical.selected_web_contents_id()) + .background_len(self.browser_context.selected_web_contents_id()) } pub(crate) fn has_no_background_targets(&self) -> bool { self.page_targets - .background_is_empty(self.physical.selected_web_contents_id()) + .background_is_empty(self.browser_context.selected_web_contents_id()) } #[cfg(test)] @@ -152,8 +152,7 @@ impl BrowserContext { self.page_target(target_id)? .fetch_owner .pending_fetch_response_navigation(request_id)?; - self.paused_navigation_response_for_target(target_id)? - .prepared_renderer_agent_token() + self.paused_navigation_response_renderer_agent_for_target(target_id) } #[cfg(test)] @@ -390,6 +389,7 @@ mod tests { redirect_headers: None, navigate_id: Some(1), owner: crate::conn::CommandOwnerScope::for_session("SID-1"), + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": "LID-0000000001"}), ), diff --git a/moli-protocol/src/conn/state/browser_context/page_state/loaded.rs b/moli-protocol/src/conn/state/browser_context/page_state/loaded.rs index f7e8ab527e..d66840763f 100644 --- a/moli-protocol/src/conn/state/browser_context/page_state/loaded.rs +++ b/moli-protocol/src/conn/state/browser_context/page_state/loaded.rs @@ -1,69 +1,65 @@ use crate::conn::TargetPageResidenceIdentity; use crate::conn::state::TargetPageAbsenceReason; -use crate::conn::state::web_contents::{ - DocumentNavigationDestination, PreparedDocumentNavigation, RetiringDocument, -}; use crate::conn::state::{DevToolsRendererChannelError, DocumentId, DocumentProjectionFence}; use crate::conn::{BrowserContext, PageAgentHost, TargetRuntimeSlot}; -use moli_core::page::{Page, RendererPageCommandPostResponseContinuation}; +use moli_core::browser::web_contents::DocumentNavigationDestination; +use moli_core::browser::{ + BrowserDocumentMaterialization, BrowserNavigationLoad, BrowserPreparedDocumentNavigation, + BrowserPreparedNavigationResponse, PendingDocumentRetirement, +}; +use moli_core::page::RendererPageCommandPostResponseContinuation; pub(crate) struct LoadedNavigationPageCommit { - pub(crate) lifecycle: crate::conn::state::web_contents::CommittedDocumentLifecycle, + pub(crate) lifecycle: moli_core::browser::web_contents::CommittedDocumentLifecycle, pub(crate) inspection_projection: Result, pub(crate) replaced_page_owner: Option, - pub(crate) previous_document_retirement: RetiringDocument, + pub(crate) previous_document_retirement: PendingDocumentRetirement, pub(crate) committed_document_post_response_continuation: Option, } impl BrowserContext { - pub(in crate::conn::state::browser_context) fn inherited_document_policy_for_target( + pub(in crate::conn) fn inherited_document_policy_for_target( &self, target_id: &str, fetch_defaults: moli_fetch::FetchConfig, - permissions: &moli_core::browser::PermissionDefaults, - ) -> crate::conn::state::web_contents::InheritedDocumentPolicy { - let mut inherited = self.physical.inherited_document_policy( + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> moli_core::browser::web_contents::InheritedDocumentPolicy { + let mut policy = self.browser_context.inherited_document_policy( fetch_defaults, - permissions, - &self.global_extra_headers, - self.global_network_conditions, - self.global_geolocation_override.as_ref(), + &browser_globals.extra_headers, + browser_globals.network_conditions, + browser_globals.geolocation.as_ref(), ); if let Some(target) = self.page_target(target_id) { - inherited.navigator_queries = target.devtools_sessions.navigator_emulation.effective(); + policy.navigator_queries = target.devtools_sessions.navigator_emulation.effective(); } - inherited + policy } pub(in crate::conn) fn start_initial_document_for_target( &mut self, target_id: &str, fetch_defaults: moli_fetch::FetchConfig, - permissions: &moli_core::browser::PermissionDefaults, - ) -> Result { + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> Result { let inherited = - self.inherited_document_policy_for_target(target_id, fetch_defaults, permissions); - self.web_contents_for_target_mut(target_id) - .ok_or("initial WebContents unavailable")? - .start_initial_document_build(inherited) + self.inherited_document_policy_for_target(target_id, fetch_defaults, browser_globals); + let handle = self + .web_contents_handle_for_target(target_id) + .ok_or("initial WebContents unavailable")?; + self.browser_context + .start_initial_document(handle, inherited) } pub(in crate::conn) fn commit_initial_document( &mut self, - built: crate::conn::state::web_contents::BuiltInitialDocument, + built: moli_core::browser::BrowserBuiltInitialDocument, ) -> Result< moli_core::page::RendererPageCreationDiagnostics, - Box, + Box, > { - let Some(contents) = self - .physical - .web_contents - .get_mut(&built.key().web_contents()) - else { - return Err(Box::new(built)); - }; - let commit = contents.commit_initial_document(built)?; + let commit = self.browser_context.commit_initial_document(built)?; // Native completion is final. A missing or retired AgentHost cannot // veto the Browser document or fail other Browser waiters. let Some(target_id) = self @@ -116,18 +112,77 @@ impl BrowserContext { Ok(commit.diagnostics) } - pub(crate) fn loaded_page(&self) -> Option<&Page> { - self.physical - .web_contents - .get(&self.physical.selected_web_contents_id()?)? - .main_frame - .current_document - .as_ref() - .map(|document| &document.page) + #[cfg(test)] + pub(crate) fn loaded_document_url_for_test(&self) -> Option { + let document = self.browser_context.selected_document_handle()?; + self.browser_context.document_url(document).ok() + } + + #[cfg(test)] + pub(crate) fn loaded_document_title_for_test(&self) -> Option { + let document = self.browser_context.selected_document_handle()?; + self.browser_context.document_title(document).ok() + } + + #[cfg(test)] + pub(crate) fn loaded_document_renderer_agent_for_test( + &self, + ) -> Option { + let document = self.browser_context.selected_document_handle()?; + self.browser_context + .document_renderer_devtools_agent_token_for_test(document) + } + + #[cfg(test)] + pub(crate) fn target_document_renderer_agent_for_test( + &self, + target_id: &str, + ) -> Option { + let document = self.document_handle_for_target(target_id)?; + self.browser_context + .document_renderer_devtools_agent_token_for_test(document) + } + + #[cfg(test)] + pub(crate) fn loaded_document_response_status_for_test(&self) -> Option { + let document = self.browser_context.selected_document_handle()?; + self.browser_context + .document_response_status_for_test(document) + .ok() + } + + #[cfg(test)] + pub(crate) fn loaded_document_response_headers_for_test( + &self, + ) -> Option> { + let document = self.browser_context.selected_document_handle()?; + self.browser_context + .document_response_headers(document) + .ok() + } + + #[cfg(test)] + pub(crate) fn loaded_document_script_execution_for_test( + &self, + ) -> Option { + let document = self.browser_context.selected_document_handle()?; + self.browser_context + .document_script_execution_for_test(document) + .ok() + } + + #[cfg(test)] + pub(crate) fn loaded_document_renderer_inspection_endpoint_for_test( + &self, + ) -> Option { + let document = self.browser_context.selected_document_handle()?; + self.browser_context + .document_renderer_inspection_endpoint_for_test(document) + .ok() } pub(crate) fn has_loaded_page(&self) -> bool { - self.loaded_page().is_some() + self.browser_context.selected_document_handle().is_some() } pub(crate) fn document_id(&self) -> Option { @@ -144,49 +199,73 @@ impl BrowserContext { } #[cfg(test)] - pub(crate) fn clear_target_page_for_test(&mut self, target_id: &str) -> Option { - self.clear_loaded_page_with_reason_for_target( + pub(crate) fn clear_target_page_for_test( + &mut self, + target_id: &str, + ) -> Option { + self.retire_loaded_document_with_reason_for_target( target_id, TargetPageAbsenceReason::TestFixture, ) } #[cfg(test)] - pub(crate) fn replace_target_page_for_test( + pub(crate) fn commit_target_navigation_for_test( &mut self, target_id: &str, - page: Option, - ) -> Option { - self.replace_loaded_page_for_target(target_id, page) + prepared: BrowserPreparedDocumentNavigation, + ) -> LoadedNavigationPageCommit { + let expected = self + .web_contents_handle_for_target(target_id) + .expect("navigation fixture target must own WebContents"); + assert_eq!( + prepared.web_contents_id(), + expected.id(), + "navigation fixture must commit to its exact target" + ); + self.commit_loaded_navigation(prepared) + .expect("navigation fixture commit must succeed") } #[cfg(test)] - pub(crate) fn replace_active_page_for_test(&mut self, page: Option) -> Option { + pub(crate) async fn commit_active_navigation_for_test( + &mut self, + prepared: BrowserPreparedDocumentNavigation, + ) -> LoadedNavigationPageCommit { let target_id = self .active_target_id_owned() .expect("active fixture target"); - self.replace_target_page_for_test(&target_id, page) - } - - #[cfg(test)] - pub(crate) fn replace_loaded_page(&mut self, page: Option) -> Option { - let target_id = self.active_target_id_owned().expect("active target"); - let previous = self.replace_loaded_page_for_target(&target_id, page); - self.ingest_active_target_output_updates(); - self.active_page_target_mut() - .owner_state - .clear_loaded_document_context_state(); - self.clear_active_target_loaded_document_session_state(); - previous + let committed = self.commit_target_navigation_for_test(&target_id, prepared); + // These unit fixtures retain test-only cookie policy across document + // replacement. Apply it to the exact newly committed Document before + // returning control to the test, without exposing a mutable Page. + let overrides = self + .page_targets + .get(&target_id) + .expect("committed fixture target") + .document_cookie_manager_surface + .snapshot() + .policy + .overrides; + if overrides != Default::default() { + let document = self + .document_handle_for_target(&target_id) + .expect("committed fixture Document"); + self.browser_context + .apply_document_cookie_facade_overrides_for_test(document, Some(overrides)) + .await + .expect("fixture cookie policy must apply to its exact Document"); + } + committed } #[cfg(test)] pub(crate) fn clear_loaded_page_with_reason( &mut self, reason: TargetPageAbsenceReason, - ) -> Option { + ) -> Option { let target_id = self.active_target_id_owned().expect("active target"); - let previous = self.clear_loaded_page_with_reason_for_target(&target_id, reason); + let previous = self.retire_loaded_document_with_reason_for_target(&target_id, reason); self.ingest_active_target_output_updates(); self.active_page_target_mut() .owner_state @@ -195,19 +274,6 @@ impl BrowserContext { previous } - #[cfg(test)] - pub(crate) async fn set_loaded_page_async(&mut self, mut page: Page) { - // BrowserContext owns document-cookie facade overrides for the active - // browsing context. New pages should inherit the current browser - // policy surface before any JS observes `document.cookie` or - // `navigator.cookieEnabled`. - self.active_page_target() - .document_cookie_manager_surface - .apply_to_page_async(&mut page) - .await; - let _ = self.replace_loaded_page(Some(page)); - } - #[cfg(test)] pub(crate) fn clear_loaded_page(&mut self) -> bool { self.clear_loaded_page_with_reason(TargetPageAbsenceReason::TestFixture) @@ -224,10 +290,10 @@ impl BrowserContext { #[cfg(test)] pub(crate) async fn remove_active_page_target_async(&mut self) -> bool { - let Some(target_id) = self.active_target_id_owned() else { + let Some(handle) = self.selected_web_contents_handle() else { return false; }; - let Some((_projection, closing)) = self.take_page_target_for_close(&target_id) else { + let Ok((_projection, closing)) = self.begin_web_contents_close(handle) else { return false; }; closing.close_async().await; @@ -235,7 +301,7 @@ impl BrowserContext { } pub(crate) async fn close_all_pages_async(&mut self) { - let closing_contents = self.physical.close_all_web_contents(); + let closing_contents = self.browser_context.close_all_web_contents(); let mut projections = std::mem::take(&mut self.page_targets); for target in projections.iter_mut() { target.runtime_slot.retire_for_target_close(); @@ -251,48 +317,34 @@ impl BrowserContext { impl BrowserContext { pub(crate) fn owns_web_contents(&self, id: moli_core::browser::WebContentsId) -> bool { - self.physical.web_contents.contains_key(&id) - } - - #[cfg(test)] - pub(crate) fn start_loaded_document_navigation_for_target( - &self, - target_id: &str, - navigation: moli_core::browser::NavigationId, - page: Page, - destination: DocumentNavigationDestination, - artifacts: &moli_core::page::RendererPageCreationArtifacts, - defaults: &moli_core::browser::PermissionDefaults, - ) -> Result< - impl std::future::Future> + use<>, - &'static str, - > { - let contents = self - .web_contents_for_target(target_id) - .ok_or("navigation WebContents unavailable")?; - contents.start_loaded_document_navigation( - navigation, - page, - destination, - artifacts, - self.physical.permission_overrides.snapshot(defaults), - ) + self.browser_context + .contains_web_contents(moli_core::browser::WebContentsHandle::new( + self.browser_context.id(), + id, + )) } pub(in crate::conn) fn start_document_materialization_for_target( &mut self, target_id: &str, navigation: moli_core::browser::NavigationId, - page: crate::conn::state::web_contents::PreparedNavigationResponse, + page: BrowserPreparedNavigationResponse, destination: DocumentNavigationDestination, fetch_defaults: moli_fetch::FetchConfig, - permissions: &moli_core::browser::PermissionDefaults, - ) -> Result { + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> Result { let inherited = - self.inherited_document_policy_for_target(target_id, fetch_defaults, permissions); - self.web_contents_for_target_mut(target_id) - .ok_or("navigation WebContents unavailable")? - .start_document_materialization(navigation, page, destination, inherited) + self.inherited_document_policy_for_target(target_id, fetch_defaults, browser_globals); + let handle = self + .web_contents_handle_for_target(target_id) + .ok_or("navigation WebContents unavailable")?; + self.browser_context.start_document_materialization( + handle, + navigation, + page, + destination, + inherited, + ) } pub(in crate::conn) fn start_navigation_load_for_target( @@ -301,13 +353,15 @@ impl BrowserContext { navigation: moli_core::browser::NavigationId, policy: moli_core::browser::NavigationRequestLoadPolicy, fetch_defaults: moli_fetch::FetchConfig, - permissions: &moli_core::browser::PermissionDefaults, - ) -> Result { + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> Result { let inherited = - self.inherited_document_policy_for_target(target_id, fetch_defaults, permissions); - self.web_contents_for_target_mut(target_id) - .ok_or("navigation WebContents unavailable")? - .start_navigation_load(navigation, policy, inherited) + self.inherited_document_policy_for_target(target_id, fetch_defaults, browser_globals); + let handle = self + .web_contents_handle_for_target(target_id) + .ok_or("navigation WebContents unavailable")?; + self.browser_context + .start_navigation_load(handle, navigation, policy, inherited) } #[cfg(test)] @@ -316,25 +370,24 @@ impl BrowserContext { target_id: &str, final_url: &url::Url, fetch_defaults: moli_fetch::FetchConfig, - permissions: &moli_core::browser::PermissionDefaults, + browser_globals: &crate::conn::BrowserGlobalOverrides, ) -> Result { let inherited = - self.inherited_document_policy_for_target(target_id, fetch_defaults, permissions); - self.web_contents_for_target_mut(target_id) - .ok_or("navigation WebContents unavailable")? - .capture_document_policy(inherited, final_url) + self.inherited_document_policy_for_target(target_id, fetch_defaults, browser_globals); + let handle = self + .web_contents_handle_for_target(target_id) + .ok_or("navigation WebContents unavailable")?; + self.browser_context + .capture_document_policy_for_test(handle, inherited, final_url) } pub(crate) fn commit_loaded_navigation( &mut self, - prepared: PreparedDocumentNavigation, + prepared: BrowserPreparedDocumentNavigation, ) -> anyhow::Result { let navigation = prepared.navigation(); let commit = self - .physical - .web_contents - .get_mut(&prepared.web_contents_id()) - .ok_or_else(|| anyhow::anyhow!("navigation WebContents unavailable"))? + .browser_context .commit_document_navigation(prepared) .map_err(anyhow::Error::msg)?; debug_assert_eq!(commit.navigation, navigation); @@ -354,8 +407,12 @@ impl BrowserContext { let target_id = target_id.as_str(); debug_assert_eq!(self.target_document_id(target_id), Some(commit.document)); debug_assert_eq!( - self.web_contents_for_target(target_id) - .map(|contents| (contents.id(), contents.main_frame.id())), + self.browser_context + .web_contents_identity( + self.web_contents_handle_for_target(target_id) + .expect("resolved WebContents"), + ) + .ok(), Some((commit.web_contents, commit.frame_slot)), ); diff --git a/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs b/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs index afd3b57ae7..5832d9b06f 100644 --- a/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs +++ b/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs @@ -1,7 +1,7 @@ //! Stable page-target registry behavior and foreground target selection. +use crate::conn::state::page_slot::TargetPageSlot; use crate::conn::state::{SessionStorageNamespace, TargetPageAbsenceReason}; -use crate::conn::state::{page_slot::TargetPageSlot, web_contents::WebContents}; use crate::conn::{ BrowserContext, DedicatedWorkerTargetState, InitialDocumentCreator, PageAgentHost, ServiceWorkerTargetState, SharedWorkerTargetState, TargetIdentityState, @@ -9,25 +9,51 @@ use crate::conn::{ use crate::devtools_runtime::{ DevToolsBrowserContextId, DevToolsTargetId, DevToolsTargetInfo, DevToolsTargetKind, }; +use moli_core::browser::{WebContentsCreation, WebContentsHandle}; use moli_core::network::SharedWebStorageStore; +pub(crate) struct PendingWebContentsSelection { + admission_error: Option, + surface_updates: Vec, +} + +pub(crate) struct CompletedWebContentsSelection { + admission_error: Option, + surface_updates: Vec, +} + +impl PendingWebContentsSelection { + pub(crate) async fn wait(self) -> CompletedWebContentsSelection { + let mut surface_updates = Vec::with_capacity(self.surface_updates.len()); + for update in self.surface_updates { + surface_updates.push(update.wait().await); + } + CompletedWebContentsSelection { + admission_error: self.admission_error, + surface_updates, + } + } +} + impl BrowserContext { - pub(in crate::conn) fn take_page_target_for_close( + pub(in crate::conn) fn begin_web_contents_close( &mut self, - target_id: &str, - ) -> Option<( - PageAgentHost, - crate::conn::state::web_contents::ClosingWebContents, - )> { - let id = self.page_targets.get(target_id)?.web_contents_id(); - let closing = self.physical.close_web_contents(id)?; + handle: WebContentsHandle, + ) -> Result<(PageAgentHost, moli_core::browser::PendingWebContentsClose), String> { + let target_id = self + .page_targets + .get_for_web_contents(handle.id()) + .map(PageAgentHost::target_id) + .ok_or_else(|| "WebContents projection unavailable".to_owned())? + .to_owned(); + let closing = self.browser_context.close_web_contents(handle)?; let mut target = self .page_targets - .remove(target_id) + .remove(&target_id) .expect("retired WebContents must retain its resolved projection until removal"); target.runtime_slot.retire_for_target_close(); - self.forget_target_popup_id_for_target(target_id); - Some((target, closing)) + self.forget_target_popup_id_for_target(&target_id); + Ok((target, closing)) } pub(crate) fn stage_background_target( @@ -80,10 +106,8 @@ impl BrowserContext { creator: Option<&InitialDocumentCreator>, ) -> Option { creator.and_then(|creator| { - self.physical - .web_contents - .get(&creator.web_contents_id()) - .map(|contents| contents.session_storage.deep_clone()) + self.browser_context + .clone_session_storage_namespace(creator.web_contents_id()) }) } @@ -98,20 +122,19 @@ impl BrowserContext { session_storage_namespace: Option, ) { let target_identity = background_target_identity_for_initial_url(&url, creator.as_ref()); - let mut contents = WebContents::default(); - contents.begin_initial_empty_document( + let mut creation = WebContentsCreation::with_initial_document( initial_empty_document_url.unwrap_or_else(|| url.clone()), creator, initial_empty_document_storage_key, ); if let Some(namespace) = session_storage_namespace { - contents.session_storage = namespace; + creation = creation.with_session_storage(namespace); } let inserted = self.register_web_contents_target( target_id, session_id, target_identity, - contents, + creation, TargetPageSlot::empty_for_initial_document_page_build(), ); debug_assert!(inserted, "staged page target id must be unique"); @@ -124,8 +147,7 @@ impl BrowserContext { url: String, initial_empty_document_url: Option, ) { - let mut contents = WebContents::default(); - contents.begin_initial_empty_document( + let creation = WebContentsCreation::with_initial_document( initial_empty_document_url.unwrap_or_else(|| url.clone()), None, None, @@ -134,12 +156,15 @@ impl BrowserContext { target_id.clone(), session_id, TargetIdentityState::with_url(url), - contents, + creation, TargetPageSlot::empty_for_initial_document_page_build(), ); debug_assert!(inserted, "new active page target id must be unique"); - let selected = self.select_registered_page_target(&target_id); - debug_assert!(selected, "newly inserted page target must be selectable"); + let handle = self + .web_contents_handle_for_target(&target_id) + .expect("newly inserted target must have WebContents"); + self.select_registered_web_contents(handle) + .expect("newly inserted WebContents must be selectable"); } pub(crate) fn reusable_window_open_target_name(target_name: &str) -> Option<&str> { @@ -150,44 +175,27 @@ impl BrowserContext { } pub(crate) fn target_id_for_window_name(&self, target_name: &str) -> Option<&str> { - let name = Self::reusable_window_open_target_name(target_name)?; - let id = self - .physical - .web_contents - .values() - .find(|contents| contents.window.name.as_deref() == Some(name))? - .id(); + let id = self.web_contents_handle_for_window_name(target_name)?.id(); self.page_targets .get_for_web_contents(id) .map(PageAgentHost::target_id) } + pub(crate) fn web_contents_handle_for_window_name( + &self, + target_name: &str, + ) -> Option { + let name = Self::reusable_window_open_target_name(target_name)?; + self.browser_context + .web_contents_handle_for_window_name(name) + } + pub(crate) fn has_attached_child_frame_id(&self, frame_id: &str) -> bool { self.page_targets .iter() .any(|target| target.owner_state.has_attached_child_frame_id(frame_id)) } - pub(crate) fn remember_target_window_name(&mut self, target_name: &str, target_id: &str) { - let Some(id) = self - .page_target(target_id) - .map(PageAgentHost::web_contents_id) - else { - return; - }; - let name = Self::reusable_window_open_target_name(target_name).map(str::to_owned); - for contents in self.physical.web_contents.values_mut() { - let is_target = contents.id() == id; - let window = &mut contents.window; - if is_target { - window.name = name.clone(); - } else if name.is_some() && window.name == name { - // Preserve the existing last-assignment-wins lookup rule. - window.name = None; - } - } - } - pub(crate) fn remember_target_popup_id(&mut self, popup_id: Option, target_id: &str) { if let Some(popup_id) = popup_id && let Some(replaced_popup_id) = @@ -217,22 +225,11 @@ impl BrowserContext { }) } - pub(crate) fn remember_target_opener( + pub(crate) fn set_target_opener_frame_attribution( &mut self, target_id: &str, - opener_target_id: String, opener_frame_id: String, - can_access_opener: bool, ) { - let opener = - self.page_target(&opener_target_id) - .map(|target| crate::conn::state::WindowOpener { - web_contents_id: target.web_contents_id(), - can_access: can_access_opener, - }); - if let Some(contents) = self.web_contents_for_target_mut(target_id) { - contents.window.opener = opener; - } if let Some(target) = self.page_target_mut(target_id) { target.opener_frame_id = Some(opener_frame_id); } @@ -335,138 +332,50 @@ impl BrowserContext { && self.dispose_devtools_session_for_target(target_id, session_id, session_key) } - pub(crate) async fn clear_devtools_network_session_policy_async( - &mut self, - target_id: &str, - session_key: &moli_page_types::DevToolsSessionKey, - ) -> anyhow::Result<()> { - let Some(target) = self.page_target_mut(target_id) else { - return Ok(()); - }; - let listener_session_id = session_key.wire_session_id().map(str::to_owned); - match session_key { - moli_page_types::DevToolsSessionKey::Primary => { - target.runtime_slot.disable_primary_network_events(); - } - moli_page_types::DevToolsSessionKey::Attached(attached_session_id) => { - target - .runtime_slot - .remove_attached_network_session(attached_session_id); - } - } - target - .runtime_slot - .remove_network_session_observation_cursor(listener_session_id.as_deref()); - target - .runtime_slot - .remove_captured_response_body_visibility_for_session(listener_session_id.as_deref()); - if !target.runtime_slot.has_network_event_listeners() { - target.runtime_slot.clear_captured_response_bodies(); - target.runtime_slot.clear_websocket_request_ids(); - } - self.clear_devtools_network_state_for_target(target_id, session_key); - let effective = self.effective_policy_for_target(target_id); - let headers = self.merged_extra_headers_for_target_policy(effective.extra_headers()); - if let Some(page) = self.loaded_page_for_target_mut(target_id) { - // Reconcile current Browser policy even on a repeated disposal: - // clearing a contribution is not proof the renderer applied it. - page.set_network_request_policy_async( - &headers, - effective.bypass_service_worker(), - effective.cache_disabled(), - effective.blocked_url_patterns(), - ) - .await - .map_err(|error| { - anyhow::anyhow!( - "failed to restore detached session network request policy: {error}" - ) - })?; - } - Ok(()) - } - - pub(crate) async fn clear_devtools_emulation_session_policy_async( - &mut self, - target_id: &str, - session_key: &moli_page_types::DevToolsSessionKey, - ) -> anyhow::Result<()> { - self.clear_devtools_emulation_policy_state_for_target(target_id, session_key); - Ok(()) - } - - pub(crate) async fn reset_primary_page_session_target_state_async( + pub(crate) fn start_reset_primary_page_session_target_state( &mut self, target_id: &str, session_id: &str, - ) -> anyhow::Result { + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> (bool, Option) { + let is_active = self.is_active_target(target_id); let Some(target) = self.page_target_mut(target_id) else { - return Ok(false); + return (false, None); }; if !target.is_session(session_id) { - return Ok(false); + return (false, None); } self.reset_primary_session_target_state_fields_for_target(target_id); - let navigator_overrides = self - .navigator_overrides_for_target(target_id) - .expect("disposing target retains navigator state"); - let effective_headers = self.effective_extra_headers_for_target(target_id); + let effective_headers = + self.effective_extra_headers_for_target(target_id, &browser_globals.extra_headers); let effective_policy = self.effective_policy_for_target(target_id); let script_execution_disabled = self .target_emulation_policy(target_id) .expect("live WebContents") .script_execution_disabled; - let document_activity = self - .document_activity_for_target(target_id) - .expect("disposing target retains document activity"); - if let Some(page) = self.loaded_page_for_target_mut(target_id) { - let mut first_error = None; - if let Err(error) = page - .set_network_request_policy_async( - &effective_headers, - effective_policy.bypass_service_worker(), - effective_policy.cache_disabled(), - effective_policy.blocked_url_patterns(), - ) - .await - { - first_error = Some(anyhow::anyhow!( - "failed to clear page network request policy: {error}" - )); - } - if let Err(error) = page.set_network_offline_async(false).await { - first_error.get_or_insert_with(|| { - anyhow::anyhow!("failed to clear page offline state: {error}") - }); - } - if let Err(error) = page - .set_script_execution_disabled_async(script_execution_disabled) - .await - { - first_error.get_or_insert_with(|| { - anyhow::anyhow!("failed to clear page script execution disabled state: {error}") - }); - } - if let Err(error) = page - .set_navigator_overrides_async(&navigator_overrides) - .await - { - first_error.get_or_insert_with(|| { - anyhow::anyhow!("failed to restore native navigator state: {error}") - }); - } - if let Err(error) = page.set_document_activity_async(document_activity).await { - first_error.get_or_insert_with(|| { - anyhow::anyhow!("failed to restore native document activity: {error}") - }); - } - if let Some(error) = first_error { - return Err(error); - } - } - - Ok(true) + let Some(document) = self.document_handle_for_target(target_id) else { + return (true, None); + }; + let pending = self.start_document_policy_batch_with_surface( + document, + vec![ + crate::conn::DocumentPolicyUpdate::NetworkRequestPolicy { + extra_headers: effective_headers, + bypass_service_worker: effective_policy.bypass_service_worker(), + cache_disabled: effective_policy.cache_disabled(), + blocked_url_patterns: effective_policy.blocked_url_patterns().to_vec(), + }, + crate::conn::DocumentPolicyUpdate::NetworkOffline(false), + crate::conn::DocumentPolicyUpdate::ScriptExecutionDisabled( + script_execution_disabled, + ), + ], + is_active, + browser_globals.network_conditions, + browser_globals.geolocation.as_ref(), + ); + (true, Some(pending)) } #[cfg(test)] @@ -533,27 +442,6 @@ impl BrowserContext { ) } - #[cfg(test)] - pub(crate) async fn select_first_background_target_async(&mut self) -> Option { - let selected_target_id = self - .background_targets() - .find(|target| self.target_has_loaded_page(target.target_id())) - .map(|target| target.target_id().to_owned()) - .or_else(|| { - self.background_targets() - .next() - .map(|target| target.target_id().to_owned()) - })?; - self.select_background_target_async(selected_target_id) - .await - } - - pub(crate) async fn select_last_background_target_async(&mut self) -> Option { - let selected_target_id = self.last_selectable_background_target_id()?; - self.select_background_target_async(selected_target_id) - .await - } - pub(crate) fn last_selectable_background_target_id(&self) -> Option { self.background_targets() .rev() @@ -566,49 +454,64 @@ impl BrowserContext { }) } - async fn select_background_target_async(&mut self, target_id: String) -> Option { - self.select_page_target_async(&target_id) - .await - .expect("applying selected target visibility should succeed") - .then_some(target_id) - } - - pub(crate) async fn select_page_target_async( + pub(crate) fn start_select_web_contents( &mut self, - target_id: &str, - ) -> anyhow::Result { - if self.is_active_target(target_id) { - return Ok(true); - } - if self.background_target(target_id).is_none() { - return Ok(false); - } - let synchronize_loaded_page = self - .page_targets - .active(self.physical.selected_web_contents_id()) - .is_none_or(|host| !self.has_pending_javascript_dialog_for_target(host.target_id())) - && self.page_target(target_id).is_none_or(|host| { - !self.has_pending_javascript_dialog_for_target(host.target_id()) + selected: WebContentsHandle, + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> Result { + let selected_has_dialog = self.web_contents_has_pending_javascript_dialog(selected)?; + let previous = self.selected_web_contents_handle(); + if previous == Some(selected) { + return Ok(PendingWebContentsSelection { + admission_error: None, + surface_updates: Vec::new(), }); - let previous_active_target_id = self.active_target_id_owned(); - let selected = self.select_registered_page_target(target_id); - debug_assert!(selected, "existing page target must be selectable"); - if synchronize_loaded_page { - self.apply_surface_overrides_to_loaded_page_async().await?; } - if synchronize_loaded_page - && let Some(previous_active_target_id) = previous_active_target_id - { - let activity = self - .document_activity_for_target(&previous_active_target_id) - .expect("previous active target remains registered"); - if let Some(page) = self.loaded_page_for_target_mut(&previous_active_target_id) - && let Err(error) = page.set_document_activity_async(activity).await + let previous_has_dialog = previous + .map(|handle| self.web_contents_has_pending_javascript_dialog(handle)) + .transpose()? + .unwrap_or(false); + self.select_registered_web_contents(selected)?; + + let mut admission_error = None; + let mut surface_updates = Vec::new(); + if !selected_has_dialog && !previous_has_dialog { + for (handle, foreground) in + std::iter::once((selected, true)).chain(previous.map(|handle| (handle, false))) { - tracing::warn!(target_id = previous_active_target_id, %error, "failed to update background document activity"); + let Some(document) = self.document_handle_for_web_contents(handle)? else { + continue; + }; + match self.start_document_page_surface_update( + document, + foreground, + browser_globals.network_conditions, + browser_globals.geolocation.as_ref(), + ) { + Ok(update) => surface_updates.push(update), + Err(error) => { + admission_error.get_or_insert(error); + } + } } } - Ok(true) + Ok(PendingWebContentsSelection { + admission_error, + surface_updates, + }) + } + + pub(crate) fn finish_select_web_contents( + &mut self, + completed: CompletedWebContentsSelection, + ) -> Result<(), String> { + let mut first_error = completed.admission_error; + for update in completed.surface_updates { + if let Err(error) = self.browser_context.finish_document_policy_batch(update) { + first_error.get_or_insert(error); + } + } + first_error.map_or(Ok(()), Err) } pub(crate) fn begin_active_target_initial_empty_document(&mut self, initial_url: String) { @@ -627,28 +530,37 @@ impl BrowserContext { &target_id, TargetPageAbsenceReason::InitialDocumentPageBuildPending, ); - self.web_contents_for_target_mut(&target_id) - .expect("selected WebContents") - .begin_initial_empty_document(initial_url, None, storage_key); + let handle = self + .web_contents_handle_for_target(&target_id) + .expect("selected WebContents"); + self.browser_context + .begin_initial_empty_document(handle, initial_url, None, storage_key) + .expect("selected WebContents"); } #[cfg(test)] pub(crate) fn mark_target_initial_empty_document_materialized(&mut self, target_id: &str) { - if let Some(target) = self.web_contents_for_target_mut(target_id) { - target.mark_initial_empty_document_materialized(); + if let Some(handle) = self.web_contents_handle_for_target(target_id) { + let _ = self + .browser_context + .mark_initial_empty_document_materialized_for_test(handle); } } pub(crate) fn mark_target_initial_url_replaces_empty_document(&mut self, target_id: &str) { - if let Some(target) = self.web_contents_for_target_mut(target_id) { - target.mark_next_navigation_history_replace_initial_empty_document(); + if let Some(handle) = self.web_contents_handle_for_target(target_id) { + let _ = self + .browser_context + .mark_initial_url_replaces_empty_document(handle); } } #[cfg(test)] pub(crate) fn mark_target_initial_empty_document_exited(&mut self, target_id: &str) { - if let Some(target) = self.web_contents_for_target_mut(target_id) { - target.mark_initial_empty_document_exited(); + if let Some(handle) = self.web_contents_handle_for_target(target_id) { + let _ = self + .browser_context + .mark_initial_empty_document_exited_for_test(handle); } } @@ -660,14 +572,15 @@ impl BrowserContext { pub(crate) fn devtools_target_info(&self, target_id: &str) -> Option { if let Some(target) = self.page_target(target_id) { + let handle = self.web_contents_handle_for_target(target_id)?; let opener = self - .web_contents_for_target(target_id)? - .window - .opener - .and_then(|opener| { + .browser_context + .web_contents_opener(handle) + .ok()? + .and_then(|(web_contents_id, can_access)| { self.page_targets - .get_for_web_contents(opener.web_contents_id) - .map(|target| (target, opener.can_access)) + .get_for_web_contents(web_contents_id) + .map(|target| (target, can_access)) }); let attached = target.has_session() || !self.attached_session_ids_for_target(target_id).is_empty(); @@ -679,8 +592,8 @@ impl BrowserContext { .committed_document_title() .map(str::to_owned) .or_else(|| { - self.loaded_page_for_target(target.target_id()) - .map(|page| page.document_title()) + self.document_handle_for_target(target.target_id()) + .and_then(|document| self.browser_context.document_title(document).ok()) }) .unwrap_or_default(), url: target.target_url().to_owned(), @@ -904,12 +817,8 @@ impl BrowserContext { // binding observable so normal target retirement can detach it, while // best-effort registering the live renderer session before the attach // event is published. - let _ = self - .renderer_runtime() - .attach_dedicated_worker_runtime_inspector_session( - renderer_instance_id, - Some(session_id), - ); + let _ = + self.attach_dedicated_worker_inspector_session(renderer_instance_id, Some(session_id)); true } @@ -1012,8 +921,7 @@ impl BrowserContext { (!was_attached).then_some(target.renderer_version_id) }; if let Some(version_id) = attached_version_id { - self.renderer_runtime() - .set_service_worker_devtools_attached(version_id, true); + self.set_service_worker_devtools_attached(version_id, true); }; true } @@ -1034,8 +942,7 @@ impl BrowserContext { (target_id, detached_version_id) }; if let Some(version_id) = detached_version_id { - self.renderer_runtime() - .set_service_worker_devtools_attached(version_id, false); + self.set_service_worker_devtools_attached(version_id, false); } Some(target_id) } @@ -1300,11 +1207,10 @@ mod tests { ); assert!(!Arc::ptr_eq(&opener_storage, &popup_storage)); - drop( - context - .take_page_target_for_close("TID-renamed-opener") - .unwrap(), - ); + let renamed_opener = context + .web_contents_handle_for_target("TID-renamed-opener") + .unwrap(); + drop(context.begin_web_contents_close(renamed_opener).unwrap()); context.stage_background_target( "TID-orphan-popup".into(), None, @@ -1370,12 +1276,14 @@ mod tests { context.set_active_target_id("TID-opener"); let opener = context.active_page_target().web_contents_id(); context.stage_background_target("TID-popup".into(), None, "about:blank".into(), None, None); - context.remember_target_opener( - "TID-popup", - "TID-opener".into(), - "FRAME-opener".into(), - true, - ); + let popup_handle = context.web_contents_handle_for_target("TID-popup").unwrap(); + let opener_handle = context + .web_contents_handle_for_target("TID-opener") + .unwrap(); + context + .set_web_contents_opener(popup_handle, Some(opener_handle), true) + .unwrap(); + context.set_target_opener_frame_attribution("TID-popup", "FRAME-opener".into()); assert!(context.rekey_active_target("TID-renamed")); context.stage_background_target( @@ -1390,12 +1298,15 @@ mod tests { context.target_info("TID-popup").unwrap()["openerId"], "TID-renamed" ); - drop(context.take_page_target_for_close("TID-opener")); + let replacement_opener = context + .web_contents_handle_for_target("TID-opener") + .unwrap(); + drop(context.begin_web_contents_close(replacement_opener)); let popup = context.target_info("TID-popup").unwrap(); assert_eq!(popup["openerId"], "TID-renamed"); assert_eq!(popup["canAccessOpener"], true); - drop(context.take_page_target_for_close("TID-renamed")); + drop(context.begin_web_contents_close(opener_handle)); context.set_active_target_id("TID-renamed"); let popup = context.target_info("TID-popup").unwrap(); assert!(popup.get("openerId").is_none()); @@ -1407,7 +1318,12 @@ mod tests { fn window_name_follows_web_contents_and_dies_with_its_owner() { let mut context = BrowserContext::new("BC-window-rekey".into()); context.set_active_target_id("TID-window"); - context.remember_target_window_name("report", "TID-window"); + let handle = context + .web_contents_handle_for_target("TID-window") + .unwrap(); + context + .set_web_contents_window_name(handle, Some("report".into())) + .unwrap(); assert!(context.rekey_active_target("TID-renamed")); context.stage_background_target( "TID-window".into(), @@ -1420,19 +1336,24 @@ mod tests { context.target_id_for_window_name("report"), Some("TID-renamed") ); - drop(context.take_page_target_for_close("TID-window")); + let replacement = context + .web_contents_handle_for_target("TID-window") + .unwrap(); + drop(context.begin_web_contents_close(replacement)); assert_eq!( context.target_id_for_window_name("report"), Some("TID-renamed") ); - context.remember_target_window_name("renamed-report", "TID-renamed"); + context + .set_web_contents_window_name(handle, Some("renamed-report".into())) + .unwrap(); assert_eq!(context.target_id_for_window_name("report"), None); assert_eq!( context.target_id_for_window_name("renamed-report"), Some("TID-renamed") ); - drop(context.take_page_target_for_close("TID-renamed")); + drop(context.begin_web_contents_close(handle)); context.set_active_target_id("TID-renamed"); assert_eq!(context.target_id_for_window_name("renamed-report"), None); } @@ -1456,8 +1377,16 @@ mod tests { for id in ["TID-spaced", "TID-exact"] { context.stage_background_target(id.into(), None, "about:blank".into(), None, None); } - context.remember_target_window_name(" ReportWindow ", "TID-spaced"); - context.remember_target_window_name("ReportWindow", "TID-exact"); + let spaced = context + .web_contents_handle_for_target("TID-spaced") + .unwrap(); + let exact = context.web_contents_handle_for_target("TID-exact").unwrap(); + context + .set_web_contents_window_name(spaced, Some(" ReportWindow ".into())) + .unwrap(); + context + .set_web_contents_window_name(exact, Some("ReportWindow".into())) + .unwrap(); assert_eq!( context.target_id_for_window_name(" ReportWindow "), Some("TID-spaced") @@ -1524,16 +1453,11 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn selecting_another_foreground_target_preserves_page_session_and_owner_state() { let mut ctx = TestContext::new(); - let active_page = ctx + let mut context = ctx .conn - .load_page_via_runtime_async("data:text/html,deactivate-active") - .await - .expect("active page should load"); - - let mut context = BrowserContext::new("BC-deactivate".to_owned()); + .new_browser_context_fixture_for_test("BC-deactivate".to_owned()); context.set_active_target_id("TID-deactivate".to_owned()); context.attach_active_session("SID-deactivate".to_owned()); - context.set_target_url(active_page.final_url().as_str().to_owned()); context.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state @@ -1596,11 +1520,17 @@ mod tests { .active_page_target_mut() .runtime_slot .set_network_request_counters_for_test(77, 88); + ctx.conn.install_browser_context_fixture_for_test(context); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,deactivate-active", + Some("SID-deactivate"), + ) + .await; + let mut context = ctx.conn.browser_context.take().unwrap(); context .active_page_target_mut() .runtime_slot .mark_subresource_records_emitted(None, 0, 3); - context.set_loaded_page_async(active_page).await; let active_attachment = context .active_page_target() .runtime_slot @@ -1614,13 +1544,15 @@ mod tests { None, ); - assert_eq!( - context - .select_background_target_async("TID-selected".to_owned()) - .await - .as_deref(), - Some("TID-selected") - ); + let handle = context + .web_contents_handle_for_target("TID-selected") + .unwrap(); + let completed = context + .start_select_web_contents(handle, &Default::default()) + .unwrap() + .wait() + .await; + context.finish_select_web_contents(completed).unwrap(); assert_eq!(context.active_target_id(), Some("TID-selected")); assert!( @@ -1648,9 +1580,7 @@ mod tests { "changing foreground selection must not reallocate the renderer channel" ); assert_eq!( - context - .loaded_page_for_target(background_target.target_id()) - .map(|page| page.renderer_devtools_agent_token()), + context.target_document_renderer_agent_for_test(background_target.target_id()), Some(active_attachment.agent_token()), "the background Page and its renderer channel must retain the same physical agent" ); @@ -1828,12 +1758,15 @@ mod tests { None, ); - assert!( - context - .select_page_target_async("TID-pending-bg") - .await - .expect("pending background target should remain selectable") - ); + let handle = context + .web_contents_handle_for_target("TID-pending-bg") + .expect("pending background target should remain selectable"); + let completed = context + .start_select_web_contents(handle, &Default::default()) + .unwrap() + .wait() + .await; + context.finish_select_web_contents(completed).unwrap(); assert_eq!( context.runtime_slot_diagnostics_for_target(context.active_target_id().unwrap())["loadedPageAbsenceReason"], @@ -1845,18 +1778,10 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn select_first_background_target_prefers_first_loaded_target() { let mut ctx = TestContext::new(); - let first_loaded_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,first-loaded") - .await - .expect("first loaded page should load"); - let second_loaded_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,second-loaded") - .await - .expect("second loaded page should load"); - let mut context = BrowserContext::new("BC-activate-first".to_owned()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BC-activate-first".to_owned()); context.stage_background_target( "TID-empty".to_owned(), Some("SID-empty".to_owned()), @@ -1878,8 +1803,18 @@ mod tests { None, None, ); - context.replace_target_page_for_test("TID-first-loaded", Some(first_loaded_page)); - context.replace_target_page_for_test("TID-second-loaded", Some(second_loaded_page)); + ctx.conn.install_browser_context_fixture_for_test(context); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,first-loaded", + Some("SID-first-loaded"), + ) + .await; + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,second-loaded", + Some("SID-second-loaded"), + ) + .await; + let mut context = ctx.conn.browser_context.take().unwrap(); let first_attachment = context .background_targets() .nth(1) @@ -1896,9 +1831,17 @@ mod tests { .expect("second loaded background target should have an attachment"); let selected = context - .select_first_background_target_async() - .await + .background_targets() + .find(|target| context.target_has_loaded_page(target.target_id())) + .map(|target| target.target_id().to_owned()) .expect("loaded background target should be selectable"); + let handle = context.web_contents_handle_for_target(&selected).unwrap(); + let completed = context + .start_select_web_contents(handle, &Default::default()) + .unwrap() + .wait() + .await; + context.finish_select_web_contents(completed).unwrap(); assert_eq!(selected, "TID-first-loaded"); assert_eq!(context.active_target_id(), Some("TID-first-loaded")); @@ -1933,20 +1876,11 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn active_background_swap_moves_each_target_renderer_channel_with_its_page() { let mut ctx = TestContext::new(); - let active_page = ctx + let mut context = ctx .conn - .load_page_via_runtime_async("data:text/html,active route") - .await - .expect("active page should load"); - let background_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,background route") - .await - .expect("background page should load"); - let mut context = BrowserContext::new("BC-route-swap".to_owned()); + .new_browser_context_fixture_for_test("BC-route-swap".to_owned()); context.set_active_target_id("TID-active-route"); context.attach_active_session("SID-active-route".to_owned()); - context.set_loaded_page_async(active_page).await; context.stage_background_target( "TID-background-route".to_owned(), Some("SID-background-route".to_owned()), @@ -1954,7 +1888,18 @@ mod tests { None, None, ); - context.replace_target_page_for_test("TID-background-route", Some(background_page)); + ctx.conn.install_browser_context_fixture_for_test(context); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,active route", + Some("SID-active-route"), + ) + .await; + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,background route", + Some("SID-background-route"), + ) + .await; + let mut context = ctx.conn.browser_context.take().unwrap(); let active_attachment = context .active_page_target() .runtime_slot @@ -1965,12 +1910,17 @@ mod tests { .and_then(|target| target.runtime_slot().current_renderer_attachment()) .expect("background attachment"); - assert!( - context - .select_page_target_async("TID-background-route") - .await - .expect("target selection should succeed") - ); + let handle = context + .web_contents_handle_for_target("TID-background-route") + .unwrap(); + let completed = context + .start_select_web_contents(handle, &Default::default()) + .unwrap() + .wait() + .await; + context + .finish_select_web_contents(completed) + .expect("target selection should succeed"); assert_eq!( context @@ -2029,12 +1979,15 @@ mod tests { .runtime_slot .set_session_observation_cursor_at_counts_for_test(None, 4, 5); - assert!( - context - .select_page_target_async("TID-bg") - .await - .expect("target selection should not fail") - ); + let handle = context.web_contents_handle_for_target("TID-bg").unwrap(); + let completed = context + .start_select_web_contents(handle, &Default::default()) + .unwrap() + .wait() + .await; + context + .finish_select_web_contents(completed) + .expect("target selection should not fail"); assert_eq!(context.active_target_id(), Some("TID-bg")); assert!( diff --git a/moli-protocol/src/conn/state/browser_context/page_state/surfaces.rs b/moli-protocol/src/conn/state/browser_context/page_state/surfaces.rs index f71f16cd15..76ec6add47 100644 --- a/moli-protocol/src/conn/state/browser_context/page_state/surfaces.rs +++ b/moli-protocol/src/conn/state/browser_context/page_state/surfaces.rs @@ -4,22 +4,37 @@ use crate::conn::state::PageSurface; use crate::conn::{BrowserContext, DocumentStartScript, EmulatedViewportSurface, PageAgentHost}; #[cfg(test)] use moli_cookie_jar::{BrowserCookieFacadeContextOverrides, BrowserCookieFacadeOverrides}; +use moli_core::browser::WebContentsHandle; impl BrowserContext { pub(crate) fn navigator_overrides_for_target( &self, target_id: &str, + browser_globals: &crate::conn::BrowserGlobalOverrides, ) -> Option { let target = self.page_target(target_id)?; Some( - self.page_surface_for_state(target, self.is_active_target(target_id)) - .navigator_overrides(), + self.page_surface_for_state( + target, + self.is_active_target(target_id), + browser_globals.network_conditions, + browser_globals.geolocation.as_ref(), + ) + .navigator_overrides(), ) } - pub(crate) fn active_navigator_overrides(&self) -> moli_page_types::NavigatorOverrides { - self.page_surface_for_state(self.active_page_target(), true) - .navigator_overrides() + pub(crate) fn active_navigator_overrides( + &self, + browser_globals: &crate::conn::BrowserGlobalOverrides, + ) -> moli_page_types::NavigatorOverrides { + self.page_surface_for_state( + self.active_page_target(), + true, + browser_globals.network_conditions, + browser_globals.geolocation.as_ref(), + ) + .navigator_overrides() } pub(crate) fn document_activity_for_target( @@ -28,16 +43,16 @@ impl BrowserContext { ) -> Option { let target = self.page_target(target_id)?; Some( - self.page_surface_for_state(target, self.is_active_target(target_id)) + self.page_surface_for_state(target, self.is_active_target(target_id), None, None) .document_activity(), ) } pub(crate) fn active_document_activity(&self) -> moli_page_types::DocumentActivity { self.page_targets - .active(self.physical.selected_web_contents_id()) + .active(self.browser_context.selected_web_contents_id()) .map(|target| { - self.page_surface_for_state(target, true) + self.page_surface_for_state(target, true, None, None) .document_activity() }) .unwrap_or_default() @@ -51,16 +66,23 @@ impl BrowserContext { ) -> bool { if let Some(host) = self .page_targets - .active_mut(self.physical.selected_web_contents_id()) + .active_mut(self.browser_context.selected_web_contents_id()) { let state = host; if !mutate(&mut state.document_cookie_manager_surface) { return false; } - let surface = state.document_cookie_manager_surface.clone(); + let overrides = state + .document_cookie_manager_surface + .snapshot() + .policy + .overrides; let target_id = state.target_id().to_owned(); - if let Some(page) = self.loaded_page_for_target_mut(&target_id) { - surface.apply_to_page_async(page).await; + if let Some(document) = self.document_handle_for_target(&target_id) { + let _ = self + .browser_context + .apply_document_cookie_facade_overrides_for_test(document, Some(overrides)) + .await; } } else if !mutate(&mut self.default_document_cookie_manager_surface) { return false; @@ -73,27 +95,11 @@ impl BrowserContext { &self, ) -> BrowserContextCookieManagerSurfaceSnapshot { self.page_targets - .active(self.physical.selected_web_contents_id()) + .active(self.browser_context.selected_web_contents_id()) .map(|host| host.document_cookie_manager_surface.snapshot()) .unwrap_or_else(|| self.default_document_cookie_manager_surface.snapshot()) } - pub fn document_start_script_descriptors(&self) -> Vec { - let mut scripts = Vec::new(); - scripts.extend(self.default_document_start_script_descriptors()); - let target_id = self.active_target_id(); - scripts.extend( - self.active_page_target() - .owner_state - .document_start_scripts - .iter() - .map(|(identifier, script)| { - Self::target_document_start_script_descriptor(target_id, identifier, script) - }), - ); - scripts - } - pub(crate) fn default_document_start_script_descriptors(&self) -> Vec { self.default_document_start_scripts .iter() @@ -201,33 +207,38 @@ impl BrowserContext { pub(crate) fn merged_extra_headers_for_target_policy( &self, + global_headers: &moli_fetch::RequestHeaders, target_headers: &moli_fetch::RequestHeaders, ) -> moli_fetch::RequestHeaders { merge_extra_header_layers(&[ - &self.global_extra_headers, + global_headers, &self.network_policy().extra_headers, target_headers, ]) } - pub fn effective_extra_headers(&self) -> moli_fetch::RequestHeaders { + pub fn effective_extra_headers( + &self, + global_headers: &moli_fetch::RequestHeaders, + ) -> moli_fetch::RequestHeaders { let target_headers = self .page_targets - .active(self.physical.selected_web_contents_id()) + .active(self.browser_context.selected_web_contents_id()) .map(|target| self.effective_policy_for_target(target.target_id())) .unwrap_or_default(); - self.merged_extra_headers_for_target_policy(target_headers.extra_headers()) + self.merged_extra_headers_for_target_policy(global_headers, target_headers.extra_headers()) } pub(crate) fn effective_extra_headers_for_target( &self, target_id: &str, + global_headers: &moli_fetch::RequestHeaders, ) -> moli_fetch::RequestHeaders { let target_headers = self .page_target(target_id) .map(|target| self.effective_policy_for_target(target.target_id())) .unwrap_or_default(); - self.merged_extra_headers_for_target_policy(target_headers.extra_headers()) + self.merged_extra_headers_for_target_policy(global_headers, target_headers.extra_headers()) } pub fn viewport_width(&self) -> u32 { @@ -273,72 +284,62 @@ impl BrowserContext { } pub fn document_has_focus(&self) -> bool { - self.page_surface_for_state(self.active_page_target(), true) + self.page_surface_for_state(self.active_page_target(), true, None, None) .document_has_focus() } pub fn document_hidden(&self) -> bool { - self.page_surface_for_state(self.active_page_target(), true) + self.page_surface_for_state(self.active_page_target(), true, None, None) .document_hidden() } pub fn document_visibility_state(&self) -> &'static str { - self.page_surface_for_state(self.active_page_target(), true) + self.page_surface_for_state(self.active_page_target(), true, None, None) .document_visibility_state() } - // Context default resolution stays in this residence until Commit 7; - // source generation itself only reads the embedded Browser object. - fn page_surface_for_state(&self, state: &PageAgentHost, foreground: bool) -> PageSurface { + fn page_surface_for_state( + &self, + state: &PageAgentHost, + foreground: bool, + global_network_conditions: Option, + global_geolocation: Option<&crate::conn::EmulatedGeolocationOverrideState>, + ) -> PageSurface { let mut surface = self - .web_contents_for_target(state.target_id()) - .expect("live WebContents") - .page_surface( + .page_surface_for_web_contents( + WebContentsHandle::new(self.browser_context_id(), state.web_contents_id()), foreground, - self.emulation_defaults() - .network_conditions - .or(self.global_network_conditions), - self.emulation_defaults() - .geolocation - .as_ref() - .or(self.global_geolocation_override.as_ref()), - self.emulation_defaults().device_metrics.as_ref(), - ); + global_network_conditions, + global_geolocation, + ) + .expect("live WebContents"); surface.navigator_queries = state.devtools_sessions.navigator_emulation.effective(); surface } - pub(crate) async fn apply_background_target_surface_overrides_async( - &mut self, - target_id: &str, - ) -> anyhow::Result { - let overrides = self - .navigator_overrides_for_target(target_id) - .expect("resolved background target retains navigator state"); - let activity = self - .document_activity_for_target(target_id) - .expect("resolved background target retains document activity"); - let Some(page) = self.loaded_page_for_target_mut(target_id) else { - return Ok(false); - }; - page.set_navigator_overrides_async(&overrides).await?; - page.set_document_activity_async(activity).await?; - Ok(true) + pub(in crate::conn) fn navigator_queries_for_web_contents( + &self, + handle: WebContentsHandle, + ) -> moli_page_types::NavigatorQueryOverrides { + self.page_targets + .get_for_web_contents(handle.id()) + .map(|target| target.devtools_sessions.navigator_emulation.effective()) + .unwrap_or_default() } - pub(crate) async fn apply_surface_overrides_to_loaded_page_async( - &mut self, - ) -> anyhow::Result<()> { - let overrides = self.active_navigator_overrides(); - let activity = self.active_document_activity(); - let Some(target_id) = self.active_target_id_owned() else { - return Ok(()); - }; - let Some(page) = self.loaded_page_for_target_mut(&target_id) else { - return Ok(()); - }; - page.set_navigator_overrides_async(&overrides).await?; - page.set_document_activity_async(activity).await + pub(in crate::conn::state::browser_context) fn page_surface_for_web_contents( + &self, + handle: WebContentsHandle, + foreground: bool, + global_network_conditions: Option, + global_geolocation: Option<&crate::conn::EmulatedGeolocationOverrideState>, + ) -> Result { + self.browser_context.page_surface_for_web_contents( + handle, + foreground, + global_network_conditions, + global_geolocation, + ) } #[cfg(test)] @@ -399,7 +400,7 @@ impl BrowserContext { } } -use crate::conn::state::web_contents::merge_extra_header_layers; +use moli_core::browser::web_contents::merge_extra_header_layers; #[cfg(test)] mod tests { @@ -414,30 +415,57 @@ mod tests { "TID-background-window", crate::conn::EmulationPolicyChange::FocusEnabled(true), ); - context.set_target_window_surface_state( - "TID-background-window", - WindowSurfaceState::Minimized, - ); - let id = context.selected_web_contents_id().unwrap(); + let handle = context + .web_contents_handle_for_target("TID-background-window") + .unwrap(); + context + .update_web_contents_window_surface( + handle, + Some(WindowSurfaceState::Minimized), + None, + None, + None, + None, + ) + .unwrap(); drop( context .page_targets .remove("TID-background-window") .unwrap(), ); - let contents = context.physical.web_contents.get_mut(&id).unwrap(); - let minimized = contents.page_surface(false, None, None, None); + let minimized = context + .browser_context + .page_surface_for_web_contents(handle, false, None, None) + .unwrap(); assert!(minimized.document_has_focus()); assert!( minimized.document_hidden(), "focus emulation must not unminimize a window" ); - contents.window.surface.state = WindowSurfaceState::Fullscreen; - let fullscreen = contents.page_surface(false, None, None, None); + context + .browser_context + .update_web_contents_window_surface( + handle, + Some(WindowSurfaceState::Fullscreen), + None, + None, + None, + None, + ) + .unwrap(); + let fullscreen = context + .browser_context + .page_surface_for_web_contents(handle, false, None, None) + .unwrap(); assert!(!fullscreen.document_hidden()); assert_eq!( - contents.window.surface.state, + context + .browser_context + .web_contents_window_surface(handle) + .unwrap() + .state, WindowSurfaceState::Fullscreen ); } diff --git a/moli-protocol/src/conn/state/browser_context/permissions.rs b/moli-protocol/src/conn/state/browser_context/permissions.rs index 7711a5bcef..ae1c2e98e5 100644 --- a/moli-protocol/src/conn/state/browser_context/permissions.rs +++ b/moli-protocol/src/conn/state/browser_context/permissions.rs @@ -1,157 +1,39 @@ -use super::{BrowserContext, physical::BrowserContext as PhysicalBrowserContext}; +use super::BrowserContext; use moli_core::{ - browser::{BrowserContextId, DocumentId, PermissionDefaults, WebContentsId}, - page::{CompletedPageCommand, PendingPageCommand, PermissionOverrideRegistration}, + browser::{CompletedContextPermissionUpdate, PendingContextPermissionUpdate}, + page::PermissionOverrideRegistration, }; -#[cfg(test)] -mod tests; - -pub(crate) struct PendingContextPermissionUpdate { - context_id: BrowserContextId, - pages: Vec, -} - -struct PendingPermissionPageUpdate { - contents_id: WebContentsId, - document_id: DocumentId, - pending: PendingPageCommand, -} - -pub(crate) struct CompletedContextPermissionUpdate { - context_id: BrowserContextId, - pages: Vec, -} - -struct CompletedPermissionPageUpdate { - contents_id: WebContentsId, - document_id: DocumentId, - completed: Result, -} - -impl PendingContextPermissionUpdate { - pub(crate) async fn wait(self) -> CompletedContextPermissionUpdate { - let mut pages = Vec::with_capacity(self.pages.len()); - for page in self.pages { - pages.push(CompletedPermissionPageUpdate { - contents_id: page.contents_id, - document_id: page.document_id, - completed: page.pending.wait().await.map_err(|error| error.to_string()), - }); - } - CompletedContextPermissionUpdate { - context_id: self.context_id, - pages, - } - } -} - -impl CompletedContextPermissionUpdate { - pub(crate) fn context_id(&self) -> BrowserContextId { - self.context_id - } -} - impl BrowserContext { pub(crate) fn set_permission_override( &mut self, - defaults: &mut PermissionDefaults, registration: PermissionOverrideRegistration, - ) { - self.physical - .permission_overrides - .set(defaults, registration); + ) -> Result<(), String> { + self.browser_context.set_permission_override(registration) } pub(crate) fn clear_permission_overrides(&mut self) { - self.physical.permission_overrides.clear(); + self.browser_context.clear_permission_overrides(); } pub(crate) fn permission_override_count(&self) -> usize { - self.physical.permission_overrides.override_count() + self.browser_context.permission_override_count() } - pub(crate) fn permission_snapshot( - &self, - defaults: &PermissionDefaults, - ) -> Vec { - self.physical.permission_overrides.snapshot(defaults) + pub(crate) fn permission_snapshot(&self) -> Vec { + self.browser_context.permission_snapshot() } pub(crate) fn start_permission_update( &self, - defaults: &PermissionDefaults, ) -> Result, String> { - self.physical.start_permission_update(defaults) + self.browser_context.start_permission_update() } pub(crate) fn finish_permission_update( &mut self, completed: CompletedContextPermissionUpdate, ) -> Result<(), String> { - self.physical.finish_permission_update(completed) - } -} - -impl PhysicalBrowserContext { - fn start_permission_update( - &self, - defaults: &PermissionDefaults, - ) -> Result, String> { - let mut pages = Vec::new(); - let overrides = self.permission_overrides.snapshot(defaults); - for (contents_id, contents) in &self.web_contents { - let Some(document) = &contents.main_frame.current_document else { - continue; - }; - pages.push(PendingPermissionPageUpdate { - contents_id: *contents_id, - document_id: document.id, - pending: document - .page - .start_set_permission_overrides(&overrides) - .map_err(|error| { - format!("failed to update page permission overrides: {error}") - })?, - }); - } - Ok( - (!pages.is_empty()).then_some(PendingContextPermissionUpdate { - context_id: self.id, - pages, - }), - ) - } - - fn finish_permission_update( - &mut self, - completed: CompletedContextPermissionUpdate, - ) -> Result<(), String> { - if completed.context_id != self.id { - return Err("Permission update belongs to a different BrowserContext".into()); - } - for page in completed.pages { - let completion = page.completed?; - let document = self - .web_contents - .get_mut(&page.contents_id) - .and_then(|contents| contents.main_frame.current_document.as_mut()) - .ok_or("NoDocumentLoaded")?; - if document.id == page.document_id { - document - .page - .finish_set_permission_overrides(completion) - .map_err(|error| error.to_string())?; - } else { - // The policy is Context-owned and will be inherited by the - // replacement. Keep a frozen successful acknowledgement, but - // never install the outgoing Document's snapshot into it. - completion - .into_unit_page_command_turn() - .map(drop) - .map_err(|error| error.to_string())?; - } - } - Ok(()) + self.browser_context.finish_permission_update(completed) } } diff --git a/moli-protocol/src/conn/state/browser_context/permissions/tests.rs b/moli-protocol/src/conn/state/browser_context/permissions/tests.rs deleted file mode 100644 index 205c6f2309..0000000000 --- a/moli-protocol/src/conn/state/browser_context/permissions/tests.rs +++ /dev/null @@ -1,105 +0,0 @@ -use super::*; -use moli_core::runtime::{Browser, BrowserConfig}; -use serde_json::json; - -fn rule(setting: &str) -> PermissionOverrideRegistration { - PermissionOverrideRegistration { - permission: json!({"name": "geolocation"}), - setting: setting.into(), - origin: None, - embedded_origin: None, - } -} - -#[tokio::test] -async fn permission_refresh_visits_all_physical_documents_after_projection_drop() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let mut defaults = PermissionDefaults::default(); - defaults.set(rule("denied")); - let mut physical = { - let mut projection = BrowserContext::new("BID-permission-owner".into()); - for target in ["first", "second"] { - projection.set_active_target_id(target); - projection.attach_active_session(format!("SID-{target}")); - let page = browser - .fetch(&format!("data:text/html,{target}")) - .await - .unwrap(); - assert!(projection.replace_loaded_page(Some(page)).is_none()); - } - projection.set_permission_override(&mut defaults, rule("granted")); - // All Target/session projections and their inspection bindings drop. - // The Browser operation must still reach both owned Documents. - projection.physical - }; - assert_eq!(physical.permission_overrides.override_count(), 1); - let ids = physical.web_contents.keys().copied().collect::>(); - assert_eq!(ids.len(), 2); - for expected in ["granted", "denied"] { - let pending = physical - .start_permission_update(&defaults) - .unwrap() - .unwrap(); - assert_eq!(pending.pages.len(), 2); - assert!(physical.select_web_contents(ids[0])); - let completed = pending.wait().await; - assert!(physical.select_web_contents(ids[1])); - physical.finish_permission_update(completed).unwrap(); - for (contents, title) in physical.web_contents.values_mut().zip(["first", "second"]) { - let page = &mut contents.main_frame.current_document.as_mut().unwrap().page; - assert_eq!(page.document_title(), title); - let result = page.evaluate_runtime_expression_with_await_async( - "navigator.permissions.query({name:'geolocation'}).then(status => status.state)", - true, - ).await.unwrap(); - assert_eq!(result, json!({"type": "string", "value": expected})); - } - physical.permission_overrides.clear(); - } - assert_eq!(defaults.snapshot(), vec![rule("denied")]); -} - -#[tokio::test] -async fn permission_completion_cannot_retarget_a_reused_devtools_target_id() { - let browser = Browser::new(BrowserConfig::default()).unwrap(); - let mut context = BrowserContext::new("BID-permission-close".into()); - context.set_active_target_id("same-target"); - context.replace_loaded_page(Some( - browser - .fetch("data:text/html,first") - .await - .unwrap(), - )); - let old_id = context.selected_web_contents_id().unwrap(); - let mut defaults = PermissionDefaults::default(); - context.set_permission_override(&mut defaults, rule("granted")); - let completed = context - .start_permission_update(&defaults) - .unwrap() - .unwrap() - .wait() - .await; - - let (projection, closing) = context.take_page_target_for_close("same-target").unwrap(); - drop(projection); - closing.close_async().await; - context.set_active_target_id("same-target"); - context.replace_loaded_page(Some( - browser - .fetch("data:text/html,replacement") - .await - .unwrap(), - )); - assert_ne!(context.selected_web_contents_id(), Some(old_id)); - assert_eq!( - context.finish_permission_update(completed), - Err("NoDocumentLoaded".into()) - ); - assert_eq!( - context - .loaded_page_for_target("same-target") - .unwrap() - .document_title(), - "replacement" - ); -} diff --git a/moli-protocol/src/conn/state/browser_context/physical.rs b/moli-protocol/src/conn/state/browser_context/physical.rs deleted file mode 100644 index 72b6181883..0000000000 --- a/moli-protocol/src/conn/state/browser_context/physical.rs +++ /dev/null @@ -1,399 +0,0 @@ -use std::path::PathBuf; - -use indexmap::IndexMap; -use moli_browser_profile::BrowserIdentityProfile; -use moli_core::{ - browser::{ - BrowserContextId, DownloadManager, DownloadPolicy, PermissionOverrides, WebContentsId, - }, - runtime::{ - NavigationEngine, NavigationRuntimeConfig, RendererBrowserContextRuntime, - RendererBrowserContextRuntimeOwner, RendererBrowserContextRuntimeOwnerAccess, - }, -}; - -use super::super::emulation::{ - EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, EmulatedNetworkConditions, -}; -use super::super::web_contents::{ClosingWebContents, WebContents}; -use super::{ - BrowserContextStoragePartitionHandles, - storage_partition::{StoragePartition, StoragePartitionKind}, -}; - -/// Physical context ownership, embedded in the migration wrapper until Commit 24b. -/// No projection identity, output transport or session state belongs here. -pub(super) struct BrowserContext { - pub(super) id: BrowserContextId, - pub(super) page_navigation_runtime_config: Option, - pub(super) network_policy: ContextNetworkPolicy, - pub(super) environment_owner: moli_core::ProcessEnvironmentOwner, - pub(super) emulation_defaults: ContextEmulationDefaults, - pub(super) browser_identity_override: Option, - pub(super) permission_overrides: PermissionOverrides, - pub(super) download_policy: Option, - pub(super) downloads: DownloadManager, - // The Browser collection and its only selector have the same lifetime. - // Keep insertion order when choosing a replacement foreground page. - pub(super) web_contents: IndexMap, - selected_web_contents: Option, - // Drop Documents/engines before the runtime root and its storage handles. - renderer_runtime_owner: Option, - pub(super) storage_partition: StoragePartition, -} - -impl BrowserContext { - pub(super) fn inherited_document_policy( - &self, - fetch_config: moli_fetch::FetchConfig, - defaults: &moli_core::browser::PermissionDefaults, - global_headers: &moli_fetch::RequestHeaders, - global_network_conditions: Option, - global_geolocation_override: Option<&EmulatedGeolocationOverrideState>, - ) -> super::super::web_contents::InheritedDocumentPolicy { - let mut policy = - self.inherited_resource_policy(fetch_config, global_headers, global_network_conditions); - policy.permissions = self.permission_overrides.snapshot(defaults); - policy.emulation.geolocation = policy - .emulation - .geolocation - .or_else(|| global_geolocation_override.cloned()); - policy - } - - pub(super) fn inherited_resource_policy( - &self, - mut fetch_config: moli_fetch::FetchConfig, - global_headers: &moli_fetch::RequestHeaders, - global_network_conditions: Option, - ) -> super::super::web_contents::InheritedDocumentPolicy { - if let Some(identity) = &self.browser_identity_override { - fetch_config.set_browser_identity(identity.clone()); - } - if let Some(proxy) = &self.network_policy.http_proxy { - fetch_config.set_http_proxy(Some(proxy.clone())); - } - if let Some(no_proxy) = &self.network_policy.http_no_proxy { - fetch_config.set_http_no_proxy(Some(no_proxy.clone())); - } - if let Some(verify) = self.network_policy.tls_verify_host { - fetch_config.set_tls_verify_host(verify); - } - let mut emulation = self.emulation_defaults.clone(); - emulation.network_conditions = emulation.network_conditions.or(global_network_conditions); - super::super::web_contents::InheritedDocumentPolicy { - fetch_config, - extra_headers: super::super::web_contents::merge_extra_header_layers(&[ - global_headers, - &self.network_policy.extra_headers, - ]), - emulation, - permissions: Vec::new(), - selected_web_contents: self.selected_web_contents_id(), - navigator_queries: Default::default(), - storage: self.storage_partition.handles.clone(), - } - } - - pub(super) fn new( - handles: BrowserContextStoragePartitionHandles, - kind: StoragePartitionKind, - http_cache_root: Option, - http_cache_max_bytes: Option, - ) -> Self { - Self { - id: BrowserContextId::allocate(), - page_navigation_runtime_config: None, - network_policy: ContextNetworkPolicy::default(), - environment_owner: Default::default(), - emulation_defaults: ContextEmulationDefaults::default(), - browser_identity_override: None, - permission_overrides: PermissionOverrides::default(), - download_policy: None, - downloads: DownloadManager::default(), - web_contents: IndexMap::new(), - selected_web_contents: None, - renderer_runtime_owner: Some(RendererBrowserContextRuntime::new()), - storage_partition: StoragePartition::new( - handles, - kind, - http_cache_root, - http_cache_max_bytes, - ), - } - } - - pub(super) fn selected_web_contents_id(&self) -> Option { - self.selected_web_contents - } - - pub(super) fn select_web_contents(&mut self, id: WebContentsId) -> bool { - if !self.web_contents.contains_key(&id) { - return false; - } - self.selected_web_contents = Some(id); - true - } - - pub(super) fn close_web_contents(&mut self, id: WebContentsId) -> Option { - let removed = self.web_contents.shift_remove(&id)?; - if self.selected_web_contents == Some(id) { - self.selected_web_contents = None; - } - for contents in self.web_contents.values_mut() { - if contents - .window - .opener - .is_some_and(|opener| opener.web_contents_id == id) - { - contents.window.opener = None; - } - } - Some(removed.begin_close()) - } - - pub(super) fn close_all_web_contents(&mut self) -> Vec { - self.selected_web_contents = None; - std::mem::take(&mut self.web_contents) - .into_values() - .map(WebContents::begin_close) - .collect() - } - - pub(super) fn new_page_navigation_engine( - &self, - config: NavigationRuntimeConfig, - ) -> NavigationEngine { - NavigationEngine::new_with_runtime_config_and_browser_context_access( - config, - self.renderer_runtime_owner_access(), - ) - .expect("live BrowserContext owner must accept a page engine") - } - - pub(super) fn renderer_runtime(&self) -> RendererBrowserContextRuntime { - self.renderer_runtime_owner - .as_ref() - .expect("BrowserContext renderer owner was already taken for teardown") - .handle() - } - - pub(super) fn renderer_runtime_owner_access(&self) -> RendererBrowserContextRuntimeOwnerAccess { - self.renderer_runtime_owner - .as_ref() - .expect("BrowserContext renderer owner was already taken for teardown") - .owner_access() - } - - pub(super) fn take_renderer_runtime_owner_for_teardown( - &mut self, - ) -> Option { - self.renderer_runtime_owner.take() - } -} - -/// Context-scoped request defaults, with no frontend or session attribution. -/// A missing value inherits the process policy; an empty bypass list does not. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub(crate) struct ContextNetworkPolicy { - pub(crate) http_proxy: Option, - pub(crate) http_no_proxy: Option, - pub(crate) tls_verify_host: Option, - pub(crate) extra_headers: moli_fetch::RequestHeaders, -} - -/// Installed context defaults; inherited process values are not copied here. -#[derive(Debug, Clone, Default, PartialEq)] -pub(crate) struct ContextEmulationDefaults { - pub(crate) network_conditions: Option, - pub(crate) geolocation: Option, - pub(crate) device_metrics: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn resource_maintenance_never_adopts_a_peers_transport_or_policy() { - let mut context = BrowserContext::new( - BrowserContextStoragePartitionHandles::memory(), - StoragePartitionKind::Ephemeral, - None, - None, - ); - let mut ids = Vec::new(); - for (agent, verify) in [("Native/first", false), ("Native/peer", true)] { - let mut contents = WebContents::default(); - contents.install_navigation_engine( - context.new_page_navigation_engine(NavigationRuntimeConfig::default()), - ); - contents.browser_identity_override = Some(BrowserIdentityProfile::new(agent, "en")); - contents.tls_verify_host_override = Some(verify); - ids.push(contents.id()); - context.web_contents.insert(contents.id(), contents); - } - let inherited = context.inherited_resource_policy( - moli_fetch::FetchConfig::default(), - &Default::default(), - None, - ); - let first = context - .web_contents - .get_mut(&ids[0]) - .unwrap() - .ensure_resource_request_client(&inherited) - .unwrap(); - let peer = context - .web_contents - .get_mut(&ids[1]) - .unwrap() - .ensure_resource_request_client(&inherited) - .unwrap(); - assert!(!first.shares_resource_runtime_with(&peer)); - assert!(std::sync::Arc::ptr_eq( - &first.cookie_store(), - &peer.cookie_store() - )); - let contents = context.web_contents.get_mut(&ids[0]).unwrap(); - contents.invalidate_resource_runtime(); - assert!( - contents - .start_resource_runtime_rebuild(&inherited) - .unwrap() - .is_none() - ); - let rebuilt = contents.ensure_resource_request_client(&inherited).unwrap(); - assert!(!rebuilt.shares_resource_runtime_with(&peer)); - assert!(rebuilt.shares_page_network_policy_with(&first)); - assert!( - rebuilt - .browser_resource_runtime() - .matches_fetch_config(contents.navigation_fetch_config().unwrap()) - ); - assert!( - !contents - .navigation_fetch_config() - .unwrap() - .tls_verify_host() - ); - assert_eq!( - rebuilt - .browser_resource_runtime() - .browser_identity() - .user_agent(), - "Native/first" - ); - let contents = context.web_contents.get(&ids[1]).unwrap(); - assert!( - contents - .navigation_fetch_config() - .unwrap() - .tls_verify_host() - ); - assert_eq!( - peer.browser_resource_runtime() - .browser_identity() - .user_agent(), - "Native/peer" - ); - assert!( - peer.browser_resource_runtime() - .matches_fetch_config(contents.navigation_fetch_config().unwrap()) - ); - } - - #[tokio::test] - async fn document_policy_capture_needs_no_projection_or_current_document() { - let mut context = BrowserContext::new( - BrowserContextStoragePartitionHandles::memory(), - StoragePartitionKind::Ephemeral, - None, - None, - ); - context.network_policy.extra_headers = vec![("X-Policy".into(), "context".into())].into(); - context.network_policy.tls_verify_host = Some(false); - context.environment_owner.set_locale(Some("fr-FR")).unwrap(); - let mut contents = WebContents::default(); - contents.install_navigation_engine( - context.new_page_navigation_engine(NavigationRuntimeConfig::default()), - ); - contents.network_request_policy.extra_headers = - vec![("X-Policy".into(), "page".into())].into(); - contents.emulation_policy.script_execution_disabled = true; - contents.emulation_policy.max_touch_points = 4; - assert!( - contents - .start_fetch_interception_update(true, None) - .unwrap() - .is_none() - ); - let id = contents.id(); - context.web_contents.insert(id, contents); - let geolocation = - EmulatedGeolocationOverrideState::Position(crate::conn::EmulatedGeolocationOverride { - latitude: 48.85837, - longitude: 2.294481, - accuracy: 7.0, - altitude: None, - altitude_accuracy: None, - heading: None, - speed: None, - }); - let inherited = context.inherited_document_policy( - moli_fetch::FetchConfig::default(), - &moli_core::browser::PermissionDefaults::default(), - &vec![ - ("X-Global".into(), "global".into()), - ("X-Policy".into(), "global".into()), - ] - .into(), - Some(EmulatedNetworkConditions::offline()), - Some(&geolocation), - ); - let contents = context.web_contents.get_mut(&id).unwrap(); - let policy = contents - .capture_document_policy(inherited, &url::Url::parse("about:blank").unwrap()) - .unwrap(); - assert_eq!( - policy.extra_http_headers.to_byte_strings(), - [ - ("X-Global".into(), "global".into()), - ("X-Policy".into(), "page".into()) - ] - ); - assert_eq!(context.environment_owner.locale().as_deref(), Some("fr-FR")); - assert!(policy.network_offline); - assert_eq!(policy.navigator_overrides.online, Some(false)); - assert_eq!(policy.navigator_overrides.max_touch_points, 4); - assert_eq!( - policy - .navigator_overrides - .geolocation - .as_ref() - .map(|position| position.latitude), - Some(48.85837), - ); - assert!(policy.script_execution_disabled); - assert!(policy.fetch_subresource_interception_enabled); - assert!( - !contents - .navigation_engine_for_test() - .unwrap() - .fetch_config() - .tls_verify_host() - ); - assert!(contents.main_frame.current_document.is_none()); - assert!( - contents - .start_fetch_interception_update(false, None) - .unwrap() - .is_none() - ); - assert_eq!(contents.fetch_subresource_interception(), (false, None)); - assert!( - policy.fetch_subresource_interception_enabled, - "capture is a value, not a live registration view" - ); - } -} diff --git a/moli-protocol/src/conn/state/browser_context/resource_runtime.rs b/moli-protocol/src/conn/state/browser_context/resource_runtime.rs deleted file mode 100644 index c78559f2ba..0000000000 --- a/moli-protocol/src/conn/state/browser_context/resource_runtime.rs +++ /dev/null @@ -1,130 +0,0 @@ -use super::BrowserContext; -use crate::conn::state::web_contents::WebContents; -use moli_core::{ - network::ResourceRequestClient, - page::{CompletedPageCommand, PendingPageCommand}, -}; -use moli_fetch::FetchConfig; - -impl BrowserContext { - pub(in crate::conn) fn target_has_navigation_engine(&self, target: &str) -> bool { - self.web_contents_for_target(target) - .is_some_and(WebContents::has_navigation_engine) - } - - pub(in crate::conn) fn page_navigation_fetch_config( - &self, - target: &str, - ) -> Option<&FetchConfig> { - self.web_contents_for_target(target)? - .navigation_fetch_config() - } - - pub(in crate::conn) fn page_navigation_layout_policy( - &self, - target: &str, - ) -> Option { - self.web_contents_for_target(target)? - .navigation_layout_policy() - } - - pub(in crate::conn) fn page_navigation_renderer_owner_id(&self, target: &str) -> Option { - self.web_contents_for_target(target)? - .navigation_renderer_owner_id() - } - - pub(in crate::conn) fn page_navigation_diagnostics( - &self, - target: &str, - ) -> Option { - self.web_contents_for_target(target)? - .navigation_diagnostics() - } - - pub(in crate::conn) fn invalidate_selected_resource_runtime(&mut self) { - if let Some(contents) = self - .physical - .selected_web_contents_id() - .and_then(|id| self.physical.web_contents.get_mut(&id)) - { - contents.invalidate_resource_runtime(); - } - } - - pub(in crate::conn) fn configure_selected_navigation_policy( - &mut self, - defaults: FetchConfig, - ) -> Result<(), String> { - let inherited = self.physical.inherited_resource_policy( - defaults, - &self.global_extra_headers, - self.global_network_conditions, - ); - let Some(contents) = self - .physical - .selected_web_contents_id() - .and_then(|id| self.physical.web_contents.get_mut(&id)) - else { - return Ok(()); - }; - contents - .configure_navigation_engine_policy(&inherited) - .map(drop) - } - - pub(super) fn ensure_target_resource_request_client( - &mut self, - target: &str, - defaults: FetchConfig, - ) -> Result { - let inherited = self.physical.inherited_resource_policy( - defaults, - &self.global_extra_headers, - self.global_network_conditions, - ); - self.web_contents_for_target_mut(target) - .ok_or("WebContents unavailable")? - .ensure_resource_request_client(&inherited) - } - - #[cfg(test)] - pub(in crate::conn) fn resource_request_client_for_test( - &mut self, - target: &str, - defaults: FetchConfig, - ) -> Result { - self.ensure_target_resource_request_client(target, defaults) - } - - pub(in crate::conn) fn start_target_resource_runtime_rebuild( - &mut self, - target: &str, - defaults: FetchConfig, - ) -> Result, String> { - let inherited = self.physical.inherited_resource_policy( - defaults, - &self.global_extra_headers, - self.global_network_conditions, - ); - self.web_contents_for_target_mut(target) - .ok_or("WebContents unavailable")? - .start_resource_runtime_rebuild(&inherited) - } - - pub(crate) fn finish_target_resource_runtime_update( - &mut self, - target: &str, - completion: CompletedPageCommand, - ) -> Result<(), String> { - if let Some(contents) = self.web_contents_for_target_mut(target) { - return contents.finish_resource_runtime_update(completion); - } - Self::finish_unobserved_resource_runtime_update(completion) - } - - pub(crate) fn finish_unobserved_resource_runtime_update( - completion: CompletedPageCommand, - ) -> Result<(), String> { - WebContents::finish_unobserved_resource_runtime_update(completion) - } -} diff --git a/moli-protocol/src/conn/state/browser_context/runtime_slot.rs b/moli-protocol/src/conn/state/browser_context/runtime_slot.rs index ebb50d664c..d9e48c98f4 100644 --- a/moli-protocol/src/conn/state/browser_context/runtime_slot.rs +++ b/moli-protocol/src/conn/state/browser_context/runtime_slot.rs @@ -1,7 +1,7 @@ use super::BrowserContext; -use moli_core::browser::BrowserSequence; +use moli_core::browser::{BrowserSequence, WebContentsHandle}; use moli_core::page::{ - Page, RendererAgentAttachmentId, RendererDocumentLifecycleIdentity, + RendererAgentAttachmentId, RendererDocumentLifecycleIdentity, RendererRuntimeInspectorMessageBatch, ScriptNetworkOutputItem, ScriptObservableOutputItem, SubresourceNetworkRequestHandle, }; @@ -386,26 +386,6 @@ impl TargetRuntimeSlot { } } - #[cfg(test)] - fn ensure_renderer_attachment_for_replacement( - &mut self, - document: DocumentId, - agent_token: moli_core::page::RendererDevToolsAgentToken, - endpoint: moli_renderer_v8::RendererInspectionEndpoint, - ) { - if !self - .devtools_renderer_channel - .current() - .is_some_and(|attachment| { - attachment.agent_token() == agent_token && attachment.document() == document - }) - { - self.devtools_renderer_channel - .attach_current(document, BrowserSequence::allocate(), endpoint) - .expect("a loaded page cannot be installed into a closed renderer channel"); - } - } - pub(crate) fn renderer_subresources_are_idle(&self) -> bool { // The armed network-idle lifecycle binding belongs to the current // loader. A predecessor queue is retained only to deliver its final @@ -924,39 +904,6 @@ impl BrowserContext { TargetPageAbsenceReason::TestFixture => None, } } - #[cfg(test)] - pub(super) fn replace_loaded_page_for_target( - &mut self, - target_id: &str, - page: Option, - ) -> Option { - let inspection = page.as_ref().map(|page| { - ( - page.renderer_devtools_agent_token(), - page.renderer_inspection_endpoint(), - ) - }); - let previous_document = self.target_document_id(target_id).zip( - self.loaded_page_for_target(target_id) - .map(RendererPageResidenceIdentity::from_page), - ); - let retiring = - self.begin_document_projection_replacement_for_target(target_id, previous_document); - let previous = self.replace_target_document(target_id, page); - if let Some((agent_token, endpoint)) = inspection { - let document = self - .target_document_id(target_id) - .expect("a replacement Page must install a Document"); - self.page_targets - .get_mut(target_id) - .expect("resolved target projection") - .runtime_slot - .ensure_renderer_attachment_for_replacement(document, agent_token, endpoint); - } - self.finish_document_projection_replacement_for_target(target_id, retiring); - previous - } - pub(super) fn begin_document_projection_replacement_for_target( &mut self, target_id: &str, @@ -1007,30 +954,27 @@ impl BrowserContext { } self.ingest_owner_page_observable_output_updates_for_target(target_id); } - pub(super) fn clear_loaded_page_with_reason_for_target( + pub(super) fn retire_loaded_document_with_reason_for_target( &mut self, target_id: &str, reason: TargetPageAbsenceReason, - ) -> Option { + ) -> Option { self.page_targets - .get_mut(target_id) - .expect("resolved target projection must remain live") + .get_mut(target_id)? .runtime_slot .javascript_dialog_scope .retire(); - let previous = self.replace_loaded_page_with_reason_for_target(target_id, None, reason); - self.page_targets - .get_mut(target_id) - .expect("resolved target projection must remain live") - .runtime_slot - .transition_renderer_channel_for_page_absence(reason); - self.page_targets - .get_mut(target_id) - .expect("resolved target projection must remain live") - .runtime_slot - .reset_document_output_state(); + let handle = self.web_contents_handle_for_target(target_id)?; + let has_physical_document = self.browser_context.has_loaded_document(handle); + self.reset_document_projection_for_target(target_id, false, reason); + let retiring = has_physical_document + .then(|| self.browser_context.retire_document(handle).ok()) + .flatten(); + let runtime = &mut self.page_targets.get_mut(target_id)?.runtime_slot; + runtime.transition_renderer_channel_for_page_absence(reason); + runtime.reset_document_output_state(); self.ingest_owner_page_observable_output_updates_for_target(target_id); - previous + retiring } pub(crate) fn mark_loaded_page_absent_for_target( &mut self, @@ -1054,9 +998,10 @@ impl BrowserContext { &self, target_id: &str, ) -> Option { - self.web_contents_for_target(target_id) - .expect("resolved WebContents must remain live") - .performance_metric_snapshot() + let handle = self.web_contents_handle_for_target(target_id)?; + self.browser_context + .performance_metric_snapshot(handle) + .ok()? } pub(crate) fn routes_current_renderer_page_owner_for_target( &self, @@ -1102,9 +1047,13 @@ impl BrowserContext { preferred_request_id: Option>, network_request_id_allocator: &mut ConnectionNetworkRequestIdAllocator, ) -> Option { - let current_renderer_page = self - .loaded_page_for_target(target_id) - .map(RendererPageResidenceIdentity::from_page); + let current_renderer_page = + self.document_handle_for_target(target_id) + .and_then(|document| { + self.browser_context + .document_renderer_residence(document) + .ok() + }); if let Some(binding) = self.renderer_document_lifecycle_binding_for_target(target_id) && binding.renderer_document_identity() == source_document && source_renderer_page.is_none_or(|page| Some(page) == current_renderer_page) @@ -1242,29 +1191,40 @@ impl BrowserContext { let Some(projection) = self.page_targets.get_mut(target_id) else { return false; }; - let page = self - .physical - .web_contents - .get(&projection.web_contents_id()) - .and_then(|contents| contents.main_frame.current_document.as_ref()); + let web_contents = projection.web_contents_id(); let queue = &mut projection.runtime_slot.observable_queue; - let Some(document) = page else { + let handle = WebContentsHandle::new(self.browser_context.id(), web_contents); + let Some(document) = self + .browser_context + .document_handle_for_web_contents(handle) + .ok() + .flatten() + else { queue.reset_output_queue(); return false; }; - queue.ingest_observable_output_snapshot( - document.page.script_execution().observable_output_items(), - ); + let Ok(snapshot) = self + .browser_context + .document_observable_output_snapshot(document) + else { + queue.reset_output_queue(); + return false; + }; + queue.ingest_observable_output_snapshot(&snapshot); true } + pub(crate) fn observe_renderer_page_state_for_target( &mut self, target_id: &str, snapshot: &std::sync::Arc, ) -> bool { - self.web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .observe_renderer_page_state(snapshot) + let Some(handle) = self.web_contents_handle_for_target(target_id) else { + return false; + }; + self.browser_context + .observe_renderer_page_state(handle, snapshot.clone()) + .unwrap_or(false) } } diff --git a/moli-protocol/src/conn/state/browser_context/session.rs b/moli-protocol/src/conn/state/browser_context/session.rs index a4be0a8551..bd5b7ba5b1 100644 --- a/moli-protocol/src/conn/state/browser_context/session.rs +++ b/moli-protocol/src/conn/state/browser_context/session.rs @@ -2,8 +2,8 @@ use super::BrowserContext; use crate::conn::state::devtools_session::DevToolsNetworkSessionState; use crate::conn::state::javascript_dialog::TargetJavaScriptDialogState; use crate::conn::state::page_agent_host::PageAgentHost; -use crate::conn::state::web_contents::NetworkRequestPolicy; use crate::domains::audits_output_state::TargetAuditsSessionState; +use moli_core::browser::web_contents::NetworkRequestPolicy; use moli_core::page::V8InspectorSessionState; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -72,10 +72,23 @@ impl PageAgentHost { } impl BrowserContext { + fn dismiss_javascript_dialog_projection_for_session( + &mut self, + target_id: &str, + session_key: &moli_page_types::DevToolsSessionKey, + ) { + let projections = + self.take_projected_javascript_dialogs_for_session(target_id, session_key); + self.dismiss_projected_javascript_dialogs(projections); + } + pub(crate) fn bypass_content_security_policy_for_target(&self, target_id: &str) -> bool { - self.web_contents_for_target(target_id) + let handle = self + .web_contents_handle_for_target(target_id) + .expect("resolved WebContents must remain live"); + self.browser_context + .web_contents_bypass_content_security_policy(handle) .expect("resolved WebContents must remain live") - .bypass_content_security_policy } // Value-only bridge until AgentHost/BrowserHandle install (Commits 14/22). @@ -86,9 +99,12 @@ impl BrowserContext { .expect("resolved target projection must remain live") .devtools_sessions .page_bypass_csp_enabled(); - self.web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .set_bypass_content_security_policy(bypass); + let handle = self + .web_contents_handle_for_target(target_id) + .expect("resolved WebContents must remain live"); + self.browser_context + .set_web_contents_bypass_content_security_policy(handle, bypass) + .expect("resolved WebContents must remain live"); } pub(crate) fn set_devtools_bypass_csp_enabled_for_target( @@ -112,7 +128,7 @@ impl BrowserContext { target_id: &str, session_key: &moli_page_types::DevToolsSessionKey, ) { - self.dismiss_devtools_javascript_dialogs_for_target(target_id, session_key); + self.dismiss_javascript_dialog_projection_for_session(target_id, session_key); let state = &mut self .page_targets .get_mut(target_id) @@ -145,13 +161,11 @@ impl BrowserContext { let Some(mut removed) = removed else { return false; }; - self.dismiss_javascript_dialog_projections_for_target( - target_id, - removed - .page_session_state - .javascript_dialog_state - .take_pending(), - ); + let projections = removed + .page_session_state + .javascript_dialog_state + .take_pending(); + self.dismiss_projected_javascript_dialogs(projections); self.install_effective_content_security_policy_for_target(target_id); true } @@ -161,20 +175,28 @@ impl BrowserContext { pub(crate) fn browser_identity_override_for_target( &self, target_id: &str, - ) -> Option<&moli_browser_profile::BrowserIdentityProfile> { - self.web_contents_for_target(target_id) + ) -> Option { + let handle = self + .web_contents_handle_for_target(target_id) + .expect("resolved WebContents must remain live"); + self.browser_context + .web_contents_browser_identity_override(handle) .expect("resolved WebContents must remain live") - .browser_identity_override - .as_ref() } pub(crate) fn effective_policy_for_target(&self, target_id: &str) -> EffectiveTargetPolicy { - let contents = &self - .web_contents_for_target(target_id) + let handle = self + .web_contents_handle_for_target(target_id) .expect("resolved WebContents must remain live"); EffectiveTargetPolicy { - network_request: contents.network_request_policy.clone(), - browser_identity_override: contents.browser_identity_override.clone(), + network_request: self + .browser_context + .web_contents_network_request_policy(handle) + .expect("resolved WebContents must remain live"), + browser_identity_override: self + .browser_context + .web_contents_browser_identity_override(handle) + .expect("resolved WebContents must remain live"), } } @@ -190,9 +212,12 @@ impl BrowserContext { let mut headers = projection.base_network_request_policy.extra_headers.clone(); overlay_extra_headers(&mut headers, &policy.extra_headers); policy.extra_headers = headers; - self.web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .set_network_request_policy(policy); + let handle = self + .web_contents_handle_for_target(target_id) + .expect("resolved WebContents must remain live"); + self.browser_context + .set_web_contents_network_request_policy(handle, policy) + .expect("resolved WebContents must remain live"); } pub(crate) fn set_base_cache_disabled_for_target(&mut self, target_id: &str, disabled: bool) { @@ -204,6 +229,17 @@ impl BrowserContext { self.install_effective_network_request_policy_for_target(target_id); } + pub(in crate::conn) fn apply_browser_cache_disabled(&mut self, disabled: bool) { + let target_ids = self + .page_targets + .iter() + .map(|target| target.target_id().to_owned()) + .collect::>(); + for target_id in target_ids { + self.set_base_cache_disabled_for_target(&target_id, disabled); + } + } + pub(crate) fn set_base_extra_headers_for_target( &mut self, target_id: &str, @@ -218,22 +254,31 @@ impl BrowserContext { } pub(crate) fn network_offline_for_target(&self, target_id: &str) -> bool { - self.web_contents_for_target(target_id) + let handle = self + .web_contents_handle_for_target(target_id) + .expect("resolved WebContents must remain live"); + self.browser_context + .web_contents_network_offline(handle) .expect("resolved WebContents must remain live") - .network_offline } // Value-only bridge until AgentHost/BrowserHandle install (Commits 14/22). pub(crate) fn set_network_offline_for_target(&mut self, target_id: &str, offline: bool) { - self.web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .set_network_offline(offline); + let handle = self + .web_contents_handle_for_target(target_id) + .expect("resolved WebContents must remain live"); + self.browser_context + .set_web_contents_network_offline(handle, offline) + .expect("resolved WebContents must remain live"); } pub(crate) fn tls_verify_host_override_for_target(&self, target_id: &str) -> Option { - self.web_contents_for_target(target_id) + let handle = self + .web_contents_handle_for_target(target_id) + .expect("resolved WebContents must remain live"); + self.browser_context + .web_contents_tls_verify_host_override(handle) .expect("resolved WebContents must remain live") - .tls_verify_host_override } // Value-only bridge until AgentHost/BrowserHandle install (Commits 14/22). @@ -242,9 +287,12 @@ impl BrowserContext { target_id: &str, enabled: Option, ) { - self.web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .set_tls_verify_host_override(enabled); + let handle = self + .web_contents_handle_for_target(target_id) + .expect("resolved WebContents must remain live"); + self.browser_context + .set_web_contents_tls_verify_host_override(handle, enabled) + .expect("resolved WebContents must remain live"); } // Like request policy, this value-only bridge is replaced by the typed @@ -263,9 +311,12 @@ impl BrowserContext { .base_browser_identity .profile_owned() }); - self.web_contents_for_target_mut(target_id) - .expect("resolved WebContents must remain live") - .set_browser_identity_override(identity); + let handle = self + .web_contents_handle_for_target(target_id) + .expect("resolved WebContents must remain live"); + self.browser_context + .set_web_contents_browser_identity_override(handle, identity) + .expect("resolved WebContents must remain live"); } pub(crate) fn set_base_browser_identity_override_for_target( @@ -394,22 +445,24 @@ impl BrowserContext { &mut self, target_id: &str, locale_override: Option, - ) -> Result<(), &'static str> { - self.web_contents_for_target(target_id) - .expect("resolved WebContents must remain live") - .environment_owner - .set_locale(locale_override.as_deref()) + ) -> Result<(), String> { + let handle = self + .web_contents_handle_for_target(target_id) + .expect("live WebContents"); + self.browser_context + .set_web_contents_locale_override(handle, locale_override) } pub(crate) fn set_base_timezone_override_for_target( &mut self, target_id: &str, timezone_override: Option, - ) -> Result<(), &'static str> { - self.web_contents_for_target(target_id) - .expect("resolved WebContents must remain live") - .environment_owner - .set_timezone(timezone_override.as_deref()) + ) -> Result<(), String> { + let handle = self + .web_contents_handle_for_target(target_id) + .expect("live WebContents"); + self.browser_context + .set_web_contents_timezone_override(handle, timezone_override) } pub(crate) fn clear_devtools_network_state_for_target( @@ -438,34 +491,23 @@ impl BrowserContext { self.install_effective_browser_identity_for_target(target_id); } - pub(crate) fn has_pending_javascript_dialog_for_target(&self, target_id: &str) -> bool { - !self - .web_contents_for_target(target_id) - .expect("resolved WebContents must remain live") - .javascript_dialogs - .is_empty() - } - pub(crate) fn has_non_default_session_state_for_target(&self, target_id: &str) -> bool { let projection = self .page_targets .get(target_id) .expect("resolved target projection must remain live"); - let contents = self - .web_contents_for_target(target_id) + let handle = self + .web_contents_handle_for_target(target_id) .expect("resolved WebContents must remain live"); projection.devtools_sessions.has_non_default_state() || projection.runtime_slot.primary_network_events_enabled() || projection.base_network_request_policy != BaseNetworkRequestPolicy::default() - || contents.network_offline || projection.base_browser_identity != crate::conn::state::BaseBrowserIdentityOverrideState::default() - || contents.browser_identity_override.is_some() - || contents.tls_verify_host_override.is_some() - || contents.bypass_content_security_policy - || contents.environment_owner.has_override() - || contents.emulation_policy != crate::conn::state::EmulationPolicy::default() - || contents.network_request_policy != NetworkRequestPolicy::default() + || self + .browser_context + .web_contents_has_non_default_policy(handle) + .expect("resolved WebContents must remain live") || projection.input_intercept_drags_enabled || projection.input_drag_intercepted || projection.css_enabled @@ -799,19 +841,11 @@ pub(in crate::conn::state) struct BaseNetworkRequestPolicy { extra_headers: moli_fetch::RequestHeaders, } -impl BaseNetworkRequestPolicy { - pub(in crate::conn::state) fn with_cache_disabled(cache_disabled: bool) -> Self { - Self { - cache_disabled, - ..Self::default() - } - } -} - #[cfg(test)] mod tests { use super::{PageScreencastConfig, PageScreencastFormat, PageScreencastSessionState}; use crate::conn::BrowserContext; + use moli_core::browser::WebContentsHandle; use moli_page_types::DevToolsSessionKey; fn policy_context(target_id: &str) -> BrowserContext { @@ -831,12 +865,30 @@ mod tests { ); context.set_network_offline_for_target(target_id, true); let contents_id = context.selected_web_contents_id().unwrap(); + let handle = WebContentsHandle::new(context.browser_context_id(), contents_id); drop(context.page_targets.remove(target_id).unwrap()); - let contents = context.physical.web_contents.get_mut(&contents_id).unwrap(); - assert!(contents.bypass_content_security_policy); - contents.set_bypass_content_security_policy(false); - assert!(!contents.bypass_content_security_policy); - assert!(contents.network_offline); + assert!( + context + .browser_context + .web_contents_bypass_content_security_policy(handle) + .unwrap() + ); + context + .browser_context + .set_web_contents_bypass_content_security_policy(handle, false) + .unwrap(); + assert!( + !context + .browser_context + .web_contents_bypass_content_security_policy(handle) + .unwrap() + ); + assert!( + context + .browser_context + .web_contents_network_offline(handle) + .unwrap() + ); } #[test] @@ -847,14 +899,32 @@ mod tests { context.set_network_offline_for_target(target_id, true); let id = context.selected_web_contents_id().unwrap(); let contents_id = context.selected_web_contents_id().unwrap(); + let handle = WebContentsHandle::new(context.browser_context_id(), contents_id); drop(context.page_targets.remove(target_id).unwrap()); - let contents = context.physical.web_contents.get_mut(&contents_id).unwrap(); - assert_eq!(contents.id(), id); - assert_eq!(contents.tls_verify_host_override, Some(false)); + assert_eq!(handle.id(), id); + assert_eq!( + context + .browser_context + .web_contents_tls_verify_host_override(handle), + Ok(Some(false)) + ); for enabled in [Some(true), None] { - contents.set_tls_verify_host_override(enabled); - assert_eq!(contents.tls_verify_host_override, enabled); - assert!(contents.network_offline); + context + .browser_context + .set_web_contents_tls_verify_host_override(handle, enabled) + .unwrap(); + assert_eq!( + context + .browser_context + .web_contents_tls_verify_host_override(handle), + Ok(enabled) + ); + assert!( + context + .browser_context + .web_contents_network_offline(handle) + .unwrap() + ); } } @@ -868,13 +938,32 @@ mod tests { assert!(context.network_offline_for_target(target_id)); let id = context.selected_web_contents_id().unwrap(); let contents_id = context.selected_web_contents_id().unwrap(); + let handle = WebContentsHandle::new(context.browser_context_id(), contents_id); drop(context.page_targets.remove(target_id).unwrap()); - let contents = context.physical.web_contents.get_mut(&contents_id).unwrap(); - assert_eq!(contents.id(), id); - assert!(contents.network_offline); - contents.set_network_offline(false); - assert!(!contents.network_offline); - assert!(contents.network_request_policy.cache_disabled); + assert_eq!(handle.id(), id); + assert!( + context + .browser_context + .web_contents_network_offline(handle) + .unwrap() + ); + context + .browser_context + .set_web_contents_network_offline(handle, false) + .unwrap(); + assert!( + !context + .browser_context + .web_contents_network_offline(handle) + .unwrap() + ); + assert!( + context + .browser_context + .web_contents_network_request_policy(handle) + .unwrap() + .cache_disabled + ); } #[test] @@ -898,13 +987,14 @@ mod tests { .unwrap() .clone(); let snapshot = context.effective_policy_for_target(target_id); + let handle = context.web_contents_handle_for_target(target_id).unwrap(); context - .web_contents_for_target_mut(target_id) - .unwrap() - .set_browser_identity_override(Some(base.clone())); + .browser_context + .set_web_contents_browser_identity_override(handle, Some(base.clone())) + .unwrap(); assert_eq!( context.browser_identity_override_for_target(target_id), - Some(&base) + Some(base.clone()) ); assert_eq!( context @@ -919,14 +1009,35 @@ mod tests { assert_eq!(snapshot.browser_identity_override(), Some(&installed)); let id = context.selected_web_contents_id().unwrap(); let contents_id = context.selected_web_contents_id().unwrap(); + let handle = WebContentsHandle::new(context.browser_context_id(), contents_id); drop(context.page_targets.remove(target_id).unwrap()); - let contents = context.physical.web_contents.get_mut(&contents_id).unwrap(); - assert_eq!(contents.id(), id); - assert_eq!(contents.browser_identity_override, Some(base)); - contents.set_browser_identity_override(Some(installed.clone())); - assert_eq!(contents.browser_identity_override, Some(installed)); - contents.set_browser_identity_override(None); - assert!(contents.browser_identity_override.is_none()); + assert_eq!(handle.id(), id); + assert_eq!( + context + .browser_context + .web_contents_browser_identity_override(handle), + Ok(Some(base)) + ); + context + .browser_context + .set_web_contents_browser_identity_override(handle, Some(installed.clone())) + .unwrap(); + assert_eq!( + context + .browser_context + .web_contents_browser_identity_override(handle), + Ok(Some(installed)) + ); + context + .browser_context + .set_web_contents_browser_identity_override(handle, None) + .unwrap(); + assert_eq!( + context + .browser_context + .web_contents_browser_identity_override(handle), + Ok(None) + ); } #[test] @@ -955,7 +1066,7 @@ mod tests { assert_eq!(profile.navigator_platform(), "TestPlatform"); assert_eq!( context.browser_identity_override_for_target(target_id), - Some(profile) + Some(profile.clone()) ); assert_eq!( context.active_page_target().reported_user_agent_override(), @@ -1010,10 +1121,11 @@ mod tests { cache_disabled: false, ..installed.clone() }; + let handle = context.web_contents_handle_for_target(target_id).unwrap(); context - .web_contents_for_target_mut(target_id) - .unwrap() - .set_network_request_policy(independent.clone()); + .browser_context + .set_web_contents_network_request_policy(handle, independent.clone()) + .unwrap(); assert_eq!( context .effective_policy_for_target(target_id) @@ -1030,12 +1142,25 @@ mod tests { ); let id = context.selected_web_contents_id().unwrap(); let contents_id = context.selected_web_contents_id().unwrap(); + let handle = WebContentsHandle::new(context.browser_context_id(), contents_id); drop(context.page_targets.remove(target_id).unwrap()); - let contents = context.physical.web_contents.get_mut(&contents_id).unwrap(); - assert_eq!(contents.id(), id); - assert_eq!(contents.network_request_policy, independent); - contents.set_network_request_policy(installed.clone()); - assert_eq!(contents.network_request_policy, installed); + assert_eq!(handle.id(), id); + assert_eq!( + context + .browser_context + .web_contents_network_request_policy(handle), + Ok(independent) + ); + context + .browser_context + .set_web_contents_network_request_policy(handle, installed.clone()) + .unwrap(); + assert_eq!( + context + .browser_context + .web_contents_network_request_policy(handle), + Ok(installed) + ); } #[test] @@ -1140,8 +1265,8 @@ mod tests { ); } - #[test] - fn base_environment_claims_survive_projection_drop() { + #[tokio::test] + async fn base_environment_claims_survive_projection_drop() { let target_id = "TID-environment"; let mut context = policy_context(target_id); context @@ -1150,27 +1275,38 @@ mod tests { context .set_base_timezone_override_for_target(target_id, Some("Europe/Paris".into())) .unwrap(); - let id = context.selected_web_contents_id().unwrap(); + let handle = context.web_contents_handle_for_target(target_id).unwrap(); drop(context.page_targets.remove(target_id).unwrap()); - let contents = context.physical.web_contents.shift_remove(&id).unwrap(); + let contents = context.browser_context; assert_eq!( - contents.environment_owner.locale().as_deref(), + contents + .web_contents_locale_override(handle) + .unwrap() + .as_deref(), Some("fr-FR") ); assert_eq!( - contents.environment_owner.timezone().as_deref(), + contents + .web_contents_timezone_override(handle) + .unwrap() + .as_deref(), Some("Europe/Paris") ); contents - .environment_owner - .set_timezone(Some("Asia/Tokyo")) + .set_web_contents_timezone_override(handle, Some("Asia/Tokyo".into())) .unwrap(); assert_eq!( - contents.environment_owner.locale().as_deref(), + contents + .web_contents_locale_override(handle) + .unwrap() + .as_deref(), Some("fr-FR") ); assert_eq!( - contents.environment_owner.timezone().as_deref(), + contents + .web_contents_timezone_override(handle) + .unwrap() + .as_deref(), Some("Asia/Tokyo") ); let peer = moli_core::ProcessEnvironmentOwner::default(); @@ -1182,7 +1318,11 @@ mod tests { peer.set_timezone(Some("Europe/Berlin")), Err("Timezone override is already in effect") ); - drop(contents); + contents + .close_web_contents(handle) + .unwrap() + .close_async() + .await; peer.set_locale(Some("de-DE")).unwrap(); peer.set_timezone(Some("Europe/Berlin")).unwrap(); } @@ -1239,11 +1379,20 @@ mod tests { .is_err() ); assert!( - !context - .web_contents_for_target(target_id) + context + .browser_context + .web_contents_locale_override( + context.web_contents_handle_for_target(target_id).unwrap() + ) .unwrap() - .environment_owner - .has_override() + .is_none() + && context + .browser_context + .web_contents_timezone_override( + context.web_contents_handle_for_target(target_id).unwrap() + ) + .unwrap() + .is_none() ); assert_eq!( context diff --git a/moli-protocol/src/conn/state/browser_context/tests.rs b/moli-protocol/src/conn/state/browser_context/tests.rs index 300580a2eb..3f319f3452 100644 --- a/moli-protocol/src/conn/state/browser_context/tests.rs +++ b/moli-protocol/src/conn/state/browser_context/tests.rs @@ -13,25 +13,24 @@ fn context_download_policy_outlives_the_devtools_projection() { projection.attach_active_session("SID-download"); projection.automation_download_events_enabled = Some(true); projection.set_download_policy(Some(policy.clone())); - projection.physical + projection.browser_context }; - assert_eq!(physical.download_policy.as_ref(), Some(&policy)); + assert_eq!(physical.download_policy(), Some(policy.clone())); assert!( physical - .download_policy - .as_ref() + .download_policy() .unwrap() .behavior .allows_download() ); - assert_eq!(physical.web_contents.len(), 1); + assert_eq!(physical.web_contents_count(), 1); } #[test] fn context_header_defaults_preserve_layer_order_and_clear_fallback() { let header = |name: &str, value: &str| (name.to_owned(), value.to_owned()); let mut context = BrowserContext::new("CTX-headers".into()); - context.global_extra_headers = + let global_headers: moli_fetch::RequestHeaders = vec![header("X-Order", "global"), header("X-Global", "1")].into(); context.set_default_extra_headers( vec![ @@ -44,7 +43,7 @@ fn context_header_defaults_preserve_layer_order_and_clear_fallback() { let page_headers: moli_fetch::RequestHeaders = vec![header("X-Order", "page"), header("X-Page", "1")].into(); assert_eq!( - context.merged_extra_headers_for_target_policy(&page_headers), + context.merged_extra_headers_for_target_policy(&global_headers, &page_headers), vec![ header("X-Global", "1"), header("X-Context", "1"), @@ -55,7 +54,7 @@ fn context_header_defaults_preserve_layer_order_and_clear_fallback() { ); context.set_default_extra_headers(Default::default()); assert_eq!( - context.merged_extra_headers_for_target_policy(&page_headers), + context.merged_extra_headers_for_target_policy(&global_headers, &page_headers), vec![ header("X-Global", "1"), header("X-Order", "page"), @@ -64,8 +63,8 @@ fn context_header_defaults_preserve_layer_order_and_clear_fallback() { .into() ); assert_eq!( - context.effective_extra_headers(), - context.global_extra_headers + context.effective_extra_headers(&global_headers), + global_headers ); assert!(!context.has_active_target()); } @@ -74,6 +73,8 @@ fn context_header_defaults_preserve_layer_order_and_clear_fallback() { fn context_header_defaults_outlive_projection_without_replacing_network_policy() { let headers: moli_fetch::RequestHeaders = vec![("X-Context".to_owned(), "context".to_owned())].into(); + let global_headers: moli_fetch::RequestHeaders = + vec![("X-Global".into(), "global".into())].into(); let expected = ContextNetworkPolicy { http_proxy: Some("http://proxy.example:8080".into()), http_no_proxy: Some(String::new()), @@ -83,7 +84,6 @@ fn context_header_defaults_outlive_projection_without_replacing_network_policy() let physical = { let mut projection = BrowserContext::new_with_page_for_test("CTX-headers", "page-headers"); projection.attach_active_session("session-headers"); - projection.global_extra_headers = vec![("X-Global".into(), "global".into())].into(); projection.set_network_policy(ContextNetworkPolicy { extra_headers: Default::default(), ..expected.clone() @@ -101,8 +101,8 @@ fn context_header_defaults_outlive_projection_without_replacing_network_policy() expected.tls_verify_host ); assert_eq!( - projection.effective_extra_headers(), - projection.global_extra_headers + projection.effective_extra_headers(&global_headers), + global_headers ); assert_eq!(snapshot, expected); projection.set_default_extra_headers(headers); @@ -110,9 +110,9 @@ fn context_header_defaults_outlive_projection_without_replacing_network_policy() projection.detach_active_session().as_deref(), Some("session-headers") ); - projection.physical + projection.browser_context }; - assert_eq!(physical.network_policy, expected); + assert_eq!(physical.network_policy(), expected); let other = BrowserContext::new("CTX-other".into()); assert!(other.network_policy().extra_headers.is_empty()); } @@ -133,7 +133,7 @@ fn context_browser_identity_preserves_independent_inputs_and_update_time_fallbac .unwrap(); assert_eq!( context.reported_active_user_agent_override(), - Some("Base-UA") + Some("Base-UA".to_owned()) ); context.set_default_user_agent_override(Some("Context-UA".into()), &fallback); } else { @@ -151,7 +151,7 @@ fn context_browser_identity_preserves_independent_inputs_and_update_time_fallbac Some(&combined) ); assert_eq!( - context.physical.environment_owner.locale().as_deref(), + context.browser_context.default_locale_override().as_deref(), Some("fr-FR") ); assert!(!context.has_active_target()); @@ -159,14 +159,14 @@ fn context_browser_identity_preserves_independent_inputs_and_update_time_fallbac context.set_default_user_agent_override(None, &next_fallback); assert_eq!( context.default_browser_identity_override(), - Some(&BrowserIdentityProfile::new("Next-UA", "fr-FR")) + Some(BrowserIdentityProfile::new("Next-UA", "fr-FR")) ); assert_eq!( context.reported_active_user_agent_override(), - Some("Next-UA") + Some("Next-UA".to_owned()) ); assert_eq!( - context.physical.environment_owner.locale().as_deref(), + context.browser_context.default_locale_override().as_deref(), Some("fr-FR") ); context @@ -174,7 +174,7 @@ fn context_browser_identity_preserves_independent_inputs_and_update_time_fallbac .unwrap(); assert!(context.default_browser_identity_override().is_none()); assert!(context.reported_active_user_agent_override().is_none()); - assert!(context.physical.environment_owner.locale().is_none()); + assert!(context.browser_context.default_locale_override().is_none()); context.set_default_user_agent_override(Some("Context-UA".into()), &fallback); context @@ -185,9 +185,9 @@ fn context_browser_identity_preserves_independent_inputs_and_update_time_fallbac .unwrap(); assert_eq!( context.default_browser_identity_override(), - Some(&BrowserIdentityProfile::new("Context-UA", "de-DE")) + Some(BrowserIdentityProfile::new("Context-UA", "de-DE")) ); - assert!(context.physical.environment_owner.locale().is_none()); + assert!(context.browser_context.default_locale_override().is_none()); assert_eq!(snapshot, combined); context.set_default_user_agent_override(None, &next_fallback); assert!(context.default_browser_identity_override().is_none()); @@ -217,13 +217,13 @@ fn context_browser_identity_outlives_projection() { projection.detach_active_session().as_deref(), Some("session-identity") ); - projection.physical + projection.browser_context }; - assert_eq!(physical.browser_identity_override.as_ref(), Some(&expected)); assert_eq!( - physical.environment_owner.locale().as_deref(), - Some("fr-FR") + physical.browser_identity_override().as_ref(), + Some(&expected) ); + assert_eq!(physical.default_locale_override().as_deref(), Some("fr-FR")); let other = BrowserContext::new("CTX-other".into()); assert!(other.default_browser_identity_override().is_none()); } @@ -267,14 +267,15 @@ fn context_emulation_defaults_outlive_projection_without_inherited_mirrors() { projection.set_default_timezone_override(None).unwrap(); projection.set_default_network_conditions(None); - projection.global_network_conditions = Some(EmulatedNetworkConditions::offline()); - assert!(projection.effective_active_network_offline()); + assert!( + projection.effective_active_network_offline(Some(EmulatedNetworkConditions::offline())) + ); assert!(projection.set_default_device_metrics(resized.clone())); assert_eq!( projection.detach_active_session().as_deref(), Some("session-emulation") ); - (projection.physical, snapshot) + (projection.browser_context, snapshot) }; assert_eq!( snapshot, @@ -284,13 +285,10 @@ fn context_emulation_defaults_outlive_projection_without_inherited_mirrors() { device_metrics: Some(metrics), } ); + assert_eq!(physical.default_locale_override().as_deref(), Some("fr-FR")); + assert_eq!(physical.default_timezone_override(), None); assert_eq!( - physical.environment_owner.locale().as_deref(), - Some("fr-FR") - ); - assert_eq!(physical.environment_owner.timezone(), None); - assert_eq!( - physical.emulation_defaults, + physical.emulation_defaults(), ContextEmulationDefaults { network_conditions: None, device_metrics: Some(resized), @@ -300,7 +298,7 @@ fn context_emulation_defaults_outlive_projection_without_inherited_mirrors() { let other = BrowserContext::new("CTX-other".into()); assert_eq!( other.emulation_defaults(), - &ContextEmulationDefaults::default() + ContextEmulationDefaults::default() ); } @@ -314,19 +312,19 @@ fn physical_storage_operations_outlive_protocol_projection() { let sibling_origin = sibling.origin().ascii_serialization(); let sibling_key = moli_storage_key::MoliStorageKey::first_party_from_url(&sibling, None) .serialized_storage_key(); - let mut physical = { + let physical = { let projection = BrowserContext::new_with_page_for_test("CTX-storage", "page-storage"); { - let mut store = projection.web_storage_store_for_test().lock(); + let storage = projection.web_storage_store_for_test(); + let mut store = storage.lock(); assert!(store.set_item(&key_a, "local", "aaa")); assert!(store.set_item(&key_b, "local", "bb")); assert!(store.set_item(&sibling_key, "local", "c")); } - projection.physical + projection.browser_context }; - let partition = &mut physical.storage_partition; assert_eq!( - partition.usage_for_origin(&origin_key).unwrap(), + physical.storage_usage_for_origin(&origin_key).unwrap(), OriginStorageUsage { local_storage_usage: 5, indexed_db_usage: 0, @@ -339,34 +337,42 @@ fn physical_storage_operations_outlive_protocol_projection() { ..Default::default() }; let key = moli_storage_key::deserialize_serialized_storage_key(&key_a).unwrap(); - partition + physical .clear_site_data_for_storage_key(&key, options) .unwrap(); assert_eq!( - partition.usage_for_origin(&origin_key).unwrap().total_usage, + physical + .storage_usage_for_origin(&origin_key) + .unwrap() + .total_usage, 2 ); { - let mut store = partition.web_storage_store().lock(); + let storage = physical.web_storage_store_for_test(); + let mut store = storage.lock(); assert_eq!(store.get_item(&key_a, "local"), None); assert_eq!(store.get_item(&key_b, "local"), Some("bb".into())); assert!(store.set_item(&key_a, "local", "aaa")); } - partition + physical .clear_site_data_for_origin(&origin, options) .unwrap(); assert_eq!( - partition.usage_for_origin(&origin_key).unwrap().total_usage, + physical + .storage_usage_for_origin(&origin_key) + .unwrap() + .total_usage, 0 ); assert_eq!( - partition - .usage_for_origin(&sibling_origin) + physical + .storage_usage_for_origin(&sibling_origin) .unwrap() .total_usage, 1 ); - let mut store = partition.web_storage_store().lock(); + let storage = physical.web_storage_store_for_test(); + let mut store = storage.lock(); assert_eq!(store.get_item(&key_a, "local"), None); assert_eq!(store.get_item(&key_b, "local"), None); assert_eq!(store.get_item(&sibling_key, "local"), Some("c".into())); @@ -391,100 +397,44 @@ fn context_network_policy_outlives_page_and_protocol_projection() { projection.detach_active_session().as_deref(), Some("session-policy") ); - (projection.physical, before_tls_change) + (projection.browser_context, before_tls_change) }; assert_eq!(before_tls_change, expected); assert_eq!( - physical.network_policy, + physical.network_policy(), ContextNetworkPolicy { tls_verify_host: Some(true), ..expected } ); let other = BrowserContext::new("CTX-other".into()); - assert_ne!(physical.id, other.browser_context_id()); - assert_eq!(other.network_policy(), &ContextNetworkPolicy::default()); + assert_ne!(physical.id(), other.browser_context_id()); + assert_eq!(other.network_policy(), ContextNetworkPolicy::default()); } #[test] -fn physical_context_storage_and_runtime_outlive_protocol_projection() { - let (mut physical, id, runtime_id, local_storage) = { +fn physical_context_storage_outlives_protocol_projection() { + let (physical, id, runtime_id, local_storage) = { let mut projection = BrowserContext::new_with_page_for_test("CTX-owner", "page-owner"); projection.bind_page_navigation_engines(NavigationRuntimeConfig::default(), None); projection.set_storage_quota_override("https://example.test".into(), 123.0); let id = projection.browser_context_id(); - let runtime_id = projection.renderer_runtime().id(); + let runtime_id = projection.browser_context.renderer_runtime_id_for_test(); let local_storage = projection.web_storage_store_for_test().clone(); // Moving the sole Browser owner out lets the protocol shell and its // legacy embedded page/engine go away without retiring the context. - (projection.physical, id, runtime_id, local_storage) + (projection.browser_context, id, runtime_id, local_storage) }; - assert_eq!(physical.id, id); - assert_eq!(physical.renderer_runtime().id(), runtime_id); + assert_eq!(physical.id(), id); + assert_eq!(physical.renderer_runtime_id_for_test(), runtime_id); assert!(Arc::ptr_eq( - physical.storage_partition.web_storage_store(), + &physical.web_storage_store_for_test(), &local_storage )); assert_eq!( - physical - .storage_partition - .storage_quota_for_origin("https://example.test"), + physical.storage_quota_for_origin("https://example.test"), (123.0, true) ); - physical - .storage_partition - .clear_storage_quota_override("https://example.test"); - assert!( - !physical - .storage_partition - .storage_quota_for_origin("https://example.test") - .1 - ); - let engine = physical.new_page_navigation_engine( - physical - .page_navigation_runtime_config - .clone() - .expect("Browser-owned configuration"), - ); - let access = physical.renderer_runtime_owner_access(); - drop(engine); - drop(physical); - assert!( - NavigationEngine::new_with_runtime_config_and_browser_context_access( - NavigationRuntimeConfig::default(), - access, - ) - .is_err(), - "a retained runtime handle must not resurrect a retired Browser context" - ); -} - -#[test] -fn context_runtime_teardown_handoff_retains_exactly_one_owner() { - let mut projection = BrowserContext::new("CTX-teardown".into()); - let access = projection.renderer_runtime_owner_access(); - let mut root = projection - .take_renderer_runtime_owner_for_teardown() - .unwrap(); - assert!( - projection - .take_renderer_runtime_owner_for_teardown() - .is_none() - ); - drop(projection); - - let engine = NavigationEngine::new_with_runtime_config_and_browser_context_access( - NavigationRuntimeConfig::default(), - access.clone(), - ) - .expect("the moved teardown participant is still the live owner"); - drop(engine); - root.shutdown_and_join(); - assert!( - NavigationEngine::new_with_runtime_config_and_browser_context_access( - NavigationRuntimeConfig::default(), - access, - ) - .is_err() - ); + physical.clear_storage_quota_override("https://example.test"); + assert!(!physical.storage_quota_for_origin("https://example.test").1); } diff --git a/moli-protocol/src/conn/state/browser_context/workers.rs b/moli-protocol/src/conn/state/browser_context/workers.rs new file mode 100644 index 0000000000..115980db6c --- /dev/null +++ b/moli-protocol/src/conn/state/browser_context/workers.rs @@ -0,0 +1,131 @@ +use moli_core::{browser::ServiceWorkerCommand, runtime::RendererBrowserContextRuntime}; +use moli_shared_worker::SharedWorkerInstanceId; + +use super::BrowserContext; + +impl BrowserContext { + pub(in crate::conn) fn execute_service_worker_command( + &self, + command: ServiceWorkerCommand, + ) -> Result<(), String> { + self.browser_context.execute_service_worker_command(command) + } + + #[cfg(test)] + pub(crate) fn service_worker_force_update_on_page_load(&self) -> bool { + self.browser_context + .service_worker_force_update_on_page_load_for_test() + } + + #[cfg(test)] + pub(crate) fn service_worker_pause_on_start(&self) -> bool { + self.browser_context + .service_worker_pause_on_start_for_test() + } + + pub(crate) fn controlled_service_worker_window_client_ids( + &self, + registration_id: u64, + version_id: u64, + ) -> Vec { + self.browser_context + .controlled_service_worker_window_client_ids(registration_id, version_id) + } + + pub(crate) fn set_service_worker_pause_on_start_for_version( + &self, + version_id: u64, + pause: bool, + ) -> bool { + self.browser_context + .set_service_worker_pause_on_start_for_version(version_id, pause) + } + + pub(crate) fn set_service_worker_pause_on_start(&self, pause: bool) { + self.browser_context + .set_service_worker_pause_on_start(pause); + } + + pub(crate) fn set_service_worker_related_pause_on_start_policies( + &self, + policies: Vec<(u64, u64, String, String)>, + ) { + self.browser_context + .set_service_worker_related_pause_on_start_policies(policies); + } + + pub(crate) fn set_dedicated_worker_pause_on_start(&self, pause: bool) { + self.browser_context + .set_dedicated_worker_pause_on_start(pause); + } + + pub(crate) fn set_service_worker_devtools_attached(&self, version_id: u64, attached: bool) { + self.browser_context + .set_service_worker_inspection_attached(version_id, attached); + } + + pub(in crate::conn) fn close_shared_worker(&self, instance_id: SharedWorkerInstanceId) -> bool { + self.browser_context.close_shared_worker(instance_id) + } + + pub(in crate::conn) fn close_dedicated_worker(&self, instance_id: u64) -> bool { + self.browser_context.close_dedicated_worker(instance_id) + } + + pub(crate) fn attach_dedicated_worker_inspector_session( + &self, + instance_id: u64, + session_id: Option, + ) -> bool { + self.browser_context + .attach_dedicated_worker_inspector_session(instance_id, session_id) + } + + pub(crate) fn detach_shared_worker_inspector_session( + &self, + instance_id: SharedWorkerInstanceId, + session_id: Option, + ) -> bool { + self.browser_context + .detach_shared_worker_inspector_session(instance_id, session_id) + } + + pub(crate) fn detach_dedicated_worker_inspector_session( + &self, + instance_id: u64, + session_id: Option, + ) -> bool { + self.browser_context + .detach_dedicated_worker_inspector_session(instance_id, session_id) + } + + pub(crate) fn detach_service_worker_inspector_session( + &self, + version_id: u64, + session_id: Option, + ) -> bool { + self.browser_context + .detach_service_worker_inspector_session(version_id, session_id) + } + + pub(in crate::conn) fn run_dedicated_worker_if_waiting_for_debugger( + &self, + instance_id: u64, + ) -> bool { + self.browser_context + .run_dedicated_worker_if_waiting_for_debugger(instance_id) + } + + pub(crate) fn run_service_worker_if_waiting_for_debugger(&self, version_id: u64) -> bool { + self.browser_context + .run_service_worker_if_waiting_for_debugger(version_id) + } + + /// Layered DevTools inspection is the only Protocol path allowed to retain + /// the cloneable renderer endpoint while a worker command is in flight. + pub(in crate::conn) fn worker_runtime_inspection_endpoint( + &self, + ) -> RendererBrowserContextRuntime { + self.browser_context.worker_runtime_inspection_endpoint() + } +} diff --git a/moli-protocol/src/conn/state/devtools_session.rs b/moli-protocol/src/conn/state/devtools_session.rs index 7aa9c8a403..f39f293c26 100644 --- a/moli-protocol/src/conn/state/devtools_session.rs +++ b/moli-protocol/src/conn/state/devtools_session.rs @@ -12,8 +12,8 @@ use super::{ }, session::{InspectorSessionState, TargetPageSessionState, TargetRuntimeSessionState}, target_state::{PendingInspectorAwait, TargetPendingInspectorAwaitRegistry}, - web_contents::NetworkRequestPolicy, }; +use moli_core::browser::web_contents::NetworkRequestPolicy; use moli_core::{ network::WebStorageMutationSubscription, page::{ @@ -1802,3 +1802,33 @@ mod tests { ); } } + +#[cfg(test)] +mod emulation_policy_tests { + use crate::conn::EmulationPolicy; + #[test] + fn handler_disable_uses_raw_state_for_conditional_target_resets() { + let mut effective = EmulationPolicy { + focus_emulation_enabled: true, + script_execution_disabled: true, + emulated_media: crate::conn::EmulatedMediaOverrides { + color_scheme: Some("dark".to_owned()), + ..Default::default() + }, + ..Default::default() + }; + let mut raw = crate::conn::DevToolsEmulationSessionState::default(); + + effective.apply_changes(raw.disable_policy_changes()); + + assert!( + effective.focus_emulation_enabled, + "an untouched handler must not clear another session's focus setting" + ); + assert!( + effective.emulated_media.color_scheme.is_none(), + "Blink clears the shared media override on every handler disable" + ); + assert!(!effective.script_execution_disabled); + } +} diff --git a/moli-protocol/src/conn/state/fetch.rs b/moli-protocol/src/conn/state/fetch.rs index 2d0ebd1cc9..0d4ba73ad2 100644 --- a/moli-protocol/src/conn/state/fetch.rs +++ b/moli-protocol/src/conn/state/fetch.rs @@ -2341,6 +2341,7 @@ mod tests { crate::conn::CdpSessionRoute::Browser, ) }), + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: NavigationResultProjection::Cdp( json!({"frameId": "FRAME-1", "loaderId": "LOADER-1"}), ), diff --git a/moli-protocol/src/conn/state/mod.rs b/moli-protocol/src/conn/state/mod.rs index 9c2f11d819..a143fb5a41 100644 --- a/moli-protocol/src/conn/state/mod.rs +++ b/moli-protocol/src/conn/state/mod.rs @@ -1,11 +1,9 @@ -mod bounds; mod browser_context; mod browser_identity; mod dedicated_worker_target; mod devtools_renderer_channel; mod devtools_session; mod document_lifecycle_observer; -mod emulation; mod fetch; mod identity; mod inspector; @@ -25,19 +23,24 @@ mod shared_worker_target; mod target_state; #[cfg(test)] mod tests; -mod web_contents; -pub(in crate::conn) use web_contents::{AdmittedNavigationLoad, PreparedNavigationResponse}; -pub(in crate::conn) use web_contents::{ - BuiltInitialDocument, InitialDocumentAdmission, InitialDocumentBuildKey, - InitialDocumentPageBuildWaiter, +pub(crate) use moli_core::browser::web_contents::{ + ClaimedNavigationRequest, NavigationInterceptionPermit, NavigationRequestInterception, }; -pub(crate) use web_contents::{ - ClaimedNavigationRequest, InterceptedNavigationLoad, InterceptedNavigationResponse, - NavigationInterceptionPermit, NavigationRequestInterception, +pub(in crate::conn) use moli_core::browser::web_contents::{ + InitialDocumentBuildKey, InitialDocumentPageBuildWaiter, +}; +pub(in crate::conn) use moli_core::browser::{ + BrowserBuiltInitialDocument as BuiltInitialDocument, + BrowserInitialDocumentAdmission as InitialDocumentAdmission, +}; +pub(crate) use moli_core::browser::{ + BrowserInterceptedNavigationLoad as InterceptedNavigationLoad, + BrowserInterceptedNavigationResponse as InterceptedNavigationResponse, +}; +pub(in crate::conn) use moli_core::browser::{ + BrowserNavigationLoad as AdmittedNavigationLoad, + BrowserPreparedNavigationResponse as PreparedNavigationResponse, }; - -#[cfg(test)] -pub(in crate::conn) use web_contents::DocumentHost; // Re-export everything so `use super::state::*` paths continue to work. @@ -69,8 +72,6 @@ pub(crate) use document_lifecycle_observer::{ RendererDocumentLifecycleObservation, RendererDocumentLifecycleObserver, }; -pub use bounds::BrowserWindowBounds; - pub(crate) use page_resource::MainDocumentResourceSnapshot; #[cfg(test)] pub(crate) use page_slot::TargetPageSlot; @@ -91,6 +92,21 @@ pub(crate) use javascript_dialog::{ TargetJavaScriptDialogScope, TargetJavaScriptDialogScopeObserver, TargetPreparedJavaScriptDialog, TargetPreparedJavaScriptDialogRoute, }; +pub(crate) use moli_core::browser::BrowserPreparedDocumentNavigation as PreparedDocumentNavigation; +#[cfg(test)] +pub(crate) use moli_core::browser::web_contents::JavaScriptDialogKey; +pub(in crate::conn) use moli_core::browser::web_contents::PageSurface; +pub(crate) use moli_core::browser::web_contents::SameDocumentNavigationCommitted; +pub(crate) use moli_core::browser::web_contents::{ + CommittedDocumentLifecycle, DocumentLifecycleEvent, DocumentNavigationDestination, +}; +pub(crate) use moli_core::browser::web_contents::{ + EmulationPolicy, EmulationPolicyChange, SessionStorageNamespace, WindowSurface, + WindowSurfaceState, +}; +pub(crate) use moli_core::browser::web_contents::{ + JavaScriptDialogClosed, JavaScriptDialogError, JavaScriptDialogSnapshot, +}; pub(crate) use pending_renderer_command::{ DuplicatePendingRendererCommand, PreparedRendererCallDispatch, PreparedRendererCallTermination, RegisterRendererCallError, RendererCommandCorrelation, RendererCommandDescriptor, @@ -114,46 +130,57 @@ pub(crate) use shared_worker_attachment::{ TargetSharedWorkerProtocolAttachmentIdentity, TargetSharedWorkerProtocolAttachmentRetirement, }; pub(crate) use shared_worker_target::SharedWorkerTargetState; -#[cfg(test)] -pub(crate) use web_contents::JavaScriptDialogKey; -pub(crate) use web_contents::SameDocumentNavigationCommitted; -pub(crate) use web_contents::{ - CommittedDocumentLifecycle, DocumentLifecycleEvent, DocumentNavigationDestination, - PreparedDocumentNavigation, -}; -pub(crate) use web_contents::{ - EmulationPolicy, EmulationPolicyChange, SessionStorageNamespace, WindowSurface, - WindowSurfaceState, -}; -pub(crate) use web_contents::{ - JavaScriptDialogClosed, JavaScriptDialogError, JavaScriptDialogSnapshot, -}; -pub(in crate::conn) use web_contents::{PageSurface, WindowOpener}; pub use browser_context::BrowserContext; -#[cfg(test)] -pub(crate) use browser_context::BrowserContextResourceStorageHandles; pub(crate) use browser_context::{ + BrowserAppManifestLoadPreparation, CompletedAppManifestLoadPreparation, + CompletedAppManifestPublication, CompletedCaptureDocumentImage, + CompletedCaptureDocumentScreencastFrame, CompletedCaptureDocumentSnapshot, + CompletedChildFrameNavigation, CompletedChildFrameTreeSnapshot, + CompletedDocumentAutofillTrigger, CompletedDocumentBlobRead, + CompletedDocumentCookieOwnerSnapshot, CompletedDocumentCspBypassUpdate, + CompletedDocumentDiagnosticsSnapshot, CompletedDocumentFetchCommand, + CompletedDocumentInputCommand, CompletedDocumentLifecycleStop, CompletedDocumentPolicyUpdate, + CompletedDocumentResourceRuntimeUpdate, CompletedDocumentResourceTextSearch, + CompletedDocumentStorageKeySnapshot, CompletedNavigationHistoryReset, + CompletedNetworkResourceLoadPreparation, CompletedSetDocumentContent, + CompletedTopLevelHistoryTraversal, CompletedTopLevelSameDocumentNavigation, + DocumentFetchCommand, DocumentFetchCommandOutcome, DocumentPolicyUpdate, + DocumentRuntimePolicyReconciliation, PendingAppManifestLoadPreparation, + PendingAppManifestPublication, PendingCaptureDocumentImage, + PendingCaptureDocumentScreencastFrame, PendingCaptureDocumentSnapshot, + PendingChildFrameLifecycleWork, PendingChildFrameNavigation, PendingChildFrameTreeSnapshot, + PendingDocumentAutofillTrigger, PendingDocumentBlobRead, PendingDocumentCookieOwnerSnapshot, + PendingDocumentCspBypassUpdate, PendingDocumentDiagnosticsSnapshot, + PendingDocumentFetchCommand, PendingDocumentInputCommand, PendingDocumentLifecycleStop, + PendingDocumentPolicyBatch, PendingDocumentPolicyUpdate, PendingDocumentResourceRuntimeUpdate, + PendingDocumentResourceTextSearch, PendingDocumentStorageKeySnapshot, + PendingNavigationHistoryReset, PendingNetworkResourceLoadPreparation, + PendingSetDocumentContent, PendingTopLevelHistoryTraversal, + PendingTopLevelSameDocumentNavigation, +}; +pub(crate) use browser_context::{LoadedNavigationPageCommit, PageInputCommand}; +#[cfg(test)] +pub(crate) use moli_core::browser::BrowserContextResourceStorageHandles; +pub(crate) use moli_core::browser::{ BrowserContextPageStorageHandles, BrowserContextStoragePartitionHandles, ContextNetworkPolicy, SiteDataClearOptions, }; -pub(crate) use browser_context::{ +pub(crate) use moli_core::browser::{ CompletedContextPermissionUpdate, PendingContextPermissionUpdate, }; -pub(crate) use browser_context::{ - LoadedNavigationPageCommit, NetworkPolicyUpdateKind, PageInputCommand, PagePolicyUpdateKind, + +pub use moli_core::browser::web_contents::PageNavigationHistoryEntry; +pub(crate) use moli_core::browser::web_contents::{ + HistoryTraversalDestination, InitialDocumentCreator, InitialDocumentSnapshot, + ResolvedHistoryTraversal, }; -pub use web_contents::PageNavigationHistoryEntry; -pub(crate) use web_contents::{ - HistoryTraversalDestination, InitialDocument, InitialDocumentCreator, ResolvedHistoryTraversal, -}; - -pub use emulation::{ +pub use moli_core::browser::{ EmulatedDeviceMetrics, EmulatedGeolocationOverride, EmulatedGeolocationOverrideState, EmulatedMediaOverrides, }; -pub(crate) use emulation::{EmulatedNetworkConditions, EmulatedViewportSurface}; +pub(crate) use moli_core::browser::{EmulatedNetworkConditions, EmulatedViewportSurface}; pub use page_agent_host::PageAgentHost; pub(crate) use target_state::{ PendingBidiChannelListener, PendingInspectorAwait, TargetOwnerState, diff --git a/moli-protocol/src/conn/state/navigation_outcome.rs b/moli-protocol/src/conn/state/navigation_outcome.rs index 1bfdfac2cd..bfaf06cd97 100644 --- a/moli-protocol/src/conn/state/navigation_outcome.rs +++ b/moli-protocol/src/conn/state/navigation_outcome.rs @@ -1,4 +1,4 @@ -use moli_core::browser::DownloadBody; +use moli_core::browser::{DownloadBody, WebContentsHandle}; #[cfg(test)] use moli_core::page::RendererPageCreationArtifacts; use moli_core::page::{ @@ -6,8 +6,6 @@ use moli_core::page::{ }; use serde::Serialize; use serde_json::Value; -#[cfg(test)] -use std::sync::Arc; use url::Url; use crate::conn::{CommandOwnerScope, ResponseCommitReady}; @@ -191,13 +189,11 @@ pub struct LoadedNavigation

{ pub response_from_cache: bool, pub initial_runtime_realms: Vec, pub renderer_output_predecessor: Option, - #[cfg(test)] - pub(crate) main_document_commit: Option>, pub(crate) document_progress_transfer: CompletedDocumentProgressTransfer, pub(crate) network_error_page: Option, } -impl LoadedNavigation { +impl

LoadedNavigation

{ #[cfg(test)] pub(crate) fn response_body(&self) -> String { self.document_progress_transfer @@ -298,6 +294,7 @@ pub struct NavigationDispatchState { pub(crate) redirect_headers: Option, pub navigate_id: Option, pub(crate) owner: CommandOwnerScope, + pub(crate) web_contents: WebContentsHandle, pub(crate) result_projection: NavigationResultProjection, pub frame_id: String, pub session_id: Option, @@ -318,6 +315,14 @@ pub struct NavigationDispatchState { } impl NavigationDispatchState { + #[cfg(test)] + pub(crate) fn detached_web_contents_for_test() -> WebContentsHandle { + WebContentsHandle::new( + moli_core::browser::BrowserContextId::allocate(), + moli_core::browser::WebContentsId::allocate(), + ) + } + pub(crate) fn clone_request_body_bytes(&self) -> Option> { self.request_body_bytes .clone() diff --git a/moli-protocol/src/conn/state/tests.rs b/moli-protocol/src/conn/state/tests.rs index f7d86ffd33..bf55e9053e 100644 --- a/moli-protocol/src/conn/state/tests.rs +++ b/moli-protocol/src/conn/state/tests.rs @@ -27,6 +27,7 @@ fn test_navigation_dispatch_state(fetch_request_id: &str) -> NavigationDispatchS redirect_headers: None, navigate_id: Some(1), owner: crate::conn::CommandOwnerScope::for_session("SID-1"), + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": "LID-0000000001"}), ), @@ -1005,16 +1006,6 @@ fn active_target_state_groups_runtime_fetch_and_owner_state() { ); } -#[test] -#[should_panic( - expected = "replace_loaded_page(None) is not a valid production transition; use clear_loaded_page_with_reason" -)] -fn replace_loaded_page_rejects_implicit_no_page_transition() { - let mut context = BrowserContext::new("CTX-reject-implicit-absence".into()); - context.set_active_target_id("TID-reject-implicit-absence"); - let _ = context.replace_active_page_for_test(None); -} - #[test] fn background_target_owns_fetch_state() { let mut context = BrowserContext::new("CTX-projection-state".into()); @@ -1227,7 +1218,7 @@ fn seed_initial_cookies_keeps_store_available_after_lock_holder_panic() { panic!("panic while holding cookie store lock"); })); - super::browser_context::seed_initial_cookies( + moli_core::browser::seed_initial_cookies_for_test( &cookie_store, vec![stored_cookie("sid", "seeded")], ); @@ -1264,7 +1255,8 @@ fn browser_context_clears_origin_site_data_through_partition_owner() { ); assert_eq!(context.snapshot_cookies().len(), 2); { - let mut store = context.web_storage_store_for_test().lock(); + let store_handle = context.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&storage_key, "local", "1")); assert!(store.set_item(&sibling_storage_key, "local", "2")); } @@ -1285,7 +1277,8 @@ fn browser_context_clears_origin_site_data_through_partition_owner() { let cookies = context.snapshot_cookies(); assert_eq!(cookies.len(), 1); assert_eq!(cookies[0].name, "sibling"); - let mut store = context.web_storage_store_for_test().lock(); + let store_handle = context.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert_eq!(store.get_item(&storage_key, "local"), None); assert_eq!( store.get_item(&sibling_storage_key, "local"), @@ -1335,7 +1328,10 @@ fn reused_devtools_ids_do_not_reuse_browser_object_identities() { context.set_active_target_id("TID-reused"); let first_web_contents = context.active_page_target().web_contents_id(); let first_main_frame_slot = context.active_page_target().main_frame_slot_id(); - drop(context.take_page_target_for_close("TID-reused")); + let first_handle = context + .web_contents_handle_for_target("TID-reused") + .unwrap(); + drop(context.begin_web_contents_close(first_handle)); context.set_active_target_id("TID-reused"); assert_ne!( @@ -1366,7 +1362,8 @@ fn navigation_lifetime_survives_devtools_target_rekey_but_not_recreation() { assert!(!cancellation.is_cancelled()); assert!(context.accepts_document_body_completion_event(&navigation)); - drop(context.take_page_target_for_close("TID-after")); + let handle = context.web_contents_handle_for_target("TID-after").unwrap(); + drop(context.begin_web_contents_close(handle)); context.set_active_target_id("TID-after"); let replacement = context .start_document_navigation_for_active_target("LOADER-reused".to_owned()) diff --git a/moli-protocol/src/conn/state/web_contents/document_host.rs b/moli-protocol/src/conn/state/web_contents/document_host.rs deleted file mode 100644 index 7093bc357d..0000000000 --- a/moli-protocol/src/conn/state/web_contents/document_host.rs +++ /dev/null @@ -1,56 +0,0 @@ -use moli_core::{ - browser::{DocumentId, DocumentLifecycle, DocumentLifetime}, - page::{Page, RendererDocumentLifecycleEvent}, -}; - -/// Exact native occurrence carried to a possibly delayed DevTools projection. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct DocumentLifecycleEvent { - document: DocumentId, - event: RendererDocumentLifecycleEvent, -} - -impl DocumentLifecycleEvent { - pub(in crate::conn::state) fn new( - document: DocumentId, - event: RendererDocumentLifecycleEvent, - ) -> Self { - Self { document, event } - } - - pub(crate) fn document(&self) -> DocumentId { - self.document - } - pub(crate) fn event(&self) -> RendererDocumentLifecycleEvent { - self.event - } -} - -/// One browser Document incarnation, including its concrete renderer Page. -/// -/// Private in the current residence until the typed API cutover (Commit 24b). -/// No Target/session state or public mutable Page capability belongs here. -#[derive(Debug)] -pub(in crate::conn) struct DocumentHost { - pub(in crate::conn) id: DocumentId, - pub(in crate::conn) page: Page, - pub(in crate::conn) lifecycle: DocumentLifecycle, - pub(in crate::conn) lifetime: DocumentLifetime, -} - -impl DocumentHost { - pub(in crate::conn) fn new(id: DocumentId, page: Page) -> Self { - Self { - id, - page, - lifecycle: DocumentLifecycle::default(), - lifetime: DocumentLifetime::default(), - } - } - - /// Retire the browser incarnation before handing its Page to async cleanup. - pub(super) fn retire(self) -> Page { - self.lifetime.supersede(); - self.page - } -} diff --git a/moli-protocol/src/conn/target/activation.rs b/moli-protocol/src/conn/target/activation.rs index a7d34a11d2..7282757471 100644 --- a/moli-protocol/src/conn/target/activation.rs +++ b/moli-protocol/src/conn/target/activation.rs @@ -75,9 +75,14 @@ impl CdpConnection { ) }) .collect(); - if let Err(error) = self - .apply_background_target_surface_overrides_async(previous_target_id) - .await + let previous = self.browser_web_contents_for_target(previous_target_id); + if let Err(error) = async { + let previous = previous?; + self.apply_browser_page_surface_async(previous, false) + .await + .map(drop) + } + .await { tracing::warn!( target_id = previous_target_id, @@ -102,11 +107,19 @@ impl CdpConnection { .deactivated_target_id() .map(|active_target_id| self.page_screencast_session_ids_for_target(active_target_id)) .unwrap_or_default(); - let Some(browser_context) = self.browser_context.as_mut() else { - anyhow::bail!("BrowserContextNotLoaded"); + let handle = { + let Some(browser_context) = self.browser_context.as_ref() else { + anyhow::bail!("BrowserContextNotLoaded"); + }; + let Some(handle) = browser_context.web_contents_handle_for_target(target_id) else { + return Ok(None); + }; + handle }; - let selected = browser_context.select_page_target_async(target_id).await?; - if !selected { + if let Err(error) = self.select_browser_web_contents_async(handle).await { + return Err(anyhow::anyhow!(error)); + } + if self.browser_context.is_none() { return Ok(None); } self.refresh_active_browser_context_loader(); diff --git a/moli-protocol/src/conn/target/route.rs b/moli-protocol/src/conn/target/route.rs index fdfdfd7233..125e3a10f1 100644 --- a/moli-protocol/src/conn/target/route.rs +++ b/moli-protocol/src/conn/target/route.rs @@ -294,7 +294,7 @@ mod tests { #[test] fn committed_page_session_route_is_stable_across_foreground_selection() { - let mut connection = CdpConnection::new(); + let mut connection = crate::test_support::connection(); let mut browser_context = BrowserContext::new("BID-route".to_owned()); browser_context.set_active_target_id("TID-a"); browser_context.attach_active_session("SID-a"); @@ -338,7 +338,7 @@ mod tests { #[test] fn target_binding_is_not_globally_routable_before_session_commit() { - let mut connection = CdpConnection::new(); + let mut connection = crate::test_support::connection(); let mut browser_context = BrowserContext::new_with_page_for_test("BID-route", "TID-page"); browser_context.attach_active_session("SID-prepared"); connection.browser_context = Some(browser_context); diff --git a/moli-protocol/src/conn/target/session_binding.rs b/moli-protocol/src/conn/target/session_binding.rs index 660c35f7d3..1da309ecfe 100644 --- a/moli-protocol/src/conn/target/session_binding.rs +++ b/moli-protocol/src/conn/target/session_binding.rs @@ -1,6 +1,6 @@ #[cfg(test)] use crate::conn::BrowserContext; -use crate::conn::CdpConnection; +use crate::conn::{CdpConnection, DocumentPolicyUpdate}; use crate::devtools_runtime::DevToolsTargetInfo; use super::{ @@ -111,6 +111,30 @@ impl CdpConnection { } } + #[cfg(test)] + pub(crate) fn new_browser_context_fixture_for_test( + &self, + id: impl Into, + ) -> BrowserContext { + let mut context = BrowserContext::new_with_browser_for_test(&self.browser, id); + context.bind_page_navigation_engines( + self.navigation_runtime_config.clone(), + self.scheduler_hooks.renderer_publication_sender(), + ); + context + } + + #[cfg(test)] + pub(crate) fn new_page_target_fixture_for_test( + &self, + id: impl Into, + target_id: impl Into, + ) -> BrowserContext { + let mut context = self.new_browser_context_fixture_for_test(id); + context.set_active_target_id(target_id); + context + } + #[cfg(test)] pub(crate) fn install_browser_context_fixture_for_test( &mut self, @@ -118,8 +142,9 @@ impl CdpConnection { ) { // Mirror production Context insertion. Document admission must never // fall back to a connection/another WebContents' navigation engine. + browser_context.apply_browser_cache_disabled(self.browser_global_overrides.cache_disabled); browser_context.bind_page_navigation_engines( - self.standalone_navigation_engine.runtime_config(), + self.navigation_runtime_config.clone(), self.scheduler_hooks.renderer_publication_sender(), ); self.browser_context = Some(browser_context); @@ -131,8 +156,9 @@ impl CdpConnection { &mut self, mut browser_context: BrowserContext, ) { + browser_context.apply_browser_cache_disabled(self.browser_global_overrides.cache_disabled); browser_context.bind_page_navigation_engines( - self.standalone_navigation_engine.runtime_config(), + self.navigation_runtime_config.clone(), self.scheduler_hooks.renderer_publication_sender(), ); self.inactive_browser_contexts.push(browser_context); @@ -151,13 +177,80 @@ impl CdpConnection { else { return Ok(()); }; + let global_extra_headers = self.browser_global_overrides.extra_headers.clone(); - let Some(browser_context) = self.browser_context_by_id_mut(&browser_context_id) else { + let pending = { + let Some(browser_context) = self.browser_context_by_id_mut(&browser_context_id) else { + return Ok(()); + }; + let Some(target) = browser_context.page_target_mut(&target_id) else { + return Ok(()); + }; + let listener_session_id = session_key.wire_session_id().map(str::to_owned); + match &session_key { + moli_page_types::DevToolsSessionKey::Primary => { + target.runtime_slot.disable_primary_network_events(); + } + moli_page_types::DevToolsSessionKey::Attached(attached_session_id) => { + target + .runtime_slot + .remove_attached_network_session(attached_session_id); + } + } + target + .runtime_slot + .remove_network_session_observation_cursor(listener_session_id.as_deref()); + target + .runtime_slot + .remove_captured_response_body_visibility_for_session( + listener_session_id.as_deref(), + ); + if !target.runtime_slot.has_network_event_listeners() { + target.runtime_slot.clear_captured_response_bodies(); + target.runtime_slot.clear_websocket_request_ids(); + } + browser_context.clear_devtools_network_state_for_target(&target_id, &session_key); + let effective = browser_context.effective_policy_for_target(&target_id); + let headers = browser_context.merged_extra_headers_for_target_policy( + &global_extra_headers, + effective.extra_headers(), + ); + browser_context + .document_handle_for_target(&target_id) + .map(|document| { + browser_context.start_document_policy_update( + document, + DocumentPolicyUpdate::NetworkRequestPolicy { + extra_headers: headers, + bypass_service_worker: effective.bypass_service_worker(), + cache_disabled: effective.cache_disabled(), + blocked_url_patterns: effective.blocked_url_patterns().to_vec(), + }, + ) + }) + .transpose() + .map_err(anyhow::Error::msg)? + }; + let Some(pending) = pending else { return Ok(()); }; - browser_context - .clear_devtools_network_session_policy_async(&target_id, &session_key) - .await + let completed = pending.wait().await; + let document = completed.document(); + match self.finish_document_policy_update(completed) { + Ok(()) => Ok(()), + Err(error) + if error == "Document changed" + && self + .browser_context_by_id(&browser_context_id) + .and_then(|context| context.document_handle_for_target(&target_id)) + != Some(document) => + { + Ok(()) + } + Err(error) => Err(anyhow::anyhow!( + "failed to restore detached session network request policy: {error}" + )), + } } pub(crate) async fn clear_devtools_emulation_session_policy_async( @@ -173,11 +266,44 @@ impl CdpConnection { return Ok(()); }; - let policy_result = match self.browser_context_by_id_mut(&browser_context_id) { - Some(browser_context) => { + let browser_globals = self.browser_global_overrides.clone(); + let pending_policy = match self.browser_context_by_id_mut(&browser_context_id) { + Some(browser_context) if browser_context.page_target(&target_id).is_some() => { browser_context - .clear_devtools_emulation_session_policy_async(&target_id, &session_key) - .await + .clear_devtools_emulation_policy_state_for_target(&target_id, &session_key); + let foreground = browser_context.is_active_target(&target_id); + browser_context + .document_handle_for_target(&target_id) + .map(|document| { + browser_context.start_document_policy_batch_with_surface( + document, + Vec::new(), + foreground, + browser_globals.network_conditions, + browser_globals.geolocation.as_ref(), + ) + }) + } + _ => None, + }; + let policy_result = match pending_policy { + Some(pending) => { + let completed = pending.wait().await; + let document = completed.document(); + match self.finish_document_policy_batch(completed) { + Ok(()) => Ok(()), + Err(_) + if self + .browser_context_by_id(&browser_context_id) + .and_then(|context| context.document_handle_for_target(&target_id)) + != Some(document) => + { + Ok(()) + } + Err(error) => Err(anyhow::anyhow!( + "failed to restore detached session document policy: {error}" + )), + } } None => Ok(()), }; @@ -194,16 +320,52 @@ impl CdpConnection { else { return Ok(()); }; - let completion = pending.wait().await.map_err(|error| { - anyhow::anyhow!("failed to restore detached session user agent: {error}") - })?; - self.finish_rebuild_resource_runtime_for_session_owner(Some(session_id), completion) + let completed = pending.wait().await; + self.finish_document_resource_runtime_update(completed) .map_err(anyhow::Error::msg) } .await; policy_result.and(identity_result) } + pub(crate) async fn reset_primary_page_session_target_state_async( + &mut self, + browser_context_id: &str, + target_id: &str, + session_id: &str, + ) -> anyhow::Result { + let browser_globals = self.browser_global_overrides.clone(); + let (found, pending) = self + .browser_context_by_id_mut(browser_context_id) + .map(|context| { + context.start_reset_primary_page_session_target_state( + target_id, + session_id, + &browser_globals, + ) + }) + .unwrap_or((false, None)); + let Some(pending) = pending else { + return Ok(found); + }; + let completed = pending.wait().await; + let document = completed.document(); + match self.finish_document_policy_batch(completed) { + Ok(()) => Ok(found), + Err(_) + if self + .browser_context_by_id(browser_context_id) + .and_then(|context| context.document_handle_for_target(target_id)) + != Some(document) => + { + Ok(found) + } + Err(error) => Err(anyhow::anyhow!( + "failed to reset primary Page session document policy: {error}" + )), + } + } + pub(crate) fn is_browser_session_id(&self, session_id: Option<&str>) -> bool { let Some(session_id) = session_id else { return false; diff --git a/moli-protocol/src/conn/target/worker_auto_attach.rs b/moli-protocol/src/conn/target/worker_auto_attach.rs index 4b058c7770..5b0c7425da 100644 --- a/moli-protocol/src/conn/target/worker_auto_attach.rs +++ b/moli-protocol/src/conn/target/worker_auto_attach.rs @@ -1,6 +1,5 @@ use crate::conn::{ - BrowserContext, CdpConnection, ServiceWorkerAutoAttachRelatedOwner, - ServiceWorkerAutoAttachRelatedOwnerSession, + CdpConnection, ServiceWorkerAutoAttachRelatedOwner, ServiceWorkerAutoAttachRelatedOwnerSession, }; impl CdpConnection { @@ -125,9 +124,7 @@ impl CdpConnection { ) }) .collect::>(); - browser_context - .renderer_runtime() - .set_service_worker_related_pause_on_start_policies_for_devtools(policies); + browser_context.set_service_worker_related_pause_on_start_policies(policies); } } @@ -145,12 +142,8 @@ impl CdpConnection { .remove(&key); } let pause = self.service_worker_pause_on_start_for_devtools(); - let runtimes = self - .browser_contexts() - .map(BrowserContext::renderer_runtime) - .collect::>(); - for runtime in runtimes { - runtime.set_service_worker_pause_on_start_for_devtools(pause); + for context in self.browser_contexts() { + context.set_service_worker_pause_on_start(pause); } pause } diff --git a/moli-protocol/src/conn/tests/cookie_surfaces/boundary.rs b/moli-protocol/src/conn/tests/cookie_surfaces/boundary.rs index f1742f4269..551bd4c3be 100644 --- a/moli-protocol/src/conn/tests/cookie_surfaces/boundary.rs +++ b/moli-protocol/src/conn/tests/cookie_surfaces/boundary.rs @@ -2,8 +2,8 @@ use super::*; #[test] fn browser_context_document_cookie_facade_snapshot_preview_does_not_bump_cookie_store_generation() { - let mut conn = CdpConnection::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-cookie-facade", "TID-cookie-facade"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-cookie-facade", "TID-cookie-facade"); bc.set_target_url("https://example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); @@ -52,9 +52,9 @@ fn browser_context_document_cookie_facade_snapshot_preview_does_not_bump_cookie_ #[test] fn browser_context_cookie_boundary_snapshot_aligns_facade_and_storage_generation() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut bc = - BrowserContext::new_with_page_for_test("BID-cookie-boundary", "TID-cookie-boundary"); + conn.new_page_target_fixture_for_test("BID-cookie-boundary", "TID-cookie-boundary"); bc.set_target_url("https://example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); @@ -96,9 +96,9 @@ fn browser_context_cookie_boundary_snapshot_aligns_facade_and_storage_generation #[test] fn connection_cookie_boundary_snapshot_for_sites_keeps_facade_but_filters_storage_slice() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut bc = - BrowserContext::new_with_page_for_test("BID-cookie-boundary", "TID-cookie-boundary"); + conn.new_page_target_fixture_for_test("BID-cookie-boundary", "TID-cookie-boundary"); bc.set_target_url("https://sub.example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); @@ -166,9 +166,9 @@ fn connection_cookie_boundary_snapshot_for_sites_keeps_facade_but_filters_storag #[test] fn browser_context_preview_cookie_boundary_operation_projects_hypothetical_storage_only() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut bc = - BrowserContext::new_with_page_for_test("BID-cookie-boundary", "TID-cookie-boundary"); + conn.new_page_target_fixture_for_test("BID-cookie-boundary", "TID-cookie-boundary"); bc.set_target_url("https://example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); @@ -276,9 +276,9 @@ fn browser_context_preview_cookie_boundary_operation_with_site_target_projects_t #[test] fn connection_apply_cookie_boundary_operation_reports_replaced_and_resulting_boundary() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut bc = - BrowserContext::new_with_page_for_test("BID-cookie-boundary", "TID-cookie-boundary"); + conn.new_page_target_fixture_for_test("BID-cookie-boundary", "TID-cookie-boundary"); bc.set_target_url("https://example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); @@ -321,9 +321,9 @@ fn connection_apply_cookie_boundary_operation_reports_replaced_and_resulting_bou #[test] fn browser_context_site_data_manager_surface_wraps_cookie_boundary_with_reserved_future_storage() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut bc = - BrowserContext::new_with_page_for_test("BID-site-data-manager", "TID-site-data-manager"); + conn.new_page_target_fixture_for_test("BID-site-data-manager", "TID-site-data-manager"); bc.set_target_url("https://sub.example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); @@ -395,9 +395,9 @@ fn browser_context_site_data_manager_surface_wraps_cookie_boundary_with_reserved #[test] fn browser_context_site_data_manager_operation_wraps_cookie_boundary_preview_and_report() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut bc = - BrowserContext::new_with_page_for_test("BID-site-data-manager", "TID-site-data-manager"); + conn.new_page_target_fixture_for_test("BID-site-data-manager", "TID-site-data-manager"); bc.set_target_url("https://example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/conn/tests/cookie_surfaces/policy.rs b/moli-protocol/src/conn/tests/cookie_surfaces/policy.rs index 0cb5ac6cbc..e65647c907 100644 --- a/moli-protocol/src/conn/tests/cookie_surfaces/policy.rs +++ b/moli-protocol/src/conn/tests/cookie_surfaces/policy.rs @@ -1,11 +1,9 @@ use super::*; #[tokio::test] async fn browser_context_document_cookie_facade_overrides_apply_to_new_loaded_page() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-cookie-facade", - "TID-cookie-facade", - )); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_page_target_fixture_for_test("BID-cookie-facade", "TID-cookie-facade")); conn.browser_context .as_mut() .unwrap() @@ -28,7 +26,7 @@ async fn browser_context_document_cookie_facade_overrides_apply_to_new_loaded_pa conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; let payload = conn @@ -61,11 +59,9 @@ async fn browser_context_document_cookie_facade_overrides_apply_to_new_loaded_pa } #[tokio::test] async fn browser_context_document_cookie_facade_overrides_update_live_page() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-cookie-facade", - "TID-cookie-facade", - )); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_page_target_fixture_for_test("BID-cookie-facade", "TID-cookie-facade")); let navigation = conn .build_loaded_navigation_from_buffered_response_async( Url::parse("https://example.com/app").unwrap(), @@ -80,7 +76,7 @@ async fn browser_context_document_cookie_facade_overrides_update_live_page() { conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; let before = conn @@ -130,8 +126,8 @@ async fn browser_context_document_cookie_facade_overrides_update_live_page() { #[tokio::test] async fn browser_context_document_cookie_browser_context_overrides_update_live_page() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-cookie-context-overrides", "TID-cookie-context-overrides", )); @@ -149,7 +145,7 @@ async fn browser_context_document_cookie_browser_context_overrides_update_live_p conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; let before = conn @@ -312,8 +308,8 @@ async fn browser_context_cookie_manager_surface_tracks_policy_without_a_live_pag #[tokio::test] async fn browser_context_document_cookie_capability_and_freshness_snapshots_project_owner_surfaces() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-cookie-capability-surface", "TID-cookie-capability-surface", )); @@ -331,7 +327,7 @@ async fn browser_context_document_cookie_capability_and_freshness_snapshots_proj conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; conn.browser_context @@ -731,8 +727,8 @@ async fn browser_context_document_cookie_capability_and_freshness_snapshots_proj #[tokio::test] async fn browser_context_cookie_manager_surface_projects_document_capability_and_activity() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-cookie-manager-projection", "TID-cookie-manager-projection", )); @@ -750,7 +746,7 @@ async fn browser_context_cookie_manager_surface_projects_document_capability_and conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; conn.browser_context .as_mut() diff --git a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/freshness.rs b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/freshness.rs index 7b0a061d8f..62fd579b4c 100644 --- a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/freshness.rs +++ b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/freshness.rs @@ -1,8 +1,8 @@ use super::*; #[test] fn browser_context_document_cookie_facade_snapshot_tracks_shared_cookie_store_generation() { - let mut conn = CdpConnection::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-cookie-facade", "TID-cookie-facade"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-cookie-facade", "TID-cookie-facade"); bc.set_target_url("https://example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); @@ -61,8 +61,8 @@ fn browser_context_document_cookie_facade_snapshot_tracks_shared_cookie_store_ge #[tokio::test] async fn browser_context_document_cookie_facade_snapshot_projects_cookie_get_freshness_state() { - let mut conn = CdpConnection::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-cookie-facade", "TID-cookie-facade"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-cookie-facade", "TID-cookie-facade"); bc.set_target_url("https://example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); @@ -80,7 +80,7 @@ async fn browser_context_document_cookie_facade_snapshot_projects_cookie_get_fre conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; let before_read = conn diff --git a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/lifecycle.rs b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/lifecycle.rs index 20ce651964..98de96049d 100644 --- a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/lifecycle.rs +++ b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/lifecycle.rs @@ -1,11 +1,9 @@ use super::*; #[tokio::test] async fn browser_context_document_id_tracks_attach_and_detach() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-cookie-facade", - "TID-cookie-facade", - )); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_page_target_fixture_for_test("BID-cookie-facade", "TID-cookie-facade")); let before = conn .browser_context @@ -87,7 +85,7 @@ async fn browser_context_document_id_tracks_attach_and_detach() { conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; let after_attach = conn @@ -173,20 +171,25 @@ async fn browser_context_document_id_tracks_attach_and_detach() { assert_eq!(after_detach.document_id, None); assert_ne!(attached_page_id, 0); assert_eq!(after_detach.cookie_store_generation, Some(0)); - assert_eq!(after_detach.structured_write.default_cookie_write_url, None); + // Retiring a Document does not retire its WebContents or last committed + // URL. The structured-cookie fallback now names that surviving owner. + assert_eq!( + after_detach.structured_write.default_cookie_write_url, + Some(Url::parse("https://example.com/app").unwrap()) + ); assert_eq!( after_detach .structured_write .default_cookie_write_url_source, - BrowserContextDefaultCookieWriteUrlSource::Unavailable + BrowserContextDefaultCookieWriteUrlSource::BrowserContextUrl ); assert_eq!( after_detach.structured_write.readiness_status, - BrowserContextStructuredCookieWriteReadinessStatus::MissingScopedUrl + BrowserContextStructuredCookieWriteReadinessStatus::ReadyUsingBrowserContextUrl ); assert_eq!( after_detach.structured_write.default_command_verdict, - BrowserContextStructuredCookieCommandVerdict::MissingScopedUrl + BrowserContextStructuredCookieCommandVerdict::Ready ); assert_eq!( after_detach.structured_write.backend_status, diff --git a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/write_url/no_live_page/mod.rs b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/write_url/no_live_page/mod.rs index 54c7b39013..633e6f47ac 100644 --- a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/write_url/no_live_page/mod.rs +++ b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/write_url/no_live_page/mod.rs @@ -5,8 +5,8 @@ mod freshness; mod structured_write; async fn configured_connection_without_live_page() -> CdpConnection { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-cookie-facade".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-cookie-facade")); conn.browser_context .as_mut() .unwrap() diff --git a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/write_url/owner.rs b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/write_url/owner.rs index 7e5db486ed..4740ce5ccd 100644 --- a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/write_url/owner.rs +++ b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/facade/write_url/owner.rs @@ -1,7 +1,8 @@ use super::*; #[tokio::test] async fn browser_context_document_cookie_facade_snapshot_projects_default_cookie_write_url_owner() { - let mut bc = BrowserContext::new_with_page_for_test("BID-cookie-facade", "TID-cookie-facade"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-cookie-facade", "TID-cookie-facade"); bc.set_target_url("https://example.com/app".into()); let before_load = bc.document_cookie_facade_snapshot(); @@ -53,7 +54,7 @@ async fn browser_context_document_cookie_facade_snapshot_projects_default_cookie before_load.structured_write ); - let mut conn = CdpConnection::new(); + conn.install_browser_context_fixture_for_test(bc); let navigation = conn .build_loaded_navigation_from_buffered_response_async( Url::parse("https://live.example.com/page").unwrap(), @@ -65,7 +66,8 @@ async fn browser_context_document_cookie_facade_snapshot_projects_default_cookie ) .await .expect("navigation should build"); - bc.set_loaded_page_async(navigation.page).await; + let bc = conn.browser_context.as_mut().unwrap(); + bc.commit_active_navigation_for_test(navigation.page).await; let after_load = bc.document_cookie_facade_snapshot_async().await; let after_load_manager = bc.cookie_manager_surface_snapshot_async().await; diff --git a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/projection.rs b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/projection.rs index a7214732a0..f1abbc4fe9 100644 --- a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/projection.rs +++ b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/projection.rs @@ -1,8 +1,8 @@ use super::*; #[tokio::test] async fn browser_context_cookie_manager_surface_projects_live_effective_browser_context() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-cookie-manager-context", "TID-cookie-manager-context", )); @@ -20,7 +20,7 @@ async fn browser_context_cookie_manager_surface_projects_live_effective_browser_ conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; conn.browser_context @@ -162,8 +162,8 @@ async fn browser_context_cookie_manager_surface_projects_live_effective_browser_ #[tokio::test] async fn browser_context_cookie_manager_surface_tracks_schemeful_site_relationships() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-cookie-manager-schemeful-context", "TID-cookie-manager-schemeful-context", )); @@ -181,7 +181,7 @@ async fn browser_context_cookie_manager_surface_tracks_schemeful_site_relationsh conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; conn.browser_context @@ -272,8 +272,8 @@ async fn browser_context_cookie_manager_surface_tracks_schemeful_site_relationsh #[tokio::test] async fn browser_context_cookie_manager_surface_tracks_redirected_navigation_transition() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-cookie-manager-redirected-navigation", "TID-cookie-manager-redirected-navigation", )); @@ -313,7 +313,7 @@ async fn browser_context_cookie_manager_surface_tracks_redirected_navigation_tra conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; let context = conn @@ -358,8 +358,8 @@ async fn browser_context_cookie_manager_surface_tracks_redirected_navigation_tra #[tokio::test] async fn browser_context_cookie_manager_surface_distinguishes_same_document_url_updates_from_redirects() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-cookie-manager-same-document-transition", "TID-cookie-manager-same-document-transition", )); @@ -377,7 +377,7 @@ async fn browser_context_cookie_manager_surface_distinguishes_same_document_url_ conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; conn.evaluate_runtime_expression_with_await_async( @@ -436,8 +436,8 @@ async fn browser_context_cookie_manager_surface_distinguishes_same_document_url_ #[tokio::test] async fn browser_context_cookie_manager_surface_projects_navigation_initiator_relationships() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-cookie-manager-navigation-initiator", "TID-cookie-manager-navigation-initiator", )); @@ -456,7 +456,7 @@ async fn browser_context_cookie_manager_surface_projects_navigation_initiator_re conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(initial_navigation.page) + .commit_active_navigation_for_test(initial_navigation.page) .await; let next_navigation = conn @@ -473,7 +473,7 @@ async fn browser_context_cookie_manager_surface_projects_navigation_initiator_re conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(next_navigation.page) + .commit_active_navigation_for_test(next_navigation.page) .await; let context = conn diff --git a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/telemetry.rs b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/telemetry.rs index a8cdb9addb..40b1bfc0ab 100644 --- a/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/telemetry.rs +++ b/moli-protocol/src/conn/tests/cookie_surfaces/snapshots/telemetry.rs @@ -1,11 +1,9 @@ use super::*; #[tokio::test] async fn browser_context_document_cookie_snapshots_reflect_live_page_state() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-cookie-facade", - "TID-cookie-facade", - )); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_page_target_fixture_for_test("BID-cookie-facade", "TID-cookie-facade")); let navigation = conn .build_loaded_navigation_from_buffered_response_async( Url::parse("https://example.com/app").unwrap(), @@ -20,7 +18,7 @@ async fn browser_context_document_cookie_snapshots_reflect_live_page_state() { conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; let before = conn @@ -117,8 +115,8 @@ async fn browser_context_document_cookie_snapshots_reflect_live_page_state() { #[tokio::test] async fn browser_context_document_cookie_facade_snapshot_projects_probe_telemetry_into_owner_view() { - let mut conn = CdpConnection::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-cookie-facade", "TID-cookie-facade"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-cookie-facade", "TID-cookie-facade"); bc.set_target_url("https://example.com/app".into()); conn.install_browser_context_fixture_for_test(bc); @@ -136,7 +134,7 @@ async fn browser_context_document_cookie_facade_snapshot_projects_probe_telemetr conn.browser_context .as_mut() .unwrap() - .set_loaded_page_async(navigation.page) + .commit_active_navigation_for_test(navigation.page) .await; let payload = conn diff --git a/moli-protocol/src/conn/tests/message.rs b/moli-protocol/src/conn/tests/message.rs index 57bef0f7c3..282ee1dc42 100644 --- a/moli-protocol/src/conn/tests/message.rs +++ b/moli-protocol/src/conn/tests/message.rs @@ -2,7 +2,7 @@ use super::*; #[tokio::test] async fn process_message_invalid_json_and_invalid_method_cases_match_current_cdp_entrypoint() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let parse_error = conn.process_message_messages_only_for_test("invalid").await; assert_eq!( @@ -51,8 +51,8 @@ async fn process_message_invalid_json_and_invalid_method_cases_match_current_cdp #[tokio::test] async fn session_scoped_unknown_domain_error_keeps_session_id() { - let mut conn = CdpConnection::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); conn.install_browser_context_fixture_for_test(bc); @@ -74,8 +74,8 @@ async fn session_scoped_unknown_domain_error_keeps_session_id() { #[tokio::test] async fn session_scoped_unknown_method_error_keeps_session_id() { - let mut conn = CdpConnection::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); conn.install_browser_context_fixture_for_test(bc); @@ -97,7 +97,7 @@ async fn session_scoped_unknown_method_error_keeps_session_id() { #[tokio::test] async fn browser_and_tab_sessions_do_not_fall_through_to_the_active_page() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.install_default_browser_target(); conn.register_browser_session("SID-browser".to_owned()); let default_tab_target_id = conn.default_tab_target_id().to_owned(); @@ -131,8 +131,8 @@ async fn browser_and_tab_sessions_do_not_fall_through_to_the_active_page() { #[tokio::test] async fn session_scoped_handler_error_keeps_session_id_even_when_domain_uses_plain_error_helper() { - let mut conn = CdpConnection::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); conn.install_browser_context_fixture_for_test(bc); @@ -154,8 +154,8 @@ async fn session_scoped_handler_error_keeps_session_id_even_when_domain_uses_pla #[tokio::test] async fn puppeteer_bootstrap_domains_are_session_scoped_noops() { - let mut conn = CdpConnection::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); conn.install_browser_context_fixture_for_test(bc); @@ -183,7 +183,7 @@ async fn puppeteer_bootstrap_domains_are_session_scoped_noops() { #[tokio::test] async fn startup_session_id_uses_startup_dispatch_without_browser_context() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let generic = conn .process_message_messages_only_for_test( @@ -212,7 +212,7 @@ async fn startup_session_id_uses_startup_dispatch_without_browser_context() { "about:blank" ); - conn.browser_context = Some(BrowserContext::new("BID-1".into())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1")); let still_startup = conn .process_message_messages_only_for_test( r#"{"id":4,"method":"Hi.there","sessionId":"STARTUP"}"#, diff --git a/moli-protocol/src/conn/tests/mod.rs b/moli-protocol/src/conn/tests/mod.rs index 80662e8992..52909386e6 100644 --- a/moli-protocol/src/conn/tests/mod.rs +++ b/moli-protocol/src/conn/tests/mod.rs @@ -28,10 +28,7 @@ use moli_cookie_jar::{ StoredCookieSetRejectionReason, StoredCookieSourceScheme, }; use moli_core::page::RendererServiceWorkerVersionStatus; -use moli_core::{ - LayoutPolicy, OptionalResourceFetchMask, - runtime::{NavigationEngine, NavigationRuntimeConfig}, -}; +use moli_core::{LayoutPolicy, OptionalResourceFetchMask, runtime::NavigationRuntimeConfig}; use moli_fetch::FetchConfig; use moli_shared_worker::SharedWorkerInstanceId; use serde_json::json; @@ -49,18 +46,24 @@ mod site_data; #[test] fn published_default_target_defers_navigation_runtime_until_materialization() { - let mut conn = CdpConnection::new_with_deferred_navigation_runtime( + let mut conn = crate::test_support::connection_with_config( crate::CdpInitialStoragePartition::memory(), NavigationRuntimeConfig::default(), ); - assert!(!conn.standalone_navigation_engine.is_materialized()); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); let (renderer_publication_sender, _renderer_publication_receiver) = moli_core::renderer_output_transport_channel(); conn.set_renderer_publication_sender(renderer_publication_sender); conn.publish_default_browser_target(); - assert!(!conn.standalone_navigation_engine.is_materialized()); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); let page = conn .devtools_target_info(conn.default_target_id()) .expect("published default page target"); @@ -72,15 +75,15 @@ fn published_default_target_defers_navigation_runtime_until_materialization() { conn.install_default_browser_target(); - assert!( - !conn.standalone_navigation_engine.is_materialized(), - "materializing the default Page must not allocate the fallback engine" + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(1), + "materializing the default Page must allocate only its Core-owned engine" ); assert!( conn.browser_context .as_ref() - .and_then(|context| context.page_navigation_engine(conn.default_target_id())) - .is_some(), + .is_some_and(|context| context.target_has_navigation_engine(conn.default_target_id())), "materializing the default target must install its Page-owned engine" ); assert!( @@ -96,7 +99,7 @@ fn published_default_target_defers_navigation_runtime_until_materialization() { } fn deferred_default_connection() -> CdpConnection { - let mut conn = CdpConnection::new_with_deferred_navigation_runtime( + let mut conn = crate::test_support::connection_with_config( crate::CdpInitialStoragePartition::memory(), NavigationRuntimeConfig::default(), ); @@ -118,7 +121,10 @@ async fn activating_the_only_default_placeholder_does_not_start_the_runtime() { let messages = conn.process_message_messages_only_for_test(&raw).await; assert_eq!(messages, vec![json!({ "id": 1, "result": {} })]); - assert!(!conn.standalone_navigation_engine.is_materialized()); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); assert!(conn.devtools_target_info(target_id).is_some()); } @@ -138,7 +144,10 @@ async fn closing_the_default_placeholder_does_not_start_the_runtime() { messages.first(), Some(&json!({ "id": 2, "result": { "success": true } })) ); - assert!(!conn.standalone_navigation_engine.is_materialized()); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); assert!( conn.devtools_target_info(conn.default_target_id()) .is_none() @@ -167,15 +176,15 @@ async fn creating_a_target_preserves_the_published_default_as_a_placeholder() { .expect("createTarget response"); assert_ne!(created_target_id, conn.default_target_id()); - assert!( - !conn.standalone_navigation_engine.is_materialized(), - "creating a WebContents must not materialize the standalone placeholder engine" + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(1), + "creating a WebContents must allocate only its Core-owned engine" ); assert!( conn.browser_context .as_ref() - .and_then(|context| context.page_navigation_engine(created_target_id)) - .is_some(), + .is_some_and(|context| context.target_has_navigation_engine(created_target_id)), "the created Page must own the materialized navigation engine" ); assert!(conn.default_target_lifecycle.is_placeholder()); @@ -219,8 +228,8 @@ async fn creating_a_target_preserves_the_published_default_as_a_placeholder() { } #[test] -fn idle_navigation_engine_reset_preserves_mock_layout_policy() { - let mut conn = CdpConnection::new_with_initial_storage_partition_and_runtime_config( +fn idle_navigation_diagnostics_preserve_layout_without_creating_an_engine() { + let conn = crate::test_support::connection_with_config( crate::CdpInitialStoragePartition::memory(), NavigationRuntimeConfig::new( FetchConfig::default(), @@ -230,22 +239,27 @@ fn idle_navigation_engine_reset_preserves_mock_layout_policy() { ), ); - assert_eq!( - conn.standalone_navigation_engine.layout_policy(), - LayoutPolicy::Mock - ); - let reset = conn.release_idle_navigation_engine_memory_if_idle(); + assert_eq!(conn.layout_policy(), LayoutPolicy::Mock); + let reset = conn.moli_reset_idle_navigation_engine_for_diagnostics(); - assert!(reset.reset); + assert_eq!(reset["reset"], json!(false)); + assert_eq!(reset["reason"], json!("no-standalone-engine")); + let diagnostics = conn.moli_memory_diagnostics(); + assert!(diagnostics["connection"]["activeNavigationEngine"].is_null()); assert_eq!( - conn.standalone_navigation_engine.layout_policy(), - LayoutPolicy::Mock + diagnostics["isolateScope"]["activeNavigationEngineRendererOwnerCount"], + json!(0) ); + assert_eq!( + diagnostics["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); + assert_eq!(conn.layout_policy(), LayoutPolicy::Mock); } #[tokio::test] async fn browser_context_install_and_removal_preserve_mock_layout_policy() { - let mut conn = CdpConnection::new_with_initial_storage_partition_and_runtime_config( + let mut conn = crate::test_support::connection_with_config( crate::CdpInitialStoragePartition::memory(), NavigationRuntimeConfig::new( FetchConfig::default(), @@ -254,26 +268,20 @@ async fn browser_context_install_and_removal_preserve_mock_layout_policy() { LayoutPolicy::Mock, ), ); - conn.insert_browser_context(BrowserContext::new("CTX-layout".to_owned())); + conn.insert_browser_context(conn.new_browser_context_fixture_for_test("CTX-layout".to_owned())); - assert_eq!( - conn.standalone_navigation_engine.layout_policy(), - LayoutPolicy::Mock - ); + assert_eq!(conn.layout_policy(), LayoutPolicy::Mock); let removed = conn .remove_browser_context_by_id_restoring_active_async("CTX-layout", None) .await; assert!(removed.is_some()); - assert_eq!( - conn.standalone_navigation_engine.layout_policy(), - LayoutPolicy::Mock - ); + assert_eq!(conn.layout_policy(), LayoutPolicy::Mock); } #[test] fn global_io_stream_ids_cross_u32_max_without_reuse() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.next_global_io_stream_id = u32::MAX as u64; let handle = conn.open_global_io_stream(b"payload".to_vec()); @@ -285,7 +293,7 @@ fn global_io_stream_ids_cross_u32_max_without_reuse() { #[test] #[should_panic(expected = "global IO stream id space exhausted")] fn global_io_stream_id_allocator_rejects_u64_exhaustion() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.next_global_io_stream_id = u64::MAX; let _ = conn.open_global_io_stream(Vec::new()); @@ -294,7 +302,7 @@ fn global_io_stream_id_allocator_rejects_u64_exhaustion() { #[test] #[should_panic(expected = "internal Runtime command id space exhausted")] fn internal_runtime_command_id_allocator_rejects_u64_exhaustion() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.next_internal_runtime_command_id = u64::MAX; let _ = conn.next_internal_runtime_command_id(); @@ -302,7 +310,7 @@ fn internal_runtime_command_id_allocator_rejects_u64_exhaustion() { #[test] fn replace_root_target_discovery_is_noop_when_already_enabled() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let filter = vec![DevToolsTargetFilterEntry { exclude: false, target_type: Some("service_worker".to_owned()), @@ -317,7 +325,7 @@ fn replace_root_target_discovery_is_noop_when_already_enabled() { #[test] fn command_response_flush_permit_is_unique_and_scoped_to_its_context() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let (first_permit, first_context) = conn.begin_command_response_flush_permit(); let first_receiver = first_context @@ -349,7 +357,7 @@ fn command_response_flush_permit_is_unique_and_scoped_to_its_context() { #[test] fn dropping_command_response_flush_permit_cancels_its_observers() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let (permit, context) = conn.begin_command_response_flush_permit(); let receiver = context .receiver() @@ -369,7 +377,7 @@ fn dropping_command_response_flush_permit_cancels_its_observers() { #[test] fn command_response_flush_permit_runs_deferred_release_exactly_once() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let (permit, context) = conn.begin_command_response_flush_permit(); let releases = Arc::new(AtomicUsize::new(0)); let release_counter = releases.clone(); @@ -388,7 +396,7 @@ fn command_response_flush_permit_runs_deferred_release_exactly_once() { #[test] fn abandoned_command_response_flush_permit_releases_fail_open() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let (permit, context) = conn.begin_command_response_flush_permit(); let releases = Arc::new(AtomicUsize::new(0)); let release_counter = releases.clone(); @@ -420,13 +428,13 @@ fn missing_command_response_flush_context_releases_immediately() { #[test] fn background_navigation_completion_sender_routes_explicit_session_owners() { - let mut conn = CdpConnection::new(); - let mut active = BrowserContext::new("BID-active".to_owned()); + let mut conn = crate::test_support::connection(); + let mut active = conn.new_browser_context_fixture_for_test("BID-active".to_owned()); active.set_active_target_id("TID-active"); active.attach_active_session("SID-active"); conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive"); inactive.attach_active_session("SID-inactive"); conn.push_inactive_browser_context_fixture_for_test(inactive); @@ -452,8 +460,8 @@ fn background_navigation_completion_sender_routes_explicit_session_owners() { #[test] fn navigation_gate_resolves_websocket_events_to_their_session_target() { - let mut conn = CdpConnection::new(); - let mut target_a = BrowserContext::new("BID-A".to_owned()); + let mut conn = crate::test_support::connection(); + let mut target_a = conn.new_browser_context_fixture_for_test("BID-A".to_owned()); target_a.set_active_target_id("TID-A"); target_a.attach_active_session("SID-A"); let navigation_a = target_a @@ -462,7 +470,7 @@ fn navigation_gate_resolves_websocket_events_to_their_session_target() { conn.install_browser_context_fixture_for_test(target_a); assert!(conn.arm_background_navigation_completion(&navigation_a, None)); - let mut target_b = BrowserContext::new("BID-B".to_owned()); + let mut target_b = conn.new_browser_context_fixture_for_test("BID-B".to_owned()); target_b.set_active_target_id("TID-B"); target_b.attach_active_session("SID-B"); conn.push_inactive_browser_context_fixture_for_test(target_b); @@ -491,8 +499,8 @@ fn navigation_gate_resolves_websocket_events_to_their_session_target() { #[test] fn navigation_background_event_queue_drains_current_token() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("CTX-nav".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("CTX-nav".to_owned()); browser_context.set_active_target_id("TID-nav"); let token = browser_context .start_document_navigation_for_active_target("LOADER-1".to_owned()) @@ -515,50 +523,54 @@ fn navigation_background_event_queue_drains_current_token() { #[test] fn active_browser_context_installs_its_renderer_runtime_on_engine() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut browser_context = conn.new_browser_context("CTX-runtime".to_owned()); browser_context.set_active_target_id("TID-runtime"); - let renderer_runtime = browser_context.renderer_runtime(); + let renderer_runtime = browser_context.renderer_runtime_id_for_test(); conn.insert_browser_context(browser_context); - assert!( - conn.active_navigation_engine() - .browser_context_runtime() - .shares_state_with(&renderer_runtime) + assert_eq!( + conn.browser_context.as_ref().and_then(|context| { + context.page_navigation_browser_context_runtime_id_for_test("TID-runtime") + }), + Some(renderer_runtime) ); } #[test] fn activating_inactive_browser_context_switches_engine_renderer_runtime() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut first = conn.new_browser_context("CTX-first".to_owned()); first.set_active_target_id("TID-first"); conn.insert_browser_context(first); let mut second = conn.new_browser_context("CTX-second".to_owned()); second.set_active_target_id("TID-second"); - let second_renderer_runtime = second.renderer_runtime(); + let second_renderer_runtime = second.renderer_runtime_id_for_test(); conn.insert_browser_context(second); assert!(conn.activate_browser_context_by_id("CTX-second")); - assert!( - conn.active_navigation_engine() - .browser_context_runtime() - .shares_state_with(&second_renderer_runtime) + assert_eq!( + conn.browser_context.as_ref().and_then(|context| { + context.page_navigation_browser_context_runtime_id_for_test("TID-second") + }), + Some(second_renderer_runtime) ); } #[test] fn activating_another_browser_context_retains_pending_initial_document_engine() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut first = conn.new_browser_context("CTX-pending-first".to_owned()); first.set_active_target_id("TID-pending-first"); first.begin_active_target_initial_empty_document("about:blank".to_owned()); conn.insert_browser_context(first); let pending_renderer_owner = conn - .active_navigation_engine() - .renderer_owner_id_for_diagnostics(); + .browser_context + .as_ref() + .and_then(|context| context.page_navigation_renderer_owner_id("TID-pending-first")) + .expect("pending Page engine"); let mut second = conn.new_browser_context("CTX-pending-second".to_owned()); second.set_active_target_id("TID-pending-second"); @@ -568,25 +580,26 @@ fn activating_another_browser_context_retains_pending_initial_document_engine() let retained = conn .browser_context_by_id("CTX-pending-first") - .and_then(|context| context.page_navigation_engine("TID-pending-first")) + .and_then(|context| context.page_navigation_renderer_owner_id("TID-pending-first")) .expect("a pending initial document must keep its renderer engine alive"); assert_eq!( - retained.renderer_owner_id_for_diagnostics(), - pending_renderer_owner, + retained, pending_renderer_owner, "context switching must retain the exact engine that owns the pending renderer page" ); } #[test] fn activating_another_target_retains_pending_initial_document_engine() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut browser_context = conn.new_browser_context("CTX-pending-target".to_owned()); browser_context.set_active_target_id("TID-pending-active"); browser_context.begin_active_target_initial_empty_document("about:blank".to_owned()); conn.insert_browser_context(browser_context); let pending_renderer_owner = conn - .active_navigation_engine() - .renderer_owner_id_for_diagnostics(); + .browser_context + .as_ref() + .and_then(|context| context.page_navigation_renderer_owner_id("TID-pending-active")) + .expect("pending Page engine"); conn.browser_context .as_mut() .expect("browser context") @@ -604,24 +617,23 @@ fn activating_another_target_retains_pending_initial_document_engine() { let retained = conn .browser_context_by_id("CTX-pending-target") - .and_then(|context| context.page_navigation_engine("TID-pending-active")) + .and_then(|context| context.page_navigation_renderer_owner_id("TID-pending-active")) .expect("a pending initial document must survive target activation"); assert_eq!( - retained.renderer_owner_id_for_diagnostics(), - pending_renderer_owner, + retained, pending_renderer_owner, "target switching must retain the exact engine that owns the pending renderer page" ); } #[tokio::test] async fn removing_active_browser_context_switches_engine_to_activated_context() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut first = conn.new_browser_context("CTX-first".to_owned()); first.set_active_target_id("TID-first"); conn.insert_browser_context(first); let mut second = conn.new_browser_context("CTX-second".to_owned()); second.set_active_target_id("TID-second"); - let second_renderer_runtime = second.renderer_runtime(); + let second_renderer_runtime = second.renderer_runtime_id_for_test(); conn.insert_browser_context(second); let removed = conn @@ -634,16 +646,20 @@ async fn removing_active_browser_context_switches_engine_to_activated_context() conn.browser_context.as_ref().map(|bc| bc.id.as_str()), Some("CTX-second") ); - assert!( - conn.active_navigation_engine() - .browser_context_runtime() - .shares_state_with(&second_renderer_runtime) + assert_eq!( + conn.browser_context.as_ref().and_then(|context| { + context.page_navigation_browser_context_runtime_id_for_test("TID-second") + }), + Some(second_renderer_runtime) ); } #[tokio::test] async fn memory_diagnostics_reports_resource_runtime_storage() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); + let mut context = conn.new_browser_context_fixture_for_test("BID-resource-storage"); + context.set_active_target_id("TID-resource-storage"); + conn.install_browser_context_fixture_for_test(context); let resource_runtime = conn .ensure_resource_request_client() .expect("resource request client should initialize") @@ -720,10 +736,7 @@ async fn memory_diagnostics_reports_resource_runtime_storage() { #[tokio::test] async fn memory_diagnostics_reports_page_vm_document_isolate_model() { - let mut conn = CdpConnection::new(); - conn.replace_standalone_navigation_engine( - NavigationEngine::new_with_page_vm_document_isolate_for_diagnostics(), - ); + let mut conn = crate::test_support::connection(); let mut browser_context = conn.new_browser_context("BID-shared-diagnostics".to_owned()); browser_context.set_active_target_id("TID-shared-diagnostics-active"); @@ -748,8 +761,8 @@ async fn memory_diagnostics_reports_page_vm_document_isolate_model() { .await .expect("second shared diagnostics page should load"); let browser_context = conn.browser_context.as_mut().expect("browser context"); - browser_context.replace_target_page_for_test("TID-shared-diagnostics-active", Some(first_page)); - browser_context.replace_target_page_for_test("TID-shared-diagnostics-bg", Some(second_page)); + browser_context.commit_target_navigation_for_test("TID-shared-diagnostics-active", first_page); + browser_context.commit_target_navigation_for_test("TID-shared-diagnostics-bg", second_page); let pending_diagnostics = conn .start_moli_diagnostics() @@ -831,38 +844,39 @@ async fn memory_diagnostics_reports_page_vm_document_isolate_model() { #[tokio::test] async fn replacing_or_retiring_a_loaded_page_changes_its_attachment_identity() { - let mut conn = CdpConnection::new(); - let first_page = conn - .load_page_via_runtime_async("data:text/html,first") - .await - .expect("first Page"); - let second_page = conn - .load_page_via_runtime_async("data:text/html,second") - .await - .expect("second Page"); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-page-attachment", - "TID-page-attachment", - )); - let context = conn.browser_context.as_mut().unwrap(); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_page_target_fixture_for_test("BID-page-attachment", "TID-page-attachment")); assert_eq!( - context.target_document_id(context.active_target_id().unwrap()), + conn.browser_context + .as_ref() + .unwrap() + .target_document_id("TID-page-attachment"), None ); - assert!(context.replace_loaded_page(Some(first_page)).is_none()); - let first_attachment = context - .target_document_id(context.active_target_id().unwrap()) + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,first", + None, + ) + .await; + let first_attachment = conn + .browser_context + .as_ref() + .unwrap() + .target_document_id("TID-page-attachment") .expect("first Page attachment"); - let first = context - .replace_loaded_page(Some(second_page)) - .expect("first Page should be replaced"); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,second", + None, + ) + .await; + let context = conn.browser_context.as_mut().unwrap(); let second_attachment = context - .target_document_id(context.active_target_id().unwrap()) + .target_document_id("TID-page-attachment") .expect("second Page attachment"); assert_ne!(second_attachment, first_attachment); - let _ = first.close_async().await; let second = context .clear_loaded_page_with_reason(TargetPageAbsenceReason::TargetClosed) @@ -871,24 +885,24 @@ async fn replacing_or_retiring_a_loaded_page_changes_its_attachment_identity() { context.target_document_id(context.active_target_id().unwrap()), None ); - let _ = second.close_async().await; + second.close().await; } #[tokio::test(flavor = "multi_thread")] async fn moli_diagnostics_preserves_runtime_observable_diagnostics() { let mut ctx = TestContext::new(); - let page = ctx + let mut browser_context = ctx .conn - .load_page_via_runtime_async("data:text/html,diagnostics capture") - .await - .expect("diagnostics capture page should load"); - let mut browser_context = BrowserContext::new("BID-diagnostics-capture".to_owned()); + .new_browser_context_fixture_for_test("BID-diagnostics-capture".to_owned()); browser_context.set_active_target_id("TID-diagnostics-capture"); browser_context.attach_active_session("SID-diagnostics-capture"); - browser_context.set_target_url(page.final_url().as_str().to_owned()); - let _ = browser_context.replace_active_page_for_test(Some(page)); ctx.conn .install_browser_context_fixture_for_test(browser_context); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,diagnostics capture", + Some("SID-diagnostics-capture"), + ) + .await; ctx.process_async(json!({ "id": 44_100, @@ -944,7 +958,7 @@ async fn moli_diagnostics_preserves_runtime_observable_diagnostics() { #[tokio::test] async fn memory_diagnostics_excludes_empty_page_hosts_from_document_isolates() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut browser_context = conn.new_browser_context("BID-doc-owner-diagnostics".to_owned()); browser_context.set_active_target_id("TID-doc-owner-diagnostics-active"); browser_context.stage_background_target( @@ -969,18 +983,23 @@ async fn memory_diagnostics_excludes_empty_page_hosts_from_document_isolates() { .expect("second shared diagnostics page should load"); let browser_context = conn.browser_context.as_mut().expect("browser context"); browser_context - .replace_target_page_for_test("TID-doc-owner-diagnostics-active", Some(first_page)); - browser_context.replace_target_page_for_test("TID-doc-owner-diagnostics-bg", Some(second_page)); + .commit_target_navigation_for_test("TID-doc-owner-diagnostics-active", first_page); + browser_context.commit_target_navigation_for_test("TID-doc-owner-diagnostics-bg", second_page); - let mut empty_context = BrowserContext::new("BID-empty-page".to_owned()); + let mut empty_context = conn.new_browser_context_fixture_for_test("BID-empty-page".to_owned()); empty_context.set_active_target_id("TID-empty-page"); conn.insert_browser_context(empty_context); let empty_page_engine = conn .browser_context_by_id("BID-empty-page") - .and_then(|context| context.page_navigation_engine("TID-empty-page")) + .and_then(|context| context.page_navigation_renderer_owner_id("TID-empty-page")) .expect("empty PageAgentHost must own its navigation engine"); - assert!( - !empty_page_engine.shares_renderer_owner_with(conn.standalone_navigation_engine.ensure()), + assert_ne!( + empty_page_engine, + conn.browser_context + .as_ref() + .and_then(|context| context + .page_navigation_renderer_owner_id("TID-doc-owner-diagnostics-active")) + .expect("loaded active target renderer owner"), "test setup must keep a distinct renderer owner without a loaded document" ); @@ -1018,14 +1037,22 @@ async fn memory_diagnostics_excludes_empty_page_hosts_from_document_isolates() { #[tokio::test] async fn memory_diagnostics_sync_counts_dedicated_worker_from_cached_page_snapshot() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-sync-dedicated-worker".to_owned())); - let mut page = conn - .load_page_via_runtime_async("data:text/html,worker") - .await - .expect("sync diagnostics dedicated-worker page should load"); - let start_worker_response = page - .evaluate_runtime_expression_async( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( + "BID-sync-dedicated-worker", + "TID-sync-dedicated-worker", + )); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,worker", + None, + ) + .await; + let start_worker_response = conn + .browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-sync-dedicated-worker", r#" (() => { globalThis.__lmSyncDiagnosticsWorkerReady = false; @@ -1036,6 +1063,7 @@ async fn memory_diagnostics_sync_counts_dedicated_worker_from_cached_page_snapsh return "started"; })() "#, + false, ) .await .expect("dedicated worker should start"); @@ -1044,11 +1072,6 @@ async fn memory_diagnostics_sync_counts_dedicated_worker_from_cached_page_snapsh json!("started"), "dedicated worker should be scheduled before sync diagnostics: {start_worker_response:?}" ); - - let browser_context = conn.browser_context.as_mut().expect("browser context"); - browser_context.set_active_target_id("TID-sync-dedicated-worker"); - browser_context.replace_loaded_page(Some(page)); - for _ in 0..64 { let ready_response = conn .browser_context @@ -1088,34 +1111,28 @@ async fn memory_diagnostics_sync_counts_dedicated_worker_from_cached_page_snapsh #[tokio::test] async fn memory_diagnostics_counts_different_browser_context_document_isolates_separately() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut first_context = conn.new_browser_context("BID-doc-owner-first".to_owned()); first_context.set_active_target_id("TID-doc-owner-first"); conn.insert_browser_context(first_context); - let first_page = conn - .load_page_via_runtime_async("data:text/html,first-context") - .await - .expect("first browser-context diagnostics page should load"); - conn.browser_context - .as_mut() - .expect("first browser context should be active") - .replace_loaded_page(Some(first_page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,first-context", + None, + ) + .await; let mut second_context = conn.new_browser_context("BID-doc-owner-second".to_owned()); second_context.set_active_target_id("TID-doc-owner-second"); conn.insert_browser_context(second_context); assert!(conn.activate_browser_context_by_id("BID-doc-owner-second")); - let second_page = conn - .load_page_via_runtime_async("data:text/html,second-context") - .await - .expect("second browser-context diagnostics page should load"); - conn.browser_context - .as_mut() - .expect("second browser context should be active") - .replace_loaded_page(Some(second_page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,second-context", + None, + ) + .await; let pending_diagnostics = conn .start_moli_diagnostics() @@ -1160,8 +1177,9 @@ async fn memory_diagnostics_counts_different_browser_context_document_isolates_s #[test] fn memory_diagnostics_counts_isolated_page_engines_by_renderer_owner() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-shared-diagnostics".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-shared-diagnostics".to_owned()); browser_context.set_active_target_id("TID-shared-diagnostics-active"); browser_context.stage_background_target( "TID-shared-diagnostics-bg".to_owned(), @@ -1172,14 +1190,15 @@ fn memory_diagnostics_counts_isolated_page_engines_by_renderer_owner() { ); conn.insert_browser_context(browser_context); - let active = conn.active_navigation_engine(); - let background = conn - .browser_context - .as_ref() - .and_then(|context| context.page_navigation_engine("TID-shared-diagnostics-bg")) + let context = conn.browser_context.as_ref().expect("browser context"); + let active = context + .page_navigation_renderer_owner_id("TID-shared-diagnostics-active") + .expect("active PageAgentHost must own an engine"); + let background = context + .page_navigation_renderer_owner_id("TID-shared-diagnostics-bg") .expect("background PageAgentHost must own an engine"); - assert!( - !background.shares_renderer_owner_with(active), + assert_ne!( + background, active, "same-context PageAgentHost engines must remain independently schedulable" ); @@ -1203,7 +1222,7 @@ fn memory_diagnostics_counts_isolated_page_engines_by_renderer_owner() { #[test] fn page_navigation_engines_remain_target_local_across_selection() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut browser_context = conn.new_browser_context("BID-engine-residence".to_owned()); browser_context.set_active_target_id("TID-engine-first"); browser_context.stage_background_target( @@ -1215,31 +1234,50 @@ fn page_navigation_engines_remain_target_local_across_selection() { ); conn.insert_browser_context(browser_context); - conn.active_navigation_engine_mut() - .set_user_agent_override("Moli/Page-First"); + let fallback = conn.base_browser_identity().clone(); + conn.browser_context + .as_mut() + .expect("browser context") + .set_base_user_agent_override_for_target( + "TID-engine-first", + Some("Moli/Page-First".to_owned()), + &fallback, + ); + conn.apply_active_engine_fetch_overrides(); conn.browser_context .as_mut() .expect("browser context") .set_active_target_id("TID-engine-second"); assert_ne!( - conn.active_navigation_engine() - .fetch_config() + conn.browser_context + .as_ref() + .and_then(|context| context.page_navigation_fetch_config("TID-engine-second")) + .expect("second Page engine") .browser_identity() .user_agent(), "Moli/Page-First", "selecting another Page must expose that host's own engine" ); - conn.active_navigation_engine_mut() - .set_user_agent_override("Moli/Page-Second"); + conn.browser_context + .as_mut() + .expect("browser context") + .set_base_user_agent_override_for_target( + "TID-engine-second", + Some("Moli/Page-Second".to_owned()), + &fallback, + ); + conn.apply_active_engine_fetch_overrides(); conn.browser_context .as_mut() .expect("browser context") .set_active_target_id("TID-engine-first"); assert_eq!( - conn.active_navigation_engine() - .fetch_config() + conn.browser_context + .as_ref() + .and_then(|context| context.page_navigation_fetch_config("TID-engine-first")) + .expect("first Page engine") .browser_identity() .user_agent(), "Moli/Page-First", @@ -1248,9 +1286,8 @@ fn page_navigation_engines_remain_target_local_across_selection() { assert_eq!( conn.browser_context .as_ref() - .and_then(|context| context.page_navigation_engine("TID-engine-second")) + .and_then(|context| context.page_navigation_fetch_config("TID-engine-second")) .expect("second Page engine") - .fetch_config() .browser_identity() .user_agent(), "Moli/Page-Second", @@ -1260,24 +1297,28 @@ fn page_navigation_engines_remain_target_local_across_selection() { #[tokio::test] async fn memory_diagnostics_splits_pending_inspector_await_counts_by_target_owner() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new( - "BID-pending-await-diagnostics".to_owned(), - )); - - let active_page = conn - .load_page_via_runtime_async("data:text/html,active") - .await - .expect("active diagnostics page should load"); - let background_page = conn - .load_page_via_runtime_async("data:text/html,background") - .await - .expect("background diagnostics page should load"); - - let browser_context = conn.browser_context.as_mut().expect("browser context"); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-pending-await-diagnostics".to_owned()); browser_context.set_active_target_id("TID-pending-await-active"); browser_context.attach_active_session("SID-pending-await-active"); - browser_context.replace_loaded_page(Some(active_page)); + browser_context.register_page_target_url_fixture( + "TID-pending-await-bg".to_owned(), + Some("SID-pending-await-bg".to_owned()), + "data:text/html,background".to_owned(), + ); + conn.install_browser_context_fixture_for_test(browser_context); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,active", + Some("SID-pending-await-active"), + ) + .await; + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,background", + Some("SID-pending-await-bg"), + ) + .await; + let browser_context = conn.browser_context.as_mut().expect("browser context"); browser_context.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .register_pending_inspector_await(10_001, Some("SID-pending-await-active"), None); @@ -1289,12 +1330,6 @@ async fn memory_diagnostics_splits_pending_inspector_await_counts_by_target_owne Some("active-group"), ); - browser_context.register_page_target_url_fixture( - "TID-pending-await-bg".to_owned(), - Some("SID-pending-await-bg".to_owned()), - "data:text/html,background".to_owned(), - ); - browser_context.replace_target_page_for_test("TID-pending-await-bg", Some(background_page)); browser_context .page_target_mut("TID-pending-await-bg") .unwrap() @@ -1408,8 +1443,8 @@ async fn memory_diagnostics_splits_pending_inspector_await_counts_by_target_owne #[test] fn navigation_background_event_queue_drops_stale_token() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("CTX-nav".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("CTX-nav".to_owned()); browser_context.set_active_target_id("TID-nav"); let stale = browser_context .start_document_navigation_for_active_target("LOADER-1".to_owned()) @@ -1446,8 +1481,8 @@ fn navigation_background_event_queue_drops_stale_token() { #[test] fn navigation_background_event_queue_preserves_order_for_current_token() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("CTX-nav-order".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("CTX-nav-order".to_owned()); browser_context.set_active_target_id("TID-nav-order"); let stale = browser_context .start_document_navigation_for_active_target("LOADER-1".to_owned()) @@ -1494,10 +1529,10 @@ fn navigation_background_event_queue_preserves_order_for_current_token() { #[test] fn navigation_background_event_sender_preserves_typed_sidecar_for_current_token() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); conn.set_background_event_sender(tx); - let mut browser_context = BrowserContext::new("CTX-nav-typed".to_owned()); + let mut browser_context = conn.new_browser_context_fixture_for_test("CTX-nav-typed".to_owned()); browser_context.set_active_target_id("TID-nav-typed"); let current = browser_context .start_document_navigation_for_active_target("LOADER-typed".to_owned()) @@ -1543,8 +1578,8 @@ fn navigation_background_event_sender_preserves_typed_sidecar_for_current_token( #[tokio::test] async fn materialized_navigation_completion_drops_stale_token() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("CTX-nav".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("CTX-nav".to_owned()); browser_context.set_active_target_id("TID-nav"); let stale = browser_context .start_document_navigation_for_active_target("LOADER-1".to_owned()) @@ -1594,8 +1629,8 @@ async fn materialized_navigation_completion_drops_stale_token() { #[tokio::test] async fn materialized_navigation_completion_drops_stale_token_without_navigate_id() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("CTX-nav-none".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("CTX-nav-none".to_owned()); browser_context.set_active_target_id("TID-nav-none"); let stale = browser_context .start_document_navigation_for_active_target("LOADER-1".to_owned()) @@ -1630,8 +1665,8 @@ async fn materialized_navigation_completion_drops_stale_token_without_navigate_i #[tokio::test] async fn materialized_navigation_completion_drains_current_token() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("CTX-nav".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("CTX-nav".to_owned()); browser_context.set_active_target_id("TID-nav"); let current = browser_context .start_document_navigation_for_active_target("LOADER-1".to_owned()) @@ -1674,6 +1709,7 @@ fn materialized_navigation_test_state( redirect_headers: None, navigate_id, owner: crate::conn::CommandOwnerScope::for_route(crate::conn::CdpSessionRoute::Browser), + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: NavigationResultProjection::Cdp( json!({ "frameId": "TID-nav", "loaderId": loader_id }), ), diff --git a/moli-protocol/src/conn/tests/navigation_error.rs b/moli-protocol/src/conn/tests/navigation_error.rs index 012442ac6e..0e3a7c799e 100644 --- a/moli-protocol/src/conn/tests/navigation_error.rs +++ b/moli-protocol/src/conn/tests/navigation_error.rs @@ -39,7 +39,6 @@ fn failing_streamed_document( request_headers: navigation.request_headers.clone(), response, network_observation_journal: Default::default(), - body_progress_source: Default::default(), prepared_document: None, } } @@ -104,7 +103,7 @@ async fn fetch_body_stream_read_preserves_error_type_and_paused_transfer() { fn navigation_fixture() -> (TestContext, NavigationDispatchState) { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut browser_context = ctx.conn.new_browser_context_fixture_for_test("BID-1"); browser_context.set_active_target_id("TID-1"); browser_context.attach_active_session("SID-1"); browser_context @@ -112,7 +111,15 @@ fn navigation_fixture() -> (TestContext, NavigationDispatchState) { .expect("fixture navigation should start"); ctx.conn .install_browser_context_fixture_for_test(browser_context); + let web_contents = ctx + .conn + .browser_context + .as_ref() + .unwrap() + .web_contents_handle_for_target("TID-1") + .unwrap(); let navigation = NavigationDispatchState { + web_contents, redirect_chain: Vec::new(), redirect_headers: None, navigate_id: Some(1), @@ -164,21 +171,29 @@ async fn offline_navigation_loaders_preserve_typed_error_causes_through_context( .await .err() .expect("offline document load must fail"), - load.fetch_intercepted_response(method, url, None, headers.clone(), None) - .await - .expect_err("offline streaming response fetch must fail"), - load.fetch_intercepted_auth_response( - method, - url, + moli_core::browser::BrowserInterceptedNavigationLoad::new( + ctx.conn.admit_navigation_load(&navigation).unwrap(), + navigation.requested_url.clone(), + method.clone(), None, headers.clone(), - SubresourceAuthCredentials { - target: SubresourceAuthTarget::Server, - scheme: SubresourceAuthScheme::Basic, - username: "test-user".to_owned(), - password: "test-password".to_owned(), - }, ) + .fetch_streaming(None) + .await + .expect_err("offline streaming response fetch must fail"), + moli_core::browser::BrowserInterceptedNavigationLoad::new( + ctx.conn.admit_navigation_load(&navigation).unwrap(), + navigation.requested_url.clone(), + method.clone(), + None, + headers.clone(), + ) + .fetch_auth(SubresourceAuthCredentials { + target: SubresourceAuthTarget::Server, + scheme: SubresourceAuthScheme::Basic, + username: "test-user".to_owned(), + password: "test-password".to_owned(), + }) .await .expect_err("offline authenticated response fetch must fail"), ]; @@ -226,9 +241,20 @@ async fn navigation_error_document_uses_typed_failure_request_after_context() { matches!(outcome, NavigationLoadOutcome::ResponseCommitReady(_)), "network failure must prepare an error document commit" ); + let NavigationLoadOutcome::ResponseCommitReady(response) = outcome else { + unreachable!() + }; + let token = ctx + .conn + .browser_context + .as_ref() + .unwrap() + .pending_navigation_id_for_loader("TID-1", "LID-test") + .unwrap(); let loaded = ctx .conn - .commit_navigation_load_outcome_for_owner_async(&navigation.owner, outcome) + .start_response_document_materialization_for_owner(&navigation.owner, token, *response) + .unwrap() .await .expect("error document should commit"); assert_eq!(loaded.requested_url, unreachable_url); diff --git a/moli-protocol/src/conn/tests/resource_runtime.rs b/moli-protocol/src/conn/tests/resource_runtime.rs index 1ac4835d38..abfb73994d 100644 --- a/moli-protocol/src/conn/tests/resource_runtime.rs +++ b/moli-protocol/src/conn/tests/resource_runtime.rs @@ -15,11 +15,14 @@ use tokio::sync::Notify; #[tokio::test] async fn resource_defaults_without_a_page_do_not_materialize_a_fallback_engine() { - let mut conn = CdpConnection::new_with_deferred_navigation_runtime( + let mut conn = crate::test_support::connection_with_config( CdpInitialStoragePartition::memory(), Default::default(), ); - assert!(conn.standalone_navigation_engine.engine.get().is_none()); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); conn.set_user_agent_override_async("Lazy/1").await; conn.set_tls_verify_host_async(false).await; assert_eq!( @@ -28,7 +31,10 @@ async fn resource_defaults_without_a_page_do_not_materialize_a_fallback_engine() ); assert!(!conn.fetch_config().tls_verify_host()); assert!(conn.browser_context.is_none()); - assert!(conn.standalone_navigation_engine.engine.get().is_none()); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); let context = conn.new_browser_context("BID-empty".into()); conn.insert_browser_context(context); @@ -46,12 +52,15 @@ async fn resource_defaults_without_a_page_do_not_materialize_a_fallback_engine() .active_target_id() .is_none() ); - assert!(conn.standalone_navigation_engine.engine.get().is_none()); + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); } #[tokio::test] async fn resource_maintenance_rejects_stale_routes_without_touching_the_selected_peer() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut context = conn.new_browser_context("BID-live".into()); context.set_active_target_id("TID-peer"); conn.insert_browser_context(context); @@ -90,7 +99,7 @@ async fn resource_maintenance_rejects_stale_routes_without_touching_the_selected #[tokio::test] async fn detached_session_cannot_rebuild_but_its_live_browser_owner_keeps_its_client() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut context = conn.new_browser_context("BID-live".into()); context.set_active_target_id("TID-owner"); context.attach_active_session("SID-owner"); @@ -155,7 +164,7 @@ fn stored_cookie(name: &str, value: &str) -> moli_cookie_jar::StoredCookie { async fn commit_navigation_outcome_for_test( conn: &mut CdpConnection, outcome: NavigationLoadOutcome, -) -> LoadedNavigation { +) -> LoadedNavigation { commit_navigation_outcome_for_session_test(conn, outcome, None).await } @@ -163,22 +172,19 @@ async fn commit_navigation_outcome_for_session_test( conn: &mut CdpConnection, outcome: NavigationLoadOutcome, session_id: Option<&str>, -) -> LoadedNavigation { +) -> LoadedNavigation { match outcome { NavigationLoadOutcome::ResponseCommitReady(navigation) => { - let navigation = *navigation; let owner = match session_id { Some(session_id) => CommandOwnerScope::for_session(session_id), None => CommandOwnerScope::capture(conn, None), }; - let policy = conn - .capture_document_policy_for_owner(&owner, navigation.final_url()) - .expect("test navigation commit configuration should resolve"); - let inspection = conn.prepared_document_inspection_for_owner(&owner); - navigation - .materialize(policy, inspection) - .await - .expect("test navigation should commit") + conn.commit_navigation_load_outcome_for_owner_async( + &owner, + NavigationLoadOutcome::ResponseCommitReady(navigation), + ) + .await + .expect("test navigation should commit") } NavigationLoadOutcome::Download(_) => { panic!("test navigation should not resolve to a download") @@ -205,13 +211,15 @@ async fn canceled_document_materialization_does_not_mutate_engine_policy() { } async fn buffered_navigation_policy_checkpoint(reject_canceled: Option) { - let mut conn = CdpConnection::new(); - let ambient_context = conn.new_browser_context("BID-ambient".to_owned()); + let mut conn = crate::test_support::connection(); + let mut ambient_context = conn.new_browser_context("BID-ambient".to_owned()); + ambient_context.set_active_target_id("TID-ambient"); conn.insert_browser_context(ambient_context); let ambient_renderer_owner = conn - .standalone_navigation_engine - .ensure() - .renderer_owner_id_for_diagnostics(); + .browser_context + .as_ref() + .and_then(|context| context.page_navigation_renderer_owner_id("TID-ambient")) + .expect("ambient target renderer owner"); let mut target_context = conn.new_browser_context("BID-target".to_owned()); target_context.set_active_target_id("TID-target"); @@ -233,6 +241,7 @@ async fn buffered_navigation_policy_checkpoint(reject_canceled: Option) { redirect_headers: None, navigate_id: Some(1), owner, + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: NavigationResultProjection::Cdp(json!({ "frameId": "TID-target", "loaderId": "LOADER-target", @@ -278,9 +287,8 @@ async fn buffered_navigation_policy_checkpoint(reject_canceled: Option) { if let Some(canceled) = reject_canceled { assert!( context - .page_navigation_engine("TID-target") + .page_navigation_fetch_config("TID-target") .unwrap() - .fetch_config() .tls_verify_host() ); context.set_tls_verify_host_override_for_target("TID-target", Some(false)); @@ -304,9 +312,8 @@ async fn buffered_navigation_policy_checkpoint(reject_canceled: Option) { assert!( conn.browser_context_by_id("BID-target") .unwrap() - .page_navigation_engine("TID-target") + .page_navigation_fetch_config("TID-target") .unwrap() - .fetch_config() .tls_verify_host(), "rejected admission must not install the new native TLS policy on the engine" ); @@ -341,14 +348,17 @@ async fn buffered_navigation_policy_checkpoint(reject_canceled: Option) { continuation.release(); } commit.previous_document_retirement.close().await; - let target_engine = conn - .browser_context_by_id("BID-target") - .and_then(|context| context.page_navigation_engine("TID-target")) - .expect("inactive target must keep its navigation engine after completion"); - let retained_client = target_engine - .resource_request_client() + let defaults = conn.document_fetch_defaults(); + let browser_globals = conn.browser_global_overrides.clone(); + let context = conn + .browser_context_by_id_mut("BID-target") + .expect("inactive target context"); + let retained_client = context + .resource_request_client_for_test("TID-target", defaults, &browser_globals) .expect("resident target engine must keep a resource request client"); - let target_renderer_owner = target_engine.renderer_owner_id_for_diagnostics(); + let target_renderer_owner = context + .page_navigation_renderer_owner_id("TID-target") + .expect("inactive target must keep its navigation engine after completion"); assert!( resident_client.shares_page_network_policy_with(&retained_client), @@ -391,10 +401,10 @@ async fn buffered_navigation_policy_checkpoint(reject_canceled: Option) { #[test] fn current_navigation_initiator_url_uses_loaded_browser_context_url_when_available() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); assert!(conn.current_navigation_initiator_url().is_none()); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.set_target_url("about:blank".into()); conn.install_browser_context_fixture_for_test(bc); assert!(conn.current_navigation_initiator_url().is_none()); @@ -411,7 +421,10 @@ fn current_navigation_initiator_url_uses_loaded_browser_context_url_when_availab #[test] fn connection_initial_cookies_seed_new_browser_contexts() { - let conn = CdpConnection::new_with_initial_cookies(vec![stored_cookie("sid", "seeded")]); + let conn = crate::test_support::connection_with_config( + CdpInitialStoragePartition::with_cookies(vec![stored_cookie("sid", "seeded")]), + Default::default(), + ); for id in ["BID-1", "BID-2"] { let bc = conn.new_browser_context(id.to_owned()); @@ -432,7 +445,8 @@ fn initial_storage_partition_derives_store_handles_from_core_owner() { vec![stored_cookie("sid", "seeded")], &storage_partition, ); - let conn = CdpConnection::new_with_initial_storage_partition(initial_storage_partition); + let conn = + crate::test_support::connection_with_config(initial_storage_partition, Default::default()); let browser_context = conn.new_browser_context("BID-owner".to_owned()); let shared_storage = storage_partition.shared_storage_handles(); @@ -441,15 +455,15 @@ fn initial_storage_partition_derives_store_handles_from_core_owner() { let expected_storage_bucket_store = shared_storage.storage_bucket_store(); assert!(Arc::ptr_eq( - browser_context.web_storage_store_for_test(), + &browser_context.web_storage_store_for_test(), &expected_web_storage_store )); assert!(Arc::ptr_eq( - browser_context.indexed_db_manager_for_test(), + &browser_context.indexed_db_manager_for_test(), &expected_indexed_db_manager )); assert!(Arc::ptr_eq( - browser_context.storage_bucket_store_for_test(), + &browser_context.storage_bucket_store_for_test(), &expected_storage_bucket_store )); let cookies = browser_context.snapshot_cookies(); @@ -463,7 +477,8 @@ fn default_browser_contexts_reuse_partition_with_distinct_target_session_storage let storage_partition = StoragePartitionState::open(None).expect("memory partition"); let initial_storage_partition = CdpInitialStoragePartition::from_storage_partition(Vec::new(), &storage_partition); - let conn = CdpConnection::new_with_initial_storage_partition(initial_storage_partition); + let conn = + crate::test_support::connection_with_config(initial_storage_partition, Default::default()); let mut first = conn.new_browser_context("BID-first".to_owned()); first.set_active_target_id("TID-first"); @@ -474,34 +489,37 @@ fn default_browser_contexts_reuse_partition_with_distinct_target_session_storage .web_storage_store(); assert!(Arc::ptr_eq( - first.cookie_store_for_test(), - second.cookie_store_for_test() + &first.cookie_store_for_test(), + &second.cookie_store_for_test() )); assert!(Arc::ptr_eq( - first.web_storage_store_for_test(), + &first.web_storage_store_for_test(), &expected_web_storage_store )); assert!(Arc::ptr_eq( - second.web_storage_store_for_test(), + &second.web_storage_store_for_test(), &expected_web_storage_store )); assert!(Arc::ptr_eq( - first.indexed_db_manager_for_test(), - second.indexed_db_manager_for_test() + &first.indexed_db_manager_for_test(), + &second.indexed_db_manager_for_test() )); assert!(Arc::ptr_eq( - first.storage_bucket_store_for_test(), - second.storage_bucket_store_for_test() + &first.storage_bucket_store_for_test(), + &second.storage_bucket_store_for_test() )); assert!(!Arc::ptr_eq( - first.session_storage_store_for_test(), - second.session_storage_store_for_test() + &first.session_storage_store_for_test(), + &second.session_storage_store_for_test() )); } #[test] fn connection_ephemeral_browser_context_uses_isolated_storage_partition() { - let conn = CdpConnection::new_with_initial_cookies(vec![stored_cookie("sid", "seeded")]); + let conn = crate::test_support::connection_with_config( + CdpInitialStoragePartition::with_cookies(vec![stored_cookie("sid", "seeded")]), + Default::default(), + ); let bc = conn.new_ephemeral_browser_context("BID-ephemeral".to_owned()); @@ -516,7 +534,8 @@ fn connection_default_and_ephemeral_context_creation_use_named_partition_paths() let storage_partition = StoragePartitionState::open(None).expect("memory partition"); let initial_storage_partition = CdpInitialStoragePartition::from_storage_partition(Vec::new(), &storage_partition); - let conn = CdpConnection::new_with_initial_storage_partition(initial_storage_partition); + let conn = + crate::test_support::connection_with_config(initial_storage_partition, Default::default()); let profile_backed = conn.new_browser_context("BID-profile".to_owned()); let ephemeral = conn.new_ephemeral_browser_context("BID-ephemeral".to_owned()); @@ -527,21 +546,21 @@ fn connection_default_and_ephemeral_context_creation_use_named_partition_paths() assert!(profile_backed.is_profile_backed_storage_partition()); assert_eq!(profile_backed.storage_partition_id(), "default"); assert!(Arc::ptr_eq( - profile_backed.web_storage_store_for_test(), + &profile_backed.web_storage_store_for_test(), &expected_web_storage_store )); assert!(!ephemeral.is_profile_backed_storage_partition()); assert_eq!(ephemeral.storage_partition_id(), "BID-ephemeral"); assert!(!Arc::ptr_eq( - ephemeral.web_storage_store_for_test(), + &ephemeral.web_storage_store_for_test(), &expected_web_storage_store )); } #[test] fn browser_context_memory_diagnostics_include_storage_partition_identity() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let profile_backed = conn.new_browser_context("BID-profile".to_owned()); let ephemeral = conn.new_ephemeral_browser_context("BID-ephemeral".to_owned()); @@ -575,7 +594,8 @@ fn browser_context_request_cookie_report_reads_storage_partition_cookie_handle() let browser_context = BrowserContext::new("BID-cookie-report".to_owned()); let request_url = Url::parse("https://example.com/app/index.html").unwrap(); { - let mut cookie_store = browser_context.cookie_store_for_test().lock(); + let cookie_store_handle = browser_context.cookie_store_for_test(); + let mut cookie_store = cookie_store_handle.lock(); cookie_store.store_response_headers( &request_url, &[( @@ -603,7 +623,8 @@ fn browser_context_cookie_snapshot_and_delete_use_storage_partition_cookie_handl let mut browser_context = BrowserContext::new("BID-cookie-snapshot".to_owned()); let request_url = Url::parse("https://example.com/app/index.html").unwrap(); { - let mut cookie_store = browser_context.cookie_store_for_test().lock(); + let cookie_store_handle = browser_context.cookie_store_for_test(); + let mut cookie_store = cookie_store_handle.lock(); cookie_store.store_response_headers( &request_url, &[( @@ -641,7 +662,8 @@ fn browser_context_storage_usage_reads_storage_partition_owner() { moli_storage_key::MoliStorageKey::first_party_from_url(&Url::parse(&origin).unwrap(), None) .serialized_storage_key(); { - let mut store = browser_context.web_storage_store_for_test().lock(); + let store_handle = browser_context.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&storage_key, "local", "owner")); } @@ -657,7 +679,7 @@ fn browser_context_storage_usage_reads_storage_partition_owner() { #[test] fn navigation_load_inputs_own_cookie_request_and_response_reports() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let response_url = Url::parse("https://example.com/app/index.html").unwrap(); let load_inputs = conn.navigation_load_inputs_for_session_owner(None); @@ -681,8 +703,8 @@ fn navigation_load_inputs_own_cookie_request_and_response_reports() { } #[test] -fn no_loaded_browser_context_navigation_inputs_reuse_initial_storage_stores() { - let mut conn = CdpConnection::new(); +fn initial_storage_is_reused_without_an_unowned_resource_runtime() { + let mut conn = crate::test_support::connection(); let first_inputs = conn.navigation_load_inputs_for_session_owner(None); let second_inputs = conn.navigation_load_inputs_for_session_owner(None); @@ -716,18 +738,37 @@ fn no_loaded_browser_context_navigation_inputs_reuse_initial_storage_stores() { .expect("initial storage bucket store"), )); + for inputs in [&first_inputs, &second_inputs] { + assert!(matches!( + conn.ensure_resource_request_client_for_navigation_load_inputs(inputs), + Err(message) if message.to_string() == "resource request fixture requires an installed WebContents" + )); + } + assert_eq!( + conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) + ); + + let mut context = conn.new_browser_context("BID-initial-storage".into()); + context.set_active_target_id("TID-initial-storage"); + conn.insert_browser_context(context); + let owned_inputs = conn.navigation_load_inputs_for_session_owner(None); let (loader_cookie_store, resource_runtime_id) = { let loader = conn - .ensure_resource_request_client_for_navigation_load_inputs(&first_inputs) - .expect("loader for first no-context inputs"); + .ensure_resource_request_client_for_navigation_load_inputs(&owned_inputs) + .expect("loader for the installed WebContents"); ( loader.cookie_store(), loader.resource_runtime_diagnostics().runtime_id, ) }; + assert!(Arc::ptr_eq( + &loader_cookie_store, + &first_storage.cookie_store + )); let loader = conn - .ensure_resource_request_client_for_navigation_load_inputs(&second_inputs) - .expect("loader for second no-context inputs"); + .ensure_resource_request_client_for_navigation_load_inputs(&owned_inputs) + .expect("reused loader for the same WebContents"); assert!(Arc::ptr_eq(&loader.cookie_store(), &loader_cookie_store)); assert_eq!( loader.resource_runtime_diagnostics().runtime_id, @@ -738,8 +779,8 @@ fn no_loaded_browser_context_navigation_inputs_reuse_initial_storage_stores() { #[test] fn page_request_client_for_navigation_inputs_inherits_service_worker_bypass() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id("TID-1"); browser_context.attach_active_session("SID-1"); { @@ -769,10 +810,10 @@ fn page_request_client_for_navigation_inputs_inherits_service_worker_bypass() { #[test] fn connection_snapshot_cookies_collects_active_and_inactive_contexts() { - let mut conn = CdpConnection::new(); - let active = BrowserContext::new("BID-active".to_owned()); + let mut conn = crate::test_support::connection(); + let active = conn.new_browser_context_fixture_for_test("BID-active".to_owned()); active.upsert_cookie_for_test(stored_cookie("active", "1")); - let inactive = BrowserContext::new("BID-inactive".to_owned()); + let inactive = conn.new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.upsert_cookie_for_test(stored_cookie("inactive", "1")); conn.install_browser_context_fixture_for_test(active); conn.push_inactive_browser_context_fixture_for_test(inactive); @@ -789,7 +830,7 @@ fn connection_snapshot_cookies_collects_active_and_inactive_contexts() { #[test] fn connection_profile_backed_cookie_snapshot_ignores_ephemeral_contexts() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let profile_backed = conn.new_browser_context("BID-profile".to_owned()); profile_backed.upsert_cookie_for_test(stored_cookie("profile", "1")); let ephemeral = conn.new_ephemeral_browser_context("BID-ephemeral".to_owned()); @@ -807,7 +848,10 @@ fn connection_profile_backed_cookie_snapshot_ignores_ephemeral_contexts() { #[test] fn connection_profile_backed_cookie_snapshot_is_none_without_profile_backed_context() { - let mut conn = CdpConnection::new_with_initial_cookies(vec![stored_cookie("sid", "seeded")]); + let mut conn = crate::test_support::connection_with_config( + CdpInitialStoragePartition::with_cookies(vec![stored_cookie("sid", "seeded")]), + Default::default(), + ); let ephemeral = conn.new_ephemeral_browser_context("BID-ephemeral".to_owned()); assert!(ephemeral.snapshot_cookies().is_empty()); conn.install_browser_context_fixture_for_test(ephemeral); @@ -817,9 +861,9 @@ fn connection_profile_backed_cookie_snapshot_is_none_without_profile_backed_cont #[tokio::test] async fn build_loaded_navigation_from_buffered_response_updates_request_cookie_access_time() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let requested_url = Url::parse("https://example.com/app/index.html").unwrap(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.set_target_url("https://example.com/origin".into()); bc.store_response_cookie_headers_for_test( &requested_url, @@ -870,9 +914,9 @@ async fn build_loaded_navigation_from_buffered_response_updates_request_cookie_a #[tokio::test] async fn rebuild_buffered_response_preserving_request_report_avoids_second_access_touch() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let requested_url = Url::parse("https://example.com/app/index.html").unwrap(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.set_target_url("https://example.com/origin".into()); bc.store_response_cookie_headers_for_test( &requested_url, @@ -939,8 +983,8 @@ async fn rebuild_buffered_response_preserving_request_report_avoids_second_acces #[tokio::test] async fn reset_resource_runtime_clears_loaded_page_cookie_backend() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let url = Url::parse("https://example.com/app").unwrap(); let navigation = conn @@ -957,7 +1001,8 @@ async fn reset_resource_runtime_clears_loaded_page_cookie_backend() { conn.browser_context .as_mut() .unwrap() - .replace_active_page_for_test(Some(navigation.page)); + .commit_active_navigation_for_test(navigation.page) + .await; let context = conn.browser_context.as_mut().unwrap(); let target_id = context.active_target_id_owned().unwrap(); @@ -1006,12 +1051,12 @@ async fn reset_resource_runtime_clears_loaded_page_cookie_backend() { #[tokio::test] async fn same_target_navigations_reuse_local_and_session_storage() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let first_url = Url::parse("https://storage.example/app/one").unwrap(); let second_url = Url::parse("https://storage.example/app/two").unwrap(); - let mut first = conn + let first = conn .build_loaded_navigation_from_buffered_response_async( first_url, "GET".into(), @@ -1021,17 +1066,26 @@ async fn same_target_navigations_reuse_local_and_session_storage() { "one".into(), ) .await - .expect("first synthetic navigation should build") - .page; - let write = first - .evaluate_runtime_expression_async( + .expect("first synthetic navigation should build"); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(first.page) + .await; + let write = conn + .browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-1", "localStorage.clear(); sessionStorage.clear(); localStorage.setItem('shared', 'yes'); sessionStorage.setItem('ephemeral', 'yes'); 'ok'", + false, ) .await .expect("storage write should evaluate"); assert_eq!(write["value"], json!("ok")); - let mut second = conn + let second = conn .build_loaded_navigation_from_buffered_response_async( second_url, "GET".into(), @@ -1041,11 +1095,20 @@ async fn same_target_navigations_reuse_local_and_session_storage() { "two".into(), ) .await - .expect("second synthetic navigation should build") - .page; - let read = second - .evaluate_runtime_expression_async( + .expect("second synthetic navigation should build"); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(second.page) + .await; + let read = conn + .browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-1", "`${localStorage.getItem('shared')}|${String(sessionStorage.getItem('ephemeral'))}`", + false, ) .await .expect("storage read should evaluate"); @@ -1055,13 +1118,13 @@ async fn same_target_navigations_reuse_local_and_session_storage() { #[tokio::test] async fn browser_context_storage_does_not_cross_context_switches() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); conn.inactive_browser_contexts - .push(BrowserContext::new("BID-2".into())); + .push(conn.new_page_target_fixture_for_test("BID-2", "TID-2")); let url = Url::parse("https://context-storage.example/app").unwrap(); - let mut first = conn + let first = conn .build_loaded_navigation_from_buffered_response_async( url.clone(), "GET".into(), @@ -1071,17 +1134,25 @@ async fn browser_context_storage_does_not_cross_context_switches() { "first".into(), ) .await - .expect("first context navigation should build") - .page; - first - .evaluate_runtime_expression_async( + .expect("first context navigation should build"); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(first.page) + .await; + conn.browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-1", "localStorage.clear(); sessionStorage.clear(); localStorage.setItem('contextOnly', 'first'); sessionStorage.setItem('sessionOnly', 'first');", + false, ) .await .expect("first context storage write should evaluate"); assert!(conn.activate_browser_context_by_id_async("BID-2").await); - let mut second = conn + let second = conn .build_loaded_navigation_from_buffered_response_async( url, "GET".into(), @@ -1091,11 +1162,20 @@ async fn browser_context_storage_does_not_cross_context_switches() { "second".into(), ) .await - .expect("second context navigation should build") - .page; - let read = second - .evaluate_runtime_expression_async( + .expect("second context navigation should build"); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(second.page) + .await; + let read = conn + .browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-2", "`${String(localStorage.getItem('contextOnly'))}|${String(sessionStorage.getItem('sessionOnly'))}`", + false, ) .await .expect("second context storage read should evaluate"); @@ -1105,15 +1185,17 @@ async fn browser_context_storage_does_not_cross_context_switches() { #[tokio::test] async fn browser_context_storage_buckets_reuse_within_context_and_isolate_between_contexts() { - let mut conn = CdpConnection::new(); - let context_a = conn.new_ephemeral_browser_context("BID-1".into()); - let context_b = conn.new_ephemeral_browser_context("BID-2".into()); + let mut conn = crate::test_support::connection(); + let mut context_a = conn.new_ephemeral_browser_context("BID-1".into()); + context_a.set_active_target_id("TID-1"); + let mut context_b = conn.new_ephemeral_browser_context("BID-2".into()); + context_b.set_active_target_id("TID-2"); conn.install_browser_context_fixture_for_test(context_a); conn.push_inactive_browser_context_fixture_for_test(context_b); let first_url = Url::parse("https://context-storage-buckets.example/app/one").unwrap(); let second_url = Url::parse("https://context-storage-buckets.example/app/two").unwrap(); - let mut first = conn + let first = conn .build_loaded_navigation_from_buffered_response_async( first_url.clone(), "GET".into(), @@ -1123,10 +1205,18 @@ async fn browser_context_storage_buckets_reuse_within_context_and_isolate_betwee "first".into(), ) .await - .expect("first context navigation should build") - .page; - let write = first - .evaluate_runtime_expression_with_await_async( + .expect("first context navigation should build"); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(first.page) + .await; + let write = conn + .browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-1", r#" (async () => { await navigator.storageBuckets.open("bucket-a"); @@ -1140,7 +1230,7 @@ async fn browser_context_storage_buckets_reuse_within_context_and_isolate_betwee .expect("first context storage bucket write should evaluate"); assert_eq!(write["value"], json!("bucket-a|bucket-b")); - let mut same_context = conn + let same_context = conn .build_loaded_navigation_from_buffered_response_async( second_url.clone(), "GET".into(), @@ -1150,10 +1240,18 @@ async fn browser_context_storage_buckets_reuse_within_context_and_isolate_betwee "same context".into(), ) .await - .expect("same context navigation should build") - .page; - let same_context_keys = same_context - .evaluate_runtime_expression_with_await_async( + .expect("same context navigation should build"); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(same_context.page) + .await; + let same_context_keys = conn + .browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-1", r#" (async () => (await navigator.storageBuckets.keys()).join("|"))() "#, @@ -1164,7 +1262,7 @@ async fn browser_context_storage_buckets_reuse_within_context_and_isolate_betwee assert_eq!(same_context_keys["value"], json!("bucket-a|bucket-b")); assert!(conn.activate_browser_context_by_id_async("BID-2").await); - let mut other_context = conn + let other_context = conn .build_loaded_navigation_from_buffered_response_async( second_url, "GET".into(), @@ -1174,10 +1272,18 @@ async fn browser_context_storage_buckets_reuse_within_context_and_isolate_betwee "other context".into(), ) .await - .expect("other context navigation should build") - .page; - let other_context_keys = other_context - .evaluate_runtime_expression_with_await_async( + .expect("other context navigation should build"); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(other_context.page) + .await; + let other_context_keys = conn + .browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-2", r#" (async () => (await navigator.storageBuckets.keys()).join("|"))() "#, @@ -1190,8 +1296,8 @@ async fn browser_context_storage_buckets_reuse_within_context_and_isolate_betwee #[tokio::test] async fn user_agent_override_rebinds_live_document_after_engine_runtime_invalidation() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let url = Url::parse("https://example.com/app").unwrap(); let navigation = conn @@ -1208,7 +1314,8 @@ async fn user_agent_override_rebinds_live_document_after_engine_runtime_invalida conn.browser_context .as_mut() .unwrap() - .replace_active_page_for_test(Some(navigation.page)); + .commit_active_navigation_for_test(navigation.page) + .await; // Invalidate only the NavigationEngine's cached browser runtime. The // committed Document keeps its exact lifecycle authority so the setting @@ -1227,8 +1334,8 @@ async fn user_agent_override_rebinds_live_document_after_engine_runtime_invalida #[tokio::test] async fn tls_and_proxy_overrides_rebind_live_document_after_engine_runtime_invalidation() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let url = Url::parse("https://example.com/app").unwrap(); let navigation = conn @@ -1245,7 +1352,8 @@ async fn tls_and_proxy_overrides_rebind_live_document_after_engine_runtime_inval conn.browser_context .as_mut() .unwrap() - .replace_active_page_for_test(Some(navigation.page)); + .commit_active_navigation_for_test(navigation.page) + .await; // Network settings rebuild the transport behind the live Document // authority; they must not retire that authority first. @@ -1279,11 +1387,11 @@ fn build_loaded_navigation_from_buffered_response_works_inside_current_thread_ru .expect("test runtime should build"); runtime.block_on(async { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-1".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let url = Url::parse("https://example.com/app").unwrap(); - let mut navigation = conn + let navigation = conn .build_loaded_navigation_from_buffered_response_async( url.clone(), "GET".into(), @@ -1297,10 +1405,20 @@ fn build_loaded_navigation_from_buffered_response_works_inside_current_thread_ru assert_eq!(navigation.final_url, url); assert_eq!(navigation.response_status, 200); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(navigation.page) + .await; assert_eq!( - navigation - .page - .evaluate_runtime_expression_async("document.getElementById('ok').textContent") + conn.browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-1", + "document.getElementById('ok').textContent", + false, + ) .await .expect("dom evaluation should succeed")["value"], json!("ok") @@ -1310,8 +1428,8 @@ fn build_loaded_navigation_from_buffered_response_works_inside_current_thread_ru #[tokio::test] async fn loader_uses_active_browser_context_user_agent_override() { - let mut conn = CdpConnection::new(); - let mut first = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut conn = crate::test_support::connection(); + let mut first = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); { let context = &mut first; let target_id = context @@ -1321,7 +1439,7 @@ async fn loader_uses_active_browser_context_user_agent_override() { }; conn.install_browser_context_fixture_for_test(first); - let mut second = BrowserContext::new_with_page_for_test("BID-2", "TID-2"); + let mut second = conn.new_page_target_fixture_for_test("BID-2", "TID-2"); { let context = &mut second; let target_id = context @@ -1349,15 +1467,15 @@ async fn loader_uses_active_browser_context_user_agent_override() { #[tokio::test] async fn loader_uses_active_browser_context_http_proxy_override() { - let mut conn = CdpConnection::new(); - let mut first = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut conn = crate::test_support::connection(); + let mut first = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); first.set_network_policy(crate::conn::ContextNetworkPolicy { http_proxy: Some("http://proxy-a.test:8080".into()), ..Default::default() }); conn.install_browser_context_fixture_for_test(first); - let mut second = BrowserContext::new_with_page_for_test("BID-2", "TID-2"); + let mut second = conn.new_page_target_fixture_for_test("BID-2", "TID-2"); second.set_network_policy(crate::conn::ContextNetworkPolicy { http_proxy: Some("http://proxy-b.test:8080".into()), ..Default::default() @@ -1382,15 +1500,15 @@ async fn loader_uses_active_browser_context_http_proxy_override() { #[tokio::test] async fn loader_uses_active_browser_context_http_no_proxy_override() { - let mut conn = CdpConnection::new(); - let mut first = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut conn = crate::test_support::connection(); + let mut first = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); first.set_network_policy(crate::conn::ContextNetworkPolicy { http_no_proxy: Some("localhost,127.0.0.1".into()), ..Default::default() }); conn.install_browser_context_fixture_for_test(first); - let mut second = BrowserContext::new_with_page_for_test("BID-2", "TID-2"); + let mut second = conn.new_page_target_fixture_for_test("BID-2", "TID-2"); second.set_network_policy(crate::conn::ContextNetworkPolicy { http_no_proxy: Some("::1,.example.com".into()), ..Default::default() @@ -1415,8 +1533,8 @@ async fn loader_uses_active_browser_context_http_no_proxy_override() { #[tokio::test] async fn loader_uses_active_browser_context_tls_verify_host_override() { - let mut conn = CdpConnection::new(); - let mut first = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut conn = crate::test_support::connection(); + let mut first = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); { let context = &mut first; let target_id = context @@ -1426,7 +1544,7 @@ async fn loader_uses_active_browser_context_tls_verify_host_override() { }; conn.install_browser_context_fixture_for_test(first); - let mut second = BrowserContext::new_with_page_for_test("BID-2", "TID-2"); + let mut second = conn.new_page_target_fixture_for_test("BID-2", "TID-2"); { let context = &mut second; let target_id = context @@ -1453,9 +1571,9 @@ async fn loader_uses_active_browser_context_tls_verify_host_override() { #[tokio::test] async fn removing_an_inactive_browser_context_keeps_the_previously_active_context() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut first = BrowserContext::new_with_page_for_test("BID-A", "TID-A"); + let mut first = conn.new_page_target_fixture_for_test("BID-A", "TID-A"); { let context = &mut first; let target_id = context @@ -1465,7 +1583,7 @@ async fn removing_an_inactive_browser_context_keeps_the_previously_active_contex }; conn.install_browser_context_fixture_for_test(first); - let mut second = BrowserContext::new_with_page_for_test("BID-B", "TID-B"); + let mut second = conn.new_page_target_fixture_for_test("BID-B", "TID-B"); { let context = &mut second; let target_id = context @@ -1475,7 +1593,7 @@ async fn removing_an_inactive_browser_context_keeps_the_previously_active_contex }; conn.push_inactive_browser_context_fixture_for_test(second); - let mut third = BrowserContext::new_with_page_for_test("BID-C", "TID-C"); + let mut third = conn.new_page_target_fixture_for_test("BID-C", "TID-C"); { let context = &mut third; let target_id = context @@ -1515,9 +1633,9 @@ async fn removing_an_inactive_browser_context_keeps_the_previously_active_contex #[tokio::test] async fn manual_browser_context_restore_reselects_original_context_after_switch() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut first = BrowserContext::new_with_page_for_test("BID-A", "TID-A"); + let mut first = conn.new_page_target_fixture_for_test("BID-A", "TID-A"); { let context = &mut first; let target_id = context @@ -1527,7 +1645,7 @@ async fn manual_browser_context_restore_reselects_original_context_after_switch( }; conn.install_browser_context_fixture_for_test(first); - let mut second = BrowserContext::new_with_page_for_test("BID-B", "TID-B"); + let mut second = conn.new_page_target_fixture_for_test("BID-B", "TID-B"); { let context = &mut second; let target_id = context @@ -1571,12 +1689,12 @@ async fn manual_browser_context_restore_reselects_original_context_after_switch( #[tokio::test] async fn session_scoped_process_message_restores_previously_active_context_after_dispatch() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let first = BrowserContext::new_with_page_for_test("BID-A", "TID-A"); + let first = conn.new_page_target_fixture_for_test("BID-A", "TID-A"); conn.install_browser_context_fixture_for_test(first); - let mut second = BrowserContext::new_with_page_for_test("BID-B", "TID-B"); + let mut second = conn.new_page_target_fixture_for_test("BID-B", "TID-B"); second.attach_active_session("SID-B"); conn.push_inactive_browser_context_fixture_for_test(second); @@ -1610,12 +1728,12 @@ async fn session_scoped_process_message_restores_previously_active_context_after #[tokio::test(flavor = "multi_thread")] async fn session_scoped_process_message_async_restores_previously_active_context_after_dispatch() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let first = BrowserContext::new_with_page_for_test("BID-A", "TID-A"); + let first = conn.new_page_target_fixture_for_test("BID-A", "TID-A"); conn.install_browser_context_fixture_for_test(first); - let mut second = BrowserContext::new_with_page_for_test("BID-B", "TID-B"); + let mut second = conn.new_page_target_fixture_for_test("BID-B", "TID-B"); second.attach_active_session("SID-B"); conn.push_inactive_browser_context_fixture_for_test(second); @@ -1649,9 +1767,9 @@ async fn session_scoped_process_message_async_restores_previously_active_context #[tokio::test] async fn direct_network_enable_routes_to_inactive_active_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-B".to_owned()); inactive.attach_active_session("SID-B"); conn.push_inactive_browser_context_fixture_for_test(inactive); @@ -1807,8 +1925,8 @@ async fn direct_runtime_evaluate_document_replacement_lifecycle_uses_inactive_ow #[test] fn devtools_document_lifecycle_wait_key_observes_interruption_and_target_loss() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-lifecycle-wait".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-lifecycle-wait"); browser_context.set_active_target_id("TID-lifecycle-wait".to_owned()); browser_context.attach_active_session("SID-lifecycle-wait"); browser_context.set_active_document_fixture_for_test(901); @@ -1937,7 +2055,7 @@ fn devtools_document_lifecycle_wait_key_observes_interruption_and_target_loss() DevToolsDocumentLifecycleWaitState::Unavailable ); - let mut replacement_context = BrowserContext::new("BID-other".into()); + let mut replacement_context = conn.new_browser_context_fixture_for_test("BID-other"); replacement_context.set_active_target_id("TID-other".to_owned()); replacement_context.attach_active_session("SID-lifecycle-wait"); replacement_context.set_active_document_fixture_for_test(902); @@ -1978,8 +2096,8 @@ fn devtools_document_lifecycle_wait_key_observes_interruption_and_target_loss() #[test] fn devtools_target_context_resolves_background_page_without_ambient_route() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-explicit-owner".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-explicit-owner"); browser_context.set_active_target_id("TID-active"); browser_context.set_active_document_fixture_for_test(1001); assert!(browser_context.register_page_target_url_fixture( @@ -2281,7 +2399,9 @@ async fn direct_runtime_evaluate_self_popup_does_not_navigate_active_target_for_ ctx.conn.install_browser_context_fixture_for_test(active); let page_url = "data:text/html,self-popup"; - let mut inactive = BrowserContext::new("BID-self-popup-background".into()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-self-popup-background"); inactive.register_page_target_url_fixture( "TID-self-popup-background".to_owned(), Some("SID-self-popup-background".to_owned()), @@ -2454,9 +2574,9 @@ async fn direct_runtime_evaluate_routes_to_inactive_attached_owner_without_activ #[tokio::test] async fn direct_network_enable_disable_routes_to_inactive_attached_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-B".to_owned()); inactive.attach_active_session("SID-primary"); assert!(inactive.assign_attached_session_to_target("TID-B", "SID-attached".to_owned())); @@ -2522,9 +2642,9 @@ async fn direct_network_enable_disable_routes_to_inactive_attached_owner_without #[tokio::test] async fn direct_page_preload_routes_to_inactive_active_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-B".to_owned()); inactive.attach_active_session("SID-B"); conn.push_inactive_browser_context_fixture_for_test(inactive); @@ -2576,9 +2696,9 @@ async fn direct_page_preload_routes_to_inactive_active_owner_without_activating_ #[tokio::test] async fn direct_page_preload_routes_to_inactive_background_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -2632,9 +2752,9 @@ async fn direct_page_preload_routes_to_inactive_background_owner_without_activat #[tokio::test] async fn direct_network_enable_routes_to_inactive_background_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -2673,9 +2793,9 @@ async fn direct_network_enable_routes_to_inactive_background_owner_without_activ #[tokio::test] async fn direct_attached_network_enable_for_background_target_does_not_enable_primary_listener() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -2715,9 +2835,9 @@ async fn direct_attached_network_enable_for_background_target_does_not_enable_pr #[tokio::test] async fn direct_network_enable_routes_to_active_background_owner_without_activating_target() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut active = BrowserContext::new("BID-A".into()); + let mut active = conn.new_browser_context_fixture_for_test("BID-A"); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active"); active.register_page_target_fixture( @@ -2876,22 +2996,15 @@ async fn direct_network_enable_for_loaded_background_owner_starts_at_network_tai #[tokio::test] async fn direct_background_command_does_not_emit_active_observable_output_under_background_session() { - let mut conn = CdpConnection::new(); - let active_page = conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("active page should load"); + let mut conn = crate::test_support::connection(); - let mut active = BrowserContext::new("BID-A".into()); + let mut active = conn.new_browser_context_fixture_for_test("BID-A"); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active"); active.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state .console_enabled = true; - active.replace_loaded_page(Some(active_page)); active.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -2899,6 +3012,11 @@ async fn direct_background_command_does_not_emit_active_observable_output_under_ TargetPageSlot::empty_for_test_fixture(), ); conn.install_browser_context_fixture_for_test(active); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,", + Some("SID-active"), + ) + .await; let response = conn .process_message_messages_only_for_test( @@ -3033,9 +3151,9 @@ async fn direct_console_routes_to_inactive_active_owner_without_activating_slot( #[tokio::test] async fn direct_console_routes_to_inactive_background_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -3225,19 +3343,16 @@ async fn direct_console_routes_to_loaded_background_owner_and_advances_backgroun #[tokio::test] async fn direct_log_enable_routes_to_inactive_active_owner_without_activating_slot_or_replaying_console_api() { - let mut conn = CdpConnection::new(); - - let page = conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("test page should load"); - let mut inactive = BrowserContext::new_with_page_for_test("BID-B", "TID-B"); + let mut conn = crate::test_support::connection(); + let mut inactive = conn.new_page_target_fixture_for_test("BID-B", "TID-B"); inactive.set_target_url("data:text/html,log-direct-test".to_owned()); inactive.attach_active_session("SID-B"); - inactive.replace_loaded_page(Some(page)); conn.push_inactive_browser_context_fixture_for_test(inactive); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,", + Some("SID-B"), + ) + .await; let response = conn .process_message_messages_only_for_test( @@ -3282,9 +3397,9 @@ async fn direct_log_enable_routes_to_inactive_active_owner_without_activating_sl #[tokio::test] async fn direct_log_enable_routes_to_inactive_background_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -3325,27 +3440,20 @@ async fn direct_log_enable_routes_to_inactive_background_owner_without_activatin #[tokio::test] async fn direct_log_enable_routes_to_loaded_background_owner_without_replaying_console_api() { - let mut conn = CdpConnection::new(); - let page = conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("background page should load"); - - let mut inactive = BrowserContext::new("BID-B".into()); + let mut conn = crate::test_support::connection(); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), TargetIdentityState::about_blank(), TargetPageSlot::empty_for_test_fixture(), ); - inactive - .page_target_mut("TID-background") - .unwrap() - .set_target_url(page.final_url().as_str().to_owned()); - inactive.replace_target_page_for_test("TID-background", Some(page)); conn.push_inactive_browser_context_fixture_for_test(inactive); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,", + Some("SID-background"), + ) + .await; let response = conn .process_message_messages_only_for_test( @@ -3448,9 +3556,9 @@ async fn direct_log_disable_routes_to_inactive_active_owner_without_activating_s #[tokio::test] async fn direct_log_disable_routes_to_inactive_background_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -3497,9 +3605,9 @@ async fn direct_log_disable_routes_to_inactive_background_owner_without_activati #[tokio::test] async fn direct_network_policy_routes_to_inactive_active_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-B".to_owned()); inactive.attach_active_session("SID-B"); conn.push_inactive_browser_context_fixture_for_test(inactive); @@ -3569,9 +3677,9 @@ async fn direct_network_policy_routes_to_inactive_active_owner_without_activatin #[tokio::test] async fn direct_network_policy_invalid_params_return_owner_plan_error_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-B".to_owned()); inactive.attach_active_session("SID-B"); conn.push_inactive_browser_context_fixture_for_test(inactive); @@ -3621,9 +3729,9 @@ async fn direct_network_policy_invalid_params_return_owner_plan_error_without_ac #[tokio::test] async fn direct_network_policy_routes_to_inactive_background_owner_without_activating_slot() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-B"); inactive.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -3751,13 +3859,13 @@ async fn streaming_navigation_collect_transition_preserves_redirect_cookie_and_b }); let start_url = format!("http://{addr}/start"); - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-1".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let outcome = conn .load_navigation_request_via_runtime_async("GET", &start_url, None, Vec::new().into()) .await .expect("streaming navigation should load"); - let mut navigation = commit_navigation_outcome_for_test(&mut conn, outcome).await; + let navigation = commit_navigation_outcome_for_test(&mut conn, outcome).await; assert_eq!( navigation.final_url.as_str(), @@ -3787,10 +3895,20 @@ async fn streaming_navigation_collect_transition_preserves_redirect_cookie_and_b .map(|cookie| cookie.name) .collect::>(); assert!(cookie_names.iter().any(|name| name == "final")); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(navigation.page) + .await; assert_eq!( - navigation - .page - .evaluate_runtime_expression_async("document.getElementById('from-stream').textContent") + conn.browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-1", + "document.getElementById('from-stream').textContent", + false, + ) .await .expect("loaded page should be evaluable")["value"], json!("streamed") @@ -3803,7 +3921,10 @@ async fn streaming_navigation_collect_transition_preserves_redirect_cookie_and_b async fn data_image_navigation_loads_from_synthetic_response_without_curl() { let data_url = "data:image/png;base64,AP9h"; let request_headers = vec![("accept".to_owned(), "image/png".to_owned())]; - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); + conn.install_browser_context_fixture_for_test( + conn.new_page_target_fixture_for_test("BID-image", "TID-image"), + ); let outcome = conn .load_navigation_request_via_runtime_async( @@ -3922,9 +4043,9 @@ async fn streaming_navigation_feeds_parser_before_body_eof() { }); let page_url = format!("http://{addr}/page"); - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-1".into())); - let mut navigation = tokio::time::timeout(std::time::Duration::from_secs(4), async { + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); + let navigation = tokio::time::timeout(std::time::Duration::from_secs(4), async { let outcome = conn .load_navigation_request_via_runtime_async("GET", &page_url, None, Vec::new().into()) .await @@ -3939,20 +4060,33 @@ async fn streaming_navigation_feeds_parser_before_body_eof() { "parser should request the external script before the main body EOF" ); assert!(navigation.response_body().contains("id=\"tail\"")); + conn.browser_context + .as_mut() + .unwrap() + .commit_active_navigation_for_test(navigation.page) + .await; assert_eq!( - navigation - .page - .evaluate_runtime_expression_async( - "document.documentElement.getAttribute('data-script')" + conn.browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-1", + "document.documentElement.getAttribute('data-script')", + false, ) .await .expect("loaded page should be evaluable")["value"], json!("seen") ); assert_eq!( - navigation - .page - .evaluate_runtime_expression_async("document.getElementById('tail').textContent") + conn.browser_context + .as_mut() + .unwrap() + .evaluate_target_expression_for_test( + "TID-1", + "document.getElementById('tail').textContent", + false, + ) .await .expect("loaded page should be evaluable")["value"], json!("done") diff --git a/moli-protocol/src/conn/tests/site_data.rs b/moli-protocol/src/conn/tests/site_data.rs index 86affa0427..3bf9ed6890 100644 --- a/moli-protocol/src/conn/tests/site_data.rs +++ b/moli-protocol/src/conn/tests/site_data.rs @@ -4,7 +4,8 @@ use super::*; fn browser_context_cookie_sites_and_clear_for_sites_use_site_keys() { let bc = BrowserContext::new("BID-sites".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"a=1; Path=/app".to_vec())], @@ -31,7 +32,8 @@ fn browser_context_cookie_sites_and_clear_for_sites_use_site_keys() { fn browser_context_clear_cookies_for_sites_report_projects_replaced_and_remaining_state() { let bc = BrowserContext::new("BID-site-clear-report".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"session=1; Path=/app".to_vec())], @@ -68,7 +70,8 @@ fn browser_context_clear_cookies_for_sites_report_projects_replaced_and_remainin fn browser_context_cookie_site_data_summarizes_counts_by_site() { let bc = BrowserContext::new("BID-site-data".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"a=1; Path=/app".to_vec())], @@ -99,7 +102,8 @@ fn browser_context_cookie_site_data_summarizes_counts_by_site() { fn browser_context_preview_clear_cookies_for_sites_reports_targeted_removal_without_mutation() { let bc = BrowserContext::new("BID-site-clear-preview".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"session=1; Path=/app".to_vec())], @@ -142,7 +146,8 @@ fn browser_context_preview_clear_cookies_for_sites_reports_targeted_removal_with fn browser_context_preview_clear_cookies_for_sites_with_persistent_scope_keeps_session_slice() { let bc = BrowserContext::new("BID-site-clear-preview-persistent".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"session=1; Path=/app".to_vec())], @@ -179,7 +184,8 @@ fn browser_context_preview_clear_cookies_for_sites_with_persistent_scope_keeps_s fn browser_context_preview_clear_cookie_store_defaults_to_all_scope() { let bc = BrowserContext::new("BID-store-clear-preview-all".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"session=1; Path=/app".to_vec())], @@ -211,8 +217,8 @@ fn browser_context_preview_clear_cookie_store_defaults_to_all_scope() { #[tokio::test] async fn connection_preview_clear_cookie_store_with_persistent_scope_does_not_invalidate_live_document_cookie_cache() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-store-clear-preview-live", "TID-store-clear-preview-live", )); @@ -238,7 +244,8 @@ async fn connection_preview_clear_cookie_store_with_persistent_scope_does_not_in conn.browser_context .as_mut() .unwrap() - .replace_active_page_for_test(Some(navigation.page)); + .commit_active_navigation_for_test(navigation.page) + .await; let before = conn .evaluate_runtime_expression_with_await_async("document.cookie", false) @@ -268,7 +275,8 @@ async fn connection_preview_clear_cookie_store_with_persistent_scope_does_not_in fn browser_context_preview_clear_cookie_storage_with_site_target_projects_target() { let bc = BrowserContext::new("BID-store-clear-target-preview".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"session=1; Path=/app".to_vec())], @@ -301,8 +309,8 @@ fn browser_context_preview_clear_cookie_storage_with_site_target_projects_target #[tokio::test] async fn connection_clear_cookie_store_with_session_scope_invalidates_live_document_cookie_cache_but_preserves_persistent_cookie() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-store-clear-session-live", "TID-store-clear-session-live", )); @@ -328,7 +336,8 @@ async fn connection_clear_cookie_store_with_session_scope_invalidates_live_docum conn.browser_context .as_mut() .unwrap() - .replace_active_page_for_test(Some(navigation.page)); + .commit_active_navigation_for_test(navigation.page) + .await; let before = conn .evaluate_runtime_expression_with_await_async("document.cookie", false) @@ -356,16 +365,14 @@ async fn connection_clear_cookie_store_with_session_scope_invalidates_live_docum #[test] fn connection_clear_cookie_storage_with_site_target_projects_targeted_report() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-store-clear-target-report".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-store-clear-target-report")); { - let mut store = conn - .browser_context - .as_ref() - .unwrap() - .cookie_store_for_test() - .lock(); + let browser_context = conn.browser_context.as_ref().unwrap(); + let store_handle = browser_context.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"session=1; Path=/app".to_vec())], @@ -410,7 +417,8 @@ fn connection_clear_cookie_storage_with_site_target_projects_targeted_report() { fn browser_context_preview_cookie_site_data_operation_clear_projects_generic_owner_seam() { let bc = BrowserContext::new("BID-op-preview-clear".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"session=1; Path=/app".to_vec())], @@ -443,7 +451,8 @@ fn browser_context_preview_cookie_site_data_operation_clear_projects_generic_own fn browser_context_cookie_storage_state_snapshot_distinguishes_live_and_persistent_views() { let bc = BrowserContext::new("BID-site-state".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"session=1; Path=/app".to_vec())], @@ -482,7 +491,8 @@ fn browser_context_cookie_storage_state_snapshot_distinguishes_live_and_persiste fn browser_context_cookie_storage_state_snapshot_for_sites_filters_views() { let bc = BrowserContext::new("BID-site-state-scoped".into()); { - let mut store = bc.cookie_store_for_test().lock(); + let store_handle = bc.cookie_store_for_test(); + let mut store = store_handle.lock(); store.store_response_headers( &Url::parse("https://app.example.com/app/index.html").unwrap(), &[("set-cookie".to_owned(), b"session=1; Path=/app".to_vec())], @@ -514,11 +524,9 @@ fn browser_context_cookie_storage_state_snapshot_for_sites_filters_views() { #[tokio::test] async fn connection_cookie_site_clear_invalidates_live_document_cookie_cache() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-live-sites", - "TID-live-sites", - )); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_page_target_fixture_for_test("BID-live-sites", "TID-live-sites")); let url = Url::parse("https://app.example.com/app").unwrap(); let navigation = conn @@ -535,7 +543,8 @@ async fn connection_cookie_site_clear_invalidates_live_document_cookie_cache() { conn.browser_context .as_mut() .unwrap() - .replace_active_page_for_test(Some(navigation.page)); + .commit_active_navigation_for_test(navigation.page) + .await; let before = conn .evaluate_runtime_expression_with_await_async("document.cookie", false) @@ -554,8 +563,9 @@ async fn connection_cookie_site_clear_invalidates_live_document_cookie_cache() { #[test] fn connection_clear_cookies_for_sites_report_projects_targeted_state() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-live-site-clear-report".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-live-site-clear-report")); { let cookie_store = conn.ensure_cookie_store().unwrap(); let mut store = cookie_store.lock(); @@ -592,8 +602,8 @@ fn connection_clear_cookies_for_sites_report_projects_targeted_state() { #[tokio::test] async fn connection_preview_clear_cookies_for_sites_does_not_invalidate_live_document_cookie_cache() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test( "BID-live-site-clear-preview", "TID-live-site-clear-preview", )); @@ -613,7 +623,8 @@ async fn connection_preview_clear_cookies_for_sites_does_not_invalidate_live_doc conn.browser_context .as_mut() .unwrap() - .replace_active_page_for_test(Some(navigation.page)); + .commit_active_navigation_for_test(navigation.page) + .await; let before = conn .evaluate_runtime_expression_with_await_async("document.cookie", false) @@ -657,8 +668,8 @@ async fn connection_preview_clear_cookies_for_sites_does_not_invalidate_live_doc #[test] fn connection_cookie_sites_reflect_active_browser_store() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-live-sites".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-live-sites")); { let cookie_store = conn.ensure_cookie_store().unwrap(); let mut store = cookie_store.lock(); @@ -680,8 +691,8 @@ fn connection_cookie_sites_reflect_active_browser_store() { #[test] fn connection_cookie_site_data_reflects_active_browser_store() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-live-site-data".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-live-site-data")); { let cookie_store = conn.ensure_cookie_store().unwrap(); let mut store = cookie_store.lock(); @@ -710,8 +721,8 @@ fn connection_cookie_site_data_reflects_active_browser_store() { #[test] fn connection_cookie_storage_state_snapshot_reflects_active_browser_store() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-live-site-state".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-live-site-state")); { let cookie_store = conn.ensure_cookie_store().unwrap(); let mut store = cookie_store.lock(); @@ -737,8 +748,9 @@ fn connection_cookie_storage_state_snapshot_reflects_active_browser_store() { #[test] fn connection_cookie_storage_state_snapshot_for_sites_filters_active_store() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new("BID-live-site-state-scoped".into())); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-live-site-state-scoped")); { let cookie_store = conn.ensure_cookie_store().unwrap(); let mut store = cookie_store.lock(); diff --git a/moli-protocol/src/domains/accessibility/tests.rs b/moli-protocol/src/domains/accessibility/tests.rs index f72f5fdeca..44ce4b7287 100644 --- a/moli-protocol/src/domains/accessibility/tests.rs +++ b/moli-protocol/src/domains/accessibility/tests.rs @@ -33,7 +33,7 @@ async fn complete_command_task_for_test( } async fn load_page_async(ctx: &mut TestContext, html: &str) { - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); let data_url = format!("data:text/html,{html}"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -54,14 +54,12 @@ async fn complete_child_frame_lifecycle(ctx: &mut TestContext) { .conn .start_child_frame_lifecycle_work_for_owner(owner, std::time::Duration::from_secs(2)) .expect("loaded page should expose child-frame lifecycle work"); - let completed = pending - .wait() - .await - .expect("child-frame lifecycle work should complete"); + let completed = pending.wait().await; assert!( ctx.conn - .complete_child_frame_lifecycle_work_for_session_owner(completed) - .expect("child-frame lifecycle completion should apply"), + .finish_document_child_frame_lifecycle_work(completed) + .expect("child-frame lifecycle completion should apply") + .0, "child-frame lifecycle should settle before inspecting the nested frame tree" ); } @@ -400,7 +398,9 @@ fn renderer_backed_ax_node_id(node: &Value) -> String { async fn accessibility_loaded_page_methods_target_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-A".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-A".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -517,12 +517,16 @@ async fn accessibility_loaded_page_methods_target_background_owner_without_activ #[tokio::test(flavor = "multi_thread")] async fn accessibility_loaded_page_methods_target_inactive_owner_without_activation() { let mut ctx = TestContext::new(); - let mut active = BrowserContext::new("BID-active".to_owned()); + let mut active = ctx + .conn + .new_browser_context_fixture_for_test("BID-active".to_owned()); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active".to_owned()); ctx.conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.set_target_url("about:blank".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); @@ -576,7 +580,7 @@ async fn get_full_ax_tree_requires_browser_context() { #[tokio::test(flavor = "multi_thread")] async fn get_full_ax_tree_requires_loaded_page() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({"id": 2, "method": "Accessibility.getFullAXTree"})) .await; ctx.expect_error(2, -32000, "NoDocumentLoaded"); @@ -890,7 +894,7 @@ async fn get_root_ax_node_rejects_foreign_frame() { #[tokio::test(flavor = "multi_thread")] async fn get_root_ax_node_requires_loaded_page() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({ "id": 350, @@ -1081,7 +1085,7 @@ async fn get_child_ax_nodes_validates_ax_id_and_frame() { #[tokio::test(flavor = "multi_thread")] async fn get_child_ax_nodes_requires_loaded_page() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({ "id": 390, @@ -2031,7 +2035,7 @@ async fn get_ax_node_and_ancestors_requires_context_loaded_page_and_bound_node() .await; ctx.expect_error(530, -31998, "BrowserContextNotLoaded"); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({ "id": 531, "method": "Accessibility.getAXNodeAndAncestors", @@ -2518,7 +2522,7 @@ async fn query_ax_tree_requires_context_loaded_page_and_bound_node() { .await; ctx.expect_error(580, -31998, "BrowserContextNotLoaded"); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({ "id": 581, "method": "Accessibility.queryAXTree", diff --git a/moli-protocol/src/domains/activity/main_document.rs b/moli-protocol/src/domains/activity/main_document.rs index cdbe0c5eb9..65db04112d 100644 --- a/moli-protocol/src/domains/activity/main_document.rs +++ b/moli-protocol/src/domains/activity/main_document.rs @@ -570,12 +570,13 @@ impl MainDocumentNavigationActivity { pending_download: Option, ) { let mut command_context = CommandDispatchContext::default(); - let error = if let Some(download) = pending_download { + let error = if let Some(download) = pending_download.and_then(|download| { + conn.prepare_download_activation(&self.state.owner, self.state.web_contents, download) + }) { let mut download_events = Vec::new(); let error = conn - .handle_pending_download_activation_inline_async( + .handle_prepared_download_activation_inline_async( &mut download_events, - &self.state.owner, download, &mut command_context, ) @@ -936,6 +937,7 @@ mod tests { redirect_headers: None, navigate_id: Some(77), owner: CommandOwnerScope::for_session("SID-nav"), + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: NavigationResultProjection::Cdp( json!({ "frameId": "FRAME-1", "loaderId": "LID-1" }), ), @@ -960,8 +962,9 @@ mod tests { CommittedRendererDocumentBinding, RendererDocumentLifecycleEvent, ) { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-deferred-load-observer".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-deferred-load-observer".to_owned()); browser_context.set_active_target_id("TID-deferred-load-observer"); browser_context.attach_active_session("SID-nav"); browser_context.set_target_url("https://example.test/start".to_owned()); @@ -1101,8 +1104,8 @@ mod tests { #[test] fn navigation_activity_error_drains_progress_before_error_response() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new_with_page_for_test("BID-1", "TID-page"); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_page_target_fixture_for_test("BID-1", "TID-page"); browser_context.attach_active_session("SID-page"); browser_context .active_page_target_mut() @@ -1143,7 +1146,7 @@ mod tests { } fn failed_navigation_messages(navigation: anyhow::Result) -> Vec { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut browser_context = BrowserContext::new("BID-1".to_owned()); browser_context.set_active_target_id("TID-page"); browser_context.attach_active_session("SID-page"); @@ -1367,7 +1370,7 @@ mod tests { [moli_page_types::DevToolsSessionKey::Primary] .page_session_state .page_domain_enabled = true; - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.install_browser_context_fixture_for_test(browser_context); let state = navigation_state(); let activity = MainDocumentNavigationActivity::new( @@ -1461,7 +1464,7 @@ mod tests { .expect("old navigation token"); browser_context.commit_document_navigation_if_matches(&old_token); - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.install_browser_context_fixture_for_test(browser_context); let activity = MainDocumentNavigationActivity::new( navigation_state(), @@ -1513,7 +1516,7 @@ mod tests { .expect("navigation token"); browser_context.commit_document_navigation_if_matches(&token); - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.install_browser_context_fixture_for_test(browser_context); let activity = MainDocumentNavigationActivity::new( navigation_state(), diff --git a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs index cbc969e9ab..107a4da05c 100644 --- a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs +++ b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs @@ -218,7 +218,7 @@ impl PreparedProtocolOutputs { } RendererOwnerAction::Download(activation) => { crate::domains::input::InputPreparedOutputs::from_renderer_download_activation( - activation, + conn, owner, activation, ) .append_to_output_sink(&mut prepared); } diff --git a/moli-protocol/src/domains/activity/output_ingress/renderer.rs b/moli-protocol/src/domains/activity/output_ingress/renderer.rs index 81898b339f..a7e88d6b31 100644 --- a/moli-protocol/src/domains/activity/output_ingress/renderer.rs +++ b/moli-protocol/src/domains/activity/output_ingress/renderer.rs @@ -308,7 +308,7 @@ async fn project_renderer_output_records_for_owner( mod tests { use moli_core::RendererRuntimeCommandCausalIdentity; - use crate::conn::{BrowserContext, CdpConnection, CommandOwnerScope}; + use crate::conn::CommandOwnerScope; use super::renderer_owner_action_owner; @@ -334,8 +334,8 @@ mod tests { Some(RendererPublicationProjection::InspectionOnly), Some(RendererPublicationProjection::CurrentOwner), ] { - let mut conn = CdpConnection::default(); - let mut context = BrowserContext::new(CONTEXT.into()); + let mut conn = crate::test_support::connection(); + let mut context = conn.new_browser_context_fixture_for_test(CONTEXT); context.set_active_target_id(TARGET); context.set_active_document_fixture_for_test(1); let page_id = moli_core::PageId::new_for_testing(42); @@ -475,8 +475,9 @@ mod tests { #[test] fn unbound_owner_actions_choose_a_stable_attachment_without_overriding_exact_root_cause() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-owner-action".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-owner-action".to_owned()); browser_context.set_active_target_id("TID-owner-action".to_owned()); assert!( browser_context.assign_attached_session_to_target( diff --git a/moli-protocol/src/domains/activity/renderer_command_response_order.rs b/moli-protocol/src/domains/activity/renderer_command_response_order.rs index 836d36b0e2..4ba306ed7d 100644 --- a/moli-protocol/src/domains/activity/renderer_command_response_order.rs +++ b/moli-protocol/src/domains/activity/renderer_command_response_order.rs @@ -488,8 +488,7 @@ mod tests { use serde_json::Value; use crate::conn::{ - BrowserContext, CdpConnection, CommandDispatchContext, CommandOwnerScope, - RendererCommandDescriptor, + CdpConnection, CommandDispatchContext, CommandOwnerScope, RendererCommandDescriptor, }; use super::{ @@ -500,23 +499,22 @@ mod tests { const SESSION_ID: &str = "SID-runtime-command-permit"; async fn connection_with_loaded_page() -> CdpConnection { - let mut conn = CdpConnection::new(); - let page = conn - .load_page_via_runtime_async("data:text/html,runtime-command-permit") - .await - .expect("permit test page should load"); - let mut browser_context = BrowserContext::new("BID-runtime-command-permit".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-runtime-command-permit".to_owned()); browser_context.set_active_target_id("TID-runtime-command-permit"); browser_context.attach_active_session(SESSION_ID); - browser_context.set_target_url(page.final_url().as_str().to_owned()); - let _ = browser_context.replace_active_page_for_test(Some(page)); conn.install_browser_context_fixture_for_test(browser_context); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,runtime-command-permit", + Some(SESSION_ID), + ) + .await; conn } /// Returns the initial Document identity used by this minimal protocol - /// fixture. The fixture installs a loaded Page directly, so it has no - /// separate protocol-side root-Document binding to query. + /// fixture. fn loaded_page_source_document(conn: &CdpConnection) -> RendererDocumentLifecycleIdentity { let owner = crate::conn::CommandOwnerScope::capture(conn, Some(SESSION_ID)); let (context_id, target_id) = conn.resolved_page_owner_identity_for_owner(&owner).unwrap(); diff --git a/moli-protocol/src/domains/audits.rs b/moli-protocol/src/domains/audits.rs index eede81369f..0179e2db04 100644 --- a/moli-protocol/src/domains/audits.rs +++ b/moli-protocol/src/domains/audits.rs @@ -78,11 +78,12 @@ fn unknown_session_output_plan() -> CommandOutputPlan { mod tests { use serde_json::json; - use crate::conn::BrowserContext; use crate::testing::TestContext; async fn load_document(ctx: &mut TestContext, html: &str) { - let mut browser_context = BrowserContext::new("BID-audits".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-audits".to_owned()); browser_context.set_active_target_id("TID-audits".to_owned()); browser_context.set_target_url("data:text/html,audits-test".to_owned()); browser_context.attach_active_session("SID-audits".to_owned()); diff --git a/moli-protocol/src/domains/autofill.rs b/moli-protocol/src/domains/autofill.rs index 78d1d6e3bd..cfca45a8c0 100644 --- a/moli-protocol/src/domains/autofill.rs +++ b/moli-protocol/src/domains/autofill.rs @@ -1,24 +1,27 @@ use chromiumoxide_cdp::cdp::browser_protocol::autofill::TriggerParams; use moli_core::page::{ - CompletedPageCommand, PendingPageCommand, RendererAutofillAddressField, - RendererAutofillCreditCard, RendererAutofillTriggerOutcome, RendererAutofillTriggerRequest, + RendererAutofillAddressField, RendererAutofillCreditCard, RendererAutofillTriggerOutcome, + RendererAutofillTriggerRequest, }; use crate::{ - conn::{CdpConnection, Cmd, CommandOwnerScope}, + conn::{ + CdpConnection, Cmd, CommandOwnerScope, CompletedDocumentAutofillTrigger, + PendingDocumentAutofillTrigger, + }, domains::{actions::AutofillAction, command_output::CommandOutputPlan}, }; pub(crate) struct PendingAutofillCommandDispatch { command_id: Option, owner_scope: CommandOwnerScope, - pending: PendingPageCommand, + pending: PendingDocumentAutofillTrigger, } pub(crate) struct CompletedAutofillCommandDispatch { command_id: Option, owner_scope: CommandOwnerScope, - completed: Result, + completed: CompletedDocumentAutofillTrigger, } pub(crate) enum AutofillCommandTaskStep { @@ -31,7 +34,7 @@ impl PendingAutofillCommandDispatch { CompletedAutofillCommandDispatch { command_id: self.command_id, owner_scope: self.owner_scope, - completed: self.pending.wait().await.map_err(|error| error.to_string()), + completed: self.pending.wait().await, } } } @@ -109,13 +112,8 @@ pub(crate) fn try_start_autofill_command_dispatch( }; let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id); let pending = conn - .resolved_page_owner_identity_for_owner(&owner_scope) - .ok_or_else(|| "NoDocumentLoaded".to_owned()) - .and_then(|(context_id, target_id)| { - conn.browser_context_by_id(&context_id) - .ok_or("NoDocumentLoaded")? - .start_target_autofill_trigger(&target_id, request) - }); + .resolve_browser_document_for_owner(&owner_scope) + .and_then(|document| conn.start_document_autofill_trigger(document, request)); match pending { Ok(pending) => AutofillCommandTaskStep::Pending(PendingAutofillCommandDispatch { command_id: cmd.id, @@ -132,15 +130,7 @@ pub(crate) fn complete_pending_autofill_command( conn: &mut CdpConnection, completed: CompletedAutofillCommandDispatch, ) -> CommandOutputPlan { - let outcome = completed.completed.and_then(|completion| { - conn.ensure_document_accessible_for_owner(&completed.owner_scope)?; - let (context_id, target_id) = conn - .resolved_page_owner_identity_for_owner(&completed.owner_scope) - .ok_or("NoDocumentLoaded")?; - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_target_autofill_trigger(&target_id, completion) - }); + let outcome = conn.finish_document_autofill_trigger(completed.completed); match outcome { Ok(RendererAutofillTriggerOutcome::Applied { .. }) => CommandOutputPlan::success(), Ok(RendererAutofillTriggerOutcome::FieldNotFound) => { @@ -164,11 +154,11 @@ pub(crate) fn complete_pending_autofill_command( #[cfg(test)] mod tests { - use crate::{conn::BrowserContext, testing::TestContext}; + use crate::testing::TestContext; use serde_json::{Value, json}; async fn load_document(ctx: &mut TestContext, html: &str) { - let mut browser_context = BrowserContext::new("BID-1".into()); + let mut browser_context = ctx.conn.new_browser_context_fixture_for_test("BID-1"); browser_context.set_active_target_id("TID-1".to_owned()); browser_context.set_target_url("data:text/html,autofill-test".to_owned()); browser_context.attach_active_session("SID-1".to_owned()); diff --git a/moli-protocol/src/domains/browser.rs b/moli-protocol/src/domains/browser.rs index 0c610ed9b1..aaca64530a 100644 --- a/moli-protocol/src/domains/browser.rs +++ b/moli-protocol/src/domains/browser.rs @@ -6,8 +6,8 @@ use serde_json::{Value, json}; use std::str::FromStr; use crate::conn::{ - BrowserWindowBounds, CdpConnection, Cmd, CompletedContextPermissionUpdate, - PendingContextPermissionUpdate, + CdpConnection, Cmd, CommandOwnerScope, CompletedContextPermissionUpdate, + PendingContextPermissionUpdate, WindowSurface, WindowSurfaceState, }; use crate::devtools_runtime::{ DevToolsCommand, DevToolsCommandResult, DevToolsError, DevToolsErrorKind, @@ -19,8 +19,6 @@ use crate::version; use moli_core::browser::DownloadPolicy; use moli_core::page::PermissionOverrideRegistration; -const DEV_TOOLS_WINDOW_ID: u32 = 1_923_710_101; - /// Disables Browser-domain observation owned by one DevTools session. pub(in crate::domains) fn dispose_session_handler(conn: &mut CdpConnection, session_id: &str) { conn.set_browser_download_events_enabled_for_session(Some(session_id), false); @@ -133,7 +131,7 @@ pub(crate) fn try_start_browser_command_dispatch( match action { BrowserAction::GetVersion => BrowserCommandTaskStep::Complete(get_version(conn)), BrowserAction::GetWindowForTarget => { - BrowserCommandTaskStep::Complete(get_window_for_target(conn)) + BrowserCommandTaskStep::Complete(get_window_for_target(conn, cmd)) } BrowserAction::SetWindowBounds => { BrowserCommandTaskStep::Complete(set_window_bounds(conn, cmd)) @@ -161,32 +159,46 @@ fn get_version(conn: &CdpConnection) -> CommandOutputPlan { })) } -fn bounds_json(bounds: &BrowserWindowBounds) -> Value { - let mut value = json!({ - "windowState": bounds.window_state, - }); - let object = value - .as_object_mut() - .expect("browser bounds json must be an object"); - if let Some(left) = bounds.left { - object.insert("left".to_owned(), json!(left)); - } - if let Some(top) = bounds.top { - object.insert("top".to_owned(), json!(top)); - } - if let Some(width) = bounds.width { - object.insert("width".to_owned(), json!(width)); - } - if let Some(height) = bounds.height { - object.insert("height".to_owned(), json!(height)); - } - value +fn bounds_json(surface: WindowSurface) -> Value { + json!({ + "windowState": surface.state.label(), + "left": surface.x, + "top": surface.y, + "width": surface.width, + "height": surface.height, + }) } -fn get_window_for_target(conn: &CdpConnection) -> CommandOutputPlan { +#[derive(Default, Deserialize)] +#[serde(rename_all = "camelCase")] +struct GetWindowForTargetParams { + #[serde(default)] + target_id: Option, +} + +fn get_window_for_target(conn: &CdpConnection, cmd: &Cmd<'_>) -> CommandOutputPlan { + let params = match cmd.get_params::() { + Ok(Some(params)) => params, + Ok(None) => GetWindowForTargetParams::default(), + Err(_) => return CommandOutputPlan::error(-32602, "InvalidParams"), + }; + let handle = match params.target_id { + Some(target_id) => conn.browser_web_contents_for_target(&target_id), + None => { + conn.browser_web_contents_for_owner(&CommandOwnerScope::capture(conn, cmd.session_id)) + } + }; + let handle = match handle { + Ok(handle) => handle, + Err(message) => return CommandOutputPlan::error(-32000, message), + }; + let surface = match conn.browser_window_surface(handle) { + Ok(surface) => surface, + Err(message) => return CommandOutputPlan::error(-32000, message), + }; CommandOutputPlan::result(json!({ - "windowId": DEV_TOOLS_WINDOW_ID, - "bounds": bounds_json(&conn.window_bounds) + "windowId": handle.id().get(), + "bounds": bounds_json(surface) })) } @@ -248,9 +260,13 @@ fn set_window_bounds(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> CommandOutputPl } }; - if *params.window_id.inner() != i64::from(DEV_TOOLS_WINDOW_ID) { + let Ok(window_id) = u64::try_from(*params.window_id.inner()) else { return CommandOutputPlan::error(-32602, "InvalidParams"); - } + }; + let handle = match conn.browser_web_contents_for_window_id(window_id) { + Ok(handle) => handle, + Err(_) => return CommandOutputPlan::error(-32602, "InvalidParams"), + }; let left = match optional_i64_to_i32(params.bounds.left) { Ok(left) => left, @@ -276,12 +292,17 @@ fn set_window_bounds(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> CommandOutputPl return CommandOutputPlan::error(-32602, "InvalidParams"); } }; - conn.window_bounds.left = left; - conn.window_bounds.top = top; - conn.window_bounds.width = width; - conn.window_bounds.height = height; - if let Some(window_state) = params.bounds.window_state { - conn.window_bounds.window_state = window_state.as_ref().to_owned(); + let state = match params.bounds.window_state { + Some(window_state) => match WindowSurfaceState::from_label(window_state.as_ref()) { + Some(state) => Some(state), + None => return CommandOutputPlan::error(-32602, "InvalidParams"), + }, + None => None, + }; + if let Err(message) = + conn.update_browser_window_surface(handle, state, width, height, left, top) + { + return CommandOutputPlan::error(-32000, message); } CommandOutputPlan::success() diff --git a/moli-protocol/src/domains/console.rs b/moli-protocol/src/domains/console.rs index 6ecf3b403b..18b70a3be7 100644 --- a/moli-protocol/src/domains/console.rs +++ b/moli-protocol/src/domains/console.rs @@ -146,7 +146,7 @@ pub(in crate::domains) fn pending_console_activity_snapshot( #[cfg(test)] mod tests { - use crate::conn::{BrowserContext, CdpCommandTaskStep}; + use crate::conn::CdpCommandTaskStep; use crate::domains::observable_output::{ ObservableOutputProjectionStep, observable_backlog_activity_outputs_for_session_owner as observable_backlog_activity_outputs, @@ -181,7 +181,7 @@ mod tests { } async fn load_document(ctx: &mut TestContext, html: &str) { - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,console-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); @@ -280,7 +280,9 @@ mod tests { } fn load_shared_worker_target(ctx: &mut TestContext, session_id: &str) { - let mut bc = BrowserContext::new("BID-shared".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-shared".to_owned()); let mut target = crate::conn::SharedWorkerTargetState::new( moli_core::RendererOwnerLocalHostId::new_for_testing(1), SharedWorkerInstanceId::from_u64(91), @@ -362,8 +364,9 @@ mod tests { .browser_context .as_ref() .expect("browser context should be loaded"); - let page = bc.loaded_page().expect("page should be loaded"); - let script_execution = page.script_execution(); + let script_execution = bc + .loaded_document_script_execution_for_test() + .expect("page should be loaded"); let snapshot = super::pending_console_activity_snapshot(super::ConsoleActivitySource { observable: bc .active_page_target() @@ -551,7 +554,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn console_enable_stages_background_target_session_state() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.stage_background_target( diff --git a/moli-protocol/src/domains/css.rs b/moli-protocol/src/domains/css.rs index 5bba44b559..5af2e7b5a4 100644 --- a/moli-protocol/src/domains/css.rs +++ b/moli-protocol/src/domains/css.rs @@ -736,7 +736,7 @@ mod tests { } async fn with_loaded_document_async(ctx: &mut TestContext, html: &str) { - let mut bc = crate::conn::BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); ctx.install_navigation_fixture_for_session_owner(&format!("data:text/html,{html}"), None) @@ -954,7 +954,9 @@ mod tests { async fn css_loaded_page_methods_target_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-A".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-A".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -1083,12 +1085,16 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn css_loaded_page_methods_target_inactive_owner_without_activation() { let mut ctx = TestContext::new(); - let mut active = BrowserContext::new("BID-active".to_owned()); + let mut active = ctx + .conn + .new_browser_context_fixture_for_test("BID-active".to_owned()); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active".to_owned()); ctx.conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.set_target_url("about:blank".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); @@ -1165,9 +1171,8 @@ mod tests { #[tokio::test] async fn css_enable_and_disable_toggle_browser_context_state() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(crate::conn::BrowserContext::new_with_page_for_test( - "BID-1", "TID-1", - )); + ctx.conn.browser_context = + Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({"id": 101, "method": "CSS.enable"})) .await; @@ -1610,24 +1615,11 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn pending_css_command_keeps_background_owner_route_across_completion() { let mut ctx = TestContext::new(); - let active_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("active page should load"); - let background_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("background page should load"); - let mut browser_context = BrowserContext::new("BID-css-owner-route".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-css-owner-route".to_owned()); browser_context.set_active_target_id("TID-css-active".to_owned()); - browser_context.replace_active_page_for_test(Some(active_page)); browser_context.stage_background_target( "TID-css-background".to_owned(), None, @@ -1635,11 +1627,20 @@ mod tests { None, None, ); - browser_context.replace_target_page_for_test("TID-css-background", Some(background_page)); ctx.conn .install_browser_context_fixture_for_test(browser_context); let background_session = attach_page_session_async(&mut ctx, "TID-css-background").await; + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,", + None, + ) + .await; + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,", + Some(&background_session), + ) + .await; let active_style_sheet_id = inline_style_sheet_id_for_session_async(&mut ctx, None).await; let style_sheet_id = inline_style_sheet_id_for_session_async(&mut ctx, Some(&background_session)).await; diff --git a/moli-protocol/src/domains/debugger.rs b/moli-protocol/src/domains/debugger.rs index ffd952aae7..10acd092ab 100644 --- a/moli-protocol/src/domains/debugger.rs +++ b/moli-protocol/src/domains/debugger.rs @@ -32,27 +32,21 @@ mod tests { use serde_json::{Value, json}; - use crate::conn::{BrowserContext, CommandOwnerScope}; + use crate::conn::CommandOwnerScope; use crate::testing::TestContext; async fn with_loaded_document(ctx: &mut TestContext) { - ctx.conn - .insert_browser_context(BrowserContext::new("BID-debugger".into())); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .set_active_target_id("TID-debugger"); - let page = ctx + let mut browser_context = ctx .conn - .load_page_via_runtime_async("data:text/html,debugger") - .await - .expect("load debugger test page"); + .new_browser_context_fixture_for_test("BID-debugger"); + browser_context.set_active_target_id("TID-debugger"); ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + .install_browser_context_fixture_for_test(browser_context); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,debugger", + None, + ) + .await; } async fn command(ctx: &mut TestContext, message: Value, command_id: u64) -> Value { diff --git a/moli-protocol/src/domains/dom/patchright_shadow_tests.rs b/moli-protocol/src/domains/dom/patchright_shadow_tests.rs index c747252546..dbe916e21e 100644 --- a/moli-protocol/src/domains/dom/patchright_shadow_tests.rs +++ b/moli-protocol/src/domains/dom/patchright_shadow_tests.rs @@ -1,11 +1,10 @@ -use crate::conn::BrowserContext; use crate::testing::TestContext; use serde_json::{Value, json}; use super::tests::navigate_to_data_html_async; fn load_bc(ctx: &mut TestContext, bc_id: &str) { - let mut bc = BrowserContext::new(bc_id.into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test(bc_id); bc.set_active_target_id("TID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); } diff --git a/moli-protocol/src/domains/dom/resolve.rs b/moli-protocol/src/domains/dom/resolve.rs index f03fb4946d..245e8e2702 100644 --- a/moli-protocol/src/domains/dom/resolve.rs +++ b/moli-protocol/src/domains/dom/resolve.rs @@ -6017,7 +6017,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_document_builds_protocol_neutral_document_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "depth": 2, "pierce": true @@ -6053,7 +6053,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_flattened_document_builds_protocol_neutral_document_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(62), @@ -6080,7 +6080,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_document_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(63), @@ -6114,7 +6114,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_frame_owner_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "frameId": "TID-child" }); @@ -6137,7 +6137,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_get_frame_owner_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "frameId": "TID-child" }); @@ -6172,7 +6172,7 @@ mod protocol_neutral_tests { #[test] fn cdp_request_child_nodes_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "nodeId": 24, "depth": 3, @@ -6202,7 +6202,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_request_child_nodes_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 25 }); @@ -6234,7 +6234,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_complete_routes_request_child_nodes_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let mut out = super::DomCommandOutput::default(); let params = json!({ "nodeId": 26 @@ -6267,7 +6267,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_node_for_location_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "x": 12, "y": 34, @@ -6296,7 +6296,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_get_node_for_location_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "x": 10, "y": 20 @@ -6329,7 +6329,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_complete_routes_get_node_for_location_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "x": 10, "y": 20 @@ -6360,7 +6360,7 @@ mod protocol_neutral_tests { #[test] fn cdp_query_selector_builds_protocol_neutral_dom_query_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "nodeId": 7, "selector": "section > p.target" @@ -6393,7 +6393,7 @@ mod protocol_neutral_tests { #[test] fn cdp_query_selector_all_builds_protocol_neutral_dom_query_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "nodeId": 8, "selector": ".item" @@ -6422,7 +6422,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_query_selector_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 9, "selector": "main" @@ -6455,7 +6455,7 @@ mod protocol_neutral_tests { #[test] fn cdp_resolve_node_builds_protocol_neutral_dom_resolve_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "nodeId": 10, "executionContextId": 42, @@ -6489,7 +6489,7 @@ mod protocol_neutral_tests { #[test] fn cdp_resolve_node_preserves_backend_node_reference_source() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "backendNodeId": 31, "objectGroup": "backend" @@ -6516,7 +6516,7 @@ mod protocol_neutral_tests { #[test] fn cdp_resolve_node_without_node_reference_keeps_cdp_invalid_param_shape() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "objectGroup": "webdriver" }); @@ -6563,7 +6563,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_resolve_node_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 11 }); @@ -6595,7 +6595,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_attributes_builds_protocol_neutral_dom_attributes_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "nodeId": 12 }); @@ -6625,7 +6625,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_complete_entry_requires_pending_get_attributes_command() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 13 }); @@ -6655,7 +6655,7 @@ mod protocol_neutral_tests { #[test] fn cdp_push_nodes_by_backend_ids_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "backendNodeIds": [14, 15, 16] }); @@ -6682,7 +6682,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_complete_entry_requires_pending_push_nodes_by_backend_ids_command() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "backendNodeIds": [17] }); @@ -6712,7 +6712,7 @@ mod protocol_neutral_tests { #[test] fn cdp_request_node_builds_protocol_neutral_object_reference_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "objectId": "remote-object-1" }); @@ -6747,7 +6747,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_outer_html_without_object_id_keeps_node_reference_path() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "nodeId": 14 }); @@ -6774,7 +6774,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_outer_html_builds_protocol_neutral_node_reference_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "nodeId": 15, "includeShadowDOM": true @@ -6802,7 +6802,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_outer_html_object_id_keeps_object_reference_path() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "objectId": "remote-object-outer", "includeShadowDOM": true @@ -6842,7 +6842,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_complete_entry_requires_pending_get_outer_html_command() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 16 }); @@ -6870,7 +6870,7 @@ mod protocol_neutral_tests { #[test] fn cdp_scroll_into_view_builds_protocol_neutral_node_reference_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "backendNodeId": 17, "rect": { "x": 1, "y": 2, "width": 3, "height": 4 } @@ -6937,7 +6937,7 @@ mod protocol_neutral_tests { #[test] fn cdp_scroll_into_view_object_id_keeps_object_reference_path() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "objectId": "remote-object-scroll" }); @@ -6958,7 +6958,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_start_entry_routes_scroll_command_to_renderer_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 18 }); @@ -6990,7 +6990,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_box_model_builds_protocol_neutral_geometry_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "backendNodeId": 19 }); @@ -7021,7 +7021,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_content_quads_object_id_keeps_object_reference_path() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "objectId": "remote-object-geometry" }); @@ -7046,7 +7046,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_geometry_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 20 }); @@ -7082,7 +7082,7 @@ mod protocol_neutral_tests { #[test] fn cdp_remove_node_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "backendNodeId": 21 }); @@ -7108,7 +7108,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_remove_node_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 22 }); @@ -7140,7 +7140,7 @@ mod protocol_neutral_tests { #[test] fn cdp_describe_node_builds_protocol_neutral_node_reference_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "backendNodeId": 21, "depth": 2, @@ -7170,7 +7170,7 @@ mod protocol_neutral_tests { #[test] fn cdp_describe_node_object_id_keeps_object_reference_path() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "objectId": "remote-object-describe" }); @@ -7191,7 +7191,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_describe_node_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 22 }); @@ -7223,7 +7223,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_complete_entry_requires_pending_describe_node_command() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "nodeId": 23 }); @@ -7251,7 +7251,7 @@ mod protocol_neutral_tests { #[test] fn cdp_describe_node_object_id_builds_protocol_neutral_object_reference_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "objectId": "remote-object-3", "depth": 2, @@ -7290,7 +7290,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_routes_object_reference_command_to_dom_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "objectId": "remote-object-2" }); @@ -7326,7 +7326,7 @@ mod protocol_neutral_tests { #[test] fn cdp_set_file_input_files_builds_protocol_neutral_object_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let file_path = unique_test_file_path("set-file-object"); std::fs::write(&file_path, b"upload bytes").expect("test upload file should be writable"); let params = json!({ @@ -7368,7 +7368,7 @@ mod protocol_neutral_tests { #[test] fn cdp_set_file_input_files_node_reference_falls_back_to_pending_node_reference_path() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "backendNodeId": 25, "files": ["/tmp/upload.txt"] diff --git a/moli-protocol/src/domains/dom/search.rs b/moli-protocol/src/domains/dom/search.rs index 9c31d1d2ae..68247c7a26 100644 --- a/moli-protocol/src/domains/dom/search.rs +++ b/moli-protocol/src/domains/dom/search.rs @@ -249,11 +249,11 @@ mod protocol_neutral_tests { use crate::devtools_runtime::DevToolsProtocol; use serde_json::{Value, json}; - use crate::conn::{CdpConnection, Cmd, CommandOwnerScope}; + use crate::conn::{Cmd, CommandOwnerScope}; #[test] fn cdp_perform_search_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "query": "article.result", "includeUserAgentShadowDOM": true @@ -278,7 +278,7 @@ mod protocol_neutral_tests { #[test] fn devtools_dom_entry_keeps_perform_search_without_loaded_page_on_sync_empty_path() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "query": ".hit" }); @@ -305,7 +305,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_search_results_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "searchId": "search-1", "fromIndex": 2, @@ -336,7 +336,7 @@ mod protocol_neutral_tests { #[test] fn cdp_discard_search_results_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "searchId": "search-2" }); @@ -363,7 +363,7 @@ mod protocol_neutral_tests { #[test] fn cdp_discard_search_results_keeps_invalid_params_as_noop_success_path() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(112), diff --git a/moli-protocol/src/domains/dom/tests/frames.rs b/moli-protocol/src/domains/dom/tests/frames.rs index 900209bf55..f27a703587 100644 --- a/moli-protocol/src/domains/dom/tests/frames.rs +++ b/moli-protocol/src/domains/dom/tests/frames.rs @@ -5013,7 +5013,10 @@ async fn get_frame_owner_invalid_params_error() { #[tokio::test(flavor = "multi_thread")] async fn get_frame_owner_without_selected_target_errors() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(crate::conn::BrowserContext::new("BID-A".to_owned())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-A".to_owned()), + ); ctx.process_async(json!({ "id": 1, diff --git a/moli-protocol/src/domains/dom/tests/geometry.rs b/moli-protocol/src/domains/dom/tests/geometry.rs index 0636d73123..401116d2f4 100644 --- a/moli-protocol/src/domains/dom/tests/geometry.rs +++ b/moli-protocol/src/domains/dom/tests/geometry.rs @@ -41,7 +41,9 @@ async fn dom_resolve_geometry_and_mutation_target_loaded_background_owner_withou let mut ctx = TestContext::new(); let background_url = url::Url::parse("https://background.test/owned").unwrap(); - let mut bc = BrowserContext::new("BID-A".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-A".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -287,12 +289,16 @@ async fn dom_resolve_geometry_and_mutation_target_loaded_background_owner_withou #[tokio::test(flavor = "multi_thread")] async fn dom_resolve_geometry_targets_inactive_loaded_owner_without_activation() { let mut ctx = TestContext::new(); - let mut active = BrowserContext::new("BID-active".to_owned()); + let mut active = ctx + .conn + .new_browser_context_fixture_for_test("BID-active".to_owned()); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active".to_owned()); ctx.conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.set_target_url("about:blank".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); diff --git a/moli-protocol/src/domains/dom/tests/misc.rs b/moli-protocol/src/domains/dom/tests/misc.rs index 4e25d11160..bbc3de3892 100644 --- a/moli-protocol/src/domains/dom/tests/misc.rs +++ b/moli-protocol/src/domains/dom/tests/misc.rs @@ -214,7 +214,9 @@ async fn dom_enable_include_whitespace_controls_inspector_tree_projection() { async fn dom_document_reads_target_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-A".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-A".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -291,12 +293,16 @@ async fn dom_document_reads_target_loaded_background_owner_without_activation() #[tokio::test(flavor = "multi_thread")] async fn dom_document_reads_target_inactive_loaded_owner_without_activation() { let mut ctx = TestContext::new(); - let mut active = BrowserContext::new("BID-active".to_owned()); + let mut active = ctx + .conn + .new_browser_context_fixture_for_test("BID-active".to_owned()); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active".to_owned()); ctx.conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.set_target_url("about:blank".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); diff --git a/moli-protocol/src/domains/dom/tests/mod.rs b/moli-protocol/src/domains/dom/tests/mod.rs index 483c4cb981..7946bdb1cd 100644 --- a/moli-protocol/src/domains/dom/tests/mod.rs +++ b/moli-protocol/src/domains/dom/tests/mod.rs @@ -9,7 +9,7 @@ use moli_page_types::{DocumentNodeSnapshot, DocumentSnapshotNodeId}; use serde_json::{Value, json}; fn load_bc(ctx: &mut TestContext, bc_id: &str) { - let mut bc = BrowserContext::new(bc_id.into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test(bc_id); bc.set_active_target_id("TID-1"); ctx.conn.insert_browser_context(bc); } diff --git a/moli-protocol/src/domains/dom/tests/search.rs b/moli-protocol/src/domains/dom/tests/search.rs index 9af8e255f0..a6f07a9c75 100644 --- a/moli-protocol/src/domains/dom/tests/search.rs +++ b/moli-protocol/src/domains/dom/tests/search.rs @@ -474,7 +474,9 @@ async fn perform_search_selectors_cover_all_frame_documents_and_author_shadow_ro async fn dom_search_targets_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-A".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-A".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -555,12 +557,16 @@ async fn dom_search_targets_loaded_background_owner_without_activation() { #[tokio::test(flavor = "multi_thread")] async fn dom_search_targets_inactive_loaded_owner_without_activation() { let mut ctx = TestContext::new(); - let mut active = BrowserContext::new("BID-active".to_owned()); + let mut active = ctx + .conn + .new_browser_context_fixture_for_test("BID-active".to_owned()); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active".to_owned()); ctx.conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.set_target_url("about:blank".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); diff --git a/moli-protocol/src/domains/dom_debugger.rs b/moli-protocol/src/domains/dom_debugger.rs index e8bf123df2..797c83f0ef 100644 --- a/moli-protocol/src/domains/dom_debugger.rs +++ b/moli-protocol/src/domains/dom_debugger.rs @@ -368,10 +368,7 @@ mod tests { use serde_json::{Value, json}; - use crate::{ - conn::{BrowserContext, CdpCommandTaskStep}, - testing::TestContext, - }; + use crate::{conn::CdpCommandTaskStep, testing::TestContext}; // Full-workspace CI runs these renderer-owner tests alongside CPU-heavy // suites. Keep the guard diagnostic, but allow the same scheduling @@ -477,7 +474,7 @@ mod tests { } async fn load_document(ctx: &mut TestContext, html: &str) { - let mut browser_context = BrowserContext::new("BID-1".into()); + let mut browser_context = ctx.conn.new_browser_context_fixture_for_test("BID-1"); browser_context.set_active_target_id("TID-1".to_owned()); browser_context.set_target_url("data:text/html,dom-debugger-test".to_owned()); browser_context.attach_active_session("SID-1".to_owned()); diff --git a/moli-protocol/src/domains/dom_snapshot.rs b/moli-protocol/src/domains/dom_snapshot.rs index d733f37313..11f2e77a89 100644 --- a/moli-protocol/src/domains/dom_snapshot.rs +++ b/moli-protocol/src/domains/dom_snapshot.rs @@ -211,7 +211,9 @@ mod tests { }; async fn load_document(ctx: &mut TestContext, html: &str) { - let mut bc = BrowserContext::new("BID-1".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-1".to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); ctx.install_navigation_fixture_for_session_owner(&format!("data:text/html,{html}"), None) @@ -1021,24 +1023,22 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn capture_snapshot_targets_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); - let page = ctx + let mut bc = ctx .conn - .load_page_via_runtime_async( - "data:text/html,Background Snapshot

owner
", - ) - .await - .expect("background page should load"); - - let mut bc = BrowserContext::new("BID-DS-BG".to_owned()); + .new_browser_context_fixture_for_test("BID-DS-BG".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), - page.final_url().as_str().to_owned(), + "about:blank".to_owned(), ); - bc.replace_target_page_for_test("TID-background", Some(page)); ctx.conn.install_browser_context_fixture_for_test(bc); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,Background Snapshot
owner
", + Some("SID-background"), + ) + .await; ctx.process_async(json!({ "id": 101, @@ -1078,26 +1078,25 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn capture_snapshot_targets_inactive_owner_without_activation() { let mut ctx = TestContext::new(); - let page = ctx + let mut active = ctx .conn - .load_page_via_runtime_async( - "data:text/html,Inactive Snapshot
inactive
", - ) - .await - .expect("inactive page should load"); - - let mut active = BrowserContext::new("BID-active".to_owned()); + .new_browser_context_fixture_for_test("BID-active".to_owned()); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active".to_owned()); ctx.conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); - inactive.set_target_url(page.final_url().as_str().to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); - inactive.replace_loaded_page(Some(page)); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,Inactive Snapshot
inactive
", + Some("SID-inactive"), + ) + .await; ctx.process_async(json!({ "id": 111, @@ -1129,7 +1128,10 @@ mod tests { #[tokio::test] async fn capture_snapshot_reports_no_document_without_loaded_page() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-1".to_owned()), + ); ctx.process_async(json!({ "id": 11, diff --git a/moli-protocol/src/domains/dom_storage/commands.rs b/moli-protocol/src/domains/dom_storage/commands.rs index 570e3cf10e..78ae828171 100644 --- a/moli-protocol/src/domains/dom_storage/commands.rs +++ b/moli-protocol/src/domains/dom_storage/commands.rs @@ -1,12 +1,13 @@ -use moli_core::{ - network::WebStorageAreaKind, - page::{ChildFrameTreeSnapshot, CompletedPageCommand, PendingPageCommand}, -}; +use moli_core::{network::WebStorageAreaKind, page::ChildFrameTreeSnapshot}; use serde_json::json; use url::Url; use crate::{ - conn::{BrowserContextPageStorageHandles, CdpConnection, Cmd, CommandOwnerScope}, + conn::{ + BrowserContextPageStorageHandles, CdpConnection, Cmd, CommandOwnerScope, + CompletedChildFrameTreeSnapshot, CompletedDocumentStorageKeySnapshot, + PendingChildFrameTreeSnapshot, PendingDocumentStorageKeySnapshot, + }, domains::{actions::DomStorageAction, command_output::CommandOutputPlan}, }; @@ -18,14 +19,12 @@ pub(crate) struct PendingDomStorageCommandDispatch { command_id: Option, session_id: Option, kind: PendingDomStorageCommandKind, - pending: PendingPageCommand, } pub(crate) struct CompletedDomStorageCommandDispatch { command_id: Option, session_id: Option, - kind: PendingDomStorageCommandKind, - completed: Result, + kind: CompletedDomStorageCommandKind, } pub(crate) enum DomStorageCommandTaskStep { @@ -38,11 +37,28 @@ enum PendingDomStorageCommandKind { owner_scope: CommandOwnerScope, storage_id: DomStorageId, operation: DomStorageOperation, + pending: PendingDocumentStorageKeySnapshot, }, ResolveChildFrames { owner_scope: CommandOwnerScope, storage_id: DomStorageId, operation: DomStorageOperation, + pending: PendingChildFrameTreeSnapshot, + }, +} + +enum CompletedDomStorageCommandKind { + ResolveTopFrame { + owner_scope: CommandOwnerScope, + storage_id: DomStorageId, + operation: DomStorageOperation, + completed: CompletedDocumentStorageKeySnapshot, + }, + ResolveChildFrames { + owner_scope: CommandOwnerScope, + storage_id: DomStorageId, + operation: DomStorageOperation, + completed: CompletedChildFrameTreeSnapshot, }, } @@ -56,11 +72,34 @@ enum DomStorageOperation { impl PendingDomStorageCommandDispatch { pub(crate) async fn wait(self) -> CompletedDomStorageCommandDispatch { + let kind = match self.kind { + PendingDomStorageCommandKind::ResolveTopFrame { + owner_scope, + storage_id, + operation, + pending, + } => CompletedDomStorageCommandKind::ResolveTopFrame { + owner_scope, + storage_id, + operation, + completed: pending.wait().await, + }, + PendingDomStorageCommandKind::ResolveChildFrames { + owner_scope, + storage_id, + operation, + pending, + } => CompletedDomStorageCommandKind::ResolveChildFrames { + owner_scope, + storage_id, + operation, + completed: pending.wait().await, + }, + }; CompletedDomStorageCommandDispatch { command_id: self.command_id, session_id: self.session_id, - kind: self.kind, - completed: self.pending.wait().await.map_err(|error| error.to_string()), + kind, } } } @@ -217,11 +256,8 @@ fn start_storage_operation( )); } - if let Ok((context_id, target_id)) = conn.resolve_document_command_owner(&owner_scope) { - let context = conn - .browser_context_by_id(&context_id) - .expect("admitted document context remains registered"); - return match context.start_document_storage_key_snapshot_for_target(&target_id) { + if let Ok(document) = conn.resolve_browser_document_for_owner(&owner_scope) { + return match conn.start_document_storage_key_snapshot(document) { Ok(pending) => { DomStorageCommandTaskStep::Pending(Box::new(PendingDomStorageCommandDispatch { command_id: cmd.id, @@ -230,8 +266,8 @@ fn start_storage_operation( owner_scope, storage_id, operation, + pending, }, - pending, })) } Err(error) => DomStorageCommandTaskStep::Complete(CommandOutputPlan::error( @@ -261,31 +297,32 @@ pub(crate) fn complete_pending_dom_storage_command( conn: &mut CdpConnection, completed: CompletedDomStorageCommandDispatch, ) -> DomStorageCommandTaskStep { - match completed.kind { - PendingDomStorageCommandKind::ResolveTopFrame { + let CompletedDomStorageCommandDispatch { + command_id, + session_id, + kind, + } = completed; + match kind { + CompletedDomStorageCommandKind::ResolveTopFrame { owner_scope, storage_id, operation, + completed, } => complete_top_frame_resolution( conn, - completed.command_id, - completed.session_id, + command_id, + session_id, owner_scope, storage_id, operation, - completed.completed, + completed, ), - PendingDomStorageCommandKind::ResolveChildFrames { + CompletedDomStorageCommandKind::ResolveChildFrames { owner_scope, storage_id, operation, - } => complete_child_frame_resolution( - conn, - owner_scope, - storage_id, - operation, - completed.completed, - ), + completed, + } => complete_child_frame_resolution(conn, owner_scope, storage_id, operation, completed), } } @@ -296,29 +333,16 @@ fn complete_top_frame_resolution( owner_scope: CommandOwnerScope, storage_id: DomStorageId, operation: DomStorageOperation, - completed: Result, + completed: CompletedDocumentStorageKeySnapshot, ) -> DomStorageCommandTaskStep { - let completion = match completed { - Ok(completion) => completion, + let document = completed.document(); + let storage_key = match conn.finish_document_storage_key_snapshot(completed) { + Ok(storage_key) => storage_key, Err(error) => { - return DomStorageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, error)); - } - }; - let storage_key = { - let Ok((context_id, target_id)) = conn.resolve_document_command_owner(&owner_scope) else { - return DomStorageCommandTaskStep::Complete(frame_not_found_plan()); - }; - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - match context.finish_document_storage_key_snapshot_for_target(&target_id, completion) { - Ok(storage_key) => storage_key, - Err(error) => { - return DomStorageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - error.to_string(), - )); - } + return DomStorageCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, + error.to_string(), + )); } }; @@ -332,14 +356,7 @@ fn complete_top_frame_resolution( )); } - let child_pending = { - let Ok((context_id, target_id)) = conn.resolve_document_command_owner(&owner_scope) else { - return DomStorageCommandTaskStep::Complete(frame_not_found_plan()); - }; - conn.browser_context_by_id(&context_id) - .expect("admitted document context remains registered") - .start_child_frame_tree_snapshot_for_target(&target_id) - }; + let child_pending = conn.start_child_frame_tree_snapshot(document); match child_pending { Ok(pending) => { DomStorageCommandTaskStep::Pending(Box::new(PendingDomStorageCommandDispatch { @@ -349,8 +366,8 @@ fn complete_top_frame_resolution( owner_scope, storage_id, operation, + pending, }, - pending, })) } Err(error) => { @@ -364,29 +381,15 @@ fn complete_child_frame_resolution( owner_scope: CommandOwnerScope, storage_id: DomStorageId, operation: DomStorageOperation, - completed: Result, + completed: CompletedChildFrameTreeSnapshot, ) -> DomStorageCommandTaskStep { - let completion = match completed { - Ok(completion) => completion, + let child_frames = match conn.finish_child_frame_tree_snapshot(completed) { + Ok(child_frames) => child_frames, Err(error) => { - return DomStorageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, error)); - } - }; - let child_frames = { - let Ok((context_id, target_id)) = conn.resolve_document_command_owner(&owner_scope) else { - return DomStorageCommandTaskStep::Complete(frame_not_found_plan()); - }; - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - match context.finish_child_frame_tree_snapshot_for_target(&target_id, completion) { - Ok(child_frames) => child_frames, - Err(error) => { - return DomStorageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - error.to_string(), - )); - } + return DomStorageCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, + error.to_string(), + )); } }; let Some(storage_key) = matching_child_frame_storage_key(&storage_id, &child_frames) else { diff --git a/moli-protocol/src/domains/dom_storage/tests.rs b/moli-protocol/src/domains/dom_storage/tests.rs index a19226082c..f4f8331b27 100644 --- a/moli-protocol/src/domains/dom_storage/tests.rs +++ b/moli-protocol/src/domains/dom_storage/tests.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use serde_json::{Value, json}; use url::Url; -use crate::{conn::BrowserContext, testing::TestContext}; +use crate::testing::TestContext; fn take_response_by_id(ctx: &mut TestContext, id: u64) -> Value { let index = ctx @@ -111,7 +111,9 @@ async fn loaded_dom_storage_context() -> (TestContext, String, String, tokio::ta .origin() .ascii_serialization(); let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-DOM-STORAGE".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-DOM-STORAGE".to_owned()); browser_context.set_active_target_id("TID-DOM-STORAGE"); browser_context.set_target_url(page_url.clone()); browser_context.set_target_security_origin(origin.clone()); @@ -119,16 +121,8 @@ async fn loaded_dom_storage_context() -> (TestContext, String, String, tokio::ta ctx.conn .install_browser_context_fixture_for_test(browser_context); - let page = ctx - .conn - .load_page_via_runtime_async(&page_url) - .await - .expect("DOMStorage test page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context should exist") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(&page_url, None) + .await; (ctx, page_url, origin, server) } @@ -446,23 +440,17 @@ async fn dom_storage_resolves_child_frame_storage_ids_without_collapsing_to_top_ .origin() .ascii_serialization(); let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-DOM-STORAGE-CHILD".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-DOM-STORAGE-CHILD".to_owned()); browser_context.set_active_target_id("TID-DOM-STORAGE-CHILD"); browser_context.set_target_url(page_url.clone()); browser_context.set_target_security_origin(top_origin.clone()); browser_context.set_target_secure_context_type("Secure".to_owned()); ctx.conn .install_browser_context_fixture_for_test(browser_context); - let page = ctx - .conn - .load_page_via_runtime_async(&page_url) - .await - .expect("child-frame DOMStorage test page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(&page_url, None) + .await; let child_frame_loaded = tokio::time::timeout( std::time::Duration::from_secs(10), diff --git a/moli-protocol/src/domains/emulation.rs b/moli-protocol/src/domains/emulation.rs index e87eb1a26b..e8056ea57a 100644 --- a/moli-protocol/src/domains/emulation.rs +++ b/moli-protocol/src/domains/emulation.rs @@ -1,7 +1,9 @@ use crate::conn::{ - BrowserContext, CdpConnection, CdpSessionRoute, Cmd, CommandOwnerScope, EmulatedDeviceMetrics, - EmulatedGeolocationOverrideState, EmulatedViewportSurface, EmulationPolicyChange, - PagePolicyUpdateKind, RendererCommandCorrelation, RendererCommandDescriptor, + BrowserContext, CdpConnection, CdpSessionRoute, Cmd, CommandOwnerScope, + CompletedDocumentPolicyUpdate, CompletedDocumentResourceRuntimeUpdate, DocumentPolicyUpdate, + DocumentRuntimePolicyReconciliation, EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, + EmulatedViewportSurface, EmulationPolicyChange, PendingDocumentPolicyUpdate, + PendingDocumentResourceRuntimeUpdate, RendererCommandCorrelation, RendererCommandDescriptor, WindowSurfaceState, }; use crate::devtools_runtime::{ @@ -16,10 +18,7 @@ use crate::devtools_runtime::{ }; use crate::domains::actions::EmulationAction; use crate::domains::command_output::CommandOutputPlan; -use moli_core::page::{ - CompletedDevToolsIoCommandDispatch, CompletedPageCommand, PendingDevToolsIoCommandDispatch, - PendingPageCommand, -}; +use moli_core::page::{CompletedDevToolsIoCommandDispatch, PendingDevToolsIoCommandDispatch}; use serde_json::json; mod device; @@ -63,14 +62,24 @@ struct PendingEmulationPageCommand { target: PendingEmulationPageTarget, operation: PendingEmulationPageOperation, source: EmulationPageCommandSource, - pending: PendingPageCommand, + pending: PendingEmulationPageWork, } struct CompletedEmulationPageCommand { target: PendingEmulationPageTarget, operation: PendingEmulationPageOperation, source: EmulationPageCommandSource, - completed: Result, + completed: CompletedEmulationPageWork, +} + +enum PendingEmulationPageWork { + DocumentPolicy(PendingDocumentPolicyUpdate), + ResourceRuntime(PendingDocumentResourceRuntimeUpdate), +} + +enum CompletedEmulationPageWork { + DocumentPolicy(CompletedDocumentPolicyUpdate), + ResourceRuntime(CompletedDocumentResourceRuntimeUpdate), } #[derive(Clone, Copy)] @@ -79,6 +88,7 @@ enum EmulationPageCommandSource { } impl EmulationPageCommandSource { + #[cfg(test)] fn browser(context: &BrowserContext, target_id: &str) -> Self { Self::Browser(context.target_renderer_page_residence_identity(target_id)) } @@ -110,14 +120,19 @@ pub(crate) enum EmulationCommandTaskStep { } enum PendingEmulationPageOperation { - Policy(PagePolicyUpdateKind), + DocumentPolicy { replay_on_replacement: bool }, RebuildResourceRuntime, } impl PendingEmulationPageOperation { fn has_authoritative_replay_state(&self) -> bool { // Idle state belongs to a Document; other settings have authoritative replay policy. - !matches!(self, Self::Policy(PagePolicyUpdateKind::SetIdleOverride)) + !matches!( + self, + Self::DocumentPolicy { + replay_on_replacement: false + } + ) } } @@ -143,7 +158,14 @@ impl PendingEmulationCommandDispatch { source, pending, } = pending; - let completed_page = pending.wait().await.map_err(|error| error.to_string()); + let completed_page = match pending { + PendingEmulationPageWork::DocumentPolicy(pending) => { + CompletedEmulationPageWork::DocumentPolicy(pending.wait().await) + } + PendingEmulationPageWork::ResourceRuntime(pending) => { + CompletedEmulationPageWork::ResourceRuntime(pending.wait().await) + } + }; completed.push(CompletedEmulationPageCommand { target, operation, @@ -424,13 +446,13 @@ fn start_navigator_override_page_command( let overrides = conn .navigation_load_inputs_for_owner(&owner_scope) .navigator_overrides; - let Some((context_id, target_id)) = page_configuration_owner(conn, &owner_scope) else { + let Ok(document) = conn.loaded_browser_document_for_owner(&owner_scope) else { return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({}))); }; - let context = conn - .browser_context_by_id(&context_id) - .expect("resolved context remains registered"); - let pending = match context.start_set_navigator_overrides_for_target(&target_id, &overrides) { + let pending = match conn.start_document_policy_update( + document, + DocumentPolicyUpdate::NavigatorOverrides(overrides), + ) { Ok(pending) => pending, Err(error) => { return EmulationCommandTaskStep::Complete(CommandOutputPlan::error( @@ -442,9 +464,8 @@ fn start_navigator_override_page_command( EmulationCommandTaskStep::Pending(single_pending_emulation_dispatch( cmd.id, owner_scope, - PendingEmulationPageOperation::Policy(PagePolicyUpdateKind::SetNavigatorOverrides), pending, - EmulationPageCommandSource::browser(context, &target_id), + true, )) } @@ -677,20 +698,17 @@ fn start_update_idle_override_command( return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({}))); } let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id); - let Some((context_id, target_id)) = page_configuration_owner(conn, &owner_scope) else { + let Ok(document) = conn.loaded_browser_document_for_owner(&owner_scope) else { return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({}))); }; - - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("resolved context remains registered"); - match context.start_set_idle_override_for_target(&target_id, idle_override) { + match conn + .start_document_policy_update(document, DocumentPolicyUpdate::IdleOverride(idle_override)) + { Ok(pending) => EmulationCommandTaskStep::Pending(single_pending_emulation_dispatch( cmd.id, owner_scope, - PendingEmulationPageOperation::Policy(PagePolicyUpdateKind::SetIdleOverride), pending, - EmulationPageCommandSource::browser(context, &target_id), + false, )), Err(error) => { EmulationCommandTaskStep::Complete(CommandOutputPlan::error(-32000, error.to_string())) @@ -878,17 +896,21 @@ fn start_emulated_media_command( return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({}))); }; - let context = conn + let document = conn .browser_context_by_id(&context_id) - .expect("resolved context remains registered"); - match context.start_set_emulated_media_for_target(&target_id, &page_overrides) { + .and_then(|context| context.document_handle_for_target(&target_id)) + .expect("resolved loaded document remains registered"); + match conn.start_document_policy_update( + document, + DocumentPolicyUpdate::EmulatedMedia(page_overrides.clone()), + ) { Ok(pending) => vec![PendingEmulationPageCommand { - source: EmulationPageCommandSource::browser(context, &target_id), + source: EmulationPageCommandSource::Browser(None), target: PendingEmulationPageTarget::SessionOwner { owner_scope }, - operation: PendingEmulationPageOperation::Policy( - PagePolicyUpdateKind::SetEmulatedMedia, - ), - pending, + operation: PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, + pending: PendingEmulationPageWork::DocumentPolicy(pending), }], Err(error) => { return EmulationCommandTaskStep::Complete(CommandOutputPlan::error( @@ -932,7 +954,7 @@ fn start_user_agent_override_command( source: EmulationPageCommandSource::browser_for_owner(conn, &owner_scope), target: PendingEmulationPageTarget::SessionOwner { owner_scope }, operation: PendingEmulationPageOperation::RebuildResourceRuntime, - pending, + pending: PendingEmulationPageWork::ResourceRuntime(pending), }]), }), Ok(None) => EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({}))), @@ -963,14 +985,18 @@ fn start_device_metrics_override_command( let mut base = conn .target_owner_identity_for_owner(&owner) .and_then(|(context_id, _)| conn.browser_context_by_id(&context_id)) - .and_then(|context| context.emulation_defaults().device_metrics.as_ref()) - .map(EmulatedDeviceMetrics::viewport_surface) + .and_then(|context| context.emulation_defaults().device_metrics) + .map(|metrics| metrics.viewport_surface()) .unwrap_or_default(); if let Some(geometry) = conn.target_owner_identity_for_owner(&owner) .and_then(|(context_id, target_id)| { - conn.browser_context_by_id(&context_id)? - .target_window_surface(target_id.as_deref()?) + let context = conn.browser_context_by_id(&context_id)?; + context + .web_contents_window_surface( + context.web_contents_handle_for_target(target_id.as_deref()?)?, + ) + .ok() }) { if geometry.width != 0 { @@ -1035,24 +1061,27 @@ fn start_clear_device_metrics_override_command( }; let context = conn - .browser_context_by_id(&context_id) + .browser_context_by_id_mut(&context_id) .expect("resolved context remains registered"); - let pending_viewport = - match context.start_set_viewport_surface_for_target(&target_id, viewport_surface) { - Ok(pending) => pending, - Err(error) => { - return EmulationCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - error.to_string(), - )); - } - }; + let pending_viewport = match context.start_document_policy_update( + context + .document_handle_for_target(&target_id) + .expect("loaded Document"), + DocumentPolicyUpdate::ViewportSurface(viewport_surface), + ) { + Ok(pending) => pending, + Err(error) => { + return EmulationCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, + error.to_string(), + )); + } + }; EmulationCommandTaskStep::Pending(single_pending_emulation_dispatch( cmd.id, owner_scope, - PendingEmulationPageOperation::Policy(PagePolicyUpdateKind::SetViewportSurface), pending_viewport, - EmulationPageCommandSource::browser(context, &target_id), + true, )) } @@ -1090,17 +1119,21 @@ fn start_apply_device_metrics( return Ok(None); }; let context = conn - .browser_context_by_id(&context_id) + .browser_context_by_id_mut(&context_id) .expect("resolved context remains registered"); let pending = context - .start_set_viewport_surface_for_target(&target_id, Some(metrics.viewport_surface())) + .start_document_policy_update( + context + .document_handle_for_target(&target_id) + .expect("loaded Document"), + DocumentPolicyUpdate::ViewportSurface(Some(metrics.viewport_surface())), + ) .map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?; Ok(Some(single_pending_emulation_dispatch( command_id, owner_scope, - PendingEmulationPageOperation::Policy(PagePolicyUpdateKind::SetViewportSurface), pending, - EmulationPageCommandSource::browser(context, &target_id), + true, ))) } @@ -1486,7 +1519,10 @@ fn start_network_conditions_update_for_current_route( } => conn .browser_context_by_id(browser_context_id) .is_some_and(|browser_context| { - browser_context.effective_network_offline_for_target(target_id) + browser_context.effective_network_offline_for_target( + target_id, + conn.browser_global_overrides.network_conditions, + ) }), PendingEmulationPageTarget::SessionOwner { .. } => false, }; @@ -1497,12 +1533,12 @@ fn start_network_conditions_update_for_current_route( let mut pending = Vec::new(); if let Some(network_update) = network_update { pending.push(PendingEmulationPageCommand { - source: EmulationPageCommandSource::browser_for_owner(conn, &owner), + source: EmulationPageCommandSource::Browser(None), target: target.clone(), - operation: PendingEmulationPageOperation::Policy( - PagePolicyUpdateKind::SetNetworkConditions, - ), - pending: network_update, + operation: PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, + pending: PendingEmulationPageWork::DocumentPolicy(network_update), }); } pending.extend( @@ -1525,12 +1561,12 @@ fn start_extra_headers_for_current_route( Ok(pending .map(|pending| { vec![PendingEmulationPageCommand { - source: EmulationPageCommandSource::browser_for_owner(conn, &owner), + source: EmulationPageCommandSource::Browser(None), target, - operation: PendingEmulationPageOperation::Policy( - PagePolicyUpdateKind::SetExtraHttpHeaders, - ), - pending, + operation: PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, + pending: PendingEmulationPageWork::DocumentPolicy(pending), }] }) .unwrap_or_default()) @@ -1559,7 +1595,12 @@ fn start_extra_headers_update_for_route( target_id, } => conn .browser_context_by_id(browser_context_id) - .map(|browser_context| browser_context.effective_extra_headers_for_target(target_id)), + .map(|browser_context| { + browser_context.effective_extra_headers_for_target( + target_id, + &conn.browser_global_overrides.extra_headers, + ) + }), PendingEmulationPageTarget::SessionOwner { .. } => None, }; let Some(headers) = headers else { @@ -1574,14 +1615,19 @@ fn start_extra_headers_update_for_route( else { return Ok(Vec::new()); }; - let pending = context - .start_set_extra_http_headers_for_target(&target_id, &headers) + let document = context + .document_handle_for_target(&target_id) + .expect("loaded target must have an exact Document"); + let pending = conn + .start_document_policy_update(document, DocumentPolicyUpdate::ExtraHttpHeaders(headers)) .map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error.to_string()))?; Ok(vec![PendingEmulationPageCommand { - source: EmulationPageCommandSource::browser(context, &target_id), + source: EmulationPageCommandSource::Browser(None), target, - operation: PendingEmulationPageOperation::Policy(PagePolicyUpdateKind::SetExtraHttpHeaders), - pending, + operation: PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, + pending: PendingEmulationPageWork::DocumentPolicy(pending), }]) } @@ -1764,7 +1810,7 @@ fn start_user_agent_override_for_current_route( source: EmulationPageCommandSource::browser_for_owner(conn, &owner), target, operation: PendingEmulationPageOperation::RebuildResourceRuntime, - pending, + pending: PendingEmulationPageWork::ResourceRuntime(pending), })) } @@ -1781,7 +1827,7 @@ fn start_user_agent_loader_update_for_current_route( source: EmulationPageCommandSource::browser_for_owner(conn, &owner), target, operation: PendingEmulationPageOperation::RebuildResourceRuntime, - pending, + pending: PendingEmulationPageWork::ResourceRuntime(pending), })) } @@ -2199,10 +2245,15 @@ async fn execute_devtools_set_viewport_for_browser_contexts( let browser_context_ids = resolve_set_viewport_browser_context_ids(conn, &command)?; let mut pending = Vec::new(); for browser_context_id in browser_context_ids { - let current_default = conn + let current_defaults = conn .browser_context_by_id(&browser_context_id) - .and_then(|context| context.emulation_defaults().device_metrics.as_ref()); - let metrics = set_viewport_metrics_from_current(current_default, &command)?; + .map(|context| context.emulation_defaults()); + let metrics = set_viewport_metrics_from_current( + current_defaults + .as_ref() + .and_then(|defaults| defaults.device_metrics.as_ref()), + &command, + )?; let browser_context = conn .browser_context_by_id_mut(&browser_context_id) .expect("resolved browser context must remain addressable"); @@ -2282,32 +2333,41 @@ fn start_browser_context_default_device_metrics_page_commands( ) -> Result, DevToolsError> { let mut pending = Vec::new(); let viewport_surface = Some(metrics.viewport_surface()); - for target in context + let target_ids = context .page_targets .active(context.selected_web_contents_id()) .into_iter() .chain(context.background_targets()) - { - let target_id = target.target_id(); - if !context.target_has_loaded_page(target_id) + .map(|target| target.target_id().to_owned()) + .collect::>(); + for target_id in target_ids { + if !context.target_has_loaded_page(&target_id) || context - .target_emulation_policy(target_id) + .target_emulation_policy(&target_id) .is_none_or(|policy| policy.emulated_device_metrics.is_some()) { continue; } + let document = context + .document_handle_for_target(&target_id) + .expect("loaded target must have an exact Document"); pending.push(PendingEmulationPageCommand { - source: EmulationPageCommandSource::browser(context, target_id), + source: EmulationPageCommandSource::Browser(None), target: PendingEmulationPageTarget::BrowserContextTarget { browser_context_id: context.id.clone(), - target_id: target_id.to_owned(), + target_id: target_id.clone(), }, - operation: PendingEmulationPageOperation::Policy( - PagePolicyUpdateKind::SetViewportSurface, + operation: PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, + pending: PendingEmulationPageWork::DocumentPolicy( + context + .start_document_policy_update( + document, + DocumentPolicyUpdate::ViewportSurface(viewport_surface), + ) + .map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?, ), - pending: context - .start_set_viewport_surface_for_target(target_id, viewport_surface) - .map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?, }); } Ok(pending) @@ -2330,20 +2390,7 @@ fn complete_pending_devtools_emulation_command( source, completed, } = completed_page; - let completion = match completed { - Ok(completion) => completion, - Err(_) - if pending_emulation_page_configuration_will_be_replayed( - conn, &target, &operation, source, - ) => - { - continue; - } - Err(error) => { - return Err(DevToolsError::new(DevToolsErrorKind::Internal, error)); - } - }; - finish_pending_emulation_page_command(conn, operation, target, completion) + finish_completed_emulation_page_command(conn, operation, target, source, completed) .map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?; } Ok(DevToolsCommandResult::Empty) @@ -2405,18 +2452,8 @@ pub(crate) fn complete_pending_emulation_command( source, completed, } = completed_page; - let completion = match completed { - Ok(completion) => completion, - Err(_) - if pending_emulation_page_configuration_will_be_replayed( - conn, &target, &operation, source, - ) => - { - continue; - } - Err(error) => return CommandOutputPlan::error(-32000, error), - }; - let result = finish_pending_emulation_page_command(conn, operation, target, completion); + let result = + finish_completed_emulation_page_command(conn, operation, target, source, completed); if let Err(error) = result { return CommandOutputPlan::error(-32000, error); } @@ -2424,6 +2461,56 @@ pub(crate) fn complete_pending_emulation_command( CommandOutputPlan::result(json!({})) } +fn finish_completed_emulation_page_command( + conn: &mut CdpConnection, + operation: PendingEmulationPageOperation, + target: PendingEmulationPageTarget, + source: EmulationPageCommandSource, + completed: CompletedEmulationPageWork, +) -> Result<(), String> { + match completed { + CompletedEmulationPageWork::DocumentPolicy(completed) => { + let document = completed.document(); + match conn.finish_document_policy_update(completed) { + Ok(()) => Ok(()), + Err(error) + if error == "Document changed" + && (!operation.has_authoritative_replay_state() + || document_policy_will_be_replayed(conn, &target, document)) => + { + Ok(()) + } + Err(error) => Err(error), + } + } + CompletedEmulationPageWork::ResourceRuntime(completed) => { + match conn.finish_document_resource_runtime_update(completed) { + Ok(()) => Ok(()), + Err(_) + if pending_emulation_page_configuration_will_be_replayed( + conn, &target, &operation, source, + ) => + { + Ok(()) + } + Err(error) => Err(error), + } + } + } +} + +fn document_policy_will_be_replayed( + conn: &CdpConnection, + target: &PendingEmulationPageTarget, + document: moli_core::browser::DocumentHandle, +) -> bool { + let Some((context_id, target_id)) = target.resolve(conn) else { + return false; + }; + conn.browser_context_by_id(&context_id) + .is_some_and(|context| context.document_handle_for_target(&target_id) != Some(document)) +} + fn pending_emulation_page_configuration_will_be_replayed( conn: &CdpConnection, target: &PendingEmulationPageTarget, @@ -2481,20 +2568,46 @@ pub(crate) async fn dispose_page_session_async( let owner = CommandOwnerScope::capture(conn, Some(session_id)); let load_inputs = conn.navigation_load_inputs_for_owner(&owner); // A retry must install current effective Browser policy, even after the raw contribution was removed. - if let Some((context_id, target_id)) = page_configuration_owner(conn, &owner) - && let Some(context) = conn.browser_context_by_id_mut(&context_id) - && let Err(error) = context - .reconcile_target_runtime_policy_async( - &target_id, - load_inputs.script_execution_disabled, - &load_inputs.emulated_media, - load_inputs.network_offline, - load_inputs.viewport_surface, + let pending_reconciliation = page_configuration_owner(conn, &owner) + .and_then(|(context_id, target_id)| { + conn.browser_context_by_id(&context_id) + .and_then(|context| context.document_handle_for_target(&target_id)) + }) + .map(|document| { + conn.start_document_runtime_policy_reconciliation( + document, + DocumentRuntimePolicyReconciliation { + script_execution_disabled: load_inputs.script_execution_disabled, + emulated_media: load_inputs.emulated_media, + network_offline: load_inputs.network_offline, + viewport_surface: load_inputs.viewport_surface, + }, ) - .await - { - first_error - .get_or_insert_with(|| anyhow::anyhow!("failed to clear detached session {error}")); + }) + .transpose(); + match pending_reconciliation { + Ok(Some(pending)) => { + let completed = pending.wait().await; + let document = completed.document(); + if let Err(error) = conn.finish_document_policy_batch(completed) + && !document_policy_will_be_replayed( + conn, + &PendingEmulationPageTarget::SessionOwner { + owner_scope: owner.clone(), + }, + document, + ) + { + first_error.get_or_insert_with(|| { + anyhow::anyhow!("failed to clear detached session {error}") + }); + } + } + Ok(None) => {} + Err(error) => { + first_error + .get_or_insert_with(|| anyhow::anyhow!("failed to clear detached session {error}")); + } } record_emulation_disposal_result( &mut first_error, @@ -2520,19 +2633,17 @@ async fn apply_session_surface_state_async( else { return Ok(()); }; - let Some(browser_context) = conn.browser_context_by_id_mut(&browser_context_id) else { + let Some(browser_context) = conn.browser_context_by_id(&browser_context_id) else { return Ok(()); }; - if browser_context.is_active_target(&target_id) { - browser_context - .apply_surface_overrides_to_loaded_page_async() - .await - } else { - browser_context - .apply_background_target_surface_overrides_async(&target_id) - .await - .map(|_applied| ()) - } + let foreground = browser_context.is_active_target(&target_id); + let Some(handle) = browser_context.web_contents_handle_for_target(&target_id) else { + return Ok(()); + }; + conn.apply_browser_page_surface_async(handle, foreground) + .await + .map(drop) + .map_err(anyhow::Error::msg) } fn record_emulation_disposal_result( @@ -2550,19 +2661,20 @@ fn record_emulation_disposal_result( fn single_pending_emulation_dispatch( command_id: Option, owner_scope: CommandOwnerScope, - operation: PendingEmulationPageOperation, - pending: PendingPageCommand, - source: EmulationPageCommandSource, + pending: PendingDocumentPolicyUpdate, + replay_on_replacement: bool, ) -> PendingEmulationCommandDispatch { let session_id = owner_scope.session_id().map(str::to_owned); PendingEmulationCommandDispatch { command_id, session_id: session_id.clone(), pending: PendingEmulationRendererDispatch::Pages(vec![PendingEmulationPageCommand { - source, + source: EmulationPageCommandSource::Browser(None), target: PendingEmulationPageTarget::SessionOwner { owner_scope }, - operation, - pending, + operation: PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement, + }, + pending: PendingEmulationPageWork::DocumentPolicy(pending), }]), } } @@ -2581,25 +2693,35 @@ fn start_context_emulated_media_page_commands( conn: &mut CdpConnection, overrides: &moli_core::page::EmulatedMediaOverrides, ) -> Result, String> { - let Some(context) = conn.browser_context.as_ref() else { + let Some(context) = conn.browser_context.as_mut() else { return Ok(Vec::new()); }; let mut pending = Vec::new(); - for target in context.page_targets.iter() { - let target_id = target.target_id(); - if !context.target_has_loaded_page(target_id) { - continue; - } + let target_ids = context + .page_targets + .iter() + .filter(|target| context.target_has_loaded_page(target.target_id())) + .map(|target| target.target_id().to_owned()) + .collect::>(); + for target_id in target_ids { + let document = context + .document_handle_for_target(&target_id) + .expect("loaded target must have an exact Document"); pending.push(PendingEmulationPageCommand { - source: EmulationPageCommandSource::browser(context, target_id), + source: EmulationPageCommandSource::Browser(None), target: PendingEmulationPageTarget::BrowserContextTarget { browser_context_id: context.id.clone(), - target_id: target_id.to_owned(), + target_id, }, - operation: PendingEmulationPageOperation::Policy( - PagePolicyUpdateKind::SetEmulatedMedia, + operation: PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, + pending: PendingEmulationPageWork::DocumentPolicy( + context.start_document_policy_update( + document, + DocumentPolicyUpdate::EmulatedMedia(overrides.clone()), + )?, ), - pending: context.start_set_emulated_media_for_target(target_id, overrides)?, }); } Ok(pending) @@ -2615,7 +2737,8 @@ fn start_surface_override_page_commands( let Some(browser_context) = conn.browser_context.as_mut() else { return Ok(Vec::new()); }; - let navigator_overrides = browser_context.active_navigator_overrides(); + let navigator_overrides = + browser_context.active_navigator_overrides(&conn.browser_global_overrides); let document_activity = browser_context.active_document_activity(); let browser_context_id = browser_context.id.clone(); let Some(target_id) = browser_context.active_target_id_owned() else { @@ -2665,13 +2788,13 @@ fn start_session_surface_override_page_command_for_owner( .document_activity_for_target(target_id) .expect("resolved target"), browser_context - .navigator_overrides_for_target(target_id) + .navigator_overrides_for_target(target_id, &conn.browser_global_overrides) .expect("resolved target"), ) } else { ( browser_context.active_document_activity(), - browser_context.active_navigator_overrides(), + browser_context.active_navigator_overrides(&conn.browser_global_overrides), ) } }; @@ -2679,7 +2802,7 @@ fn start_session_surface_override_page_command_for_owner( return Ok(Vec::new()); }; let context = conn - .browser_context_by_id(&context_id) + .browser_context_by_id_mut(&context_id) .expect("resolved context remains registered"); start_surface_override_page_command( PendingEmulationPageTarget::SessionOwner { @@ -2710,7 +2833,7 @@ fn start_surface_override_for_route( .document_activity_for_target(target_id) .unwrap_or_default(), browser_context - .navigator_overrides_for_target(target_id) + .navigator_overrides_for_target(target_id, &conn.browser_global_overrides) .unwrap_or_default(), ) } @@ -2723,7 +2846,7 @@ fn start_surface_override_for_route( return Ok(Vec::new()); }; let context = conn - .browser_context_by_id(&context_id) + .browser_context_by_id_mut(&context_id) .expect("resolved context remains registered"); start_surface_override_page_command( target, @@ -2736,60 +2859,30 @@ fn start_surface_override_for_route( fn start_surface_override_page_command( target: PendingEmulationPageTarget, - context: &BrowserContext, + context: &mut BrowserContext, target_id: &str, navigator_overrides: moli_page_types::NavigatorOverrides, document_activity: moli_page_types::DocumentActivity, ) -> Result, String> { - let native_update = - context.start_set_navigator_overrides_for_target(target_id, &navigator_overrides)?; - let activity_update = - context.start_set_document_activity_for_target(target_id, document_activity)?; - Ok(vec![ - PendingEmulationPageCommand { - source: EmulationPageCommandSource::browser(context, target_id), + let document = context + .document_handle_for_target(target_id) + .ok_or("NoDocumentLoaded")?; + [ + DocumentPolicyUpdate::NavigatorOverrides(navigator_overrides), + DocumentPolicyUpdate::DocumentActivity(document_activity), + ] + .into_iter() + .map(|update| { + Ok(PendingEmulationPageCommand { + source: EmulationPageCommandSource::Browser(None), target: target.clone(), - operation: PendingEmulationPageOperation::Policy( - PagePolicyUpdateKind::SetNavigatorOverrides, + operation: PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, + pending: PendingEmulationPageWork::DocumentPolicy( + context.start_document_policy_update(document, update)?, ), - pending: native_update, - }, - PendingEmulationPageCommand { - source: EmulationPageCommandSource::browser(context, target_id), - target, - operation: PendingEmulationPageOperation::Policy( - PagePolicyUpdateKind::SetDocumentActivity, - ), - pending: activity_update, - }, - ]) -} - -fn finish_pending_emulation_page_command( - conn: &mut CdpConnection, - operation: PendingEmulationPageOperation, - target: PendingEmulationPageTarget, - completion: CompletedPageCommand, -) -> Result<(), String> { - match operation { - PendingEmulationPageOperation::RebuildResourceRuntime => { - if let PendingEmulationPageTarget::SessionOwner { owner_scope } = target { - return conn.finish_rebuild_resource_runtime_for_owner(&owner_scope, completion); - } - if let Some((context_id, target_id)) = target.resolve(conn) - && let Some(context) = conn.browser_context_by_id_mut(&context_id) - { - return context.finish_target_resource_runtime_update(&target_id, completion); - } - BrowserContext::finish_unobserved_resource_runtime_update(completion) - } - PendingEmulationPageOperation::Policy(kind) => { - if let Some((context_id, target_id)) = target.resolve(conn) - && let Some(context) = conn.browser_context_by_id_mut(&context_id) - { - return context.finish_target_page_policy_update(&target_id, kind, completion); - } - BrowserContext::finish_unobserved_page_policy_update(completion) - } - } + }) + }) + .collect() } diff --git a/moli-protocol/src/domains/emulation/tests.rs b/moli-protocol/src/domains/emulation/tests.rs index 8a071f286d..05d80d7d0d 100644 --- a/moli-protocol/src/domains/emulation/tests.rs +++ b/moli-protocol/src/domains/emulation/tests.rs @@ -80,14 +80,14 @@ async fn load_session_page_for_pending_emulation_test(ctx: &mut TestContext) { } async fn load_session_page_for_pending_emulation_test_at_url(ctx: &mut TestContext, url: &str) { - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); install_session_page_for_emulation_test(ctx, bc, url).await; } fn install_multi_session_page_state(ctx: &mut TestContext) { - let mut browser_context = BrowserContext::new("BID-1".into()); + let mut browser_context = ctx.conn.new_browser_context_fixture_for_test("BID-1"); browser_context.set_active_target_id("TID-1"); browser_context.attach_active_session("SID-primary"); assert!(browser_context.assign_attached_session_to_target("TID-1", "SID-attached".to_owned())); @@ -203,7 +203,7 @@ async fn execute_set_extra_headers_for_test( #[tokio::test(flavor = "multi_thread")] async fn bidi_set_extra_headers_merges_global_user_context_and_context_layers() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -216,7 +216,9 @@ async fn bidi_set_extra_headers_merges_global_user_context_and_context_layers() .await; let future_context = ctx.conn.new_browser_context("BID-future".to_owned()); assert_eq!( - future_context.effective_extra_headers().to_byte_strings(), + future_context + .effective_extra_headers(&ctx.conn.browser_global_overrides.extra_headers) + .to_byte_strings(), vec![ ("some_header_name".to_owned(), "global".to_owned()), ("global_header".to_owned(), "1".to_owned()) @@ -243,7 +245,7 @@ async fn bidi_set_extra_headers_merges_global_user_context_and_context_layers() .browser_context .as_ref() .expect("active browser context") - .effective_extra_headers() + .effective_extra_headers(&ctx.conn.browser_global_overrides.extra_headers) .to_byte_strings(); assert_eq!( headers, @@ -261,7 +263,7 @@ async fn bidi_set_extra_headers_merges_global_user_context_and_context_layers() .browser_context .as_ref() .expect("active browser context") - .effective_extra_headers() + .effective_extra_headers(&ctx.conn.browser_global_overrides.extra_headers) .to_byte_strings(); assert_eq!( headers, @@ -434,6 +436,12 @@ async fn emulated_media_can_complete_through_pending_command_dispatch() { async fn pending_emulation_completion_follows_the_exact_target_across_activation_and_navigation() { let mut ctx = TestContext::new(); load_session_page_for_pending_emulation_test(&mut ctx).await; + let pending = ctx + .conn + .start_rebuild_resource_runtime_for_session_owner(Some("SID-1")) + .unwrap() + .unwrap(); + let resource_completion = pending.wait().await; let dispatched_page = ctx .conn .browser_context @@ -458,9 +466,9 @@ async fn pending_emulation_completion_follows_the_exact_target_across_activation !super::pending_emulation_page_configuration_will_be_replayed( &ctx.conn, &target, - &super::PendingEmulationPageOperation::Policy( - crate::conn::PagePolicyUpdateKind::SetEmulatedMedia - ), + &super::PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, super::EmulationPageCommandSource::Browser(Some(dispatched_page)), ), "changing foreground selection must not make an error from the same Page look stale" @@ -478,9 +486,9 @@ async fn pending_emulation_completion_follows_the_exact_target_across_activation super::pending_emulation_page_configuration_will_be_replayed( &ctx.conn, &target, - &super::PendingEmulationPageOperation::Policy( - crate::conn::PagePolicyUpdateKind::SetEmulatedMedia - ), + &super::PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, super::EmulationPageCommandSource::Browser(Some(dispatched_page)), ), "only replacement of the exact target attachment may retire its renderer error" @@ -489,9 +497,9 @@ async fn pending_emulation_completion_follows_the_exact_target_across_activation !super::pending_emulation_page_configuration_will_be_replayed( &ctx.conn, &target, - &super::PendingEmulationPageOperation::Policy( - crate::conn::PagePolicyUpdateKind::SetIdleOverride - ), + &super::PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: false, + }, super::EmulationPageCommandSource::Browser(Some(dispatched_page)), ), "frame-host idle state must not use the target-policy replay path", @@ -505,11 +513,11 @@ async fn pending_emulation_completion_follows_the_exact_target_across_activation completed: super::CompletedEmulationRendererDispatch::Pages(vec![ super::CompletedEmulationPageCommand { target, - operation: super::PendingEmulationPageOperation::Policy( - crate::conn::PagePolicyUpdateKind::SetEmulatedMedia, - ), + operation: super::PendingEmulationPageOperation::RebuildResourceRuntime, source: super::EmulationPageCommandSource::Browser(Some(dispatched_page)), - completed: Err("renderer attachment retired".to_owned()), + completed: super::CompletedEmulationPageWork::ResourceRuntime( + resource_completion, + ), }, ]), }, @@ -573,9 +581,8 @@ async fn inspection_reattach_does_not_retire_outgoing_browser_policy_commands() let context = ctx.conn.browser_context.as_mut().unwrap(); let page_source = super::EmulationPageCommandSource::browser(context, "TID-1"); let endpoint = context - .loaded_page() - .unwrap() - .renderer_inspection_endpoint(); + .loaded_document_renderer_inspection_endpoint_for_test() + .unwrap(); let document_id = context.target_document_id("TID-1"); let old_attachment = context .page_target("TID-1") @@ -605,9 +612,9 @@ async fn inspection_reattach_does_not_retire_outgoing_browser_policy_commands() !super::pending_emulation_page_configuration_will_be_replayed( &ctx.conn, &target, - &super::PendingEmulationPageOperation::Policy( - crate::conn::PagePolicyUpdateKind::SetEmulatedMedia - ), + &super::PendingEmulationPageOperation::DocumentPolicy { + replay_on_replacement: true, + }, page_source, ), "an inspection reattach must not hide a Browser error on the still-current Page" @@ -854,7 +861,7 @@ async fn idle_override_updates_idle_detector_and_clear_restores_actual_state() { #[tokio::test(flavor = "multi_thread")] async fn pure_state_emulation_commands_complete_through_command_dispatch() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -919,7 +926,7 @@ async fn pure_state_emulation_commands_complete_through_command_dispatch() { #[tokio::test(flavor = "multi_thread")] async fn set_cpu_throttling_rate_rejects_invalid_params() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -937,7 +944,7 @@ async fn set_cpu_throttling_rate_rejects_invalid_params() { #[tokio::test(flavor = "multi_thread")] async fn live_apply_emulation_commands_without_loaded_page_do_not_use_legacy_fallback() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -993,7 +1000,7 @@ async fn live_apply_emulation_commands_without_loaded_page_do_not_use_legacy_fal #[tokio::test(flavor = "multi_thread")] async fn live_geolocation_override_uses_pending_command_dispatch() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); install_session_page_for_emulation_test(&mut ctx, bc, "data:text/html,geo").await; @@ -1533,7 +1540,7 @@ async fn multi_session_emulation_separates_handler_input_from_target_effective_s #[tokio::test(flavor = "multi_thread")] async fn async_emulation_device_state_updates_browser_context() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 501, @@ -1601,9 +1608,8 @@ async fn async_emulation_device_state_updates_browser_context() { bc.target_emulation_policy(bc.active_target_id().unwrap()) .expect("registered WebContents") .geolocation_override - .as_ref() }) - .and_then(EmulatedGeolocationOverrideState::position) + .and_then(|state| state.position().cloned()) .expect("geolocation override should be set"); assert_eq!(geolocation.latitude, 37.33182); assert_eq!(geolocation.longitude, -122.03118); @@ -1624,7 +1630,6 @@ async fn async_emulation_device_state_updates_browser_context() { bc.target_emulation_policy(bc.active_target_id().unwrap()) .expect("registered WebContents") .emulated_device_metrics - .as_ref() }) .expect("device metrics should be set"); assert_eq!(metrics.width, 800); @@ -1700,7 +1705,7 @@ async fn set_user_agent_override_applies_to_subsequent_navigation_requests() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -1773,7 +1778,7 @@ async fn emulation_user_agent_override_replaces_network_override() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1841,7 +1846,7 @@ async fn set_user_agent_override_applies_to_current_page_xhr_requests() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1929,7 +1934,7 @@ async fn set_user_agent_override_applies_complete_chromium_identity_profile() { let mut ctx = TestContext::new(); let natural_identity = ctx.conn.base_browser_identity().clone(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2205,7 +2210,7 @@ async fn emulation_async_dispatch_updates_live_page_user_agent_and_xhr_header() }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); install_session_page_for_emulation_test(&mut ctx, bc, "data:text/html,ok").await; @@ -2297,7 +2302,7 @@ async fn emulation_async_dispatch_updates_live_page_surface_without_mutating_acc }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2483,7 +2488,7 @@ async fn evaluate_geolocation_once_for_session( #[tokio::test(flavor = "multi_thread")] async fn set_geolocation_override_updates_loaded_page_geolocation_surface() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); install_geolocation_page_for_test(&mut ctx, bc).await; @@ -2524,7 +2529,7 @@ async fn set_geolocation_override_applies_to_subsequent_navigation_surface() { .unwrap(); }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2560,7 +2565,7 @@ async fn set_geolocation_override_applies_to_subsequent_navigation_surface() { #[tokio::test(flavor = "multi_thread")] async fn set_geolocation_override_missing_position_reports_unavailable() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); install_geolocation_page_for_test(&mut ctx, bc).await; @@ -2581,7 +2586,7 @@ async fn set_geolocation_override_missing_position_reports_unavailable() { #[tokio::test(flavor = "multi_thread")] async fn clear_geolocation_override_restores_default_after_explicit_unavailable() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.set_default_geolocation_override(Some(EmulatedGeolocationOverrideState::Position( @@ -2612,8 +2617,7 @@ async fn clear_geolocation_override_restores_default_after_explicit_unavailable( .and_then(|browser_context| browser_context .target_emulation_policy(browser_context.active_target_id().unwrap()) .expect("registered WebContents") - .geolocation_override - .as_ref()), + .geolocation_override), Some(EmulatedGeolocationOverrideState::PositionUnavailable) )); assert_eq!( @@ -2651,7 +2655,7 @@ async fn clear_geolocation_override_restores_default_after_explicit_unavailable( #[tokio::test(flavor = "multi_thread")] async fn set_geolocation_override_respects_denied_permission() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); install_geolocation_page_for_test(&mut ctx, bc).await; @@ -2684,7 +2688,7 @@ async fn set_geolocation_override_respects_denied_permission() { #[tokio::test(flavor = "multi_thread")] async fn device_metrics_override_updates_layout_metrics() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2771,7 +2775,7 @@ async fn locale_override_updates_intl_without_mutating_language_surfaces() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2827,7 +2831,7 @@ async fn bidi_user_context_locale_composes_with_user_agent_on_all_identity_surfa }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2931,7 +2935,7 @@ async fn live_locale_override_updates_intl_without_mutating_navigator() { #[tokio::test(flavor = "multi_thread")] async fn touch_and_timezone_overrides_apply_to_document_start_surface() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2970,7 +2974,7 @@ async fn touch_and_timezone_overrides_apply_to_document_start_surface() { #[tokio::test(flavor = "multi_thread")] async fn locale_and_timezone_overrides_apply_to_locale_date_formatting() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -3011,7 +3015,7 @@ async fn locale_and_timezone_overrides_apply_to_locale_date_formatting() { async fn context_emulated_media_applies_to_loaded_background_page_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active"); bc.register_page_target_fixture( @@ -3086,7 +3090,7 @@ async fn context_emulated_media_applies_to_loaded_background_page_without_activa async fn context_locale_override_applies_to_loaded_background_page_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active"); bc.register_page_target_fixture( @@ -3177,7 +3181,7 @@ async fn context_locale_override_applies_to_loaded_background_page_without_activ async fn session_emulation_routes_to_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active"); bc.register_page_target_fixture( @@ -3453,7 +3457,7 @@ async fn reduced_transparency_updates_css_media_events_and_clears_on_the_loaded_ #[tokio::test(flavor = "multi_thread")] async fn generated_surface_refresh_does_not_freeze_match_media_override() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -3474,11 +3478,9 @@ async fn generated_surface_refresh_does_not_freeze_match_media_override() { .await; ctx.expect_result(187, json!({}), Some("SID-1")); + let handle = ctx.conn.browser_web_contents_for_target("TID-1").unwrap(); ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .apply_surface_overrides_to_loaded_page_async() + .apply_browser_page_surface_async(handle, true) .await .expect("surface refresh should succeed"); @@ -3511,7 +3513,7 @@ async fn generated_surface_refresh_does_not_freeze_match_media_override() { #[tokio::test(flavor = "multi_thread")] async fn target_session_detach_disposes_non_aggregated_emulation_state_before_reattach() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); ctx.install_navigation_fixture_for_session_owner("data:text/html,", None) @@ -3659,7 +3661,7 @@ async fn target_session_detach_disposes_non_aggregated_emulation_state_before_re #[tokio::test(flavor = "multi_thread")] async fn emulated_media_color_scheme_applies_to_match_media_surface() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -3695,7 +3697,7 @@ async fn emulated_media_color_scheme_applies_to_match_media_surface() { #[tokio::test(flavor = "multi_thread")] async fn active_document_start_surface_reports_active_focus_and_visibility() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -3718,7 +3720,7 @@ async fn active_document_start_surface_reports_active_focus_and_visibility() { #[tokio::test(flavor = "multi_thread")] async fn focus_emulation_override_applies_to_document_start_surface() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/domains/fetch.rs b/moli-protocol/src/domains/fetch.rs index 22e63d6481..e0c2c8323d 100644 --- a/moli-protocol/src/domains/fetch.rs +++ b/moli-protocol/src/domains/fetch.rs @@ -9,8 +9,10 @@ mod state; mod subresource; use crate::conn::{ - BackgroundProtocolEvent, CdpConnection, Cmd, CommandOwnerScope, DevToolsCommandExecutionOutput, + BackgroundProtocolEvent, CdpConnection, Cmd, CommandOwnerScope, CompletedDocumentFetchCommand, + DevToolsCommandExecutionOutput, DocumentFetchCommand, DocumentFetchCommandOutcome, FetchInterceptionPattern, FetchRequestStage as ConnFetchRequestStage, + PendingDocumentFetchCommand, }; use crate::devtools_runtime::{ DevToolsAddNetworkInterceptCommand, DevToolsAddNetworkInterceptResult, DevToolsCommand, @@ -86,23 +88,12 @@ pub(in crate::domains) async fn dispose_owner_async( None } }; - if let Some(pending_page_command) = pending_page_command { - match pending_page_command.wait().await { - Ok(completion) => match finish_fetch_interception_update(conn, &owner, completion) { - Ok(()) => {} - Err(message) => { - renderer_cleanup_error = Some(anyhow::anyhow!( - "failed to finish Fetch interception disable while disposing session: {message}" - )); - } - }, - Err(error) => { - renderer_cleanup_error = - Some(error.context( - "renderer failed Fetch interception disable while disposing session", - )); - } - } + if let Some(pending_page_command) = pending_page_command + && let Err(message) = conn.finish_document_fetch_command(pending_page_command.wait().await) + { + renderer_cleanup_error = Some(anyhow::anyhow!( + "failed to finish Fetch interception disable while disposing session: {message}" + )); } let ( @@ -193,9 +184,7 @@ enum PendingFetchCommandKind { FulfillRequest { state: Box, }, - DispatchWebSocketMessage { - operation: commands::PendingWebSocketCommandOperation, - }, + DispatchWebSocketMessage, CloseWebSocket, ContinueResponse { state: Box, @@ -205,7 +194,7 @@ enum PendingFetchCommandKind { enum PendingFetchCommandOperation { Ready, - Page(Result), + DocumentFetch(Result), MaterializeResponseBody { request_id: String, transfer: Box, @@ -215,7 +204,7 @@ enum PendingFetchCommandOperation { enum CompletedFetchCommandOperation { Ready, - Page(Box>), + DocumentFetch(Box>), MaterializeResponseBody { request_id: String, result: Box< @@ -227,6 +216,23 @@ enum CompletedFetchCommandOperation { }, } +fn start_document_fetch_command_for_owner( + conn: &CdpConnection, + owner: &CommandOwnerScope, + command: DocumentFetchCommand, +) -> Result { + let document = conn.resolve_browser_document_for_owner(owner)?; + conn.start_document_fetch_command(document, command) +} + +fn finish_document_fetch_command( + conn: &mut CdpConnection, + completed: Option>, +) -> Result { + let completion = completed.ok_or_else(|| "Missing renderer completion".to_owned())??; + conn.finish_document_fetch_command(completion) +} + type FetchDisablePendingState = ( Vec, Vec, @@ -346,9 +352,9 @@ impl PendingFetchCommandDispatch { pub(crate) async fn wait(self) -> CompletedFetchCommandDispatch { let completed = match self.pending { PendingFetchCommandOperation::Ready => CompletedFetchCommandOperation::Ready, - PendingFetchCommandOperation::Page(pending) => { - CompletedFetchCommandOperation::Page(Box::new(match pending { - Ok(pending) => pending.wait().await.map_err(|error| error.to_string()), + PendingFetchCommandOperation::DocumentFetch(pending) => { + CompletedFetchCommandOperation::DocumentFetch(Box::new(match pending { + Ok(pending) => Ok(pending.wait().await), Err(error) => Err(error), })) } @@ -383,18 +389,20 @@ impl CompletedFetchCommandDispatch { impl CompletedFetchCommandOperation { fn renderer_output_predecessor(&self) -> Option { match self { - Self::Page(completed) => completed + Self::DocumentFetch(completed) => completed .as_ref() .as_ref() .ok() - .and_then(moli_core::page::CompletedPageCommand::renderer_output_predecessor), + .and_then(CompletedDocumentFetchCommand::renderer_output_predecessor), Self::Ready | Self::MaterializeResponseBody { .. } => None, } } - fn into_page_completion(self) -> Option> { + fn into_document_fetch_completion( + self, + ) -> Option> { match self { - Self::Page(completed) => Some(*completed), + Self::DocumentFetch(completed) => Some(*completed), Self::Ready | Self::MaterializeResponseBody { .. } => None, } } @@ -602,7 +610,7 @@ fn start_enable_command(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> FetchCommand cmd.id, cmd.session_id, PendingFetchCommandKind::Enable, - PendingFetchCommandOperation::Page(Ok(pending)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending)), )), Ok(None) => FetchCommandTaskStep::Complete(CommandOutputPlan::success()), Err(message) if message == "BrowserContextNotLoaded" => FetchCommandTaskStep::Complete( @@ -644,7 +652,7 @@ fn start_devtools_add_network_intercept_command( PendingFetchCommandKind::AddNetworkIntercept { intercept_id: command.intercept_id.as_str().to_owned(), }, - PendingFetchCommandOperation::Page(Ok(pending)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending)), )) } Ok(None) => FetchCommandTaskStep::Complete(CommandOutputPlan::from_devtools_result( @@ -672,7 +680,7 @@ fn start_devtools_remove_network_intercept_command( command_id, owner.clone(), PendingFetchCommandKind::RemoveNetworkIntercept, - PendingFetchCommandOperation::Page(Ok(pending)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending)), )) } Ok(None) => FetchCommandTaskStep::Complete(CommandOutputPlan::success()), @@ -803,7 +811,7 @@ async fn complete_pending_fetch_command_inner( complete_disable_command_async( conn, &owner_scope, - completed.completed.into_page_completion(), + completed.completed.into_document_fetch_completion(), *pending_fetch_state, &mut out, ) @@ -812,8 +820,7 @@ async fn complete_pending_fetch_command_inner( PendingFetchCommandKind::ContinueRequest { state } => { commands::complete_continue_request_command_async( conn, - &owner_scope, - completed.completed.into_page_completion(), + completed.completed.into_document_fetch_completion(), *state, &mut out, ) @@ -823,7 +830,7 @@ async fn complete_pending_fetch_command_inner( auth::complete_continue_with_auth_command_async( conn, &owner_scope, - completed.completed.into_page_completion(), + completed.completed.into_document_fetch_completion(), *state, &mut out, ) @@ -833,7 +840,7 @@ async fn complete_pending_fetch_command_inner( commands::complete_fail_request_command_async( conn, &owner_scope, - completed.completed.into_page_completion(), + completed.completed.into_document_fetch_completion(), *state, &mut out, ) @@ -843,27 +850,23 @@ async fn complete_pending_fetch_command_inner( commands::complete_fulfill_request_command_async( conn, &owner_scope, - completed.completed.into_page_completion(), + completed.completed.into_document_fetch_completion(), *state, &mut out, ) .await; } - PendingFetchCommandKind::DispatchWebSocketMessage { operation } => { + PendingFetchCommandKind::DispatchWebSocketMessage => { commands::complete_websocket_page_command( conn, - &owner_scope, - completed.completed.into_page_completion(), - operation, + completed.completed.into_document_fetch_completion(), &mut out, ); } PendingFetchCommandKind::CloseWebSocket => { commands::complete_websocket_page_command( conn, - &owner_scope, - completed.completed.into_page_completion(), - commands::PendingWebSocketCommandOperation::Close, + completed.completed.into_document_fetch_completion(), &mut out, ); } @@ -871,7 +874,7 @@ async fn complete_pending_fetch_command_inner( commands::complete_continue_response_command_async( conn, &owner_scope, - completed.completed.into_page_completion(), + completed.completed.into_document_fetch_completion(), *state, &mut out, ) @@ -901,44 +904,20 @@ fn complete_fetch_config_update_command( completed: CompletedFetchCommandDispatch, result: DevToolsCommandResult, ) -> CommandOutputPlan { - let owner_scope = completed.owner_scope.clone(); - let Some(completed_page_command) = completed.completed.into_page_completion() else { + let Some(completed_page_command) = completed.completed.into_document_fetch_completion() else { return CommandOutputPlan::error(-32000, "Missing renderer completion"); }; let completion = match completed_page_command { Ok(completion) => completion, Err(error) => return CommandOutputPlan::error(-32000, error), }; - let finish = if matches!( - completed.kind, - PendingFetchCommandKind::RemoveNetworkIntercept - ) { - conn.finish_removed_network_interception(completion) - } else { - finish_fetch_interception_update(conn, &owner_scope, completion) - }; + let finish = conn.finish_document_fetch_command(completion); match finish { - Ok(()) => CommandOutputPlan::from_devtools_result(result), + Ok(_) => CommandOutputPlan::from_devtools_result(result), Err(error) => CommandOutputPlan::error(-32000, error), } } -fn finish_fetch_interception_update( - conn: &mut CdpConnection, - owner: &CommandOwnerScope, - completion: moli_core::page::CompletedPageCommand, -) -> Result<(), String> { - // Interception is configuration, not an Inspector document query. The - // outgoing Page must accept cleanup while a navigation is suspended, but - // a completion for that Page must never update its replacement. - let Some((context_id, target_id)) = conn.resolved_page_owner_identity_for_owner(owner) else { - return Ok(()); - }; - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_target_fetch_interception_update(&target_id, completion) -} - fn start_disable_command(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> FetchCommandTaskStep { match conn.start_disable_fetch_for_session_owner(cmd.session_id) { Some((pending_fetch_state, pending)) => { @@ -950,9 +929,9 @@ fn start_disable_command(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> FetchComman pending_fetch_state: Box::new(pending_fetch_state), }, match pending { - Ok(Some(pending)) => PendingFetchCommandOperation::Page(Ok(pending)), + Ok(Some(pending)) => PendingFetchCommandOperation::DocumentFetch(Ok(pending)), Ok(None) => PendingFetchCommandOperation::Ready, - Err(error) => PendingFetchCommandOperation::Page(Err(error)), + Err(error) => PendingFetchCommandOperation::DocumentFetch(Err(error)), }, )) } @@ -966,14 +945,14 @@ fn start_disable_command(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> FetchComman async fn complete_disable_command_async( conn: &mut CdpConnection, owner: &CommandOwnerScope, - completed: Option>, + completed: Option>, pending_fetch_state: FetchDisablePendingState, out: &mut FetchCommandOutput, ) { let renderer_result = completed .map(|completion| { let completion = completion?; - finish_fetch_interception_update(conn, owner, completion) + conn.finish_document_fetch_command(completion).map(drop) }) .transpose(); match renderer_result { diff --git a/moli-protocol/src/domains/fetch/auth.rs b/moli-protocol/src/domains/fetch/auth.rs index 9d0a2d4029..d762309e02 100644 --- a/moli-protocol/src/domains/fetch/auth.rs +++ b/moli-protocol/src/domains/fetch/auth.rs @@ -1,6 +1,7 @@ use crate::conn::{ - BackgroundProtocolEvent, CdpConnection, Cmd, CommandOwnerScope, PendingFetchAuthNavigation, - PendingFetchNavigation, PendingSubresourceFetchAuthRequest, PendingSubresourceFetchRequest, + BackgroundProtocolEvent, CdpConnection, Cmd, CommandOwnerScope, CompletedDocumentFetchCommand, + DocumentFetchCommand, PendingFetchAuthNavigation, PendingFetchNavigation, + PendingSubresourceFetchAuthRequest, PendingSubresourceFetchRequest, }; use crate::devtools_runtime::{ DevToolsAuthChallengeAction, DevToolsCommand, DevToolsContinueWithAuthCommand, @@ -8,7 +9,7 @@ use crate::devtools_runtime::{ }; use crate::domains::command_output::CommandOutputPlan; use crate::domains::{activity, network}; -use moli_core::page::{CompletedPageCommand, SubresourceAuthCredentials}; +use moli_core::page::SubresourceAuthCredentials; use super::PendingFetchCommandOperation; use super::helpers::{ @@ -179,28 +180,18 @@ pub(super) fn start_devtools_continue_with_auth_command_for_pending( } else { None }; - let pending_page = conn.resolve_document_command_owner(owner).and_then( - |(context_id, target_id)| { - let context = conn - .browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")?; - (match command.action { - DevToolsAuthChallengeAction::Default => context - .start_fail_pending_subresource_auth_for_target( - &target_id, - pending.internal_id, - "Fetch auth challenge aborted".to_owned(), - ), - DevToolsAuthChallengeAction::Cancel => context - .start_cancel_pending_subresource_auth_for_target( - &target_id, - pending.internal_id, - ), - DevToolsAuthChallengeAction::ProvideCredentials => unreachable!(), - }) - .map_err(|error| error.to_string()) + let browser_command = match command.action { + DevToolsAuthChallengeAction::Default => DocumentFetchCommand::FailAuth { + internal_id: pending.internal_id, + error_text: "Fetch auth challenge aborted".to_owned(), }, - ); + DevToolsAuthChallengeAction::Cancel => DocumentFetchCommand::CancelAuth { + internal_id: pending.internal_id, + }, + DevToolsAuthChallengeAction::ProvideCredentials => unreachable!(), + }; + let pending_page = + super::start_document_fetch_command_for_owner(conn, owner, browser_command); let pending_page = match pending_page { Ok(pending_page) => pending_page, Err(error) => { @@ -237,7 +228,7 @@ pub(super) fn start_devtools_continue_with_auth_command_for_pending( PendingFetchCommandKind::ContinueWithAuth { state: Box::new(state), }, - PendingFetchCommandOperation::Page(Ok(pending_page)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending_page)), ), )); } @@ -276,20 +267,15 @@ pub(super) fn start_devtools_continue_with_auth_command_for_pending( ))); } }; - let pending_page = conn.resolve_document_command_owner(owner).and_then( - |(context_id, target_id)| { - let context = conn - .browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")?; - context - .start_continue_pending_subresource_auth_for_target( - &target_id, - pending.internal_id, - auth, - ) - .map_err(|error| format!("subresource auth continue failed: {error}")) + let pending_page = super::start_document_fetch_command_for_owner( + conn, + owner, + DocumentFetchCommand::ContinueAuth { + internal_id: pending.internal_id, + auth, }, - ); + ) + .map_err(|error| format!("subresource auth continue failed: {error}")); let pending_page = match pending_page { Ok(pending_page) => pending_page, Err(message) => { @@ -313,7 +299,7 @@ pub(super) fn start_devtools_continue_with_auth_command_for_pending( }, ), }, - PendingFetchCommandOperation::Page(Ok(pending_page)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending_page)), ), )); } @@ -552,7 +538,7 @@ fn next_chained_subresource_auth_required_event( pub(super) async fn complete_continue_with_auth_command_async( conn: &mut CdpConnection, owner: &CommandOwnerScope, - completed: Option>, + completed: Option>, state: PendingContinueWithAuthState, out: &mut FetchCommandOutput, ) { @@ -566,20 +552,17 @@ pub(super) async fn complete_continue_with_auth_command_async( owner, completed, *pending, - true, correlation, out, ) .await; } PendingContinueWithAuthState::SubresourceAuthFail { pending } => { - complete_subresource_auth_terminal_async( - conn, owner, completed, *pending, false, None, out, - ) - .await; + complete_subresource_auth_terminal_async(conn, owner, completed, *pending, None, out) + .await; } PendingContinueWithAuthState::SubresourceAuthContinue { correlation } => { - if let Err(error) = finish_continue_subresource_auth(conn, owner, completed) { + if let Err(error) = finish_continue_subresource_auth(conn, completed) { correlation.rollback(conn); out.push_error(-32000, error); return; @@ -647,50 +630,13 @@ pub(super) async fn complete_continue_with_auth_command_async( async fn complete_subresource_auth_terminal_async( conn: &mut CdpConnection, owner: &CommandOwnerScope, - completed: Option>, + completed: Option>, pending: crate::conn::PendingSubresourceFetchAuthRequest, - expose_challenged_response: bool, correlation: Option, out: &mut FetchCommandOutput, ) { let activity_session_id = pending.owner_session_id.as_deref().or(owner.session_id()); - let activity_owner = activity_session_id - .map(CommandOwnerScope::for_session) - .unwrap_or_else(|| owner.clone()); - let Some(completed) = completed else { - if let Some(correlation) = correlation { - correlation.rollback(conn); - } - out.push_error(-32000, "Missing renderer completion"); - return; - }; - let completion = match completed { - Ok(completion) => completion, - Err(error) => { - if let Some(correlation) = correlation { - correlation.rollback(conn); - } - out.push_error(-32000, error); - return; - } - }; - let result = match conn.resolve_document_command_owner(&activity_owner) { - Ok((context_id, target_id)) => conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered") - .finish_target_subresource_auth_terminal( - &target_id, - completion, - expose_challenged_response, - ), - Err(message) => { - if let Some(correlation) = correlation { - correlation.rollback(conn); - } - out.push_error(-32000, message); - return; - } - }; + let result = super::finish_document_fetch_command(conn, completed); if let Err(error) = result { if let Some(correlation) = correlation { correlation.rollback(conn); @@ -715,16 +661,10 @@ async fn complete_subresource_auth_terminal_async( fn finish_continue_subresource_auth( conn: &mut CdpConnection, - owner: &CommandOwnerScope, - completed: Option>, + completed: Option>, ) -> Result<(), String> { - let completion = completed.ok_or_else(|| "Missing renderer completion".to_owned())??; - let (context_id, target_id) = conn.resolve_document_command_owner(owner)?; - let context = conn - .browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")?; - context - .finish_continue_pending_subresource_auth_for_target(&target_id, completion) + super::finish_document_fetch_command(conn, completed) + .and_then(crate::conn::DocumentFetchCommandOutcome::into_continue_outcome) .map(|_| ()) .map_err(|error| format!("subresource auth continue failed: {error}")) } diff --git a/moli-protocol/src/domains/fetch/commands.rs b/moli-protocol/src/domains/fetch/commands.rs index 1c8ff21f84..1d3b281544 100644 --- a/moli-protocol/src/domains/fetch/commands.rs +++ b/moli-protocol/src/domains/fetch/commands.rs @@ -1,6 +1,7 @@ use crate::conn::{ BackgroundNavigationBodyCompletionSink, CapturedBody, CdpConnection, Cmd, CommandOwnerScope, - DEFAULT_LOADER_ID, PendingStreamingDocumentResponseNavigation, monotonic_timestamp_seconds, + CompletedDocumentFetchCommand, DEFAULT_LOADER_ID, DocumentFetchCommand, + PendingStreamingDocumentResponseNavigation, monotonic_timestamp_seconds, }; use crate::devtools_runtime::{ DevToolsAuthChallengeAction, DevToolsCommand, DevToolsContinueInterceptedRequestCommand, @@ -10,9 +11,7 @@ use crate::devtools_runtime::{ }; use crate::domains::command_output::CommandOutputPlan; use crate::domains::{activity, network, page}; -use moli_core::page::{ - CompletedPageCommand, RendererSyntheticResponseBody, SubresourceResourceType, -}; +use moli_core::page::{RendererSyntheticResponseBody, SubresourceResourceType}; use moli_url_policy::BrowserUrlScheme; use url::Url; @@ -327,25 +326,20 @@ fn start_devtools_continue_intercepted_request_command( )); } }; - let pending_page = - conn.resolve_document_command_owner(owner) - .and_then(|(context_id, target_id)| { - let context = conn - .browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")?; - context - .start_continue_pending_subresource_fetch_for_target( - &target_id, - pending.internal_id, - parsed_url, - method, - post_data, - headers, - intercept_response, - handle_auth_requests, - ) - .map_err(|error| format!("subresource fetch continue failed: {error}")) - }); + let pending_page = super::start_document_fetch_command_for_owner( + conn, + owner, + DocumentFetchCommand::ContinueRequest { + internal_id: pending.internal_id, + url: parsed_url, + method, + body: post_data, + headers, + intercept_response, + handle_auth_requests, + }, + ) + .map_err(|error| format!("subresource fetch continue failed: {error}")); let pending_page = match pending_page { Ok(pending_page) => pending_page, Err(message) => { @@ -362,7 +356,7 @@ fn start_devtools_continue_intercepted_request_command( PendingFetchCommandKind::ContinueRequest { state: Box::new(PendingContinueRequestState::SubresourceFetch { correlation }), }, - PendingFetchCommandOperation::Page(Ok(pending_page)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending_page)), )); } if let Some(mut pending) = take_pending_navigation(conn, owner, action_session_id, &request_id) @@ -428,14 +422,13 @@ fn start_devtools_continue_intercepted_request_command( pub(super) async fn complete_continue_request_command_async( conn: &mut CdpConnection, - owner: &CommandOwnerScope, - completed: Option>, + completed: Option>, state: PendingContinueRequestState, out: &mut FetchCommandOutput, ) { match state { PendingContinueRequestState::SubresourceFetch { correlation } => { - if let Err(error) = finish_continue_subresource_request(conn, owner, completed) { + if let Err(error) = finish_continue_subresource_request(conn, completed) { correlation.rollback(conn); out.push_error(-32000, error); return; @@ -451,16 +444,10 @@ pub(super) async fn complete_continue_request_command_async( fn finish_continue_subresource_request( conn: &mut CdpConnection, - owner: &CommandOwnerScope, - completed: Option>, + completed: Option>, ) -> Result<(), String> { - let completion = completed.ok_or_else(|| "Missing renderer completion".to_owned())??; - let (context_id, target_id) = conn.resolve_document_command_owner(owner)?; - let context = conn - .browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")?; - context - .finish_continue_pending_subresource_fetch_for_target(&target_id, completion) + super::finish_document_fetch_command(conn, completed) + .and_then(crate::conn::DocumentFetchCommandOutcome::into_continue_outcome) .map(|_| ()) .map_err(|error| format!("subresource fetch continue failed: {error}")) } @@ -583,26 +570,13 @@ fn start_devtools_fail_intercepted_request_command( plan.extend_background_events(events); return FetchCommandTaskStep::Complete(plan); } - let (context_id, target_id) = match conn.resolve_document_command_owner(owner) { - Ok(route) => route, - Err(message) if message == "NoDocumentLoaded" => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - let pending_page = match context.start_fail_pending_subresource_fetch_for_target( - &target_id, - pending.internal_id, - error_text.clone(), + let pending_page = match super::start_document_fetch_command_for_owner( + conn, + owner, + DocumentFetchCommand::FailRequest { + internal_id: pending.internal_id, + error_text: error_text.clone(), + }, ) { Ok(pending_page) => pending_page, Err(error) => { @@ -620,7 +594,7 @@ fn start_devtools_fail_intercepted_request_command( pending: Box::new(pending), }), }, - PendingFetchCommandOperation::Page(Ok(pending_page)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending_page)), )); } @@ -630,26 +604,13 @@ fn start_devtools_fail_intercepted_request_command( action_session_id, &request_id, ) { - let (context_id, target_id) = match conn.resolve_document_command_owner(owner) { - Ok(route) => route, - Err(message) if message == "NoDocumentLoaded" => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - let pending_page = match context.start_fail_pending_subresource_response_for_target( - &target_id, - pending.internal_id, - error_text.clone(), + let pending_page = match super::start_document_fetch_command_for_owner( + conn, + owner, + DocumentFetchCommand::FailResponse { + internal_id: pending.internal_id, + error_text: error_text.clone(), + }, ) { Ok(pending_page) => pending_page, Err(error) => { @@ -667,7 +628,7 @@ fn start_devtools_fail_intercepted_request_command( pending: Box::new(pending), }), }, - PendingFetchCommandOperation::Page(Ok(pending_page)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending_page)), )); } @@ -696,7 +657,7 @@ fn start_devtools_fail_intercepted_request_command( pub(super) async fn complete_fail_request_command_async( conn: &mut CdpConnection, owner: &CommandOwnerScope, - completed: Option>, + completed: Option>, state: PendingFailRequestState, out: &mut FetchCommandOutput, ) { @@ -726,27 +687,7 @@ pub(super) async fn complete_fail_request_command_async( .await; } PendingFailRequestState::SubresourceFetch { pending } => { - let Some(completed) = completed else { - out.push_error(-32000, "Missing renderer completion"); - return; - }; - let completion = match completed { - Ok(completion) => completion, - Err(error) => { - out.push_error(-32000, error); - return; - } - }; - let result = - conn.resolve_document_command_owner(owner) - .and_then(|(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_fail_pending_subresource_fetch_for_target( - &target_id, completion, - ) - }); - if let Err(message) = result { + if let Err(message) = super::finish_document_fetch_command(conn, completed) { out.push_error(-32000, message); return; } @@ -762,27 +703,7 @@ pub(super) async fn complete_fail_request_command_async( out.extend_background_events(events); } PendingFailRequestState::SubresourceResponse { pending } => { - let Some(completed) = completed else { - out.push_error(-32000, "Missing renderer completion"); - return; - }; - let completion = match completed { - Ok(completion) => completion, - Err(error) => { - out.push_error(-32000, error); - return; - } - }; - let result = - conn.resolve_document_command_owner(owner) - .and_then(|(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_fail_pending_subresource_response_for_target( - &target_id, completion, - ) - }); - if let Err(message) = result { + if let Err(message) = super::finish_document_fetch_command(conn, completed) { out.push_error(-32000, message); return; } @@ -843,12 +764,6 @@ pub(super) enum PendingFulfillRequestState { }, } -pub(super) enum PendingWebSocketCommandOperation { - DispatchText, - DispatchBinary, - Close, -} - pub(super) fn start_fulfill_request_command( conn: &mut CdpConnection, cmd: &Cmd<'_>, @@ -967,28 +882,15 @@ fn start_devtools_fulfill_intercepted_request_command( let websocket_socket_id = pending.websocket_socket_id; let register_synthetic_websocket = pending.resource_type == SubresourceResourceType::WebSocket && response_code == 101; - let (context_id, target_id) = match conn.resolve_document_command_owner(owner) { - Ok(route) => route, - Err(message) if message == "NoDocumentLoaded" => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - let pending_page = match context.start_fulfill_pending_subresource_fetch_for_target( - &target_id, - pending.internal_id, - response_code, - response_headers.clone(), - decoded_body.unwrap_or_else(RendererSyntheticResponseBody::empty), + let pending_page = match super::start_document_fetch_command_for_owner( + conn, + owner, + DocumentFetchCommand::FulfillRequest { + internal_id: pending.internal_id, + response_code, + response_headers: response_headers.clone(), + response_body: decoded_body.unwrap_or_else(RendererSyntheticResponseBody::empty), + }, ) { Ok(pending_page) => pending_page, Err(error) => { @@ -1010,7 +912,7 @@ fn start_devtools_fulfill_intercepted_request_command( register_synthetic_websocket, }), }, - PendingFetchCommandOperation::Page(Ok(pending_page)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending_page)), )); } @@ -1020,28 +922,15 @@ fn start_devtools_fulfill_intercepted_request_command( action_session_id, &request_id, ) { - let (context_id, target_id) = match conn.resolve_document_command_owner(owner) { - Ok(route) => route, - Err(message) if message == "NoDocumentLoaded" => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - let pending_page = match context.start_fulfill_pending_subresource_response_for_target( - &target_id, - pending.internal_id, - response_code, - response_headers.clone(), - decoded_body.unwrap_or_else(RendererSyntheticResponseBody::empty), + let pending_page = match super::start_document_fetch_command_for_owner( + conn, + owner, + DocumentFetchCommand::FulfillResponse { + internal_id: pending.internal_id, + response_code, + response_headers: response_headers.clone(), + response_body: decoded_body.unwrap_or_else(RendererSyntheticResponseBody::empty), + }, ) { Ok(pending_page) => pending_page, Err(error) => { @@ -1059,7 +948,7 @@ fn start_devtools_fulfill_intercepted_request_command( pending: Box::new(pending), }), }, - PendingFetchCommandOperation::Page(Ok(pending_page)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending_page)), )); } @@ -1106,7 +995,7 @@ fn start_devtools_fulfill_intercepted_request_command( pub(super) async fn complete_fulfill_request_command_async( conn: &mut CdpConnection, owner: &CommandOwnerScope, - completed: Option>, + completed: Option>, state: PendingFulfillRequestState, out: &mut FetchCommandOutput, ) { @@ -1118,27 +1007,7 @@ pub(super) async fn complete_fulfill_request_command_async( websocket_socket_id, register_synthetic_websocket, } => { - let Some(completed) = completed else { - out.push_error(-32000, "Missing renderer completion"); - return; - }; - let completion = match completed { - Ok(completion) => completion, - Err(error) => { - out.push_error(-32000, error); - return; - } - }; - let result = - conn.resolve_document_command_owner(owner) - .and_then(|(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_fulfill_pending_subresource_fetch_for_target( - &target_id, completion, - ) - }); - if let Err(message) = result { + if let Err(message) = super::finish_document_fetch_command(conn, completed) { out.push_error(-32000, message); return; } @@ -1162,27 +1031,7 @@ pub(super) async fn complete_fulfill_request_command_async( out.extend_background_events(events); } PendingFulfillRequestState::SubresourceResponse { pending } => { - let Some(completed) = completed else { - out.push_error(-32000, "Missing renderer completion"); - return; - }; - let completion = match completed { - Ok(completion) => completion, - Err(error) => { - out.push_error(-32000, error); - return; - } - }; - let result = - conn.resolve_document_command_owner(owner) - .and_then(|(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_fulfill_pending_subresource_response_for_target( - &target_id, completion, - ) - }); - if let Err(message) = result { + if let Err(message) = super::finish_document_fetch_command(conn, completed) { out.push_error(-32000, message); return; } @@ -1299,33 +1148,12 @@ pub(super) fn start_dispatch_websocket_message_command( return FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, "RequestNotFound")); }; - let (context_id, target_id) = match conn - .resolve_document_command_owner(&CommandOwnerScope::capture(conn, cmd.session_id)) - { - Ok(route) => route, - Err(message) if message == "NoDocumentLoaded" => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - let (operation, pending_page) = match WebSocketMessageOpcode::parse(¶ms.opcode) { - Some(WebSocketMessageOpcode::Text) => ( - PendingWebSocketCommandOperation::DispatchText, - context.start_receive_synthetic_websocket_text_for_target( - &target_id, - socket_id, - params.data, - ), - ), + let owner = CommandOwnerScope::capture(conn, cmd.session_id); + let command = match WebSocketMessageOpcode::parse(¶ms.opcode) { + Some(WebSocketMessageOpcode::Text) => DocumentFetchCommand::DispatchWebSocketText { + socket_id, + data: params.data, + }, Some(WebSocketMessageOpcode::Binary) => { let bytes = match decode_base64_bytes(¶ms.data) { Ok(bytes) => bytes, @@ -1336,12 +1164,10 @@ pub(super) fn start_dispatch_websocket_message_command( )); } }; - ( - PendingWebSocketCommandOperation::DispatchBinary, - context.start_receive_synthetic_websocket_binary_for_target( - &target_id, socket_id, bytes, - ), - ) + DocumentFetchCommand::DispatchWebSocketBinary { + socket_id, + data: bytes, + } } None => { return FetchCommandTaskStep::Complete(CommandOutputPlan::error( @@ -1350,7 +1176,7 @@ pub(super) fn start_dispatch_websocket_message_command( )); } }; - let pending = match pending_page { + let pending = match super::start_document_fetch_command_for_owner(conn, &owner, command) { Ok(pending) => pending, Err(error) => { return FetchCommandTaskStep::Complete(CommandOutputPlan::error( @@ -1363,8 +1189,8 @@ pub(super) fn start_dispatch_websocket_message_command( conn, cmd.id, cmd.session_id, - PendingFetchCommandKind::DispatchWebSocketMessage { operation }, - PendingFetchCommandOperation::Page(Ok(pending)), + PendingFetchCommandKind::DispatchWebSocketMessage, + PendingFetchCommandOperation::DocumentFetch(Ok(pending)), )) } @@ -1387,29 +1213,15 @@ pub(super) fn start_close_websocket_command( return FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, "RequestNotFound")); }; - let (context_id, target_id) = match conn - .resolve_document_command_owner(&CommandOwnerScope::capture(conn, cmd.session_id)) - { - Ok(route) => route, - Err(message) if message == "NoDocumentLoaded" => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - let pending = match context.start_close_synthetic_websocket_from_server_for_target( - &target_id, - socket_id, - params.code, - params.reason, + let owner = CommandOwnerScope::capture(conn, cmd.session_id); + let pending = match super::start_document_fetch_command_for_owner( + conn, + &owner, + DocumentFetchCommand::CloseWebSocket { + socket_id, + code: params.code, + reason: params.reason, + }, ) { Ok(pending) => pending, Err(error) => { @@ -1424,55 +1236,17 @@ pub(super) fn start_close_websocket_command( cmd.id, cmd.session_id, PendingFetchCommandKind::CloseWebSocket, - PendingFetchCommandOperation::Page(Ok(pending)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending)), )) } pub(super) fn complete_websocket_page_command( conn: &mut CdpConnection, - owner: &CommandOwnerScope, - completed: Option>, - operation: PendingWebSocketCommandOperation, + completed: Option>, out: &mut FetchCommandOutput, ) { - let Some(completed) = completed else { - out.push_error(-32000, "Missing renderer completion"); - return; - }; - let completion = match completed { - Ok(completion) => completion, - Err(error) => { - out.push_error(-32000, error); - return; - } - }; - let result = match conn.resolve_document_command_owner(owner) { - Ok((context_id, target_id)) => { - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - match operation { - PendingWebSocketCommandOperation::DispatchText => context - .finish_receive_synthetic_websocket_text_for_target(&target_id, completion), - PendingWebSocketCommandOperation::DispatchBinary => context - .finish_receive_synthetic_websocket_binary_for_target(&target_id, completion), - PendingWebSocketCommandOperation::Close => context - .finish_close_synthetic_websocket_from_server_for_target( - &target_id, completion, - ), - } - } - Err(message) if message == "NoDocumentLoaded" => { - out.push_error(-32000, "NoDocumentLoaded"); - return; - } - Err(message) => { - out.push_error(-32000, message); - return; - } - }; - match result { - Ok(()) => out.push_success(), + match super::finish_document_fetch_command(conn, completed) { + Ok(_) => out.push_success(), Err(error) => out.push_error(-32000, &error), } } @@ -1717,27 +1491,14 @@ fn start_devtools_continue_intercepted_response_command( response_headers }; let pending_internal_id = pending.internal_id; - let (context_id, target_id) = match conn.resolve_document_command_owner(owner) { - Ok(route) => route, - Err(message) if message == "NoDocumentLoaded" => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - let pending_page = match context.start_continue_pending_subresource_response_for_target( - &target_id, - pending_internal_id, - continue_response_code, - continue_response_headers, + let pending_page = match super::start_document_fetch_command_for_owner( + conn, + owner, + DocumentFetchCommand::ContinueResponse { + internal_id: pending_internal_id, + response_code: continue_response_code, + response_headers: continue_response_headers, + }, ) { Ok(pending_page) => pending_page, Err(error) => { @@ -1755,7 +1516,7 @@ fn start_devtools_continue_intercepted_response_command( pending: Box::new(pending), }), }, - PendingFetchCommandOperation::Page(Ok(pending_page)), + PendingFetchCommandOperation::DocumentFetch(Ok(pending_page)), )); } if let Some(step) = super::auth::start_devtools_continue_with_auth_command_for_pending( @@ -1834,7 +1595,7 @@ async fn continue_response_transfer_inline( pub(super) async fn complete_continue_response_command_async( conn: &mut CdpConnection, owner: &CommandOwnerScope, - completed: Option>, + completed: Option>, state: PendingContinueResponseState, out: &mut FetchCommandOutput, ) { @@ -1856,27 +1617,7 @@ pub(super) async fn complete_continue_response_command_async( } PendingContinueResponseState::SubresourceResponse { pending } => { let pending = *pending; - let Some(completed) = completed else { - out.push_error(-32000, "Missing renderer completion"); - return; - }; - let completion = match completed { - Ok(completion) => completion, - Err(error) => { - out.push_error(-32000, error); - return; - } - }; - let result = - conn.resolve_document_command_owner(owner) - .and_then(|(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_continue_pending_subresource_response_for_target( - &target_id, completion, - ) - }); - if let Err(message) = result { + if let Err(message) = super::finish_document_fetch_command(conn, completed) { out.push_error(-32000, message); return; } @@ -1909,12 +1650,14 @@ fn continue_streaming_document_response_in_background( network_observation_journal, body_progress_source, prepared_document, + prepared_document_projection, } = pending; let document_navigation_token = permit.navigation(); let cancellation = response.cancellation_handle(); if response_code.is_none() && response_headers.is_empty() - && let Some(prepared_document) = prepared_document + && let (Some(prepared_document), Some(projection)) = + (prepared_document, prepared_document_projection) { conn.arm_background_navigation_completion(&document_navigation_token, Some(cancellation)); tokio::task::spawn_local(async move { @@ -1923,8 +1666,11 @@ fn continue_streaming_document_response_in_background( document_navigation_token, navigation.clone(), ); - let navigation_result = - prepared_document.resume_streaming(response, Some(body_completion_sink)); + let navigation_result = projection.resume_streaming( + *prepared_document, + response, + Some(body_completion_sink), + ); let _ = sender.send(page::BackgroundNavigationCompletion::new( document_navigation_token, navigation, @@ -1970,10 +1716,10 @@ mod protocol_neutral_tests { use url::Url; use crate::conn::{ - BrowserContext, CapturedBody, CdpConnection, Cmd, PendingSubresourceFetchOwnerKind, - PendingSubresourceFetchRequest, PendingSubresourceFetchRequestStage, - PendingSubresourceFetchRequestStageChain, PendingSubresourceFetchResponseRequest, - PendingSubresourceFetchResponseStage, PendingSubresourceFetchResponseStageChain, + CapturedBody, Cmd, PendingSubresourceFetchOwnerKind, PendingSubresourceFetchRequest, + PendingSubresourceFetchRequestStage, PendingSubresourceFetchRequestStageChain, + PendingSubresourceFetchResponseRequest, PendingSubresourceFetchResponseStage, + PendingSubresourceFetchResponseStageChain, }; use super::{ @@ -1985,7 +1731,7 @@ mod protocol_neutral_tests { #[test] fn cdp_continue_request_builds_protocol_neutral_intercepted_request_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "requestId": "interception-job-0", "url": "https://example.test/next", @@ -2035,7 +1781,7 @@ mod protocol_neutral_tests { #[test] fn cdp_continue_response_builds_protocol_neutral_intercepted_response_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(21), @@ -2072,7 +1818,7 @@ mod protocol_neutral_tests { #[test] fn devtools_fetch_entry_routes_continue_request_command() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({"requestId": "missing-request"}); let cmd = Cmd::for_test( Some(24), @@ -2106,8 +1852,8 @@ mod protocol_neutral_tests { #[test] fn cdp_continue_request_repauses_next_matching_fetch_session() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-chain".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-chain".to_owned()); browser_context.set_active_target_id("TID-chain".to_owned()); browser_context.attach_active_session("SID-primary".to_owned()); assert!( @@ -2251,8 +1997,9 @@ mod protocol_neutral_tests { #[test] fn cdp_continue_response_repauses_next_matching_fetch_session() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-response-chain".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-response-chain".to_owned()); browser_context.set_active_target_id("TID-response-chain".to_owned()); browser_context.attach_active_session("SID-primary".to_owned()); assert!( @@ -2388,8 +2135,9 @@ mod protocol_neutral_tests { #[test] fn cdp_continue_request_repauses_network_or_bidi_stage_on_target_owner_route() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-chain-bidi".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-chain-bidi".to_owned()); browser_context.set_active_target_id("TID-chain-bidi".to_owned()); browser_context.attach_active_session("SID-primary".to_owned()); browser_context.set_active_document_fixture_for_test(1); @@ -2502,7 +2250,7 @@ mod protocol_neutral_tests { #[test] fn cdp_fail_request_builds_protocol_neutral_intercepted_request_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(22), @@ -2532,7 +2280,7 @@ mod protocol_neutral_tests { #[test] fn cdp_fulfill_request_builds_protocol_neutral_intercepted_request_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(23), diff --git a/moli-protocol/src/domains/fetch/navigation.rs b/moli-protocol/src/domains/fetch/navigation.rs index 8b3ddf5a30..c1348f680d 100644 --- a/moli-protocol/src/domains/fetch/navigation.rs +++ b/moli-protocol/src/domains/fetch/navigation.rs @@ -557,6 +557,13 @@ async fn handle_streaming_response_head_for_navigation_into_buffer_async( return; } }; + let (prepared_document, prepared_document_projection) = match prepared_document { + Some((prepared_document, projection)) => ( + Some(Box::new(prepared_document)), + Some(Box::new(projection)), + ), + None => (None, None), + }; let (response, network_observation_journal) = response.into_parts_with_observation_journal(); conn.register_pending_fetch_response_navigation_for_owner( &pending.navigation.owner, @@ -569,9 +576,10 @@ async fn handle_streaming_response_head_for_navigation_into_buffer_async( request_headers: pending.navigation.request_headers.clone(), response, network_observation_journal, - body_progress_source, - prepared_document: prepared_document.map(Box::new), + prepared_document, }, + body_progress_source, + prepared_document_projection, ); out.extend_background_events_after_messages([navigation_response_stage_request_paused_event( pending.interception_session_id.as_deref(), @@ -645,6 +653,8 @@ fn pause_buffered_raw_response_stage_navigation_into_buffer( response, network_observation_journal, }, + network::MainDocumentBodyProgressSource::default(), + None, ); out.extend_background_events_after_messages([navigation_response_stage_request_paused_event( pending.interception_session_id.as_deref(), @@ -721,18 +731,10 @@ pub(super) async fn continue_navigation_response_neutrally_as_background_events_ pending: crate::conn::PendingFetchResponseNavigation, transfer: Option, ) { - let token = Some(pending.permit.navigation()); - let navigation_state = pending.navigation; - let navigation = match transfer { - Some(transfer) => { - transfer - .continue_response_neutrally_async(conn, pending.permit, &navigation_state) - .await - } - None => Err(anyhow::anyhow!( - "renderer channel navigation was superseded by a newer navigation" - )), - }; + let (token, navigation_state, navigation) = + crate::conn::ClaimedFetchResponseNavigation::new(String::new(), pending, transfer) + .continue_response_neutrally_async(conn) + .await; let navigation = network::materialize_navigation_load_result(conn, &navigation_state, navigation); complete_tokened_materialized_navigation_as_background_events_async( diff --git a/moli-protocol/src/domains/fetch/subresource.rs b/moli-protocol/src/domains/fetch/subresource.rs index 3066965087..866295b0db 100644 --- a/moli-protocol/src/domains/fetch/subresource.rs +++ b/moli-protocol/src/domains/fetch/subresource.rs @@ -592,8 +592,8 @@ mod tests { use url::Url; use crate::conn::{ - BackgroundProtocolEvent, BrowserContext, CdpConnection, CommandOwnerScope, - PendingSubresourceFetchOwnerKind, PendingSubresourceFetchRequest, + BackgroundProtocolEvent, CommandOwnerScope, PendingSubresourceFetchOwnerKind, + PendingSubresourceFetchRequest, }; use crate::devtools_runtime::{AutomationEvent, DevToolsNetworkResourceType}; use crate::domains::network::{ @@ -695,8 +695,8 @@ mod tests { #[test] fn prepared_subresource_fetch_pause_pairs_emit_network_then_fetch_per_item() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new_with_page_for_test("BID-1", "TID-active"); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_page_target_fixture_for_test("BID-1", "TID-active"); browser_context.set_active_document_fixture_for_test(1); conn.install_browser_context_fixture_for_test(browser_context); let page_owner = conn @@ -771,8 +771,8 @@ mod tests { #[test] fn fetch_pause_does_not_synthesize_cookie_extra_info() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new_with_page_for_test("BID-1", "TID-active"); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_page_target_fixture_for_test("BID-1", "TID-active"); browser_context.set_active_document_fixture_for_test(1); conn.install_browser_context_fixture_for_test(browser_context); let page_owner = conn @@ -813,8 +813,8 @@ mod tests { #[test] fn prepared_subresource_fetch_pause_does_not_emit_after_page_replacement() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new_with_page_for_test("BID-1", "TID-active"); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_page_target_fixture_for_test("BID-1", "TID-active"); browser_context.set_active_document_fixture_for_test(1); conn.install_browser_context_fixture_for_test(browser_context); let page_owner = conn @@ -864,8 +864,8 @@ mod tests { #[test] fn prepared_subresource_fetch_pause_can_emit_for_background_owner() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-active"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-1", "TID-active"); bc.register_page_target_url_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), diff --git a/moli-protocol/src/domains/fetch/tests/basics.rs b/moli-protocol/src/domains/fetch/tests/basics.rs index 395a517a27..a9e9ca6802 100644 --- a/moli-protocol/src/domains/fetch/tests/basics.rs +++ b/moli-protocol/src/domains/fetch/tests/basics.rs @@ -129,7 +129,7 @@ async fn enable_without_browser_context_errors() { #[tokio::test] async fn enable_sets_fetch_flags_for_supported_patterns() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 2, @@ -150,7 +150,9 @@ async fn enable_sets_fetch_flags_for_supported_patterns() { #[tokio::test] async fn enable_and_disable_are_session_local_for_same_target() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-session-fetch".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-session-fetch"); bc.set_active_target_id("TID-session-fetch".to_owned()); bc.attach_active_session("SID-primary".to_owned()); assert!(bc.assign_attached_session_to_target("TID-session-fetch", "SID-attached".to_owned())); @@ -267,7 +269,9 @@ async fn enable_and_disable_are_session_local_for_same_target() { async fn global_intercept_removal_updates_background_document_policy() { for loaded in [false, true] { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new("BID-interception".into()); + let mut context = ctx + .conn + .new_browser_context_fixture_for_test("BID-interception"); context.set_active_target_id("TID-active"); context.attach_active_session("SID-active"); context.register_page_target_url_fixture( @@ -302,12 +306,9 @@ async fn global_intercept_removal_updates_background_document_policy() { .unwrap(); assert_eq!(pending.is_some(), loaded); if let Some(pending) = pending { - super::super::finish_fetch_interception_update( - &mut ctx.conn, - &background, - pending.wait().await.unwrap(), - ) - .unwrap(); + ctx.conn + .finish_document_fetch_command(pending.wait().await) + .unwrap(); } assert!( ctx.conn @@ -327,7 +328,7 @@ async fn global_intercept_removal_updates_background_document_policy() { ); if let Some(pending) = pending { ctx.conn - .finish_removed_network_interception(pending.wait().await.unwrap()) + .finish_document_fetch_command(pending.wait().await) .unwrap(); } let policy = ctx @@ -346,7 +347,9 @@ async fn global_intercept_removal_updates_background_document_policy() { #[tokio::test] async fn disable_drains_only_current_session_pending_subresource_fetches() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-session-fetch-pending".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-session-fetch-pending"); bc.set_active_target_id("TID-session-fetch".to_owned()); bc.attach_active_session("SID-primary".to_owned()); assert!(bc.assign_attached_session_to_target("TID-session-fetch", "SID-attached".to_owned())); @@ -420,7 +423,9 @@ async fn disable_drains_only_current_session_pending_subresource_fetches() { #[tokio::test] async fn disable_drains_fetch_owned_pending_when_same_session_network_intercept_remains() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-session-mixed-fetch-pending".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-session-mixed-fetch-pending"); bc.set_active_target_id("TID-session-fetch".to_owned()); bc.attach_active_session("SID-primary".to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -503,7 +508,9 @@ async fn disable_drains_fetch_owned_pending_when_same_session_network_intercept_ async fn enable_targets_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-fetch-bg".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-fetch-bg".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -555,7 +562,9 @@ async fn enable_targets_loaded_background_owner_without_activation() { async fn pending_fetch_enable_keeps_background_owner_route_across_completion() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-fetch-owner-route".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-fetch-owner-route".to_owned()); bc.set_active_target_id("TID-fetch-active".to_owned()); bc.register_page_target_url_fixture( "TID-fetch-background".to_owned(), @@ -629,12 +638,16 @@ async fn pending_fetch_enable_keeps_background_owner_route_across_completion() { #[tokio::test(flavor = "multi_thread")] async fn enable_targets_inactive_owner_without_activation() { let mut ctx = TestContext::new(); - let mut active = BrowserContext::new("BID-active".to_owned()); + let mut active = ctx + .conn + .new_browser_context_fixture_for_test("BID-active".to_owned()); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active".to_owned()); ctx.conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); ctx.conn @@ -694,7 +707,9 @@ async fn enable_targets_inactive_owner_without_activation() { async fn disable_targets_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-fetch-disable-bg".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-fetch-disable-bg".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -746,7 +761,7 @@ async fn disable_targets_loaded_background_owner_without_activation() { #[tokio::test] async fn enable_without_params_enables_default_fetch_interception() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 12, @@ -763,7 +778,7 @@ async fn enable_without_params_enables_default_fetch_interception() { #[tokio::test] async fn enable_with_invalid_request_stage_errors_and_keeps_fetch_disabled() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 3, @@ -785,7 +800,7 @@ async fn enable_with_invalid_request_stage_errors_and_keeps_fetch_disabled() { #[tokio::test] async fn enable_with_specific_url_pattern_sets_pattern() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 18, @@ -826,7 +841,7 @@ fn url_pattern_matches_supports_wildcards_and_escapes() { #[tokio::test] async fn enable_with_document_resource_type_filter_sets_document_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 14, @@ -852,7 +867,7 @@ async fn enable_with_document_resource_type_filter_sets_document_filter() { #[tokio::test] async fn enable_with_script_resource_type_filter_sets_script_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 15, @@ -878,7 +893,7 @@ async fn enable_with_script_resource_type_filter_sets_script_filter() { #[tokio::test] async fn enable_with_fetch_resource_type_filter_sets_fetch_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 15, @@ -904,7 +919,7 @@ async fn enable_with_fetch_resource_type_filter_sets_fetch_filter() { #[tokio::test] async fn enable_with_xhr_resource_type_filter_sets_xhr_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 16, @@ -930,7 +945,7 @@ async fn enable_with_xhr_resource_type_filter_sets_xhr_filter() { #[tokio::test] async fn enable_with_ping_resource_type_filter_sets_ping_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 18, @@ -956,7 +971,7 @@ async fn enable_with_ping_resource_type_filter_sets_ping_filter() { #[tokio::test] async fn enable_with_csp_violation_report_resource_type_filter_sets_csp_report_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 19, @@ -986,7 +1001,7 @@ async fn enable_with_csp_violation_report_resource_type_filter_sets_csp_report_f #[tokio::test] async fn enable_with_websocket_resource_type_filter_sets_websocket_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 20, @@ -1012,7 +1027,7 @@ async fn enable_with_websocket_resource_type_filter_sets_websocket_filter() { #[tokio::test] async fn enable_with_other_resource_type_filter_sets_other_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 21, @@ -1039,10 +1054,10 @@ async fn enable_with_other_resource_type_filter_sets_other_filter() { async fn enable_rejects_unimplemented_parser_discovered_resource_type_filters() { for resource_type in ["Stylesheet", "Media", "TextTrack"] { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test( - format!("BID-{resource_type}"), - "TID-1", - )); + ctx.conn.browser_context = Some( + ctx.conn + .new_page_target_fixture_for_test(format!("BID-{resource_type}"), "TID-1"), + ); ctx.process_async(json!({ "id": 20, @@ -1062,7 +1077,10 @@ async fn enable_rejects_unimplemented_parser_discovered_resource_type_filters() #[tokio::test] async fn enable_accepts_image_resource_type_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-Image", "TID-1")); + ctx.conn.browser_context = Some( + ctx.conn + .new_page_target_fixture_for_test("BID-Image", "TID-1"), + ); ctx.process_async(json!({ "id": 22, @@ -1088,7 +1106,7 @@ async fn enable_accepts_image_resource_type_filter() { #[tokio::test] async fn enable_with_response_stage_pattern_sets_response_stage() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 17, @@ -1110,7 +1128,7 @@ async fn enable_with_response_stage_pattern_sets_response_stage() { #[tokio::test] async fn enable_with_multiple_supported_patterns_enables_fetch_interception() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 13, @@ -1152,7 +1170,7 @@ async fn disable_without_browser_context_errors() { #[tokio::test] async fn disable_clears_fetch_state() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .fetch_owner .configure(None, true, Vec::new()); @@ -1169,6 +1187,8 @@ async fn disable_clears_fetch_state() { owner: crate::conn::CommandOwnerScope::for_route( crate::conn::CdpSessionRoute::Browser, ), + web_contents: crate::conn::NavigationDispatchState::detached_web_contents_for_test( + ), result_projection: crate::conn::NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": "LID-0000000001"}), ), @@ -1210,6 +1230,8 @@ async fn disable_clears_fetch_state() { owner: crate::conn::CommandOwnerScope::for_route( crate::conn::CdpSessionRoute::Browser, ), + web_contents: + crate::conn::NavigationDispatchState::detached_web_contents_for_test(), result_projection: crate::conn::NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": "LID-0000000001"}), ), @@ -1261,7 +1283,7 @@ async fn disable_clears_fetch_state() { #[tokio::test] async fn disable_after_enable_resets_pending_requests() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .fetch_owner .configure(None, false, Vec::new()); @@ -1289,7 +1311,7 @@ async fn disable_after_enable_resets_pending_requests() { #[tokio::test] async fn continue_request_validates_request_id_and_pending_state() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 6, @@ -1311,7 +1333,7 @@ async fn continue_request_validates_request_id_and_pending_state() { #[tokio::test] async fn continue_request_with_intercept_response_still_validates_request_id() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 60, @@ -1328,7 +1350,7 @@ async fn continue_request_with_intercept_response_still_validates_request_id() { #[tokio::test] async fn continue_response_and_take_response_body_as_stream_validate_like_fetch_actions() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .fetch_owner .register_pending_fetch_request_id_for_test("INT-42".to_owned()); @@ -1365,7 +1387,7 @@ async fn continue_response_and_take_response_body_as_stream_validate_like_fetch_ #[tokio::test] async fn pending_fetch_request_can_be_consumed_once() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .fetch_owner .register_pending_fetch_request_id_for_test("INT-7".to_owned()); @@ -1391,7 +1413,7 @@ async fn pending_fetch_request_can_be_consumed_once() { #[tokio::test] async fn continue_with_auth_and_get_response_body_share_request_validation() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 10, @@ -1416,7 +1438,7 @@ async fn continue_with_auth_and_get_response_body_share_request_validation() { #[tokio::test] async fn continue_with_auth_rejects_invalid_response_without_consuming_pending_auth_navigation() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .fetch_owner .register_pending_fetch_auth_navigation( @@ -1433,6 +1455,8 @@ async fn continue_with_auth_rejects_invalid_response_without_consuming_pending_a redirect_headers: None, navigate_id: Some(1), owner: crate::conn::CommandOwnerScope::for_session("SID-1"), + web_contents: + crate::conn::NavigationDispatchState::detached_web_contents_for_test(), result_projection: crate::conn::NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": "LID-0000000001"}), ), @@ -1490,7 +1514,7 @@ async fn continue_with_auth_rejects_invalid_response_without_consuming_pending_a #[tokio::test] async fn continue_with_auth_unsupported_challenge_preserves_pending_auth_navigation() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .fetch_owner .register_pending_fetch_auth_navigation( @@ -1507,6 +1531,8 @@ async fn continue_with_auth_unsupported_challenge_preserves_pending_auth_navigat redirect_headers: None, navigate_id: Some(1), owner: crate::conn::CommandOwnerScope::for_session("SID-1"), + web_contents: + crate::conn::NavigationDispatchState::detached_web_contents_for_test(), result_projection: crate::conn::NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": "LID-0000000001"}), ), @@ -1679,6 +1705,7 @@ fn emit_auth_required_preserves_request_headers_and_post_data_shape() { redirect_headers: None, navigate_id: Some(1), owner: crate::conn::CommandOwnerScope::for_session("SID-1"), + web_contents: crate::conn::NavigationDispatchState::detached_web_contents_for_test(), result_projection: crate::conn::NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": "LID-0000000001"}), ), diff --git a/moli-protocol/src/domains/fetch/tests/command_correlation.rs b/moli-protocol/src/domains/fetch/tests/command_correlation.rs index 68ae30196e..217ea2f7ac 100644 --- a/moli-protocol/src/domains/fetch/tests/command_correlation.rs +++ b/moli-protocol/src/domains/fetch/tests/command_correlation.rs @@ -1,7 +1,7 @@ use super::*; use crate::conn::{ - CapturedBody, CdpCommandTaskStep, ClaimedSubresourceContinueRequest, FetchAuthChallenge, - PendingSubresourceFetchAuthRequest, PendingSubresourceFetchOwnerKind, + CapturedBody, CdpCommandTaskStep, ClaimedSubresourceContinueRequest, DocumentFetchCommand, + FetchAuthChallenge, PendingSubresourceFetchAuthRequest, PendingSubresourceFetchOwnerKind, PendingSubresourceFetchRequest, PendingSubresourceFetchResponseRequest, }; use crate::domains::fetch::{ @@ -11,15 +11,16 @@ use moli_core::page::SubresourceResourceType; async fn context_with_loaded_fetch_page() -> TestContext { let mut ctx = TestContext::new(); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,fetch correlation") - .await - .expect("fetch correlation page should load"); - let mut browser_context = attached_browser_context(); - browser_context.replace_active_page_for_test(Some(page)); + let mut browser_context = ctx.conn.new_browser_context_fixture_for_test("BID-1"); + browser_context.set_active_target_id("TID-1".to_owned()); + browser_context.attach_active_session("SID-1".to_owned()); ctx.conn .install_browser_context_fixture_for_test(browser_context); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,fetch correlation", + Some("SID-1"), + ) + .await; ctx } @@ -51,30 +52,25 @@ async fn fetch_interception_update_rejects_a_replaced_page() { .conn .loaded_document_owner_identity_for_owner(&owner) .unwrap(); + let document = ctx.conn.resolve_browser_document_for_owner(&owner).unwrap(); let completion = ctx .conn .browser_context_by_id_mut(&context_id) .unwrap() - .start_target_fetch_interception_update(&target_id, false, None) + .start_web_contents_fetch_interception_update(document.web_contents(), false, None, false) .unwrap() .unwrap() .wait() - .await - .unwrap(); - let replacement = ctx - .conn - .load_page_via_runtime_async("data:text/html,replacement") - .await - .unwrap(); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(replacement)); - assert_eq!( - super::super::finish_fetch_interception_update(&mut ctx.conn, &owner, completion), - Err("Renderer Page changed".to_owned()) - ); + .await; + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,replacement", + None, + ) + .await; + assert!(matches!( + ctx.conn.finish_document_fetch_command(completion), + Err(error) if error == "Renderer Page changed" + )); assert_eq!( ctx.conn .browser_context_by_id(&context_id) @@ -88,13 +84,120 @@ async fn fetch_interception_update_rejects_a_replaced_page() { .conn .browser_context_by_id_mut(&context_id) .unwrap() - .start_target_fetch_interception_update(&target_id, false, None) + .start_web_contents_fetch_interception_update(document.web_contents(), false, None, false) .unwrap() .unwrap() .wait() - .await + .await; + ctx.conn.finish_document_fetch_command(completion).unwrap(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn subresource_continue_completion_rejects_a_replaced_document() { + let mut ctx = TestContext::new(); + with_loaded_http_document( + &mut ctx, + "data:text/html,fetch correlation", + "SID-1", + "TID-1", + ) + .await; + ctx.conn + .browser_context + .as_mut() + .unwrap() + .active_page_target_mut() + .devtools_sessions[moli_page_types::DevToolsSessionKey::Primary] + .runtime_session_state + .inspector_enabled = true; + + ctx.process_async(json!({ + "id": 80, + "method": "Fetch.enable", + "sessionId": "SID-1" + })) + .await; + ctx.expect_result(80, json!({}), Some("SID-1")); + enable_runtime_async(&mut ctx, "SID-1", 81).await; + ctx.sent.clear(); + + let request_url = "http://example.test/exact-document"; + ctx.process_async(json!({ + "id": 82, + "method": "Runtime.evaluate", + "sessionId": "SID-1", + "params": { + "expression": format!( + "fetch('{request_url}').catch(() => undefined); 'scheduled'" + ) + } + })) + .await; + let _ = take_response_by_id(&mut ctx, 82); + wait_until_messages( + &mut ctx, + Some("SID-1"), + "exact-document subresource request pause", + |messages| { + messages.iter().any(|message| { + message["method"] == json!("Fetch.requestPaused") + && message["params"]["request"]["url"] == json!(request_url) + }) + }, + ) + .await; + let paused = ctx.take_first_matching("Fetch.requestPaused event", |message| { + message["method"] == json!("Fetch.requestPaused") + && message["params"]["request"]["url"] == json!(request_url) + }); + let request_id = paused["params"]["requestId"] + .as_str() + .expect("paused request id") + .to_owned(); + ctx.sent.clear(); + + let owner = crate::conn::CommandOwnerScope::capture(&ctx.conn, Some("SID-1")); + let request = ctx + .conn + .take_pending_subresource_fetch_request_for_owner(&owner, Some("SID-1"), &request_id) + .expect("paused request must remain owned by its document"); + let document = ctx.conn.resolve_browser_document_for_owner(&owner).unwrap(); + let pending = ctx + .conn + .start_document_fetch_command( + document, + DocumentFetchCommand::ContinueRequest { + internal_id: request.internal_id, + url: None, + method: None, + body: None, + headers: None, + intercept_response: false, + handle_auth_requests: false, + }, + ) .unwrap(); - super::super::finish_fetch_interception_update(&mut ctx.conn, &owner, completion).unwrap(); + let completed = pending.wait().await; + + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,replacement", + None, + ) + .await; + + assert!(matches!( + ctx.conn.finish_document_fetch_command(completed), + Err(error) if error == "Document changed" + )); + assert_eq!( + ctx.conn + .browser_context + .as_ref() + .unwrap() + .target_document_title("TID-1") + .as_deref(), + Some("replacement") + ); } fn pending_auth( @@ -211,7 +314,9 @@ async fn assert_correlation_lifetime( #[tokio::test(flavor = "multi_thread")] async fn deferred_fetch_command_keeps_its_exact_page_for_implicit_work() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -242,7 +347,9 @@ async fn deferred_fetch_command_keeps_its_exact_page_for_implicit_work() { #[tokio::test(flavor = "multi_thread")] async fn deferred_sessionless_fetch_command_freezes_the_active_page_at_admission() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-sessionless".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-sessionless".to_owned()); browser_context.set_active_target_id("TID-original".to_owned()); browser_context.register_page_target_url_fixture( "TID-next".to_owned(), diff --git a/moli-protocol/src/domains/fetch/tests/header_bytes.rs b/moli-protocol/src/domains/fetch/tests/header_bytes.rs index 181d305ef4..5cf2229245 100644 --- a/moli-protocol/src/domains/fetch/tests/header_bytes.rs +++ b/moli-protocol/src/domains/fetch/tests/header_bytes.rs @@ -346,7 +346,7 @@ async fn navigation_header_override_expires_on_redirect_before_auth_retry() { }); let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); ctx.enable_page_events_for_test(Some("SID-1")); ctx.conn .set_global_extra_headers(moli_fetch::RequestHeaders::from_bytes(vec![( diff --git a/moli-protocol/src/domains/fetch/tests/mod.rs b/moli-protocol/src/domains/fetch/tests/mod.rs index 37b8808359..1727109dd7 100644 --- a/moli-protocol/src/domains/fetch/tests/mod.rs +++ b/moli-protocol/src/domains/fetch/tests/mod.rs @@ -29,9 +29,8 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; use url::Url; -fn attached_browser_context() -> BrowserContext { - let mut bc = BrowserContext::new("BID-1".into()); - bc.set_active_target_id("TID-1"); +fn attached_browser_context(conn: &crate::conn::CdpConnection) -> BrowserContext { + let mut bc = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.attach_active_session("SID-1"); bc } @@ -42,7 +41,7 @@ async fn with_loaded_http_document( session_id: &str, target_id: &str, ) { - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id(target_id.to_owned()); bc.attach_active_session(session_id.to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -62,7 +61,7 @@ async fn with_loaded_http_background_document( background_session_id: &str, background_target_id: &str, ) { - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id(active_target_id.to_owned()); bc.attach_active_session(active_session_id.to_owned()); bc.register_page_target_url_fixture( diff --git a/moli-protocol/src/domains/fetch/tests/navigation_auth.rs b/moli-protocol/src/domains/fetch/tests/navigation_auth.rs index f46c70adcf..0383460277 100644 --- a/moli-protocol/src/domains/fetch/tests/navigation_auth.rs +++ b/moli-protocol/src/domains/fetch/tests/navigation_auth.rs @@ -134,9 +134,8 @@ async fn supersession_retires_browser_auth_work_before_late_protocol_decision() .browser_context .as_ref() .unwrap() - .loaded_page() + .loaded_document_url_for_test() .unwrap() - .final_url() .as_str(), "https://navigation.example/winner" ); @@ -179,7 +178,7 @@ async fn continue_with_auth_retries_navigation_with_basic_credentials() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -327,7 +326,7 @@ async fn disable_uses_default_decision_for_paused_navigation_auth() { }); let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/auth"); ctx.process_async(json!({ "id": 680, "method": "Fetch.enable", "sessionId": "SID-1", @@ -410,7 +409,7 @@ async fn devtools_continue_response_credentials_retries_auth_navigation() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -556,7 +555,7 @@ async fn continue_with_auth_and_intercept_response_pauses_before_authorized_body }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -631,8 +630,7 @@ async fn continue_with_auth_and_intercept_response_pauses_before_authorized_body ctx.conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) - .is_none(), + .is_none_or(|bc| !bc.has_loaded_page()), "authorized document should not commit before Fetch.continueResponse" ); @@ -690,7 +688,7 @@ async fn continue_with_non_basic_auth_and_intercept_response_fails_explicitly_wi }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -805,13 +803,14 @@ async fn navigation_auth_required_includes_synthesized_cookie_header() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); let url = format!("http://{addr}/auth"); { - let mut jar = bc.cookie_store_for_test().lock(); + let jar_handle = bc.cookie_store_for_test(); + let mut jar = jar_handle.lock(); jar.store_response_headers( &Url::parse(&url).unwrap(), &[( @@ -902,7 +901,7 @@ async fn continue_with_auth_prefers_supported_navigation_challenge_over_unsuppor }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1051,7 +1050,7 @@ async fn run_navigation_cdp_fetch_then_bidi_network_auth_required_terminal( }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); assert!(bc.assign_attached_session_to_target("TID-1", "SID-fetch".to_owned())); bc.active_page_target_mut() .runtime_slot diff --git a/moli-protocol/src/domains/fetch/tests/navigation_control.rs b/moli-protocol/src/domains/fetch/tests/navigation_control.rs index d5ac0e77fb..65b8c2149d 100644 --- a/moli-protocol/src/domains/fetch/tests/navigation_control.rs +++ b/moli-protocol/src/domains/fetch/tests/navigation_control.rs @@ -15,7 +15,7 @@ async fn assert_stale_fetch_completion_preserves_winning_navigation(method: &str .await; } else { ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); } ctx.enable_page_events_for_test(Some("SID-1")); ctx.process_async(json!({ @@ -100,7 +100,7 @@ async fn assert_stale_fetch_completion_preserves_winning_navigation(method: &str let context = ctx.conn.browser_context.as_ref().unwrap(); assert_ne!(context.target_document_id("TID-1"), committed); assert_eq!( - context.loaded_page().unwrap().final_url().as_str(), + context.loaded_document_url_for_test().unwrap().as_str(), "https://navigation.example/winner" ); assert!(!context.has_pending_document_navigation_for_target("TID-1")); @@ -153,7 +153,7 @@ fn parse_binary_response_headers_rejects_invalid_header_value() { async fn continue_request_rejects_invalid_url_without_consuming_pending_navigation() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); ctx.process_async(json!({ "id": 62, @@ -206,7 +206,7 @@ async fn continue_request_rejects_invalid_url_without_consuming_pending_navigati async fn continue_request_rejects_invalid_post_data_without_consuming_pending_navigation() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); ctx.process_async(json!({ "id": 65, @@ -274,7 +274,7 @@ async fn request_paused_then_continue_request_resumes_main_document_navigation() let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); ctx.enable_page_events_for_test(Some("SID-1")); ctx.enable_dom_events_for_test(Some("SID-1")); let url = format!("http://{addr}/page"); @@ -342,7 +342,7 @@ async fn request_paused_then_continue_request_resumes_main_document_navigation() async fn main_document_request_uses_loader_id_as_observed_network_request_id() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); ctx.process_async(json!({ "id": 301, @@ -376,7 +376,7 @@ async fn main_document_request_uses_loader_id_as_observed_network_request_id() { #[tokio::test] async fn fail_request_blocked_by_client_maps_main_document_navigation_to_net_error_text() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -425,7 +425,7 @@ async fn fail_request_blocked_by_client_maps_main_document_navigation_to_net_err async fn request_paused_then_continue_request_preserves_blocked_url_error_text() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); ctx.process_async(json!({ "id": 16640, "method": "Network.enable", @@ -504,7 +504,7 @@ async fn request_paused_then_continue_request_with_auth_handling_commits_offline async fn assert_continued_offline_navigation_commits_error_document(handle_auth_requests: bool) { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); ctx.enable_page_events_for_test(Some("SID-1")); ctx.process_async(json!({ @@ -661,13 +661,14 @@ async fn response_stage_document_pattern_pauses_main_document_after_response() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); let url = format!("http://{addr}/page"); { - let mut jar = bc.cookie_store_for_test().lock(); + let jar_handle = bc.cookie_store_for_test(); + let mut jar = jar_handle.lock(); jar.store_response_headers( &Url::parse(&url).unwrap(), &[("set-cookie".to_owned(), b"sid=1; Path=/".to_vec())], @@ -781,7 +782,7 @@ async fn disable_neutrally_resumes_paused_response_stage_navigation() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ "id": 350, @@ -829,6 +830,7 @@ async fn disable_neutrally_resumes_paused_response_stage_navigation() { .as_ref() .unwrap() .target_document_url("TID-1") + .as_ref() .map(Url::as_str), Some(url.as_str()) ); @@ -868,7 +870,7 @@ async fn document_url_pattern_only_pauses_matching_main_document() { let plain_url = format!("http://{addr}/plain"); let match_url = format!("http://{addr}/match"); let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); { let bc = ctx.conn.browser_context.as_mut().unwrap(); bc.set_active_target_id("TID-1"); @@ -936,7 +938,7 @@ async fn continue_request_with_post_data_marks_network_request_as_having_post_da }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1050,7 +1052,7 @@ async fn intercepted_form_post_navigation_body_is_available_by_network_request_i let post_url = format!("http://{addr}/post"); let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); ctx.install_navigation_fixture_for_session_owner(&page_url, Some("SID-1")) .await; { @@ -1172,7 +1174,7 @@ async fn continue_request_with_intercept_response_pauses_after_response_until_co }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1301,7 +1303,7 @@ async fn continue_request_with_intercept_response_pauses_after_response_until_co #[tokio::test(flavor = "multi_thread")] async fn intercepted_navigation_start_events_stay_before_network_pause_with_background_sender() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1381,7 +1383,12 @@ async fn disable_failure_still_neutrally_settles_the_drained_navigation() { { let context = ctx.conn.browser_context.as_mut().unwrap(); let target_id = context.active_target_id_owned().unwrap(); - context.crash_target_renderer_from_io(&target_id); + let web_contents = context + .web_contents_handle_for_target(&target_id) + .expect("active target should own WebContents"); + context + .crash_web_contents_renderer_from_io(web_contents) + .expect("active WebContents should remain live"); } ctx.process_async(json!({"id": 3, "sessionId": "SID-1", "method": "Fetch.disable"})) @@ -1445,7 +1452,7 @@ async fn disable_neutrally_resumes_paused_main_document_navigation() { let url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1494,6 +1501,7 @@ async fn disable_neutrally_resumes_paused_main_document_navigation() { .as_ref() .unwrap() .target_document_url("TID-1") + .as_ref() .map(Url::as_str), Some(url.as_str()) ); diff --git a/moli-protocol/src/domains/fetch/tests/navigation_response_stage.rs b/moli-protocol/src/domains/fetch/tests/navigation_response_stage.rs index 7e9b579fce..e61b01c347 100644 --- a/moli-protocol/src/domains/fetch/tests/navigation_response_stage.rs +++ b/moli-protocol/src/domains/fetch/tests/navigation_response_stage.rs @@ -27,7 +27,7 @@ async fn response_stage_pause_happens_before_navigation_body_eof() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -84,8 +84,7 @@ async fn response_stage_pause_happens_before_navigation_body_eof() { ctx.conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) - .is_none(), + .is_none_or(|bc| !bc.has_loaded_page()), "page should not be committed before Fetch.continueResponse" ); @@ -105,15 +104,14 @@ async fn response_stage_pause_happens_before_navigation_body_eof() { ); { - let page = ctx + let committed_agent = ctx .conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) + .and_then(|bc| bc.loaded_document_renderer_agent_for_test()) .expect("loaded page"); assert_eq!( - page.renderer_devtools_agent_token(), - prepared_agent, + committed_agent, prepared_agent, "continueResponse must commit the exact agent reserved at the response head" ); } @@ -220,13 +218,13 @@ async fn assert_empty_http_error_response_stage(ctx: &mut TestContext, url: &str .unwrap_or_else(|| panic!("missing HTTP response-code failure: {:?}", ctx.sent)); assert!(response_index < failure_index); - let page = ctx + let page_url = ctx .conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) + .and_then(|bc| bc.loaded_document_url_for_test()) .expect("browser-owned HTTP error Document should commit"); - assert_eq!(page.final_url().as_str(), NETWORK_ERROR_PAGE_URL); + assert_eq!(page_url.as_str(), NETWORK_ERROR_PAGE_URL); assert_eq!( ctx.conn .browser_context @@ -267,7 +265,7 @@ async fn empty_http_error_response_stage_commits_browser_error_document_after_co }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -308,22 +306,16 @@ lateBinding("author-script"); }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); ctx.conn.install_browser_context_fixture_for_test(bc); - let initial_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,initial") - .await - .expect("initial document should load with its target owner installed"); - let _ = ctx - .conn - .browser_context - .as_mut() - .expect("browser context should remain installed") - .replace_active_page_for_test(Some(initial_page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,initial", + None, + ) + .await; ctx.process_async(json!({ "id": 36_500, @@ -493,23 +485,23 @@ async fn response_stage_xml_commit_uses_live_configuration_before_first_author_s }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); ctx.conn.install_browser_context_fixture_for_test(bc); - let initial_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,initial") - .await - .expect("initial document should load with its target owner installed"); - let initial_agent = initial_page.renderer_devtools_agent_token(); - let _ = ctx + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,initial", + Some("SID-1"), + ) + .await; + let initial_agent = ctx .conn .browser_context - .as_mut() + .as_ref() .expect("browser context should remain installed") - .replace_active_page_for_test(Some(initial_page)); + .loaded_document_renderer_agent_for_test() + .expect("loaded document renderer agent"); ctx.process_async(json!({ "id": 36_550, @@ -562,8 +554,7 @@ async fn response_stage_xml_commit_uses_live_configuration_before_first_author_s ctx.conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) - .map(|page| page.renderer_devtools_agent_token()), + .and_then(|bc| bc.loaded_document_renderer_agent_for_test()), Some(initial_agent), "the old document must remain installed while the XML response is paused" ); @@ -617,9 +608,8 @@ async fn response_stage_xml_commit_uses_live_configuration_before_first_author_s .conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) - .expect("XML document should commit") - .renderer_devtools_agent_token(); + .and_then(|bc| bc.loaded_document_renderer_agent_for_test()) + .expect("XML document should commit"); assert_ne!( committed_agent, initial_agent, "cross-document XML commit must install its prepared renderer agent" @@ -687,18 +677,12 @@ async fn response_stage_xml_commit_uses_live_configuration_before_first_author_s async fn fulfill_request_commit_uses_configuration_added_while_paused_before_author_script() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); - let initial_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,initial") - .await - .expect("initial document should load with its target owner installed"); - let _ = ctx - .conn - .browser_context - .as_mut() - .expect("browser context should remain installed") - .replace_active_page_for_test(Some(initial_page)); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,initial", + None, + ) + .await; ctx.process_async(json!({ "id": 36_600, @@ -876,7 +860,7 @@ async fn interleaved_response_heads_only_commit_the_current_prepared_document() }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -983,18 +967,15 @@ async fn interleaved_response_heads_only_commit_the_current_prepared_document() second_pause["params"]["networkId"] ); - let page = ctx + let committed_agent = ctx .conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) + .and_then(|bc| bc.loaded_document_renderer_agent_for_test()) .expect("current navigation should commit a page"); - assert_eq!(page.renderer_devtools_agent_token(), second_agent); - assert_ne!(page.renderer_devtools_agent_token(), first_agent); - let html = page - .serialize_html_async() - .await - .expect("current page should serialize HTML"); + assert_eq!(committed_agent, second_agent); + assert_ne!(committed_agent, first_agent); + let html = loaded_page_html_for_test(&mut ctx).await; assert!(html.contains("second")); assert_eq!( ctx.sent @@ -1011,7 +992,7 @@ async fn interleaved_response_heads_only_commit_the_current_prepared_document() #[tokio::test(flavor = "multi_thread")] async fn response_stage_continue_request_rejects_data_url_override_without_consuming_pause() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1077,7 +1058,7 @@ async fn response_stage_continue_request_rejects_data_url_override_without_consu #[tokio::test(flavor = "multi_thread")] async fn response_stage_continue_request_rejects_file_url_override_without_consuming_pause() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1156,7 +1137,7 @@ async fn response_stage_continue_response_streams_network_events_through_backgro }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1276,7 +1257,7 @@ async fn continue_response_can_override_status_and_headers() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1360,15 +1341,15 @@ async fn continue_response_can_override_status_and_headers() { "yes" ); - let page = ctx - .conn - .browser_context - .as_ref() - .and_then(|bc| bc.loaded_page()) - .expect("loaded page"); - assert_eq!(page.status(), 201); + let browser_context = ctx.conn.browser_context.as_ref().expect("loaded page"); + assert_eq!( + browser_context.loaded_document_response_status_for_test(), + Some(201) + ); assert!( - page.headers() + browser_context + .loaded_document_response_headers_for_test() + .unwrap() .iter() .any(|(name, value)| name == "x-override" && value == b"yes") ); @@ -1456,7 +1437,7 @@ async fn continue_response_header_override_keeps_streaming_parser_body() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -1515,14 +1496,14 @@ async fn continue_response_header_override_keeps_streaming_parser_body() { Some("SID-1"), ); - let page = ctx + let headers = ctx .conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) + .and_then(|bc| bc.loaded_document_response_headers_for_test()) .expect("loaded page"); assert!( - page.headers() + headers .iter() .any(|(name, value)| name == "x-override" && value == b"streaming") ); @@ -1557,10 +1538,11 @@ async fn response_stage_navigation_request_paused_includes_synthesized_cookie_he }); let mut ctx = TestContext::new(); - let bc = attached_browser_context(); + let bc = attached_browser_context(&ctx.conn); let url = format!("http://{addr}/page"); { - let mut jar = bc.cookie_store_for_test().lock(); + let jar_handle = bc.cookie_store_for_test(); + let mut jar = jar_handle.lock(); jar.store_response_headers( &Url::parse(&url).unwrap(), &[("set-cookie".to_owned(), b"sid=nav; Path=/page".to_vec())], @@ -1653,7 +1635,7 @@ async fn continue_response_with_binary_response_headers_overrides_headers() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -1703,20 +1685,21 @@ async fn continue_response_with_binary_response_headers_overrides_headers() { Some("SID-1"), ); - let page = ctx - .conn - .browser_context - .as_ref() - .and_then(|bc| bc.loaded_page()) - .expect("loaded page"); - assert_eq!(page.status(), 201); + let browser_context = ctx.conn.browser_context.as_ref().expect("loaded page"); + assert_eq!( + browser_context.loaded_document_response_status_for_test(), + Some(201) + ); + let headers = browser_context + .loaded_document_response_headers_for_test() + .unwrap(); assert!( - page.headers() + headers .iter() .any(|(name, value)| name == "x-bin" && value == b"yes") ); assert!( - page.headers() + headers .iter() .any(|(name, value)| name == "x-two" && value == b"2") ); @@ -1742,7 +1725,7 @@ async fn fail_request_at_response_stage_aborts_navigation() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1811,7 +1794,7 @@ async fn fail_request_at_response_stage_aborts_navigation() { assert_eq!(failed["params"]["requestId"], LOADER_ID); ctx.expect_error(326, -32000, "Aborted"); let browser_context = ctx.conn.browser_context.as_ref().expect("browser context"); - assert!(browser_context.loaded_page().is_none()); + assert!(!browser_context.has_loaded_page()); assert_eq!( browser_context .active_page_target() @@ -1842,7 +1825,7 @@ async fn fulfill_request_at_response_stage_replaces_the_network_candidate_once() }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1901,21 +1884,17 @@ async fn fulfill_request_at_response_stage_replaces_the_network_candidate_once() Some("SID-1"), ); - let page = ctx + let committed_agent = ctx .conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) + .and_then(|bc| bc.loaded_document_renderer_agent_for_test()) .expect("synthetic response should commit a page"); assert_ne!( - page.renderer_devtools_agent_token(), - network_agent, + committed_agent, network_agent, "fulfillRequest must discard the prepared network response candidate" ); - let html = page - .serialize_html_async() - .await - .expect("synthetic response page should serialize HTML"); + let html = loaded_page_html_for_test(&mut ctx).await; assert!(html.contains("synthetic-body")); assert!(!html.contains("network-body")); assert_eq!( @@ -1957,7 +1936,7 @@ async fn take_response_body_as_stream_at_response_stage_returns_stream_and_keeps let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -2086,7 +2065,7 @@ async fn disable_drains_active_response_body_stream_and_neutrally_resumes_naviga let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ "id": 3290, "method": "Fetch.enable", "sessionId": "SID-1" @@ -2131,16 +2110,16 @@ async fn disable_drains_active_response_body_stream_and_neutrally_resumes_naviga .await; ctx.expect_result(3340, json!({}), Some("SID-1")); assert!(ctx.take_response_by_id(3300)["result"].is_object()); - let page = ctx - .conn - .browser_context - .as_ref() - .and_then(|context| context.loaded_page()) - .expect("neutral release should commit the streamed response"); assert!( - page.serialize_html_async() + ctx.conn + .browser_context + .as_ref() + .is_some_and(|context| context.has_loaded_page()), + "neutral release should commit the streamed response" + ); + assert!( + loaded_page_html_for_test(&mut ctx) .await - .unwrap() .contains("neutral-stream") ); assert!( @@ -2183,7 +2162,7 @@ async fn continue_response_rejects_while_response_body_stream_is_active() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -2307,7 +2286,7 @@ async fn io_close_cancels_active_response_body_stream_and_request_id() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -2407,7 +2386,7 @@ async fn get_response_body_does_not_consume_active_response_body_stream() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -2562,7 +2541,7 @@ async fn fulfill_request_cancels_active_response_body_stream_and_uses_synthetic_ let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -2666,7 +2645,7 @@ async fn fail_request_cancels_active_response_body_stream_and_fails_navigation() let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -2757,7 +2736,7 @@ async fn clear_browser_cache_cancels_active_response_body_stream_without_stale_r let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -2859,7 +2838,7 @@ async fn fail_request_finishes_pending_navigation_with_error() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/page"); ctx.process_async(json!({ @@ -2902,7 +2881,7 @@ async fn fail_request_finishes_pending_navigation_with_error() { #[tokio::test(flavor = "multi_thread")] async fn fulfill_request_completes_navigation_with_synthetic_response() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -2995,7 +2974,7 @@ async fn fulfill_request_completes_navigation_with_synthetic_response() { #[tokio::test(flavor = "multi_thread")] async fn fulfill_request_navigation_get_response_body_preserves_binary_bytes() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3073,7 +3052,7 @@ async fn fulfill_request_navigation_get_response_body_preserves_binary_bytes() { #[tokio::test(flavor = "multi_thread")] async fn fulfill_request_document_body_uses_phase_one_parser_semantics() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3143,7 +3122,7 @@ async fn fulfill_request_document_body_uses_phase_one_parser_semantics() { #[tokio::test(flavor = "multi_thread")] async fn fulfill_request_accepts_binary_response_headers() { let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3234,7 +3213,7 @@ async fn continue_request_applies_url_method_headers_and_post_data() { let start_url = format!("http://{addr}/start"); let continued_url = format!("http://{addr}/continued"); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); diff --git a/moli-protocol/src/domains/fetch/tests/navigation_subresource.rs b/moli-protocol/src/domains/fetch/tests/navigation_subresource.rs index df378364c6..ecbe4aa7b1 100644 --- a/moli-protocol/src/domains/fetch/tests/navigation_subresource.rs +++ b/moli-protocol/src/domains/fetch/tests/navigation_subresource.rs @@ -70,13 +70,14 @@ async fn request_stage_navigation_request_paused_includes_synthesized_cookie_hea }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); let url = format!("http://{addr}/page"); { - let mut jar = bc.cookie_store_for_test().lock(); + let jar_handle = bc.cookie_store_for_test(); + let mut jar = jar_handle.lock(); jar.store_response_headers( &Url::parse(&url).unwrap(), &[("set-cookie".to_owned(), b"sid=req-nav; Path=/page".to_vec())], @@ -147,7 +148,7 @@ fetch('/api', { method: 'POST', body: 'nav-payload' }) }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -369,7 +370,7 @@ document.addEventListener('DOMContentLoaded', () => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -531,7 +532,7 @@ window.addEventListener('load', () => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -692,7 +693,7 @@ window.postMessage('go', '*'); }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -858,7 +859,7 @@ document.body.setAttribute('data-trigger', '1'); }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1023,7 +1024,7 @@ observer.observe(document.getElementById('target')); }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1185,7 +1186,7 @@ setTimeout(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1360,7 +1361,7 @@ const id = setInterval(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1533,7 +1534,7 @@ queueMicrotask(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1694,7 +1695,7 @@ Promise.resolve().then(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -1856,7 +1857,7 @@ setTimeout(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -2036,7 +2037,7 @@ const id = setInterval(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -2210,7 +2211,7 @@ queueMicrotask(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -2372,7 +2373,7 @@ Promise.resolve().then(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -2534,7 +2535,7 @@ requestIdleCallback(deadline => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -2692,7 +2693,7 @@ requestIdleCallback(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -2856,7 +2857,7 @@ requestAnimationFrame(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3012,7 +3013,7 @@ requestAnimationFrame(() => { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3164,7 +3165,7 @@ fetch('/api').catch(() => {}); }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3240,7 +3241,7 @@ async fn continue_with_auth_default_aborts_pending_navigation() { let mut ctx = TestContext::new(); ctx.conn - .install_browser_context_fixture_for_test(attached_browser_context()); + .install_browser_context_fixture_for_test(attached_browser_context(&ctx.conn)); let url = format!("http://{addr}/auth"); ctx.process_async(json!({ @@ -3335,7 +3336,7 @@ async fn continue_with_auth_success_clears_pending_auth_navigation() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3443,7 +3444,7 @@ async fn continue_with_auth_retries_navigation_with_basic_proxy_credentials() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3538,7 +3539,7 @@ async fn continue_with_auth_retries_navigation_with_digest_proxy_credentials() { .await; let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3682,7 +3683,7 @@ async fn cancel_https_proxy_connect_auth_emits_407_without_extra_info_and_fails_ }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3832,7 +3833,7 @@ async fn continue_with_auth_handles_multi_round_basic_navigation_challenge() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -3964,7 +3965,7 @@ async fn continue_with_auth_handles_digest_navigation_challenge() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -4095,7 +4096,7 @@ async fn continue_with_auth_handles_multi_round_digest_navigation_challenge() { }); let mut ctx = TestContext::new(); - let mut bc = attached_browser_context(); + let mut bc = attached_browser_context(&ctx.conn); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); diff --git a/moli-protocol/src/domains/input.rs b/moli-protocol/src/domains/input.rs index c89cd602f3..ff67bd5bf5 100644 --- a/moli-protocol/src/domains/input.rs +++ b/moli-protocol/src/domains/input.rs @@ -1,6 +1,7 @@ use crate::conn::{ BackgroundProtocolEvent, CdpConnection, Cmd, CommandDispatchContext, CommandOwnerScope, - PageInputCommand, TargetPageResidenceIdentity, + CompletedDocumentInputCommand, PageInputCommand, PendingDocumentInputCommand, + PreparedDownloadActivation, TargetPageResidenceIdentity, }; use crate::devtools_runtime::{ DevToolsCommand, DevToolsCommandContext, DevToolsCommandResult, @@ -13,7 +14,7 @@ use crate::devtools_runtime::{ use crate::domains::command_output::CommandOutputPlan; use moli_core::browser::{DocumentLifetimeObserver, DocumentRetirement}; use moli_core::page::{ - CompletedPageCommand, PendingPageCommand, RendererCommandTurnCompletion, RendererDragData, + RendererCommandTurnCompletion, RendererDragData, RendererDragDataItem, RendererDraggedFile, RendererInputDispatchOutcome, RendererPendingDownloadActivation, RendererPendingFileChooserActivation, RendererPointerEventProperties, RendererTouchPoint, decode_element_click_dispatch_completion, @@ -135,13 +136,13 @@ enum PendingInputCommandKind { } enum PendingInputOperation { - Page(PendingPageCommand), + Page(PendingDocumentInputCommand), #[cfg(test)] RendererAckHeldForTest, } enum CompletedInputOperation { - Page(Box>), + Page(Box), PageResidenceSuperseded, PageResidenceUnavailable, } @@ -178,9 +179,9 @@ impl PendingInputCommandDispatch { ) .await { - RendererInputWaitOutcome::Completed(result) => CompletedInputOperation::Page( - Box::new(result.map_err(|error| error.to_string())), - ), + RendererInputWaitOutcome::Completed(result) => { + CompletedInputOperation::Page(Box::new(result)) + } RendererInputWaitOutcome::PageResidence(DocumentRetirement::Superseded) => { CompletedInputOperation::PageResidenceSuperseded } @@ -650,7 +651,7 @@ fn start_pending_input_command( cmd.id, &owner, PendingInputCommandKind::InsertText, - PageInputCommand::InsertText(&text), + PageInputCommand::InsertText(text), ) } InputAction::CancelDragging @@ -1018,10 +1019,10 @@ fn start_devtools_dispatch_key_event_command( PendingInputCommandKind::DispatchKeyEvent, targets_outgoing_document, PageInputCommand::Key { - event_name: key::devtools_key_event_dom_event_name(command.event_type), - key: &command.key, - code: &command.code, - text: &command.text, + event_name: key::devtools_key_event_dom_event_name(command.event_type).to_owned(), + key: command.key, + code: command.code, + text: command.text, modifiers: command.modifiers, auto_repeat: command.auto_repeat, should_insert_text: command.should_insert_text, @@ -1034,7 +1035,7 @@ fn start_page_input_command( command_id: Option, command_owner: &CommandOwnerScope, kind: PendingInputCommandKind, - command: PageInputCommand<'_>, + command: PageInputCommand, ) -> Result, PendingInputCommandStartError> { start_page_input_command_with_access(conn, command_id, command_owner, kind, false, command) } @@ -1050,29 +1051,19 @@ fn start_page_input_command_with_access( let page_owner = conn .target_page_residence_identity_for_owner(command_owner) .ok_or_else(PendingInputCommandStartError::no_document_loaded)?; + let document = conn + .resolve_browser_document_for_owner(command_owner) + .map_err(|_| PendingInputCommandStartError::no_document_loaded())?; let document_lifetime_observer = if kind.uses_renderer_host_ack_cleanup() { Some( - conn.capture_document_lifetime_for_owner(command_owner) - .ok_or_else(PendingInputCommandStartError::no_document_loaded)?, + conn.observe_browser_document_lifetime(document) + .map_err(|_| PendingInputCommandStartError::no_document_loaded())?, ) } else { None }; - if !targets_outgoing_document { - conn.ensure_document_accessible_for_owner(command_owner) - .map_err(|_| PendingInputCommandStartError::no_document_loaded())?; - } - let target_id = page_owner - .target_id() - .ok_or_else(PendingInputCommandStartError::no_document_loaded)?; - let context = conn - .browser_context_by_id(page_owner.browser_context_id()) - .ok_or_else(PendingInputCommandStartError::no_document_loaded)?; - if !context.target_has_loaded_page(target_id) { - return Err(PendingInputCommandStartError::no_document_loaded()); - } - let pending = context - .start_target_input_command(target_id, command) + let pending = conn + .start_document_input_command(document, command) .map_err(PendingInputCommandStartError::renderer_error)?; Ok(Some(PendingInputCommandDispatch { command_id, @@ -1139,7 +1130,7 @@ fn start_devtools_dispatch_mouse_event_command( PageInputCommand::Mouse { x: command.x, y: command.y, - event_name, + event_name: event_name.to_owned(), button: command.button, buttons: command.buttons, click_count: command.click_count, @@ -1174,7 +1165,7 @@ fn start_devtools_dispatch_touch_event_command( PendingInputCommandKind::DispatchTouchEvent, PageInputCommand::Touch { points, - event_name, + event_name: event_name.to_owned(), activate: false, }, ) @@ -1259,7 +1250,7 @@ fn start_devtools_dispatch_drag_event_command( PageInputCommand::Drag { x: command.x, y: command.y, - event_name, + event_name: event_name.to_owned(), data, modifiers: command.modifiers, }, @@ -1283,7 +1274,7 @@ fn start_devtools_synthesize_tap_gesture_command( x: command.x, y: command.y, }], - event_name: "touchend", + event_name: "touchend".to_owned(), activate: true, }, ) @@ -1369,7 +1360,7 @@ async fn complete_pending_input_command( let session_id = completed.session_id.as_deref(); let owner = completed.owner; let completed_operation = match completed.completed { - CompletedInputOperation::Page(completed) => completed, + CompletedInputOperation::Page(completed) => *completed, CompletedInputOperation::PageResidenceSuperseded => { // Match InputInjector::Cleanup(): replacement of the widget/Page // retires an outstanding mouse/key ACK as protocol success even @@ -1441,8 +1432,12 @@ async fn complete_pending_input_command( | PendingInputCommandKind::DispatchTouchEvent | PendingInputCommandKind::DispatchDragEvent | PendingInputCommandKind::SynthesizeTapGesture) => { - let result = match completed_page_command_result(completed_operation) { - Ok(result) => result, + let completion = match settle_completed_input_page_command( + conn, + completed_operation, + command_context, + ) { + Ok(completion) => completion, Err(error) => { let protocol_events = side_effects.into_events(); return CompletedInputCommandResult { @@ -1451,8 +1446,6 @@ async fn complete_pending_input_command( }; } }; - let completion = - settle_completed_input_page_command(conn, &owner, result, command_context); let operation = match kind { PendingInputCommandKind::DispatchMouseEvent => "mouse event page command", PendingInputCommandKind::DispatchTouchEvent @@ -1493,8 +1486,12 @@ async fn complete_pending_input_command( } } PendingInputCommandKind::DispatchKeyEvent => { - let result = match completed_page_command_result(completed_operation) { - Ok(result) => result, + let completion = match settle_completed_input_page_command( + conn, + completed_operation, + command_context, + ) { + Ok(completion) => completion, Err(error) => { let protocol_events = side_effects.into_events(); return CompletedInputCommandResult { @@ -1503,8 +1500,6 @@ async fn complete_pending_input_command( }; } }; - let completion = - settle_completed_input_page_command(conn, &owner, result, command_context); let outcome = decode_input_dispatch_outcome_completion(completion, "key event page command"); match outcome { @@ -1534,8 +1529,12 @@ async fn complete_pending_input_command( } } PendingInputCommandKind::InsertText => { - let result = match completed_page_command_result(completed_operation) { - Ok(result) => result, + let completion = match settle_completed_input_page_command( + conn, + completed_operation, + command_context, + ) { + Ok(completion) => completion, Err(error) => { let protocol_events = side_effects.into_events(); return CompletedInputCommandResult { @@ -1544,8 +1543,6 @@ async fn complete_pending_input_command( }; } }; - let completion = - settle_completed_input_page_command(conn, &owner, result, command_context); let result = decode_insert_text_completion(completion); match result { Ok(_) => Ok(DevToolsCommandResult::Empty), @@ -1566,12 +1563,13 @@ async fn complete_pending_input_command( fn settle_completed_input_page_command( conn: &mut CdpConnection, - owner: &TargetPageResidenceIdentity, - completion: CompletedPageCommand, + completed: CompletedDocumentInputCommand, command_context: &mut CommandDispatchContext, -) -> RendererCommandTurnCompletion { - let output = conn.settle_page_command_turn_for_owner(owner, completion); - command_context.consume_renderer_command_turn_output(output) +) -> Result { + let output = conn + .finish_document_input_command(completed) + .map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?; + Ok(command_context.consume_renderer_command_turn_output(output)) } pub(crate) async fn complete_pending_input_command_output_plan( @@ -1590,18 +1588,9 @@ pub(crate) async fn complete_pending_input_command_output_plan( (InputCommandTaskStep::Complete, plan) } -fn completed_page_command_result( - completed: Box>, -) -> Result { - match *completed { - Ok(completion) => Ok(completion), - Err(error) => Err(DevToolsError::new(DevToolsErrorKind::Internal, error)), - } -} - #[derive(Debug, Default, Eq, PartialEq)] pub(crate) struct InputPreparedOutputs { - download_activations: Vec, + download_activations: Vec, file_chooser_activations: Vec, } @@ -1612,8 +1601,13 @@ pub(crate) struct InputPreparedOutputSlot { impl InputPreparedOutputs { pub(crate) fn from_renderer_download_activation( + conn: &CdpConnection, + owner: &CommandOwnerScope, activation: RendererPendingDownloadActivation, ) -> Self { + let Some(activation) = conn.prepare_download_activation_for_owner(owner, activation) else { + return Self::default(); + }; Self { download_activations: vec![activation], file_chooser_activations: Vec::new(), @@ -1664,10 +1658,17 @@ impl InputPreparedOutputs { #[cfg(test)] pub(crate) fn from_download_activations_for_test( + conn: &CdpConnection, + owner: &CommandOwnerScope, activations: Vec, ) -> Self { Self { - download_activations: activations, + download_activations: activations + .into_iter() + .filter_map(|activation| { + conn.prepare_download_activation_for_owner(owner, activation) + }) + .collect(), file_chooser_activations: Vec::new(), } } @@ -1703,9 +1704,7 @@ impl InputPreparedOutputSlot { self.outputs.extend(other.outputs); } - pub(crate) fn take_download_activations( - &mut self, - ) -> Option> { + pub(crate) fn take_download_activations(&mut self) -> Option> { (!self.outputs.download_activations.is_empty()) .then(|| std::mem::take(&mut self.outputs.download_activations)) } @@ -1730,15 +1729,19 @@ async fn handle_input_dispatch_outcome_async( .map(CommandOwnerScope::for_session) .unwrap_or_else(|| CommandOwnerScope::for_page_residence(owner)); if let Some(download) = outcome.pending_download { - let mut events = Vec::new(); - conn.handle_pending_download_activation_background_events_async( - &mut events, - &action_owner, - download, - command_context, - ) - .await?; - out.extend_protocol_events(command_context, events); + let web_contents = conn.browser_web_contents_for_page_residence(owner).ok(); + if let Some(download) = web_contents.and_then(|web_contents| { + conn.prepare_download_activation(&action_owner, web_contents, download) + }) { + let mut events = Vec::new(); + conn.handle_prepared_download_activation_background_events_async( + &mut events, + download, + command_context, + ) + .await?; + out.extend_protocol_events(command_context, events); + } } if let Some(file_chooser) = outcome.pending_file_chooser && let Some(file_chooser) = file_chooser::PreparedFileChooserActivation::capture( @@ -1766,7 +1769,6 @@ async fn handle_input_dispatch_outcome_async( pub(in crate::domains) async fn emit_download_activity_background_events_async( conn: &mut CdpConnection, out: &mut Vec, - owner: &CommandOwnerScope, prepared_outputs: Option<&mut ProtocolOutputPayloads>, command_context: &mut CommandDispatchContext, ) { @@ -1774,22 +1776,20 @@ pub(in crate::domains) async fn emit_download_activity_background_events_async( .and_then(ProtocolOutputPayloads::input_mut) .and_then(InputPreparedOutputSlot::take_download_activations) { - emit_download_activations(conn, out, owner, activations, command_context).await; + emit_download_activations(conn, out, activations, command_context).await; } } async fn emit_download_activations( conn: &mut CdpConnection, out: &mut Vec, - owner: &CommandOwnerScope, - activations: Vec, + activations: Vec, command_context: &mut CommandDispatchContext, ) { for activation in activations { if let Err(error) = conn - .handle_pending_download_activation_background_events_async( + .handle_prepared_download_activation_background_events_async( out, - owner, activation, command_context, ) @@ -1931,7 +1931,7 @@ mod protocol_neutral_tests { #[test] fn cdp_dispatch_key_event_builds_protocol_neutral_key_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(43), @@ -1970,7 +1970,7 @@ mod protocol_neutral_tests { #[test] fn devtools_input_entry_routes_key_command_to_input_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(44), @@ -2072,8 +2072,8 @@ mod producer_tests { use serde_json::json; use crate::conn::{ - BackgroundProtocolEvent, BrowserContext, CdpConnection, CommandDispatchContext, - CommandOwnerScope, TargetPageResidenceIdentity, build_event, + BackgroundProtocolEvent, CdpConnection, CommandDispatchContext, CommandOwnerScope, + TargetPageResidenceIdentity, build_event, }; use crate::devtools_runtime::{ AutomationEvent, BrowserDownloadProgressEvent, BrowserDownloadWillBeginEvent, @@ -2155,6 +2155,12 @@ mod producer_tests { #[test] fn input_prepared_slot_keeps_download_and_file_chooser_payloads_separate() { + let mut conn = crate::test_support::connection(); + let mut context = conn.new_browser_context_fixture_for_test("BID-slot".to_owned()); + context.set_active_target_id("TID-slot"); + context.attach_active_session("SID-slot"); + conn.install_browser_context_fixture_for_test(context); + let command_owner = CommandOwnerScope::for_session("SID-slot"); let source_document = renderer_document_identity_for_test(1, 1); let owner = TargetPageResidenceIdentity::new_for_test( "BID-slot".to_owned(), @@ -2162,11 +2168,17 @@ mod producer_tests { 1, ); let mut slot = super::InputPreparedOutputSlot::from_outputs(super::InputPreparedOutputs { - download_activations: vec![RendererPendingDownloadActivation { - url: "https://example.test/download".to_owned(), - suggested_filename: Some("download.txt".to_owned()), - response: None, - }], + download_activations: vec![ + conn.prepare_download_activation_for_owner( + &command_owner, + RendererPendingDownloadActivation { + url: "https://example.test/download".to_owned(), + suggested_filename: Some("download.txt".to_owned()), + response: None, + }, + ) + .unwrap(), + ], file_chooser_activations: vec![ super::file_chooser::PreparedFileChooserActivation::from_renderer_for_test( owner, @@ -2278,8 +2290,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn file_chooser_opened_preserves_typed_automation_sidecar() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new_with_page_for_test("BID-typed", "TID-typed"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-typed", "TID-typed"); bc.attach_active_session("SID-typed"); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] @@ -2333,8 +2345,8 @@ mod producer_tests { #[test] fn file_chooser_capture_resolves_root_frame_once() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-root-capture".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-root-capture"); bc.set_active_target_id("TID-root-capture"); bc.attach_active_session("SID-root-capture"); conn.install_browser_context_fixture_for_test(bc); @@ -2361,8 +2373,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn document_open_replacement_preserves_causal_file_chooser_activation() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-document-collision".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-document-collision"); bc.set_active_target_id("TID-document-collision"); bc.attach_active_session("SID-document-collision"); bc.active_page_target_mut().devtools_sessions @@ -2432,8 +2444,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn replaced_page_residence_discards_only_stale_backend_node_collision() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-page-replacement".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-page-replacement"); bc.set_active_target_id("TID-page-replacement"); bc.attach_active_session("SID-page-replacement"); bc.active_page_target_mut().devtools_sessions @@ -2500,8 +2512,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn download_activity_without_protocol_observers_keeps_typed_automation_events() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-download".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-download"); bc.set_active_target_id("FRAME-download"); bc.attach_active_session("SID-download"); conn.install_browser_context_fixture_for_test(bc); @@ -2516,21 +2528,23 @@ mod producer_tests { .unwrap(); let mut out: Vec = Vec::new(); let mut command_context = CommandDispatchContext::default(); + let owner = CommandOwnerScope::for_session("SID-download"); let mut prepared = ProtocolOutputPayloads::from_slot(super::InputPreparedOutputSlot::from_outputs( - super::InputPreparedOutputs::from_download_activations_for_test(vec![ - RendererPendingDownloadActivation { + super::InputPreparedOutputs::from_download_activations_for_test( + &conn, + &owner, + vec![RendererPendingDownloadActivation { url: "https://example.test/report.txt".to_owned(), suggested_filename: Some("report.txt".to_owned()), response: None, - }, - ]), + }], + ), )); super::emit_download_activity_background_events_async( &mut conn, &mut out, - &CommandOwnerScope::for_session("SID-download"), Some(&mut prepared), &mut command_context, ) @@ -2556,8 +2570,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn file_chooser_drain_consumes_prepared_activations_without_page_readback() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] @@ -2604,8 +2618,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn file_chooser_activity_background_events_keep_typed_sidecar() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new_with_page_for_test("BID-context", "TID-context"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-context", "TID-context"); bc.attach_active_session("SID-context"); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] diff --git a/moli-protocol/src/domains/input/tests.rs b/moli-protocol/src/domains/input/tests.rs index 4722d50edc..1d2d07fa9f 100644 --- a/moli-protocol/src/domains/input/tests.rs +++ b/moli-protocol/src/domains/input/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::conn::{BrowserContext, CdpCommandTaskStep, CommandDispatchContext}; +use crate::conn::{CdpCommandTaskStep, CommandDispatchContext}; use crate::testing::{TestContext, wait_until_frame_stopped_loading}; use moli_core::LayoutPolicy; @@ -11,7 +11,7 @@ const INPUT_HIT_X: u32 = 20; const INPUT_HIT_Y: u32 = 20; async fn with_loaded_document(ctx: &mut TestContext, html: &str) { - let mut bc = BrowserContext::new("BID-I".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-I"); bc.set_active_target_id("TID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); let data_url = format!("data:text/html,{html}"); @@ -276,17 +276,19 @@ async fn completed_mouse_event_does_not_restore_replaced_page_state() { .conn .target_page_residence_identity_for_session(None) .expect("the original Page should have a residence identity"); + let command_owner = CommandOwnerScope::capture(&ctx.conn, None); + let document = ctx + .conn + .resolve_browser_document_for_owner(&command_owner) + .expect("the original Document should resolve exactly"); let pending = ctx .conn - .browser_context - .as_ref() - .unwrap() - .start_target_input_command( - original_owner.target_id().unwrap(), + .start_document_input_command( + document, crate::conn::PageInputCommand::Mouse { x: INPUT_HIT_X.into(), y: INPUT_HIT_Y.into(), - event_name: "mousemove", + event_name: "mousemove".to_owned(), button: -1, buttons: None, click_count: 0, @@ -345,9 +347,13 @@ async fn pending_mouse_event_acknowledges_when_page_is_replaced_before_renderer_ .target_page_residence_identity_for_session(None) .expect("the original Page should have a residence identity"); let command_owner = CommandOwnerScope::capture(&ctx.conn, None); + let document = ctx + .conn + .resolve_browser_document_for_owner(&command_owner) + .expect("the original Document should resolve exactly"); let document_lifetime_observer = ctx .conn - .capture_document_lifetime_for_owner(&command_owner) + .observe_browser_document_lifetime(document) .expect("the original Page should expose its attachment lifetime"); let replacement_url = "data:text/html,replacement-before-completion"; @@ -388,9 +394,13 @@ async fn completed_renderer_ack_wins_when_page_replacement_is_already_observable with_loaded_document(&mut ctx, "origin").await; let command_owner = CommandOwnerScope::capture(&ctx.conn, None); + let document = ctx + .conn + .resolve_browser_document_for_owner(&command_owner) + .expect("the original Document should resolve exactly"); let document_lifetime_observer = ctx .conn - .capture_document_lifetime_for_owner(&command_owner) + .observe_browser_document_lifetime(document) .expect("the original Page should expose its attachment lifetime"); ctx.install_navigation_fixture_for_session_owner( "data:text/html,replacement-after-ack", @@ -431,7 +441,7 @@ fn renderer_host_ack_cleanup_is_limited_to_mouse_and_key_callbacks() { #[tokio::test(flavor = "multi_thread")] async fn coordinate_mouse_event_without_document_still_reports_no_document_loaded() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-I".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-I"); bc.set_active_target_id("TID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -618,7 +628,7 @@ async fn touch_tap_and_drag_commands_hit_test_real_layout() { #[tokio::test(flavor = "multi_thread")] async fn coordinate_input_invalid_params_keep_session_id() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-I".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-I"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/domains/inspector.rs b/moli-protocol/src/domains/inspector.rs index 8a09283771..190fa25c64 100644 --- a/moli-protocol/src/domains/inspector.rs +++ b/moli-protocol/src/domains/inspector.rs @@ -67,16 +67,14 @@ fn service_worker_inspector_command_output_plan( mod tests { use moli_core::page::RendererServiceWorkerVersionStatus; - use crate::{ - conn::{BrowserContext, ServiceWorkerTargetState}, - testing::TestContext, - }; + use crate::{conn::ServiceWorkerTargetState, testing::TestContext}; use serde_json::json; #[tokio::test] async fn inspector_enable_and_disable_toggle_browser_context_state() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = + Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({"id": 1, "method": "Inspector.enable"})) .await; @@ -110,7 +108,7 @@ mod tests { #[tokio::test] async fn inspector_enable_replays_target_crashed_for_crashed_target() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.attach_active_session("SID-1"); { let context = &mut bc; @@ -138,7 +136,7 @@ mod tests { #[tokio::test] async fn inspector_enable_replays_crash_to_exact_attached_session() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-primary"); assert!(bc.assign_attached_session_to_target("TID-1", "SID-attached".into())); @@ -178,7 +176,7 @@ mod tests { #[tokio::test] async fn inspector_enable_replays_background_target_crash_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active"); bc.attach_active_session("SID-active"); bc.register_page_target_url_fixture( @@ -214,7 +212,7 @@ mod tests { #[tokio::test] async fn inspector_enable_replays_target_crashed_for_stopped_service_worker() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); let mut target = ServiceWorkerTargetState::new( 41, 7, @@ -263,7 +261,7 @@ mod tests { #[tokio::test] async fn inspector_enable_and_disable_stage_background_target_session_state() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-A"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-A"); bc.attach_active_session("SID-active"); bc.register_page_target_fixture( "TID-B".into(), diff --git a/moli-protocol/src/domains/io.rs b/moli-protocol/src/domains/io.rs index 24d2b8b43c..51a7425ad1 100644 --- a/moli-protocol/src/domains/io.rs +++ b/moli-protocol/src/domains/io.rs @@ -5,8 +5,8 @@ use serde_json::json; use crate::{ conn::{ - CapturedBody, CdpConnection, Cmd, CommandOwnerScope, - CompletedFetchResponseBodyStreamReadDispatch, IoStreamState, + CapturedBody, CdpConnection, Cmd, CommandOwnerScope, CompletedDocumentBlobRead, + CompletedFetchResponseBodyStreamReadDispatch, IoStreamState, PendingDocumentBlobRead, PendingFetchResponseBodyStreamRead, PendingFetchResponseBodyStreamReadDispatch, PendingFetchResponseBodyStreamReadStart, }, @@ -36,7 +36,7 @@ enum PendingIoCommandKind { pending: PendingPageCommand, }, ReadBlob { - pending: PendingPageCommand, + pending: PendingDocumentBlobRead, handle: String, offset: Option, size: Option, @@ -50,7 +50,7 @@ enum CompletedIoCommandKind { completed: Result, }, ReadBlob { - completed: Result, + completed: CompletedDocumentBlobRead, handle: String, offset: Option, size: Option, @@ -80,7 +80,7 @@ impl PendingIoCommandDispatch { offset, size, } => CompletedIoCommandKind::ReadBlob { - completed: pending.wait().await.map_err(|error| error.to_string()), + completed: pending.wait().await, handle, offset, size, @@ -215,13 +215,10 @@ fn start_read_blob_command( offset: Option, size: Option, ) -> IoCommandTaskStep { + let owner = CommandOwnerScope::capture(conn, cmd.session_id); let pending = conn - .resolve_document_command_owner(&CommandOwnerScope::capture(conn, cmd.session_id)) - .and_then(|(context_id, target_id)| { - conn.browser_context_by_id(&context_id) - .ok_or("NoDocumentLoaded")? - .start_target_blob_read(&target_id, uuid.to_owned()) - }); + .resolve_browser_document_for_owner(&owner) + .and_then(|document| conn.start_document_blob_read(document, uuid.to_owned())); match pending { Ok(pending) => IoCommandTaskStep::Pending(Box::new(PendingIoCommandDispatch { command_id: cmd.id, @@ -257,22 +254,12 @@ fn complete_resolve_blob_command( fn complete_read_blob_command( conn: &mut CdpConnection, session_id: Option<&str>, - completed: Result, + completed: CompletedDocumentBlobRead, handle: String, offset: Option, size: Option, ) -> CommandOutputPlan { - let bytes = completed - .and_then(|completed| { - conn.resolve_document_command_owner(&CommandOwnerScope::capture(conn, session_id)) - .and_then(|(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_target_blob_read(&target_id, completed) - }) - }) - .ok() - .flatten(); + let bytes = conn.finish_document_blob_read(completed).ok().flatten(); let Some(bytes) = bytes else { return CommandOutputPlan::error(-32000, "Read failed"); }; @@ -385,7 +372,7 @@ mod tests { use super::{DEFAULT_IO_READ_SIZE, read_io_stream_state}; use crate::{ - conn::{BrowserContext, CdpCommandTaskStep, IoStreamState}, + conn::{CdpCommandTaskStep, IoStreamState}, testing::TestContext, }; @@ -414,7 +401,7 @@ mod tests { #[tokio::test] async fn read_supports_offsets_and_eof() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.insert_io_stream("STREAM-1".into(), b"abcdef".to_vec(), 0); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -446,7 +433,7 @@ mod tests { #[tokio::test] async fn close_removes_stream_handle() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.insert_io_stream("STREAM-1".into(), b"abcdef".to_vec(), 0); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -491,7 +478,7 @@ mod tests { #[tokio::test] async fn read_large_stream_handle_uses_captured_body_backing() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.insert_io_stream("STREAM-2".into(), vec![b'x'; 1024 * 1024 + 8], 0); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -524,7 +511,7 @@ mod tests { #[tokio::test] async fn read_command_dispatch_handles_buffered_stream_without_fallback() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.insert_io_stream("STREAM-DISPATCH".into(), b"dispatch".to_vec(), 0); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -548,7 +535,9 @@ mod tests { #[tokio::test] async fn target_scoped_stream_handle_requires_matching_session_owner() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-io-owner".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-io-owner".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( diff --git a/moli-protocol/src/domains/log.rs b/moli-protocol/src/domains/log.rs index 1cd94752f1..561d6001d1 100644 --- a/moli-protocol/src/domains/log.rs +++ b/moli-protocol/src/domains/log.rs @@ -171,7 +171,7 @@ fn append_log_replay_snapshot( #[cfg(test)] mod tests { - use crate::conn::BrowserContext; + use crate::devtools_runtime::AutomationEvent; use crate::domains::observable_output::{ ObservableOutputProjectionStep, ObservablePreparedOutputSlot, @@ -193,7 +193,7 @@ mod tests { } async fn load_document(ctx: &mut TestContext, html: &str) { - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,log-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); @@ -650,7 +650,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn log_enable_stages_background_target_session_state() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.stage_background_target( diff --git a/moli-protocol/src/domains/network.rs b/moli-protocol/src/domains/network.rs index 8dc0c704d7..fb575024ce 100644 --- a/moli-protocol/src/domains/network.rs +++ b/moli-protocol/src/domains/network.rs @@ -1,4 +1,3 @@ -use crate::conn::NetworkPolicyUpdateKind; use crate::conn::{CdpConnection, Cmd, CommandOwnerScope}; use crate::devtools_runtime::{ DevToolsAddNetworkDataCollectorCommand, DevToolsBrowserContextId, DevToolsCommand, @@ -145,34 +144,19 @@ pub(crate) struct CompletedNetworkCommandDispatch { } enum PendingNetworkCommandWork { - Page { - document: Option, - pending: moli_core::page::PendingPageCommand, - }, + DocumentPolicy(crate::conn::PendingDocumentPolicyUpdate), + ResourceRuntime(crate::conn::PendingDocumentResourceRuntimeUpdate), + NetworkResourcePreparation(crate::conn::PendingNetworkResourceLoadPreparation), Resource(Box), } enum CompletedNetworkCommandWork { - Page { - document: Option, - completed: Result, String>, - }, + DocumentPolicy(Box), + ResourceRuntime(Box), + NetworkResourcePreparation(Box), Resource(moli_core::page::RendererNetworkResourceLoadOutcome), } -impl PendingNetworkCommandWork { - fn page( - conn: &CdpConnection, - owner: &CommandOwnerScope, - pending: moli_core::page::PendingPageCommand, - ) -> Self { - Self::Page { - document: conn.current_document_id_for_owner(owner), - pending, - } - } -} - pub(crate) enum NetworkCommandTaskStep { Pending(PendingNetworkCommandDispatch), Complete(CommandOutputPlan), @@ -194,15 +178,16 @@ enum PendingNetworkCommandKind { impl PendingNetworkCommandDispatch { pub(crate) async fn wait(self) -> CompletedNetworkCommandDispatch { let completed = match self.pending { - PendingNetworkCommandWork::Page { document, pending } => { - CompletedNetworkCommandWork::Page { - document, - completed: pending - .wait() - .await - .map(Box::new) - .map_err(|error| error.to_string()), - } + PendingNetworkCommandWork::DocumentPolicy(pending) => { + CompletedNetworkCommandWork::DocumentPolicy(Box::new(pending.wait().await)) + } + PendingNetworkCommandWork::ResourceRuntime(pending) => { + CompletedNetworkCommandWork::ResourceRuntime(Box::new(pending.wait().await)) + } + PendingNetworkCommandWork::NetworkResourcePreparation(pending) => { + CompletedNetworkCommandWork::NetworkResourcePreparation(Box::new( + pending.wait().await, + )) } PendingNetworkCommandWork::Resource(pending) => { CompletedNetworkCommandWork::Resource((*pending).execute().await) @@ -476,14 +461,14 @@ fn validate_top_level_target_ids( Ok(target_ids) } -fn pending_network_page_command_step( +fn pending_network_resource_runtime_step( conn: &mut CdpConnection, command_id: Option, session_id: Option<&str>, kind: PendingNetworkCommandKind, start: impl FnOnce( &mut CdpConnection, - ) -> Result, String>, + ) -> Result, String>, ) -> NetworkCommandTaskStep { let owner_scope = CommandOwnerScope::capture(conn, session_id); let result = start(conn); @@ -491,7 +476,32 @@ fn pending_network_page_command_step( Ok(Some(pending)) => NetworkCommandTaskStep::Pending(PendingNetworkCommandDispatch { command_id, kind, - pending: PendingNetworkCommandWork::page(conn, &owner_scope, pending), + pending: PendingNetworkCommandWork::ResourceRuntime(pending), + owner_scope, + }), + Ok(None) => NetworkCommandTaskStep::Complete(CommandOutputPlan::success()), + Err(message) if message == "BrowserContextNotLoaded" => NetworkCommandTaskStep::Complete( + CommandOutputPlan::error(-31998, "BrowserContextNotLoaded"), + ), + Err(message) => NetworkCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)), + } +} + +fn pending_network_document_policy_step( + conn: &mut CdpConnection, + command_id: Option, + session_id: Option<&str>, + kind: PendingNetworkCommandKind, + start: impl FnOnce( + &mut CdpConnection, + ) -> Result, String>, +) -> NetworkCommandTaskStep { + let owner_scope = CommandOwnerScope::capture(conn, session_id); + match start(conn) { + Ok(Some(pending)) => NetworkCommandTaskStep::Pending(PendingNetworkCommandDispatch { + command_id, + kind, + pending: PendingNetworkCommandWork::DocumentPolicy(pending), owner_scope, }), Ok(None) => NetworkCommandTaskStep::Complete(CommandOutputPlan::success()), @@ -529,7 +539,7 @@ fn start_set_network_domain_enabled_command( Ok(Some(pending)) => NetworkCommandTaskStep::Pending(PendingNetworkCommandDispatch { command_id: cmd.id, kind, - pending: PendingNetworkCommandWork::page(conn, &owner_scope, pending), + pending: PendingNetworkCommandWork::DocumentPolicy(pending), owner_scope, }), Ok(None) => NetworkCommandTaskStep::Complete(if enabled { @@ -549,7 +559,7 @@ fn start_set_extra_http_headers_command( Ok(headers) => headers, Err(plan) => return NetworkCommandTaskStep::Complete(plan), }; - pending_network_page_command_step( + pending_network_document_policy_step( conn, cmd.id, cmd.session_id, @@ -566,7 +576,7 @@ fn start_set_cache_disabled_command( Ok(cache_disabled) => cache_disabled, Err(plan) => return NetworkCommandTaskStep::Complete(plan), }; - pending_network_page_command_step( + pending_network_document_policy_step( conn, cmd.id, cmd.session_id, @@ -583,7 +593,7 @@ fn start_set_blocked_urls_command( Ok(patterns) => patterns, Err(plan) => return NetworkCommandTaskStep::Complete(plan), }; - pending_network_page_command_step( + pending_network_document_policy_step( conn, cmd.id, cmd.session_id, @@ -600,7 +610,7 @@ fn start_set_bypass_service_worker_command( Ok(bypass) => bypass, Err(plan) => return NetworkCommandTaskStep::Complete(plan), }; - pending_network_page_command_step( + pending_network_document_policy_step( conn, cmd.id, cmd.session_id, @@ -617,7 +627,7 @@ fn start_emulate_network_conditions_command( Ok(offline) => offline, Err(plan) => return NetworkCommandTaskStep::Complete(plan), }; - pending_network_page_command_step( + pending_network_document_policy_step( conn, cmd.id, cmd.session_id, @@ -635,7 +645,7 @@ fn start_set_user_agent_override_command( Ok(browser_identity) => browser_identity, Err(plan) => return NetworkCommandTaskStep::Complete(plan), }; - pending_network_page_command_step( + pending_network_resource_runtime_step( conn, cmd.id, cmd.session_id, @@ -661,36 +671,20 @@ pub(crate) fn complete_pending_network_command( complete_network_policy_refresh(conn, completed, false), ), PendingNetworkCommandKind::SetCacheDisabled => NetworkCommandTaskStep::Complete( - complete_network_policy_refresh(conn, completed, false), + complete_document_policy_network_command(conn, completed), + ), + PendingNetworkCommandKind::SetExtraHttpHeaders => NetworkCommandTaskStep::Complete( + complete_document_policy_network_command(conn, completed), + ), + PendingNetworkCommandKind::SetBlockedUrls => NetworkCommandTaskStep::Complete( + complete_document_policy_network_command(conn, completed), + ), + PendingNetworkCommandKind::SetBypassServiceWorker => NetworkCommandTaskStep::Complete( + complete_document_policy_network_command(conn, completed), + ), + PendingNetworkCommandKind::EmulateNetworkConditions => NetworkCommandTaskStep::Complete( + complete_document_policy_network_command(conn, completed), ), - PendingNetworkCommandKind::SetExtraHttpHeaders => { - NetworkCommandTaskStep::Complete(complete_unit_page_network_command( - conn, - completed, - NetworkPolicyUpdateKind::ExtraHttpHeaders, - )) - } - PendingNetworkCommandKind::SetBlockedUrls => { - NetworkCommandTaskStep::Complete(complete_unit_page_network_command( - conn, - completed, - NetworkPolicyUpdateKind::BlockedUrls, - )) - } - PendingNetworkCommandKind::SetBypassServiceWorker => { - NetworkCommandTaskStep::Complete(complete_unit_page_network_command( - conn, - completed, - NetworkPolicyUpdateKind::BypassServiceWorker, - )) - } - PendingNetworkCommandKind::EmulateNetworkConditions => { - NetworkCommandTaskStep::Complete(complete_unit_page_network_command( - conn, - completed, - NetworkPolicyUpdateKind::NetworkOffline, - )) - } PendingNetworkCommandKind::SetUserAgentOverride => NetworkCommandTaskStep::Complete( complete_rebuild_loader_network_command(conn, completed), ), @@ -703,97 +697,54 @@ pub(crate) fn complete_pending_network_command( } } -fn complete_network_policy_refresh( +fn complete_document_policy_network_command( conn: &mut CdpConnection, completed: CompletedNetworkCommandDispatch, - enabled: bool, ) -> CommandOutputPlan { - let owner_scope = completed.owner_scope.clone(); - let session_id = owner_scope.session_id().map(str::to_owned); - let completion = match completed.completed { - CompletedNetworkCommandWork::Page { - completed: Ok(completion), - .. - } => *completion, - CompletedNetworkCommandWork::Page { - document, - completed: Err(error), - } => { - if network_page_configuration_will_be_replayed(conn, &owner_scope, document) { - return if enabled { - settings::enabled_command_output_plan(conn, session_id.as_deref()) - } else { - CommandOutputPlan::success() - }; - } - return CommandOutputPlan::error(-32000, error); - } - CompletedNetworkCommandWork::Resource(_) => { - return CommandOutputPlan::error(-32000, "InvalidNetworkCommandCompletion"); - } - }; - if let Err(error) = finish_network_page_operation_on_current_attachment( - conn, - &owner_scope, - NetworkPolicyUpdateKind::RequestPolicy, - completion, - ) { - return CommandOutputPlan::error(-32000, error); - } - if enabled { - settings::enabled_command_output_plan(conn, owner_scope.session_id()) - } else { - CommandOutputPlan::success() - } -} - -fn complete_unit_page_network_command( - conn: &mut CdpConnection, - completed: CompletedNetworkCommandDispatch, - finish: NetworkPolicyUpdateKind, -) -> CommandOutputPlan { - let owner_scope = completed.owner_scope.clone(); - let completion = match completed.completed { - CompletedNetworkCommandWork::Page { - completed: Ok(completion), - .. - } => *completion, - CompletedNetworkCommandWork::Page { - document, - completed: Err(error), - } => { - if network_page_configuration_will_be_replayed(conn, &owner_scope, document) { - return CommandOutputPlan::success(); - } - return CommandOutputPlan::error(-32000, error); - } - CompletedNetworkCommandWork::Resource(_) => { - return CommandOutputPlan::error(-32000, "InvalidNetworkCommandCompletion"); - } - }; - match finish_network_page_operation_on_current_attachment( - conn, - &owner_scope, - finish, - completion, - ) { + match finish_document_policy_network_command(conn, completed) { Ok(()) => CommandOutputPlan::success(), Err(error) => CommandOutputPlan::error(-32000, error), } } -fn finish_network_page_operation_on_current_attachment( +fn finish_document_policy_network_command( conn: &mut CdpConnection, - owner: &CommandOwnerScope, - finish: NetworkPolicyUpdateKind, - completion: moli_core::page::CompletedPageCommand, + completed: CompletedNetworkCommandDispatch, ) -> Result<(), String> { - if let Some((context_id, target_id)) = conn.resolved_page_owner_identity_for_owner(owner) - && let Some(context) = conn.browser_context_by_id_mut(&context_id) - { - return context.finish_target_network_policy_update(&target_id, finish, completion); + let owner_scope = completed.owner_scope; + let CompletedNetworkCommandWork::DocumentPolicy(completed) = completed.completed else { + return Err("InvalidNetworkCommandCompletion".to_owned()); + }; + let document = completed.document(); + match conn.finish_document_policy_update(*completed) { + Ok(()) => Ok(()), + Err(error) + if error == "Document changed" + && conn + .runtime_session_owner_slot_for_owner(&owner_scope) + .is_ok() + && conn.loaded_browser_document_for_owner(&owner_scope).ok() != Some(document) => + { + Ok(()) + } + Err(error) => Err(error), + } +} + +fn complete_network_policy_refresh( + conn: &mut CdpConnection, + completed: CompletedNetworkCommandDispatch, + enabled: bool, +) -> CommandOutputPlan { + let session_id = completed.owner_scope.session_id().map(str::to_owned); + if let Err(error) = finish_document_policy_network_command(conn, completed) { + return CommandOutputPlan::error(-32000, error); + } + if enabled { + settings::enabled_command_output_plan(conn, session_id.as_deref()) + } else { + CommandOutputPlan::success() } - crate::conn::BrowserContext::finish_unobserved_network_policy_update(completion) } fn complete_rebuild_loader_network_command( @@ -801,26 +752,22 @@ fn complete_rebuild_loader_network_command( completed: CompletedNetworkCommandDispatch, ) -> CommandOutputPlan { let owner_scope = completed.owner_scope.clone(); - let completion = match completed.completed { - CompletedNetworkCommandWork::Page { - completed: Ok(completion), - .. - } => *completion, - CompletedNetworkCommandWork::Page { - document, - completed: Err(error), - } => { - if network_page_configuration_will_be_replayed(conn, &owner_scope, document) { - return CommandOutputPlan::success(); - } - return CommandOutputPlan::error(-32000, error); - } - CompletedNetworkCommandWork::Resource(_) => { + let completed = match completed.completed { + CompletedNetworkCommandWork::ResourceRuntime(completed) => *completed, + CompletedNetworkCommandWork::DocumentPolicy(_) + | CompletedNetworkCommandWork::NetworkResourcePreparation(_) + | CompletedNetworkCommandWork::Resource(_) => { return CommandOutputPlan::error(-32000, "InvalidNetworkCommandCompletion"); } }; - match conn.finish_rebuild_resource_runtime_for_owner(&owner_scope, completion) { + let document = completed.document(); + match conn.finish_document_resource_runtime_update(completed) { Ok(()) => CommandOutputPlan::success(), + Err(_) + if network_page_configuration_will_be_replayed(conn, &owner_scope, document.id()) => + { + CommandOutputPlan::success() + } Err(error) => CommandOutputPlan::error(-32000, error), } } @@ -828,11 +775,8 @@ fn complete_rebuild_loader_network_command( fn network_page_configuration_will_be_replayed( conn: &CdpConnection, owner_scope: &CommandOwnerScope, - dispatched_document: Option, + dispatched_document: moli_core::browser::DocumentId, ) -> bool { - let Some(dispatched_document) = dispatched_document else { - return false; - }; conn.runtime_session_owner_slot_for_owner(owner_scope) .is_ok_and(|_| conn.current_document_id_for_owner(owner_scope) != Some(dispatched_document)) } diff --git a/moli-protocol/src/domains/network/activity.rs b/moli-protocol/src/domains/network/activity.rs index 03d6c14aed..afd2257266 100644 --- a/moli-protocol/src/domains/network/activity.rs +++ b/moli-protocol/src/domains/network/activity.rs @@ -507,7 +507,7 @@ mod tests { use super::NetworkPreparedOutputs; use crate::{ - conn::{BrowserContext, CdpConnection, PendingSubresourceFetchRequest}, + conn::PendingSubresourceFetchRequest, domains::activity::{ProtocolOutputPayloads, ProtocolOutputProjectionContext}, testing::{TestContext, wait_until_message, wait_until_messages}, }; @@ -581,8 +581,8 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn pending_subresource_continue_drain_consumes_prepared_events_without_page_readback() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.set_active_document_fixture_for_test(1); @@ -672,8 +672,8 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_subresource_continue_rejects_replacement_id_and_handle_collision() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-collision".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-collision"); bc.set_active_target_id("TID-collision"); bc.attach_active_session("SID-collision"); bc.set_active_document_fixture_for_test(1); @@ -752,8 +752,8 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn subresource_fetch_interception_drain_consumes_prepared_pause_without_page_readback() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); assert!(bc.assign_attached_session_to_target("TID-1", "FETCH-SID".to_owned())); @@ -809,8 +809,8 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn subresource_fetch_interception_omits_network_events_without_listener() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); assert!(bc.assign_attached_session_to_target("TID-1", "FETCH-SID".to_owned())); @@ -844,7 +844,7 @@ mod tests { #[test] fn network_backlog_prepared_outputs_are_absent_without_loaded_observed_page() { - let mut conn = crate::conn::CdpConnection::default(); + let mut conn = crate::test_support::connection(); let owner = CommandOwnerScope::capture(&conn, None); assert_eq!( super::network_backlog_prepared_outputs_for_owner(&mut conn, &owner, None,).outputs(), @@ -895,7 +895,7 @@ mod tests { let page_url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active"); bc.attach_active_session("SID-active"); bc.register_page_target_url_fixture( @@ -988,7 +988,7 @@ mod tests { let socket_url = format!("ws://{addr}/socket"); let socket_literal = serde_json::to_string(&socket_url).unwrap(); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/domains/network/backlog.rs b/moli-protocol/src/domains/network/backlog.rs index 908a8c3d6b..d51f58cfb4 100644 --- a/moli-protocol/src/domains/network/backlog.rs +++ b/moli-protocol/src/domains/network/backlog.rs @@ -1044,7 +1044,7 @@ mod tests { use url::Url; use crate::{ - conn::{CdpConnection, CommandOwnerScope}, + conn::CommandOwnerScope, devtools_runtime::{AutomationEvent, DevToolsNetworkResourceType}, domains::network::{ NetworkPreparedOutputs, PendingSubresourceNetworkActivity, @@ -1088,7 +1088,7 @@ mod tests { .pending_network_backlog_delivery_snapshot_from_backlog(&mut prepared) .expect("XHR output should produce a delivery snapshot"); let mut events = Vec::new(); - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.install_default_browser_target(); let owner = crate::conn::CommandOwnerScope::capture(&conn, None); emit_network_delivery_snapshot(&mut conn, &mut events, &owner, "FRAME-1", 1.0, snapshot); diff --git a/moli-protocol/src/domains/network/cookies.rs b/moli-protocol/src/domains/network/cookies.rs index 04dde143f8..cfef19f4d9 100644 --- a/moli-protocol/src/domains/network/cookies.rs +++ b/moli-protocol/src/domains/network/cookies.rs @@ -197,7 +197,7 @@ mod protocol_neutral_tests { use crate::devtools_runtime::DevToolsProtocol; use serde_json::{Value, json}; - use crate::conn::{CdpConnection, Cmd}; + use crate::conn::Cmd; use crate::domains::storage::StorageCommandTaskStep; use super::{ @@ -208,7 +208,7 @@ mod protocol_neutral_tests { #[test] fn cdp_network_get_cookies_builds_protocol_neutral_command_with_urls() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "urls": [ "https://example.com/", @@ -246,7 +246,7 @@ mod protocol_neutral_tests { #[test] fn cdp_network_get_cookies_rejects_invalid_url_params_before_owner_entry() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "urls": ["not a url"] }); @@ -272,7 +272,7 @@ mod protocol_neutral_tests { #[test] fn network_get_cookies_routes_to_shared_storage_entry() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(144), @@ -296,7 +296,7 @@ mod protocol_neutral_tests { #[test] fn cdp_network_delete_cookies_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "name": "sid", "url": "https://example.com/", @@ -340,7 +340,7 @@ mod protocol_neutral_tests { #[test] fn network_delete_cookies_routes_to_shared_storage_entry() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "name": "sid" }); let cmd = Cmd::for_test( Some(148), @@ -364,7 +364,7 @@ mod protocol_neutral_tests { #[test] fn network_clear_browser_cookies_routes_to_shared_storage_entry() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(154), @@ -388,7 +388,7 @@ mod protocol_neutral_tests { #[test] fn network_set_cookie_routes_to_shared_storage_entry() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "name": "sid", "value": "1", diff --git a/moli-protocol/src/domains/network/load_resource.rs b/moli-protocol/src/domains/network/load_resource.rs index 62c5713b85..82b27e1cd8 100644 --- a/moli-protocol/src/domains/network/load_resource.rs +++ b/moli-protocol/src/domains/network/load_resource.rs @@ -52,17 +52,14 @@ pub(super) fn start_load_network_resource_command( )); }; let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id); - let pending = match conn.resolve_document_command_owner(&owner_scope) { - Ok((context_id, target_id)) => conn - .browser_context_by_id(&context_id) - .expect("admitted document context remains registered") - .start_target_network_resource_load( - &target_id, - frame_id, - url, - params.options.disable_cache, - params.options.include_credentials, - ), + let pending = match conn.resolve_browser_document_for_owner(&owner_scope) { + Ok(document) => conn.start_network_resource_load_preparation( + document, + frame_id, + url, + params.options.disable_cache, + params.options.include_credentials, + ), Err(message) if message == "NoDocumentLoaded" => { return NetworkCommandTaskStep::Complete(CommandOutputPlan::error( -32602, @@ -77,7 +74,7 @@ pub(super) fn start_load_network_resource_command( Ok(pending) => NetworkCommandTaskStep::Pending(PendingNetworkCommandDispatch { command_id: cmd.id, kind: PendingNetworkCommandKind::PrepareNetworkResourceLoad, - pending: PendingNetworkCommandWork::page(conn, &owner_scope, pending), + pending: PendingNetworkCommandWork::NetworkResourcePreparation(pending), owner_scope, }), Err(error) => { @@ -92,27 +89,14 @@ pub(super) fn complete_network_resource_preparation( ) -> NetworkCommandTaskStep { let owner_scope = completed.owner_scope.clone(); let completion = match completed.completed { - CompletedNetworkCommandWork::Page { - completed: Ok(completion), - .. - } => *completion, - CompletedNetworkCommandWork::Page { - completed: Err(error), - .. - } => { - return NetworkCommandTaskStep::Complete(CommandOutputPlan::error(-32000, error)); - } - CompletedNetworkCommandWork::Resource(_) => { + CompletedNetworkCommandWork::NetworkResourcePreparation(completion) => *completion, + CompletedNetworkCommandWork::DocumentPolicy(_) + | CompletedNetworkCommandWork::ResourceRuntime(_) + | CompletedNetworkCommandWork::Resource(_) => { return invalid_completion_step(); } }; - let preparation = match conn.resolve_document_command_owner(&owner_scope).and_then( - |(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_target_network_resource_load_preparation(&target_id, completion) - }, - ) { + let preparation = match conn.finish_network_resource_load_preparation(completion) { Ok(preparation) => preparation, Err(message) => { return NetworkCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); @@ -149,7 +133,9 @@ pub(super) fn complete_network_resource_fetch( let owner_scope = completed.owner_scope.clone(); let outcome = match completed.completed { CompletedNetworkCommandWork::Resource(outcome) => outcome, - CompletedNetworkCommandWork::Page { .. } => { + CompletedNetworkCommandWork::DocumentPolicy(_) + | CompletedNetworkCommandWork::ResourceRuntime(_) + | CompletedNetworkCommandWork::NetworkResourcePreparation(_) => { return invalid_completion_plan(); } }; diff --git a/moli-protocol/src/domains/network/tests/blocked_urls.rs b/moli-protocol/src/domains/network/tests/blocked_urls.rs index 3c3fb1e892..1e12959966 100644 --- a/moli-protocol/src/domains/network/tests/blocked_urls.rs +++ b/moli-protocol/src/domains/network/tests/blocked_urls.rs @@ -23,7 +23,7 @@ async fn set_blocked_urls_requires_browser_context() { #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_rejects_invalid_params() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 2801, "method": "Network.setBlockedURLs", @@ -35,7 +35,7 @@ async fn set_blocked_urls_rejects_invalid_params() { #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_updates_browser_context_state() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); enable_network_domain(&mut ctx, 28_020, None).await; ctx.process_async(json!({ @@ -77,7 +77,7 @@ async fn set_blocked_urls_updates_browser_context_state() { #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_contribution_activates_with_network_handler() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 28_021, @@ -111,7 +111,7 @@ async fn set_blocked_urls_contribution_activates_with_network_handler() { #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_navigation_fails_with_blocked_by_client() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -153,7 +153,7 @@ async fn set_blocked_urls_navigation_fails_with_blocked_by_client() { #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_runtime_fetch_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -295,7 +295,9 @@ async fn background_set_blocked_urls_updates_loaded_owner_page_without_activatio ctx.conn.browser_context = None; ctx.sent.clear(); - let mut inactive = BrowserContext::new("BID-background".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-background".to_owned()); inactive.register_page_target_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), @@ -396,7 +398,7 @@ async fn background_set_blocked_urls_updates_loaded_owner_page_without_activatio #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_worker_fetch_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -540,7 +542,7 @@ async fn set_blocked_urls_worker_fetch_emits_loading_failed() { #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_worker_xhr_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -680,7 +682,7 @@ async fn set_blocked_urls_worker_xhr_emits_loading_failed() { #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_worker_websocket_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -841,7 +843,7 @@ async fn set_blocked_urls_worker_websocket_emits_loading_failed() { #[tokio::test(flavor = "multi_thread")] async fn emulate_network_conditions_offline_worker_fetch_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -957,7 +959,7 @@ async fn emulate_network_conditions_offline_worker_fetch_emits_loading_failed() #[tokio::test(flavor = "multi_thread")] async fn emulate_network_conditions_offline_worker_xhr_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -1069,7 +1071,7 @@ async fn emulate_network_conditions_offline_worker_xhr_emits_loading_failed() { #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_runtime_xhr_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -1158,7 +1160,7 @@ async fn set_blocked_urls_runtime_xhr_emits_loading_failed() { #[tokio::test(flavor = "multi_thread")] async fn set_blocked_urls_runtime_websocket_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -1279,7 +1281,7 @@ async fn emulate_network_conditions_requires_browser_context() { #[tokio::test(flavor = "multi_thread")] async fn emulate_network_conditions_rejects_invalid_params() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); for params in [ json!({ "offline": true }), json!({ "offline": true, "latency": "slow", "downloadThroughput": -1, "uploadThroughput": -1 }), @@ -1301,7 +1303,7 @@ async fn emulate_network_conditions_rejects_invalid_params() { #[tokio::test(flavor = "multi_thread")] async fn emulate_network_conditions_installs_only_the_supported_offline_value() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 30, @@ -1322,7 +1324,7 @@ async fn emulate_network_conditions_installs_only_the_supported_offline_value() #[tokio::test(flavor = "multi_thread")] async fn emulate_network_conditions_offline_navigation_commits_error_document() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -1442,7 +1444,7 @@ async fn emulate_network_conditions_offline_navigation_commits_error_document() #[tokio::test(flavor = "multi_thread")] async fn emulate_network_conditions_offline_runtime_fetch_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -1574,7 +1576,7 @@ globalThis.__lm_after_blocked_parser_script = true; let page_url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/domains/network/tests/cache.rs b/moli-protocol/src/domains/network/tests/cache.rs index 47c4cf1535..b74665c68e 100644 --- a/moli-protocol/src/domains/network/tests/cache.rs +++ b/moli-protocol/src/domains/network/tests/cache.rs @@ -112,7 +112,9 @@ async fn cached_subresource_reload_emits_served_from_cache_before_response() { let cache_dir = unique_cdp_cache_dir("subresource-served-from-cache"); let mut fetch_config = FetchConfig::default(); fetch_config.set_http_cache_dir(Some(cache_dir.display().to_string())); - let mut ctx = TestContext::from_conn(CdpConnection::new_with_fetch_config(fetch_config)); + let mut ctx = TestContext::from_conn(crate::test_support::connection_with_fetch_config( + fetch_config, + )); let mut browser_context = ctx.conn.new_browser_context("BID-1".to_owned()); browser_context.set_active_target_id("TID-1"); browser_context.attach_active_session("SID-1"); @@ -274,7 +276,9 @@ async fn cached_main_document_navigation_emits_served_from_cache_before_response let cache_dir = unique_cdp_cache_dir("main-document-served-from-cache"); let mut fetch_config = FetchConfig::default(); fetch_config.set_http_cache_dir(Some(cache_dir.display().to_string())); - let mut ctx = TestContext::from_conn(CdpConnection::new_with_fetch_config(fetch_config)); + let mut ctx = TestContext::from_conn(crate::test_support::connection_with_fetch_config( + fetch_config, + )); let mut browser_context = ctx.conn.new_browser_context("BID-1".to_owned()); browser_context.set_active_target_id("TID-1"); browser_context.attach_active_session("SID-1"); @@ -443,7 +447,7 @@ async fn clear_browser_cache_requires_browser_context() { #[tokio::test(flavor = "multi_thread")] async fn clear_browser_cache_clears_response_body_and_stream_artifacts() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.record_captured_response_body("REQ-1".to_owned(), "body".to_owned(), [None]); bc.insert_io_stream("STREAM-1".to_owned(), b"payload".to_vec(), 0); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -474,7 +478,7 @@ async fn clear_browser_cache_clears_configured_disk_http_cache() { let mut fetch_config = FetchConfig::default(); fetch_config.set_http_cache_dir(Some(cache_dir.display().to_string())); - let mut ctx = TestContext::from_conn(crate::conn::CdpConnection::new_with_fetch_config( + let mut ctx = TestContext::from_conn(crate::test_support::connection_with_fetch_config( fetch_config, )); let browser_context = ctx.conn.new_browser_context("BID-1".into()); @@ -508,7 +512,7 @@ async fn clear_browser_cache_uses_browser_context_http_cache_owner() { let mut fetch_config = FetchConfig::default(); fetch_config.set_http_cache_dir(None); - let mut ctx = TestContext::from_conn(crate::conn::CdpConnection::new_with_fetch_config( + let mut ctx = TestContext::from_conn(crate::test_support::connection_with_fetch_config( fetch_config, )); let browser_context = BrowserContext::new_with_storage_partition_and_http_cache( @@ -618,12 +622,12 @@ fn new_ephemeral_browser_context_inherits_effective_http_cache_owner() { fetch_config.set_http_cache_dir(Some(cache_dir.display().to_string())); fetch_config.set_http_cache_max_bytes(Some(77)); - let conn = crate::conn::CdpConnection::new_with_fetch_config(fetch_config); + let conn = crate::test_support::connection_with_fetch_config(fetch_config); let browser_context = conn.new_ephemeral_browser_context("BID-ephemeral".to_owned()); assert_eq!( browser_context.http_cache_configuration_for_test(), - (Some(cache_dir.as_path()), Some(77)) + (Some(cache_dir), Some(77)) ); } @@ -641,19 +645,19 @@ fn new_browser_context_inherits_effective_http_cache_owner() { fetch_config.set_http_cache_dir(Some(cache_dir.display().to_string())); fetch_config.set_http_cache_max_bytes(Some(77)); - let conn = crate::conn::CdpConnection::new_with_fetch_config(fetch_config); + let conn = crate::test_support::connection_with_fetch_config(fetch_config); let browser_context = conn.new_browser_context("BID-default".to_owned()); assert_eq!( browser_context.http_cache_configuration_for_test(), - (Some(cache_dir.as_path()), Some(77)) + (Some(cache_dir), Some(77)) ); } #[tokio::test(flavor = "multi_thread")] async fn clear_browser_cache_keeps_pending_response_navigation_transfer() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); let url = Url::parse("https://example.test/document").unwrap(); bc.register_pending_fetch_response_navigation( "INT-1".to_owned(), @@ -663,6 +667,7 @@ async fn clear_browser_cache_keeps_pending_response_navigation_transfer() { redirect_headers: None, navigate_id: Some(1), owner: crate::conn::CommandOwnerScope::for_session("SID-1"), + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: crate::conn::NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": LOADER_ID}), ), @@ -728,7 +733,7 @@ async fn set_cache_disabled_requires_browser_context() { #[tokio::test(flavor = "multi_thread")] async fn set_cache_disabled_rejects_invalid_params() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 21, @@ -741,7 +746,7 @@ async fn set_cache_disabled_rejects_invalid_params() { #[tokio::test(flavor = "multi_thread")] async fn set_cache_disabled_updates_browser_context_state() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 219, @@ -784,7 +789,7 @@ async fn set_cache_disabled_updates_browser_context_state() { #[tokio::test(flavor = "multi_thread")] async fn devtools_set_cache_behavior_global_updates_existing_targets_and_default() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.register_page_target_fixture( "TID-background".to_owned(), @@ -831,17 +836,13 @@ async fn devtools_set_cache_behavior_global_updates_existing_targets_and_default .expect("background target should retain its network policy") .cache_disabled() ); - assert!( - ctx.conn - .new_browser_context("BID-future".to_owned()) - .global_cache_disabled - ); + assert!(ctx.conn.browser_global_overrides.cache_disabled); } #[tokio::test(flavor = "multi_thread")] async fn devtools_set_cache_behavior_contexts_only_updates_requested_targets() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.register_page_target_fixture( "TID-background".to_owned(), @@ -885,17 +886,13 @@ async fn devtools_set_cache_behavior_contexts_only_updates_requested_targets() { .expect("background target should retain its network policy") .cache_disabled() ); - assert!( - !ctx.conn - .new_browser_context("BID-future".to_owned()) - .global_cache_disabled - ); + assert!(!ctx.conn.browser_global_overrides.cache_disabled); } #[tokio::test(flavor = "multi_thread")] async fn devtools_set_cache_behavior_rejects_unknown_context() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let error = ctx .conn @@ -928,7 +925,7 @@ async fn devtools_set_cache_behavior_rejects_unknown_context() { } #[tokio::test(flavor = "multi_thread")] -async fn devtools_set_cache_behavior_without_contexts_sets_future_default() { +async fn devtools_set_cache_behavior_without_contexts_applies_to_future_target() { let mut ctx = TestContext::new(); ctx.conn @@ -951,10 +948,13 @@ async fn devtools_set_cache_behavior_without_contexts_sets_future_default() { .0 .expect("global cache behavior should be accepted before contexts exist"); + assert!(ctx.conn.browser_global_overrides.cache_disabled); + ctx.conn.install_default_browser_target(); + let context = ctx.conn.browser_context.as_ref().expect("future context"); assert!( - ctx.conn - .new_browser_context("BID-future".to_owned()) - .global_cache_disabled + context + .effective_policy_for_target(context.active_target_id().expect("future target")) + .cache_disabled() ); } #[tokio::test(flavor = "multi_thread")] @@ -971,7 +971,7 @@ async fn set_bypass_service_worker_requires_browser_context() { #[tokio::test(flavor = "multi_thread")] async fn set_bypass_service_worker_rejects_invalid_params() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({ "id": 25, "method": "Network.setBypassServiceWorker", @@ -983,7 +983,7 @@ async fn set_bypass_service_worker_rejects_invalid_params() { #[tokio::test(flavor = "multi_thread")] async fn set_bypass_service_worker_updates_browser_context_state() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1")); ctx.process_async(json!({ "id": 239, diff --git a/moli-protocol/src/domains/network/tests/cookies.rs b/moli-protocol/src/domains/network/tests/cookies.rs index 23492bbd0a..1c6c0fa432 100644 --- a/moli-protocol/src/domains/network/tests/cookies.rs +++ b/moli-protocol/src/domains/network/tests/cookies.rs @@ -18,7 +18,7 @@ async fn get_all_cookies_requires_browser_context() { #[tokio::test(flavor = "multi_thread")] async fn network_get_all_cookies_returns_unfiltered_browser_context_cookies() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-GAC".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-GAC")); ctx.process_async(json!({ "id": 2_2, @@ -64,7 +64,7 @@ async fn network_get_all_cookies_returns_unfiltered_browser_context_cookies() { #[tokio::test(flavor = "multi_thread")] async fn set_extra_http_headers_replaces_previous_headers() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test("NID-A", "TID-A")); + ctx.conn.browser_context = Some(ctx.conn.new_page_target_fixture_for_test("NID-A", "TID-A")); ctx.process_async(json!({"id": 2, "method": "Network.enable"})) .await; @@ -124,7 +124,7 @@ async fn set_user_agent_override_applies_to_subsequent_navigation_requests() { let url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -183,17 +183,10 @@ async fn background_target_user_agent_override_reaches_replacement_document() { let url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-default".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-default".to_owned()); browser_context.set_active_target_id("TID-initial"); - let renderer_runtime = browser_context.renderer_runtime_owner_access(); - let engine = NavigationEngine::new_with_fetch_config_and_browser_context_access( - ctx.conn.fetch_config().clone(), - renderer_runtime, - OptionalResourceFetchMask::NONE, - true, - ) - .expect("new BrowserContext owner should be live"); - ctx.conn.replace_standalone_navigation_engine(engine); ctx.conn .install_browser_context_fixture_for_test(browser_context); @@ -325,7 +318,7 @@ async fn set_user_agent_override_applies_to_current_page_fetch_requests() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -392,7 +385,7 @@ async fn set_user_agent_override_rejects_invalid_params() { #[tokio::test(flavor = "multi_thread")] async fn network_get_cookies_omits_same_site_for_unspecified_cookie() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N-U".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-N-U")); ctx.process_async(json!({ "id": 101, @@ -422,7 +415,7 @@ async fn network_get_cookies_omits_same_site_for_unspecified_cookie() { #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_returns_success_and_cookie_report() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-N1")); ctx.process_async(json!({ "id": 40, @@ -452,7 +445,7 @@ async fn network_set_cookie_returns_success_and_cookie_report() { #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_reports_secure_access_warning_for_localhost_http() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N1W".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-N1W")); ctx.process_async(json!({ "id": 42, @@ -481,7 +474,7 @@ async fn network_set_cookie_reports_secure_access_warning_for_localhost_http() { #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_returns_rejected_cookie_report_without_protocol_error() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N2".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-N2")); ctx.process_async(json!({ "id": 41, @@ -514,7 +507,7 @@ async fn network_set_cookie_returns_rejected_cookie_report_without_protocol_erro #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_reports_structured_facade_rejections_without_protocol_error() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N3".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-N3")); ctx.process_async(json!({ "id": 43, @@ -548,7 +541,7 @@ async fn network_set_cookie_reports_structured_facade_rejections_without_protoco #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_accepts_leading_dot_domain() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N3-DOT".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-N3-DOT")); ctx.process_async(json!({ "id": 431, @@ -582,7 +575,7 @@ async fn network_set_cookie_accepts_leading_dot_domain() { #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_reports_invalid_url_as_cookie_rejection() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N4".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-N4")); ctx.process_async(json!({ "id": 44, @@ -614,7 +607,7 @@ async fn network_set_cookie_reports_invalid_url_as_cookie_rejection() { #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_reports_structured_name_value_rejections_without_protocol_error() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N5".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-N5")); ctx.process_async(json!({ "id": 45, @@ -646,7 +639,9 @@ async fn network_set_cookie_reports_structured_name_value_rejections_without_pro #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_uses_browser_context_default_cookie_url_when_missing() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-N6", "TID-N6"); + let mut bc = ctx + .conn + .new_page_target_fixture_for_test("BID-N6", "TID-N6"); bc.set_target_url("https://example.com/path".into()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -697,7 +692,7 @@ async fn network_set_cookie_uses_browser_context_default_cookie_url_when_missing #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_reports_missing_cookie_url_when_no_default_scope_exists() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N7".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-N7")); ctx.process_async(json!({ "id": 48, @@ -728,7 +723,10 @@ async fn network_set_cookie_reports_missing_cookie_url_when_no_default_scope_exi #[tokio::test(flavor = "multi_thread")] async fn network_set_cookie_keeps_cookie_store_available_after_lock_holder_panic() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-N-store-panic".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-N-store-panic"), + ); let cookie_store = ctx .conn .browser_context @@ -793,7 +791,7 @@ async fn network_get_cookies_lists_same_site_none_secure_cookie_for_loopback_htt // http:// loopback URL, even though the cookie read path (document.cookie) // treats loopback as a secure context and shows it. Chromium lists it too. let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-NONEC".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-NONEC")); ctx.process_async(json!({ "id": 50, diff --git a/moli-protocol/src/domains/network/tests/load_resource.rs b/moli-protocol/src/domains/network/tests/load_resource.rs index c8c978f357..ef901c4e04 100644 --- a/moli-protocol/src/domains/network/tests/load_resource.rs +++ b/moli-protocol/src/domains/network/tests/load_resource.rs @@ -213,6 +213,51 @@ async fn load_network_resource_matches_chromium_validation_order() { ctx.expect_error(81_014, -32602, "Unsupported URL scheme"); } +#[tokio::test(flavor = "multi_thread")] +async fn load_network_resource_preparation_cannot_finish_on_a_replacement_document() { + let mut ctx = TestContext::new(); + install_loaded_page(&mut ctx, "data:text/html,resource-owner").await; + let raw = json!({ + "id": 81_015, + "method": "Network.loadNetworkResource", + "sessionId": SESSION_ID, + "params": { + "frameId": TARGET_ID, + "url": "https://resource.example/data", + "options": { "disableCache": false, "includeCredentials": false } + } + }) + .to_string(); + let crate::conn::CdpCommandTaskStep::Pending(pending) = ctx.conn.start_command_dispatch(&raw) + else { + panic!("network resource preparation must start on the original document"); + }; + let completed = pending.wait().await; + + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,replacement-resource-owner", + Some(SESSION_ID), + ) + .await; + + let crate::conn::CdpCommandTaskStep::Complete(outcome) = + ctx.conn.complete_pending_command_dispatch(completed).await + else { + panic!("stale preparation must not start the browser resource fetch"); + }; + let (messages, _) = ctx.route_completed_command_outcome_for_test(outcome).await; + let response = messages + .iter() + .find(|message| message["id"] == json!(81_015)) + .expect("loadNetworkResource response"); + assert_eq!(response["error"]["code"], json!(-32000), "{response}"); + assert_eq!( + response["error"]["message"], + json!("Document changed while preparing the network resource load"), + "the preparation must retain its originating Browser Document: {response}" + ); +} + #[tokio::test(flavor = "multi_thread")] async fn load_network_resource_enforces_document_connect_src_before_fetch() { async fn csp_page() -> impl IntoResponse { @@ -409,7 +454,9 @@ async fn load_network_resource_disable_cache_bypasses_and_replaces_fresh_entry() )); let mut fetch_config = FetchConfig::default(); fetch_config.set_http_cache_dir(Some(cache_dir.display().to_string())); - let mut ctx = TestContext::from_conn(CdpConnection::new_with_fetch_config(fetch_config)); + let mut ctx = TestContext::from_conn(crate::test_support::connection_with_fetch_config( + fetch_config, + )); install_loaded_page(&mut ctx, &format!("http://{addr}/page")).await; let resource_url = format!("http://{addr}/cacheable"); diff --git a/moli-protocol/src/domains/network/tests/mod.rs b/moli-protocol/src/domains/network/tests/mod.rs index c228dfd4a9..048dac740d 100644 --- a/moli-protocol/src/domains/network/tests/mod.rs +++ b/moli-protocol/src/domains/network/tests/mod.rs @@ -21,12 +21,11 @@ use crate::testing::{ }; use crate::{ conn::{ - BrowserContext, CapturedBody, CdpConnection, DocumentBodySource, NETWORK_ERROR_PAGE_URL, + BrowserContext, CapturedBody, DocumentBodySource, NETWORK_ERROR_PAGE_URL, NavigationDispatchState, TargetIdentityState, TargetPageSlot, }, domains::page::LOADER_ID, }; -use moli_core::{OptionalResourceFetchMask, runtime::NavigationEngine}; use moli_fetch::{FetchConfig, RawResponse, ResponseHead}; use parking_lot::Mutex; use serde_json::json; diff --git a/moli-protocol/src/domains/network/tests/navigation.rs b/moli-protocol/src/domains/network/tests/navigation.rs index b59ba971a9..f1593ed231 100644 --- a/moli-protocol/src/domains/network/tests/navigation.rs +++ b/moli-protocol/src/domains/network/tests/navigation.rs @@ -36,7 +36,7 @@ async fn async_dispatch_updates_live_request_overrides_for_current_page() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -116,7 +116,7 @@ async fn async_dispatch_updates_live_request_overrides_for_current_page() { #[tokio::test(flavor = "multi_thread")] async fn async_dispatch_offline_runtime_fetch_emits_loading_failed() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -202,7 +202,7 @@ async fn main_document_navigation_emits_network_events_and_captures_response_bod let url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -422,7 +422,7 @@ async fn main_document_navigation_applies_extra_http_headers() { let url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -504,11 +504,12 @@ async fn main_document_navigation_events_include_synthesized_cookie_header() { let page_url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); { - let mut jar = bc.cookie_store_for_test().lock(); + let jar_handle = bc.cookie_store_for_test(); + let mut jar = jar_handle.lock(); jar.store_response_headers( &Url::parse(&page_url).unwrap(), &[("set-cookie".to_owned(), b"sid=1; Path=/page".to_vec())], @@ -600,11 +601,12 @@ async fn main_document_navigation_deduplicates_manual_cookie_header_when_store_s let page_url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); { - let mut jar = bc.cookie_store_for_test().lock(); + let jar_handle = bc.cookie_store_for_test(); + let mut jar = jar_handle.lock(); jar.store_response_headers( &Url::parse(&page_url).unwrap(), &[("set-cookie".to_owned(), b"sid=store; Path=/page".to_vec())], @@ -697,12 +699,13 @@ async fn main_document_navigation_redirect_emits_second_request_with_redirect_re }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.set_target_url(format!("http://localhost:{}/origin", addr.port())); { - let mut jar = bc.cookie_store_for_test().lock(); + let jar_handle = bc.cookie_store_for_test(); + let mut jar = jar_handle.lock(); jar.store_response_headers( &Url::parse(&final_url).unwrap(), &[( @@ -989,7 +992,7 @@ async fn main_document_critical_client_hint_restart_matches_chromium_network_cha let page_url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1152,12 +1155,13 @@ async fn main_document_multi_hop_redirect_preserves_cookie_downgrade_report() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.set_target_url(format!("http://localhost:{}/origin", addr.port())); { - let mut jar = bc.cookie_store_for_test().lock(); + let jar_handle = bc.cookie_store_for_test(); + let mut jar = jar_handle.lock(); jar.store_response_headers( &Url::parse(&final_url).unwrap(), &[( @@ -1269,7 +1273,7 @@ async fn browser_initiated_redirect_does_not_fabricate_renderer_navigation_reque let start_url = format!("http://{addr}/start"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1339,7 +1343,7 @@ async fn main_document_request_cookie_report_is_snapshotted_before_response_sets let page_url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1399,7 +1403,7 @@ async fn first_main_document_transport_failure_commits_error_document() { let url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); assert!(bc.assign_attached_session_to_target("TID-1", "SID-2".to_owned())); @@ -1637,7 +1641,7 @@ async fn main_document_navigation_failure_emits_one_failed_and_finished_terminal let url = format!("http://{addr}/missing"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/domains/network/tests/response_body.rs b/moli-protocol/src/domains/network/tests/response_body.rs index 2253add27c..67d88b9fb1 100644 --- a/moli-protocol/src/domains/network/tests/response_body.rs +++ b/moli-protocol/src/domains/network/tests/response_body.rs @@ -27,7 +27,7 @@ fn bidi_network_context(session_id: &str) -> crate::devtools_runtime::DevToolsCo #[tokio::test(flavor = "multi_thread")] async fn get_response_body_respects_recorded_session_visibility() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-primary".to_owned()); bc.active_page_target_mut() @@ -67,7 +67,7 @@ async fn get_response_body_respects_recorded_session_visibility() { #[tokio::test(flavor = "multi_thread")] async fn get_response_body_requires_calling_session_network_listener() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-primary".to_owned()); bc.active_page_target_mut() @@ -118,7 +118,7 @@ async fn get_response_body_requires_calling_session_network_listener() { #[tokio::test(flavor = "multi_thread")] async fn get_response_body_reports_pending_body_as_existing_without_data() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -140,7 +140,7 @@ async fn get_response_body_reports_pending_body_as_existing_without_data() { #[tokio::test(flavor = "multi_thread")] async fn get_response_body_ready_body_replaces_pending_body() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -169,8 +169,8 @@ async fn get_response_body_rejects_bodies_over_materialization_limit() { let mut config = FetchConfig::default(); config.set_connection_limits(None, None, Some(4)); let mut ctx = TestContext::new(); - ctx.conn = CdpConnection::new_with_fetch_config(config); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + ctx.conn = crate::test_support::connection_with_fetch_config(config); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -193,7 +193,7 @@ async fn get_response_body_rejects_bodies_over_materialization_limit() { #[tokio::test(flavor = "multi_thread")] async fn get_response_body_reports_default_single_resource_budget_eviction() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -220,7 +220,7 @@ async fn get_response_body_reports_default_single_resource_budget_eviction() { #[tokio::test(flavor = "multi_thread")] async fn get_response_body_base64_encodes_non_utf8_captured_bytes() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -247,7 +247,7 @@ async fn get_response_body_base64_encodes_non_utf8_captured_bytes() { #[tokio::test(flavor = "multi_thread")] async fn get_request_post_data_matches_chromium_errors_and_binary_encoding() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.active_page_target_mut() .runtime_slot .enable_primary_network_events(); @@ -331,7 +331,7 @@ async fn get_response_body_returns_partial_body_after_staged_loading_failed() { } let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-1".to_owned()); bc.active_page_target_mut() @@ -451,7 +451,7 @@ async fn get_response_body_returns_partial_body_after_staged_loading_failed() { #[tokio::test(flavor = "multi_thread")] async fn get_request_post_data_respects_recorded_session_visibility() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-primary".to_owned()); bc.active_page_target_mut() @@ -497,7 +497,7 @@ async fn get_request_post_data_respects_recorded_session_visibility() { #[tokio::test(flavor = "multi_thread")] async fn get_request_post_data_returns_main_document_navigation_post_body() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-1".to_owned()); bc.active_page_target_mut() @@ -511,6 +511,7 @@ async fn get_request_post_data_returns_main_document_navigation_post_body() { redirect_headers: None, navigate_id: Some(1), owner: crate::conn::CommandOwnerScope::for_session("SID-1"), + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: crate::conn::NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": LOADER_ID}), ), @@ -554,7 +555,7 @@ async fn get_request_post_data_returns_main_document_navigation_post_body() { #[tokio::test(flavor = "multi_thread")] async fn get_request_post_data_uses_text_projection_while_bidi_collector_keeps_transport_bytes() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-1".to_owned()); bc.active_page_target_mut() @@ -602,6 +603,7 @@ async fn get_request_post_data_uses_text_projection_while_bidi_collector_keeps_t redirect_headers: None, navigate_id: Some(1), owner: crate::conn::CommandOwnerScope::for_session("SID-1"), + web_contents: NavigationDispatchState::detached_web_contents_for_test(), result_projection: crate::conn::NavigationResultProjection::Cdp( json!({"frameId": "TID-1", "loaderId": LOADER_ID}), ), @@ -661,7 +663,7 @@ async fn get_request_post_data_uses_text_projection_while_bidi_collector_keeps_t #[tokio::test(flavor = "multi_thread")] async fn get_network_data_returns_bidi_response_body_bytes() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("bidi-session-1".to_owned()); bc.active_page_target_mut() @@ -998,7 +1000,7 @@ async fn get_network_data_returns_bidi_response_body_bytes() { #[tokio::test(flavor = "multi_thread")] async fn network_data_collectors_gate_get_data_disown_and_remove() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("bidi-session-1".to_owned()); bc.record_captured_response_body( @@ -1314,7 +1316,7 @@ async fn network_data_collectors_gate_get_data_disown_and_remove() { #[tokio::test(flavor = "multi_thread")] async fn network_data_collector_body_persists_after_target_artifact_cleanup() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("bidi-session-1".to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1469,7 +1471,7 @@ async fn network_data_collector_body_persists_after_target_artifact_cleanup() { #[tokio::test(flavor = "multi_thread")] async fn network_data_explicit_collector_prefers_collected_body_over_stale_target_artifact() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("bidi-session-1".to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1551,7 +1553,7 @@ async fn network_data_explicit_collector_prefers_collected_body_over_stale_targe #[tokio::test(flavor = "multi_thread")] async fn network_data_explicit_collector_rejects_unconfigured_data_type() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("bidi-session-1".to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1615,7 +1617,7 @@ async fn network_data_explicit_collector_rejects_unconfigured_data_type() { #[tokio::test(flavor = "multi_thread")] async fn network_data_without_collector_requires_matching_collected_data_type() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("bidi-session-1".to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1687,7 +1689,7 @@ async fn network_data_without_collector_requires_matching_collected_data_type() #[tokio::test(flavor = "multi_thread")] async fn network_data_collector_membership_uses_recorded_target_scope() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("bidi-session-1".to_owned()); bc.register_page_target_url_fixture("TID-other".to_owned(), None, "about:blank".to_owned()); @@ -1806,7 +1808,7 @@ async fn network_data_collector_membership_uses_recorded_target_scope() { #[tokio::test(flavor = "multi_thread")] async fn network_data_collector_gated_body_without_match_is_not_readable() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("bidi-session-1".to_owned()); bc.register_page_target_url_fixture("TID-other".to_owned(), None, "about:blank".to_owned()); @@ -1887,7 +1889,7 @@ async fn network_data_collector_gated_body_without_match_is_not_readable() { #[tokio::test(flavor = "multi_thread")] async fn get_network_data_reports_unimplemented_or_missing_data_with_bidi_errors() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.record_pending_response_body("REQ-pending".to_owned(), [None::]); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1945,7 +1947,7 @@ async fn get_network_data_reports_unimplemented_or_missing_data_with_bidi_errors #[tokio::test(flavor = "multi_thread")] async fn get_response_body_rejects_invalid_params() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({ "id": 3, @@ -1985,7 +1987,7 @@ async fn main_document_navigation_get_response_body_preserves_binary_bytes() { let url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2075,7 +2077,7 @@ fetch('/binary') let page_url = format!("http://{addr}/page"); let binary_url = format!("http://{addr}/binary"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2202,7 +2204,7 @@ fetch("/upload", {method: "POST", body: formData}) let page_url = format!("http://{addr}/page"); let upload_url = format!("http://{addr}/upload"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/domains/network/tests/runtime.rs b/moli-protocol/src/domains/network/tests/runtime.rs index 1e7f024056..58a214a1f8 100644 --- a/moli-protocol/src/domains/network/tests/runtime.rs +++ b/moli-protocol/src/domains/network/tests/runtime.rs @@ -79,19 +79,18 @@ async fn runtime_capture_without_network_listener_does_not_advance_subresource_c let page_url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); ctx.install_navigation_fixture_for_session_owner(&page_url, Some("SID-1")) .await; + let context = ctx.conn.browser_context.as_ref().unwrap(); + let document = context.document_handle_for_target("TID-1").unwrap(); assert_eq!( - ctx.conn - .browser_context - .as_ref() - .unwrap() - .target_subresource_network_records("TID-1") + context + .document_subresource_network_records(document) .unwrap() .len(), 1 @@ -146,7 +145,7 @@ async fn runtime_script_uses_document_referer_and_script_cdp_initiator() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -247,7 +246,7 @@ async fn runtime_fetch_post_body_is_available_by_network_request_id() { let page_url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-1".to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -336,7 +335,7 @@ async fn runtime_form_post_navigation_body_is_available_by_network_request_id() let page_url = format!("http://{addr}/page"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-1".to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -446,7 +445,7 @@ async fn assert_websocket_runtime_activity_events(version: axum::http::Version) let page_url = format!("http://{addr}/page"); let socket_literal = serde_json::to_string(&socket_url).unwrap(); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-1".to_owned()); ctx.conn.insert_browser_context(bc); @@ -612,7 +611,7 @@ async fn rejected_websocket_handshake_emits_frame_error_then_closed() { let socket_url = format!("ws://{addr}/rejected"); let socket_literal = serde_json::to_string(&socket_url).unwrap(); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.attach_active_session("SID-1".to_owned()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -759,7 +758,7 @@ document.write('", - ) - .await - .expect("test page should load"); - let _ = bc.replace_active_page_for_test(Some(page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state .console_enabled = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; let mut prepared_slot = observable_backlog_activity_outputs(&ctx.conn, None).into_prepared_slot(); @@ -600,23 +615,21 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_console_backlog_range_is_bounded_by_prepare_time_watermark() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,observable-prepared-range-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); - let page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("test page should load"); - let _ = bc.replace_active_page_for_test(Some(page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state .console_enabled = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; let mut prepared_slot = observable_backlog_activity_outputs(&ctx.conn, None).into_prepared_slot(); @@ -662,40 +675,35 @@ mod tests { async fn prepared_console_backlog_range_is_bound_to_document_id() { let mut ctx = TestContext::new(); let page_url = "data:text/html,observable-prepared-attachment-test"; - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url(page_url.to_owned()); bc.attach_active_session("SID-1".to_owned()); - let first_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("first test page should load"); - let _ = bc.replace_loaded_page(Some(first_page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state .console_enabled = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; let mut prepared_slot = observable_backlog_activity_outputs(&ctx.conn, None).into_prepared_slot(); - let second_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("second test page should load"); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-1"), + ) + .await; { let bc = ctx .conn .browser_context .as_mut() .expect("browser context should remain loaded"); - let _ = bc.replace_loaded_page(Some(second_page)); bc.set_target_url(page_url.to_owned()); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] @@ -725,26 +733,10 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_console_backlog_emits_for_background_owner_session() { let mut ctx = TestContext::new(); - let active_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("active test page should load"); - let background_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("background test page should load"); - - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.set_target_url("data:text/html,active-owner".to_owned()); bc.attach_active_session("SID-active".to_owned()); - let _ = bc.replace_loaded_page(Some(active_page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state @@ -755,13 +747,22 @@ mod tests { TargetIdentityState::with_url("data:text/html,background-owner".to_owned()), TargetPageSlot::empty_for_test_fixture(), ); - bc.replace_target_page_for_test("TID-background", Some(background_page)); bc.background_target_mut("TID-background") .expect("background target must exist") .devtools_sessions[moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state .console_enabled = true; ctx.conn.install_browser_context_fixture_for_test(bc); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-active"), + ) + .await; + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-background"), + ) + .await; assert_eq!( observable_backlog_activity_outputs(&ctx.conn, Some("SID-background")) @@ -815,26 +816,10 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn target_activation_preserves_page_attachment_ownership_without_retargeting_output() { let mut ctx = TestContext::new(); - let active_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("active test page should load"); - let activated_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("activated test page should load"); - - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.set_target_url("data:text/html,old-active-owner".to_owned()); bc.attach_active_session("SID-active".to_owned()); - let _ = bc.replace_loaded_page(Some(active_page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state @@ -845,13 +830,22 @@ mod tests { TargetIdentityState::with_url("data:text/html,activated-owner".to_owned()), TargetPageSlot::empty_for_test_fixture(), ); - bc.replace_target_page_for_test("TID-activated", Some(activated_page)); bc.background_target_mut("TID-activated") .expect("background target must exist") .devtools_sessions[moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state .console_enabled = true; ctx.conn.install_browser_context_fixture_for_test(bc); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-active"), + ) + .await; + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-activated"), + ) + .await; let (old_active_attachment, activated_attachment) = { let bc = ctx.conn.browser_context.as_ref().expect("browser context"); @@ -868,15 +862,14 @@ mod tests { observable_backlog_activity_outputs(&ctx.conn, None).into_prepared_slot(); let mut old_active_owner_prepared = old_active_prepared.clone(); - assert!( - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .select_page_target_async("TID-activated") - .await - .expect("target activation should succeed") - ); + let handle = ctx + .conn + .browser_web_contents_for_target("TID-activated") + .expect("target WebContents"); + ctx.conn + .select_browser_web_contents_async(handle) + .await + .expect("target activation should succeed"); { let bc = ctx.conn.browser_context.as_ref().expect("browser context"); @@ -941,21 +934,21 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_runtime_observable_range_is_bounded_by_source_summary() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,observable-runtime-prepared-range-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("test page should load"); - let _ = bc.replace_loaded_page(Some(page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state .runtime_frontend_enabled = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; ctx.sent.clear(); ctx.conn .evaluate_runtime_expression_with_await_async("console.warn('runtime prepared')", false) @@ -1000,21 +993,21 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_runtime_observable_uses_source_payload_without_page_readback() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,observable-runtime-source-payload-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("test page should load"); - let _ = bc.replace_loaded_page(Some(page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state .runtime_frontend_enabled = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; ctx.sent.clear(); let snapshot = RendererPageDiagnosticsSnapshot::from_runtime_observable_source( @@ -1056,21 +1049,21 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_runtime_observable_uses_stored_source_queue_payload() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,observable-runtime-stored-source-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("test page should load"); - let _ = bc.replace_loaded_page(Some(page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state .runtime_frontend_enabled = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; let snapshot = RendererPageDiagnosticsSnapshot::from_runtime_observable_source( RendererRuntimeObservableSourceSummary::from_source_messages( @@ -1111,16 +1104,10 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn renderer_runtime_agent_ownership_suppresses_runtime_console_source_fallback() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,observable-runtime-agent-owner-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("test page should load"); - let _ = bc.replace_loaded_page(Some(page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state @@ -1129,7 +1116,13 @@ mod tests { [moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state .renderer_runtime_agent_owns_page_console_api_events = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; let console_only_snapshot = RendererPageDiagnosticsSnapshot::from_runtime_observable_source( RendererRuntimeObservableSourceSummary::from_source_messages( @@ -1187,23 +1180,21 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn runtime_observable_drain_requires_prepared_source_payload() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,observable-runtime-no-source-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); - let page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("test page should load"); - let _ = bc.replace_loaded_page(Some(page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state .runtime_frontend_enabled = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; { let context = ctx.conn.browser_context.as_mut().unwrap(); let target_id = context.active_target_id_owned().unwrap(); @@ -1227,21 +1218,21 @@ mod tests { async fn prepared_runtime_observable_source_payload_is_bound_to_document_id() { let mut ctx = TestContext::new(); let page_url = "data:text/html,observable-runtime-source-payload-attachment-test"; - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url(page_url.to_owned()); bc.attach_active_session("SID-1".to_owned()); - let first_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("first test page should load"); - let _ = bc.replace_loaded_page(Some(first_page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state .runtime_frontend_enabled = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; let snapshot = RendererPageDiagnosticsSnapshot::from_runtime_observable_source( RendererRuntimeObservableSourceSummary::from_source_messages( @@ -1253,18 +1244,17 @@ mod tests { let mut prepared_slot = observable_source_activity_outputs(&mut ctx.conn, &snapshot, None).into_prepared_slot(); - let second_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("second test page should load"); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-1"), + ) + .await; { let bc = ctx .conn .browser_context .as_mut() .expect("browser context should remain loaded"); - let _ = bc.replace_loaded_page(Some(second_page)); bc.set_target_url(page_url.to_owned()); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] @@ -1289,21 +1279,21 @@ mod tests { async fn prepared_runtime_observable_range_is_bound_to_document_id() { let mut ctx = TestContext::new(); let page_url = "data:text/html,observable-runtime-attachment-test"; - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url(page_url.to_owned()); bc.attach_active_session("SID-1".to_owned()); - let first_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("first test page should load"); - let _ = bc.replace_loaded_page(Some(first_page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state .runtime_frontend_enabled = true; - ctx.conn.install_browser_context_fixture_for_test(bc); + install_navigation( + &mut ctx, + bc, + "SID-1", + "data:text/html,", + ) + .await; ctx.sent.clear(); ctx.conn .evaluate_runtime_expression_with_await_async("console.warn('runtime old')", false) @@ -1315,18 +1305,17 @@ mod tests { observable_source_activity_outputs(&mut ctx.conn, &old_snapshot, None) .into_prepared_slot(); - let second_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("second test page should load"); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-1"), + ) + .await; { let bc = ctx .conn .browser_context .as_mut() .expect("browser context should remain loaded"); - let _ = bc.replace_loaded_page(Some(second_page)); bc.set_target_url(page_url.to_owned()); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] @@ -1362,8 +1351,8 @@ mod tests { #[tokio::test] async fn observable_emission_plan_prepares_runtime_payloads_and_advances_cursors() { - let mut conn = crate::conn::CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let runtime_plan = ObservableActivityEmissionPlan::from_runtime_prepared_items( ObservableRuntimePreparedItems::for_test( vec![ @@ -1433,8 +1422,8 @@ mod tests { #[tokio::test] async fn observable_runtime_plan_advances_lifecycle_cursor_without_emittable_items() { - let mut conn = crate::conn::CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let runtime_plan = ObservableActivityEmissionPlan::from_runtime_prepared_items( ObservableRuntimePreparedItems::for_test( Vec::new(), diff --git a/moli-protocol/src/domains/observable_output/output_queue.rs b/moli-protocol/src/domains/observable_output/output_queue.rs index bad2964902..c0ed34e135 100644 --- a/moli-protocol/src/domains/observable_output/output_queue.rs +++ b/moli-protocol/src/domains/observable_output/output_queue.rs @@ -1366,15 +1366,16 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn observable_source_queue_snapshot_can_be_read_without_resyncing_renderer_snapshot() { let mut ctx = TestContext::new(); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("test page should load"); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); - let mut source_context = BrowserContext::new_with_page_for_test("BID-queue", "TID-queue"); - source_context.replace_active_page_for_test(Some(page)); - source_context.set_active_document_fixture_for_test(45); + let mut bc = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); + let mut source_context = ctx.conn.new_browser_context_fixture_for_test("BID-queue"); + source_context.set_active_target_id("TID-queue".to_owned()); + ctx.conn + .install_browser_context_fixture_for_test(source_context); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + None, + ) + .await; let source_snapshot = renderer_source_snapshot( RendererRuntimeObservableSourceSummary::from_source_messages( Some(5), @@ -1387,8 +1388,10 @@ mod tests { Vec::new(), ), ); + let source_context = ctx.conn.browser_context.as_mut().unwrap(); + source_context.set_active_document_fixture_for_test(45); let _ = TargetObservableOutputQueue::from_target_source_snapshot( - &mut source_context, + source_context, "TID-queue", "http://example.test/stored-source".to_owned(), &source_snapshot, @@ -1762,15 +1765,16 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn observable_queue_snapshot_is_owned_by_target_runtime_slot() { let mut ctx = TestContext::new(); - let page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("test page should load"); - let mut source_context = BrowserContext::new_with_page_for_test("BID-queue", "TID-queue"); - source_context.replace_active_page_for_test(Some(page)); + let mut source_context = ctx.conn.new_browser_context_fixture_for_test("BID-queue"); + source_context.set_active_target_id("TID-queue".to_owned()); + ctx.conn + .install_browser_context_fixture_for_test(source_context); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + None, + ) + .await; + let source_context = ctx.conn.browser_context.as_mut().unwrap(); let snapshot = source_context .active_page_target() @@ -1797,7 +1801,7 @@ mod tests { "runtime slot DTO should capture loaded-page lifecycle output" ); - let queue = TargetObservableOutputQueue::from_target(&source_context, "TID-queue") + let queue = TargetObservableOutputQueue::from_target(source_context, "TID-queue") .expect("observable queue should be built from the runtime slot snapshot"); assert_eq!( queue.console_message_count(), @@ -1809,23 +1813,16 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn observable_queue_snapshot_tracks_runtime_slot_page_replacement() { let mut ctx = TestContext::new(); - let first_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("first test page should load"); - let second_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("second test page should load"); - let mut source_context = BrowserContext::new_with_page_for_test("BID-queue", "TID-queue"); - - let _ = source_context.replace_active_page_for_test(Some(first_page)); + let mut source_context = ctx.conn.new_browser_context_fixture_for_test("BID-queue"); + source_context.set_active_target_id("TID-queue".to_owned()); + ctx.conn + .install_browser_context_fixture_for_test(source_context); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + None, + ) + .await; + let source_context = ctx.conn.browser_context.as_ref().unwrap(); assert_eq!( source_context .active_page_target() @@ -1844,7 +1841,12 @@ mod tests { "runtime slot observable snapshot should track the current loaded page" ); - let _ = source_context.replace_active_page_for_test(Some(second_page)); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + None, + ) + .await; + let source_context = ctx.conn.browser_context.as_mut().unwrap(); assert_eq!( source_context .active_page_target() diff --git a/moli-protocol/src/domains/observable_output/presence.rs b/moli-protocol/src/domains/observable_output/presence.rs index c07f656ee2..ab34d710b1 100644 --- a/moli-protocol/src/domains/observable_output/presence.rs +++ b/moli-protocol/src/domains/observable_output/presence.rs @@ -555,7 +555,7 @@ mod tests { RendererRuntimeObservableSourceSummary, RuntimeConsoleMessageSnapshot, }; - use crate::conn::{BrowserContext, CommandOwnerScope}; + use crate::conn::CommandOwnerScope; use crate::domains::observable_output::output_queue::TargetObservableOutputQueue; use crate::testing::TestContext; @@ -566,8 +566,8 @@ mod tests { }; fn runtime_lifecycle_error_audience(enabled_session_ids: &[&str]) -> Vec> { - let mut conn = crate::conn::CdpConnection::default(); - let mut bc = BrowserContext::new("BID-runtime-lifecycle".to_owned()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-runtime-lifecycle".to_owned()); bc.set_active_target_id("TID-runtime-lifecycle".to_owned()); bc.set_target_url("https://example.test/runtime-lifecycle".to_owned()); bc.attach_active_session("SID-runtime-a".to_owned()); @@ -602,8 +602,8 @@ mod tests { #[test] fn observable_source_outputs_own_runtime_observable_presence() { - let mut conn = crate::conn::CdpConnection::default(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.set_active_document_fixture_for_test(1); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] @@ -648,8 +648,8 @@ mod tests { #[test] fn observable_source_sync_is_independent_from_runtime_emission() { - let mut conn = crate::conn::CdpConnection::default(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.set_target_url("data:text/html,console-only-source".to_owned()); bc.set_active_document_fixture_for_test(1); bc.active_page_target_mut().devtools_sessions @@ -701,8 +701,8 @@ mod tests { #[test] fn observable_source_outputs_require_concrete_runtime_prepared_items() { - let mut conn = crate::conn::CdpConnection::default(); - let mut bc = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_page_target_fixture_for_test("BID-1", "TID-1"); bc.set_active_document_fixture_for_test(1); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] @@ -729,18 +729,10 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn observable_prepared_outputs_keep_console_api_out_of_log_backlog() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,observable-backlog-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); - let page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("test page should load"); - let _ = bc.replace_active_page_for_test(Some(page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .console_output_session_state @@ -750,6 +742,11 @@ mod tests { .page_session_state .log_enabled = true; ctx.conn.install_browser_context_fixture_for_test(bc); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-1"), + ) + .await; let outputs = observable_backlog_activity_outputs(&ctx.conn, None); assert_eq!( diff --git a/moli-protocol/src/domains/observable_output/runtime_emission.rs b/moli-protocol/src/domains/observable_output/runtime_emission.rs index e62c563250..b53e3ee1af 100644 --- a/moli-protocol/src/domains/observable_output/runtime_emission.rs +++ b/moli-protocol/src/domains/observable_output/runtime_emission.rs @@ -170,7 +170,7 @@ mod tests { RuntimeConsoleMessageSnapshot, }; - use crate::conn::{BrowserContext, DocumentId}; + use crate::conn::DocumentId; use super::{ advance_runtime_observable_cursors_to_current_for_session_owner, @@ -213,13 +213,13 @@ mod tests { #[test] fn prepared_source_presence_requires_runtime_frontend_enabled_and_unemitted_summary() { - let mut conn = crate::conn::CdpConnection::default(); + let mut conn = crate::test_support::connection(); assert!( retain_unemitted_runtime_observable_prepared_source(&conn, prepared_source()).is_none(), "source presence should require a browser context" ); - conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); assert!( retain_unemitted_runtime_observable_prepared_source(&conn, prepared_source()).is_none(), "source presence should require Runtime.enable" @@ -262,8 +262,8 @@ mod tests { #[tokio::test] async fn runtime_observable_cursor_advance_uses_stored_source_queue_without_page_readback() { - let mut conn = crate::conn::CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test("BID-1", "TID-1")); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_page_target_fixture_for_test("BID-1", "TID-1")); let source_snapshot = RendererPageDiagnosticsSnapshot::from_runtime_observable_source( RendererRuntimeObservableSourceSummary::from_source_messages( Some(7), @@ -313,10 +313,11 @@ mod tests { #[test] fn runtime_observable_attached_session_uses_own_browser_context() { - let mut conn = crate::conn::CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-active".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = + Some(conn.new_browser_context_fixture_for_test("BID-active".to_owned())); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = conn.new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive .active_page_target_mut() diff --git a/moli-protocol/src/domains/page.rs b/moli-protocol/src/domains/page.rs index 50e8ae332e..3fdc148380 100644 --- a/moli-protocol/src/domains/page.rs +++ b/moli-protocol/src/domains/page.rs @@ -37,8 +37,13 @@ use url::Url; use super::input; use crate::conn::{ BackgroundProtocolEvent, CapturedBody, CdpSessionRoute, CommandDispatchContext, - CommandOwnerScope, NETWORK_ERROR_PAGE_URL, PageLifecycleEventsEnableResult, - PageScreencastConfig, PageScreencastFormat, + CommandOwnerScope, CompletedCaptureDocumentImage, CompletedCaptureDocumentScreencastFrame, + CompletedCaptureDocumentSnapshot, CompletedDocumentCspBypassUpdate, + CompletedDocumentLifecycleStop, CompletedNavigationHistoryReset, CompletedSetDocumentContent, + NETWORK_ERROR_PAGE_URL, PageLifecycleEventsEnableResult, PageScreencastConfig, + PageScreencastFormat, PendingCaptureDocumentImage, PendingCaptureDocumentScreencastFrame, + PendingCaptureDocumentSnapshot, PendingDocumentCspBypassUpdate, PendingDocumentLifecycleStop, + PendingNavigationHistoryReset, PendingSetDocumentContent, }; use crate::conn::{CdpConnection, Cmd, EmulatedViewportSurface}; pub(crate) use crate::conn::{DEFAULT_LOADER_ID as LOADER_ID, monotonic_timestamp_seconds}; @@ -78,14 +83,13 @@ pub(in crate::domains) async fn dispose_primary_session_target_state_async( session_key: moli_page_types::DevToolsSessionKey::Primary, } = plan.target() { - let reset_result = match conn.browser_context_by_id_mut(browser_context_id) { - Some(browser_context) => { - browser_context - .reset_primary_page_session_target_state_async(target_id, session_id) - .await - } - None => Ok(false), - }; + let reset_result = conn + .reset_primary_page_session_target_state_async( + browser_context_id, + target_id, + session_id, + ) + .await; reset_result.and_then(|found| { anyhow::ensure!( found, @@ -169,6 +173,14 @@ const PRINT_TO_PDF_LAYOUT_DISABLED_MESSAGE: &str = const PRINT_TO_PDF_UNSUPPORTED_MESSAGE: &str = "Page.printToPDF is not supported: PDF generation is not implemented."; +fn capture_document_image_error(error: String) -> String { + if error == "Document changed" { + "capture screenshot completed for a stale renderer attachment".into() + } else { + error + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum FrameTreeCommandOutputKind { FrameTree, @@ -196,6 +208,7 @@ enum PendingPageCommandKind { AddScriptToEvaluateOnNewDocument(preload::PendingAddScriptToEvaluateOnNewDocumentCommand), GetFrameTree { output_kind: FrameTreeCommandOutputKind, + document: moli_core::browser::DocumentHandle, target_id: String, target_loader_id: String, target_url: String, @@ -208,27 +221,26 @@ enum PendingPageCommandKind { SearchInResource(resource_search::PendingSearchInResourceCommand), GetAppManifest(app_manifest::PendingGetAppManifestCommand), ResetNavigationHistory { - page: crate::conn::TargetPageResidenceIdentity, - pending: PendingPageCommand, + pending: PendingNavigationHistoryReset, }, SetDocumentContent { - pending: PendingPageCommand, + pending: PendingSetDocumentContent, }, SetBypassContentSecurityPolicy { - pending: PendingPageCommand, + pending: PendingDocumentCspBypassUpdate, }, SameDocumentNavigate(Box), CaptureSnapshot { - pending: PendingPageCommand, + pending: PendingCaptureDocumentSnapshot, }, GetLayoutMetrics { pending: PendingPageCommand, }, CaptureScreenshot { - pending: PendingPageCommand, + pending: PendingCaptureDocumentImage, }, PrintToPdf { - pending: PendingPageCommand, + pending: PendingCaptureDocumentImage, options: pdf::RasterPdfOptions, transfer_mode: DevToolsPrintToPdfTransferMode, }, @@ -238,9 +250,15 @@ enum PendingPageCommandKind { ContinueNavigationWithoutRequestPause( Box, ), - StopLoading, - Crash, - Close, + StopLoading { + pending: Option, + }, + Crash { + web_contents: Option, + }, + Close { + web_contents: Option, + }, CreateIsolatedWorld(preload::PendingCreateIsolatedWorldCommand), } @@ -265,6 +283,7 @@ enum CompletedPageCommandKind { AddScriptToEvaluateOnNewDocument(preload::CompletedAddScriptToEvaluateOnNewDocumentCommand), GetFrameTree { output_kind: FrameTreeCommandOutputKind, + document: moli_core::browser::DocumentHandle, target_id: String, target_loader_id: String, target_url: String, @@ -277,27 +296,26 @@ enum CompletedPageCommandKind { SearchInResource(Box), GetAppManifest(Box), ResetNavigationHistory { - page: crate::conn::TargetPageResidenceIdentity, - completed: Box>, + completed: CompletedNavigationHistoryReset, }, SetDocumentContent { - completed: Box>, + completed: CompletedSetDocumentContent, }, SetBypassContentSecurityPolicy { - completed: Box>, + completed: CompletedDocumentCspBypassUpdate, }, SameDocumentNavigate(Box), CaptureSnapshot { - completed: Box>, + completed: CompletedCaptureDocumentSnapshot, }, GetLayoutMetrics { completed: Box>, }, CaptureScreenshot { - completed: Box>, + completed: CompletedCaptureDocumentImage, }, PrintToPdf { - completed: Box>, + completed: CompletedCaptureDocumentImage, options: pdf::RasterPdfOptions, transfer_mode: DevToolsPrintToPdfTransferMode, }, @@ -307,9 +325,15 @@ enum CompletedPageCommandKind { ContinueNavigationWithoutRequestPause( Box, ), - StopLoading, - Crash, - Close, + StopLoading { + completed: Option, + }, + Crash { + web_contents: Option, + }, + Close { + web_contents: Option, + }, CreateIsolatedWorld(Box), } @@ -328,25 +352,32 @@ impl CompletedPageCommandKind { Self::AppendDefaultDocumentStartScript { completed, .. } | Self::RemoveDocumentStartScript { completed } | Self::GetFrameTree { completed, .. } - | Self::ResetNavigationHistory { completed, .. } - | Self::SetDocumentContent { completed } - | Self::SetBypassContentSecurityPolicy { completed } - | Self::CaptureSnapshot { completed } | Self::GetLayoutMetrics { completed } - | Self::CaptureScreenshot { completed } - | Self::PrintToPdf { completed, .. } => direct(completed), + => direct(completed), + Self::ResetNavigationHistory { completed } => { + completed.renderer_output_predecessor() + } + Self::SetBypassContentSecurityPolicy { completed } => { + completed.renderer_output_predecessor() + } + Self::SetDocumentContent { completed } => completed.renderer_output_predecessor(), + Self::CaptureSnapshot { completed } => completed.renderer_output_predecessor(), + Self::CaptureScreenshot { completed } + | Self::PrintToPdf { completed, .. } => completed.renderer_output_predecessor(), Self::SearchInResource(completed) => completed.renderer_output_predecessor(), Self::GetAppManifest(completed) => completed.renderer_output_predecessor(), Self::SameDocumentNavigate(completed) => completed.renderer_output_predecessor(), Self::TraverseSameDocumentHistory(completed) => completed.renderer_output_predecessor(), Self::ChildFrameNavigate(completed) => completed.renderer_output_predecessor(), + Self::StopLoading { completed } => completed + .as_ref() + .and_then(CompletedDocumentLifecycleStop::renderer_output_predecessor), Self::BringToFront { .. } | Self::AddScriptToEvaluateOnNewDocument(_) | Self::Navigate(_) | Self::ContinueNavigationWithoutRequestPause(_) - | Self::StopLoading - | Self::Crash - | Self::Close + | Self::Crash { .. } + | Self::Close { .. } // createIsolatedWorld may restart on a replacement renderer attachment. Its // completion handler records the fence only after rejecting a stale completion, // so an abandoned stream cannot become the predecessor of the final response. @@ -391,9 +422,9 @@ impl PendingPageCommandDispatch { | PendingPageCommandKind::TraverseSameDocumentHistory(_) | PendingPageCommandKind::ChildFrameNavigate(_) | PendingPageCommandKind::ContinueNavigationWithoutRequestPause(_) - | PendingPageCommandKind::StopLoading - | PendingPageCommandKind::Crash - | PendingPageCommandKind::Close + | PendingPageCommandKind::StopLoading { .. } + | PendingPageCommandKind::Crash { .. } + | PendingPageCommandKind::Close { .. } | PendingPageCommandKind::CreateIsolatedWorld(_) => None, } } @@ -424,6 +455,7 @@ impl PendingPageCommandDispatch { } PendingPageCommandKind::GetFrameTree { output_kind, + document, target_id, target_loader_id, target_url, @@ -434,6 +466,7 @@ impl PendingPageCommandDispatch { pending, } => CompletedPageCommandKind::GetFrameTree { output_kind, + document, target_id, target_loader_id, target_url, @@ -449,20 +482,19 @@ impl PendingPageCommandDispatch { PendingPageCommandKind::GetAppManifest(pending) => { CompletedPageCommandKind::GetAppManifest(Box::new(pending.wait().await)) } - PendingPageCommandKind::ResetNavigationHistory { page, pending } => { + PendingPageCommandKind::ResetNavigationHistory { pending } => { CompletedPageCommandKind::ResetNavigationHistory { - page, - completed: Box::new(pending.wait().await.map_err(|error| error.to_string())), + completed: pending.wait().await, } } PendingPageCommandKind::SetDocumentContent { pending } => { CompletedPageCommandKind::SetDocumentContent { - completed: Box::new(pending.wait().await.map_err(|error| error.to_string())), + completed: pending.wait().await, } } PendingPageCommandKind::SetBypassContentSecurityPolicy { pending } => { CompletedPageCommandKind::SetBypassContentSecurityPolicy { - completed: Box::new(pending.wait().await.map_err(|error| error.to_string())), + completed: pending.wait().await, } } PendingPageCommandKind::SameDocumentNavigate(pending) => { @@ -470,7 +502,7 @@ impl PendingPageCommandDispatch { } PendingPageCommandKind::CaptureSnapshot { pending } => { CompletedPageCommandKind::CaptureSnapshot { - completed: Box::new(pending.wait().await.map_err(|error| error.to_string())), + completed: pending.wait().await, } } PendingPageCommandKind::GetLayoutMetrics { pending } => { @@ -480,7 +512,7 @@ impl PendingPageCommandDispatch { } PendingPageCommandKind::CaptureScreenshot { pending } => { CompletedPageCommandKind::CaptureScreenshot { - completed: Box::new(pending.wait().await.map_err(|error| error.to_string())), + completed: pending.wait().await, } } PendingPageCommandKind::PrintToPdf { @@ -488,7 +520,7 @@ impl PendingPageCommandDispatch { options, transfer_mode, } => CompletedPageCommandKind::PrintToPdf { - completed: Box::new(pending.wait().await.map_err(|error| error.to_string())), + completed: pending.wait().await, options, transfer_mode, }, @@ -508,9 +540,20 @@ impl PendingPageCommandDispatch { pending.wait().await, )) } - PendingPageCommandKind::StopLoading => CompletedPageCommandKind::StopLoading, - PendingPageCommandKind::Crash => CompletedPageCommandKind::Crash, - PendingPageCommandKind::Close => CompletedPageCommandKind::Close, + PendingPageCommandKind::StopLoading { pending } => { + CompletedPageCommandKind::StopLoading { + completed: match pending { + Some(pending) => Some(pending.wait().await), + None => None, + }, + } + } + PendingPageCommandKind::Crash { web_contents } => { + CompletedPageCommandKind::Crash { web_contents } + } + PendingPageCommandKind::Close { web_contents } => { + CompletedPageCommandKind::Close { web_contents } + } PendingPageCommandKind::CreateIsolatedWorld(pending) => { CompletedPageCommandKind::CreateIsolatedWorld(Box::new(pending.wait().await)) } @@ -1298,7 +1341,6 @@ impl PageOutputProjectionStep { input::emit_download_activity_background_events_async( conn, &mut events, - &owner, prepared_outputs, context.command, ) @@ -1591,11 +1633,13 @@ fn disable_page_command(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> CommandOutpu if !conn.disable_page_domain_for_session_owner(cmd.session_id) { return CommandOutputPlan::error(-31998, "BrowserContextNotLoaded"); } - let dialog_handler_enabled = conn + let browser_context_id = conn .navigation_load_inputs_for_session_owner(cmd.session_id) - .renderer_runtime - .runtime() - .javascript_dialog_handler_enabled(); + .browser_context_id; + let dialog_handler_enabled = browser_context_id + .as_deref() + .and_then(|id| conn.browser_context_by_id(id)) + .is_some_and(crate::conn::BrowserContext::javascript_dialog_handler_enabled); if let Err(error) = start_set_javascript_dialog_handler_enabled(conn, cmd.session_id, dialog_handler_enabled) { @@ -1613,23 +1657,10 @@ fn start_set_javascript_dialog_handler_enabled( enabled: bool, ) -> Result<(), String> { let owner = CommandOwnerScope::capture(conn, session_id); - let route = conn - .loaded_document_owner_identity_for_owner(&owner) - .or_else(|| { - let context = conn.browser_context.as_ref()?; - let target_id = context.active_target_id()?; - context - .target_has_loaded_page(target_id) - .then(|| (context.id.clone(), target_id.to_owned())) - }); - if let Some((context_id, target_id)) = route - && let Some(context) = conn.browser_context_by_id(&context_id) - { - return context - .start_set_javascript_dialog_handler_enabled_for_target(&target_id, enabled) - .map(|_| ()); - } - Ok(()) + let Ok(document) = conn.loaded_browser_document_for_owner(&owner) else { + return Ok(()); + }; + conn.set_document_javascript_dialog_handler_enabled(document, enabled) } fn set_lifecycle_events_enabled_command( @@ -1725,16 +1756,10 @@ fn try_start_set_bypass_csp_command( )); }; let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id); - let Ok((page_context_id, page_target_id)) = conn.resolve_document_command_owner(&owner_scope) - else { + let Ok(document) = conn.resolve_browser_document_for_owner(&owner_scope) else { return PageCommandTaskStep::Complete(CommandOutputPlan::success()); }; - let page_context = conn - .browser_context_by_id(&page_context_id) - .expect("admitted document context remains registered"); - match page_context - .start_set_bypass_content_security_policy_for_target(&page_target_id, effective_bypass) - { + match conn.start_document_csp_bypass_update(document, effective_bypass) { Ok(pending) => PageCommandTaskStep::Pending(PendingPageCommandDispatch { command_id: cmd.id, owner_scope, @@ -1842,14 +1867,14 @@ pub struct PendingPageScreencastCapture { session_id: Option, generation: i32, owner_scope: CommandOwnerScope, - pending: PendingPageCommand, + pending: Box, } pub struct CompletedPageScreencastCapture { session_id: Option, generation: i32, owner_scope: CommandOwnerScope, - completed: Result, String>, + completed: CompletedCaptureDocumentScreencastFrame, } impl CompletedPageScreencastCapture { @@ -1868,12 +1893,7 @@ impl PendingPageScreencastCapture { session_id: self.session_id, generation: self.generation, owner_scope: self.owner_scope, - completed: self - .pending - .wait() - .await - .map(Box::new) - .map_err(|error| error.to_string()), + completed: (*self.pending).wait().await, } } } @@ -2023,12 +2043,8 @@ impl CdpConnection { max_height: config.max_height(), known_visual_state, }; - let pending = match self.resolve_document_command_owner(&owner_scope) { - Ok((page_context_id, page_target_id)) => match self - .browser_context_by_id(&page_context_id) - .expect("admitted document context remains registered") - .start_capture_screencast_frame_for_target(&page_target_id, request) - { + let pending = match self.resolve_browser_document_for_owner(&owner_scope) { + Ok(document) => match self.start_capture_document_screencast_frame(document, request) { Ok(pending) => pending, Err(error) => { tracing::debug!(?error, "failed to start screencast frame capture"); @@ -2049,7 +2065,7 @@ impl CdpConnection { session_id, generation, owner_scope, - pending, + pending: Box::new(pending), }) } @@ -2070,53 +2086,21 @@ impl CdpConnection { return PageScreencastCaptureCompletion::Stale; } - let frame = match completed { - Ok(completion) => { - let (page_context_id, page_target_id) = - match self.resolve_document_command_owner(&owner_scope) { - Ok(route) => route, - Err(_) => { - let _ = self.complete_page_screencast_capture_for_owner( - &owner_scope, - generation, - false, - ); - return PageScreencastCaptureCompletion::Retry; - } - }; - let page_context = self - .browser_context_by_id_mut(&page_context_id) - .expect("admitted document context remains registered"); - match page_context - .finish_capture_screencast_frame_for_target(&page_target_id, *completion) + let frame = match self.finish_capture_document_screencast_frame(completed) { + Ok(RendererCaptureScreencastFrameReply::Captured(frame)) => frame, + Ok(RendererCaptureScreencastFrameReply::Unchanged) => { + if self.complete_page_screencast_capture_for_owner(&owner_scope, generation, false) + != Some(true) { - Ok(RendererCaptureScreencastFrameReply::Captured(frame)) => frame, - Ok(RendererCaptureScreencastFrameReply::Unchanged) => { - if self.complete_page_screencast_capture_for_owner( - &owner_scope, - generation, - false, - ) != Some(true) - { - return PageScreencastCaptureCompletion::Stale; - } - return PageScreencastCaptureCompletion::Unchanged; - } - Ok( - RendererCaptureScreencastFrameReply::LayoutDisabled - | RendererCaptureScreencastFrameReply::NoDocument, - ) - | Err(_) => { - let _ = self.complete_page_screencast_capture_for_owner( - &owner_scope, - generation, - false, - ); - return PageScreencastCaptureCompletion::Retry; - } + return PageScreencastCaptureCompletion::Stale; } + return PageScreencastCaptureCompletion::Unchanged; } - Err(_) => { + Ok( + RendererCaptureScreencastFrameReply::LayoutDisabled + | RendererCaptureScreencastFrameReply::NoDocument, + ) + | Err(_) => { let _ = self.complete_page_screencast_capture_for_owner( &owner_scope, generation, @@ -2529,9 +2513,7 @@ mod producer_tests { }; use serde_json::{Value, json}; - use crate::conn::{ - BackgroundProtocolEvent, BrowserContext, CdpConnection, CdpTargetFilter, CommandOwnerScope, - }; + use crate::conn::{BackgroundProtocolEvent, CdpConnection, CdpTargetFilter, CommandOwnerScope}; use crate::devtools_runtime::{AutomationEvent, NavigationFrameEventKind}; use crate::domains::activity::{ProtocolOutputPayloads, ProtocolOutputProjectionContext}; use crate::domains::input::{InputPreparedOutputSlot, InputPreparedOutputs}; @@ -2587,13 +2569,18 @@ mod producer_tests { assert!(initial_events.is_empty()); } - fn page_residence_identity_for_test( + async fn page_residence_identity_for_test( conn: &mut CdpConnection, session_id: &str, ) -> crate::conn::TargetPageResidenceIdentity { let owner = crate::conn::CommandOwnerScope::capture(conn, Some(session_id)); - if conn.current_document_id_for_owner(&owner).is_none() { - conn.replace_document_fixture_for_owner_test(&owner); + if conn.resolve_browser_document_for_owner(&owner).is_err() { + conn.install_navigation_fixture_for_session_owner_for_test( + "about:blank", + Some(session_id), + ) + .await + .expect("test target should materialize an exact Browser Document"); } conn.target_page_residence_identity_for_session(Some(session_id)) .expect("test target should expose a Page residence identity") @@ -2733,7 +2720,7 @@ mod producer_tests { url: &str, ) -> TestContext { let mut ctx = TestContext::new(); - let mut context = BrowserContext::new(context_id.into()); + let mut context = ctx.conn.new_browser_context_fixture_for_test(context_id); context.set_active_target_id(target_id); context.attach_active_session(session_id); ctx.conn.install_browser_context_fixture_for_test(context); @@ -2816,12 +2803,12 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn javascript_dialog_drain_consumes_prepared_dialogs_without_page_readback() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active"); bc.attach_active_session("SID-1"); conn.install_browser_context_fixture_for_test(bc); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-1"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-1").await; let source_document = renderer_document_identity_for_test(1, 1); let mut out: Vec = Vec::new(); let mut prepared = @@ -2882,8 +2869,9 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn child_dialog_output_stays_with_its_exact_protocol_attachment() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-dialog-attachment".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-dialog-attachment"); browser_context.set_active_target_id("TID-dialog-attachment"); browser_context.attach_active_session("SID-primary"); assert!( @@ -2893,7 +2881,7 @@ mod producer_tests { ) ); conn.install_browser_context_fixture_for_test(browser_context); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-attached"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-attached").await; let mut prepared = ProtocolOutputPayloads::from_slot(super::PagePreparedOutputSlot::from_outputs( super::PagePreparedOutputs::from_javascript_dialogs_for_test( @@ -2941,8 +2929,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn detached_source_attachment_dismisses_prepared_child_dialog() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-dialog-detached".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-dialog-detached"); browser_context.set_active_target_id("TID-dialog-detached"); browser_context.attach_active_session("SID-primary"); assert!( @@ -2956,7 +2944,7 @@ mod producer_tests { let mut prepared = ProtocolOutputPayloads::from_slot(super::PagePreparedOutputSlot::from_outputs( super::PagePreparedOutputs::from_javascript_dialogs_for_test( - page_residence_identity_for_test(&mut conn, "SID-detached"), + page_residence_identity_for_test(&mut conn, "SID-detached").await, Some("SID-detached"), javascript_dialog_scope_for_test(&conn, "SID-detached"), "TID-dialog-detached", @@ -2997,8 +2985,9 @@ mod producer_tests { async fn pending_popup_dialog_rejects_a_retired_source_attachment() { const POPUP_ID: u64 = 76; - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-popup-stale-source".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-popup-stale-source"); browser_context.set_active_target_id("TID-popup-stale-source"); browser_context.attach_active_session("SID-primary"); assert!( @@ -3009,7 +2998,7 @@ mod producer_tests { ); conn.install_browser_context_fixture_for_test(browser_context); conn.set_auto_attach_owner(None, true, false, CdpTargetFilter::default_auto_attach()); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-source"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-source").await; let source_document = renderer_document_identity_for_test(1, 1); let completion = RendererJavaScriptDialogCompletion::pending(); let mut dialog_output = @@ -3094,13 +3083,13 @@ mod producer_tests { async fn lightweight_popup_dialog_waits_for_and_uses_popup_attachment() { const POPUP_ID: u64 = 77; - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-popup-dialog".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-popup-dialog"); browser_context.set_active_target_id("TID-opener"); browser_context.attach_active_session("SID-opener"); conn.install_browser_context_fixture_for_test(browser_context); conn.set_auto_attach_owner(None, true, false, CdpTargetFilter::default_auto_attach()); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-opener"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-opener").await; let source_dialog_scope = javascript_dialog_scope_for_test(&conn, "SID-opener"); let source_document = renderer_document_identity_for_test(1, 1); let completion = RendererJavaScriptDialogCompletion::pending(); @@ -3248,12 +3237,12 @@ mod producer_tests { async fn unattached_popup_dialog_is_dismissed_without_opener_fallback() { const POPUP_ID: u64 = 78; - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-popup-no-session".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-popup-no-session"); browser_context.set_active_target_id("TID-opener-no-session"); browser_context.attach_active_session("SID-opener-no-session"); conn.install_browser_context_fixture_for_test(browser_context); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-opener-no-session"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-opener-no-session").await; let source_document = renderer_document_identity_for_test(1, 1); let completion = RendererJavaScriptDialogCompletion::pending(); let mut prepared = @@ -3323,8 +3312,8 @@ mod producer_tests { #[test] fn renderer_document_epoch_change_retires_page_dialog_scope_once() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-dialog-epoch".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-dialog-epoch"); bc.set_active_target_id("TID-dialog-epoch"); bc.attach_active_session("SID-dialog-epoch"); conn.install_browser_context_fixture_for_test(bc); @@ -3370,12 +3359,12 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn javascript_dialog_prepared_action_dismisses_replacement_page_output() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-dialog-stale-page".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-dialog-stale-page"); bc.set_active_target_id("TID-dialog-stale-page"); bc.attach_active_session("SID-dialog-stale-page"); conn.install_browser_context_fixture_for_test(bc); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-dialog-stale-page"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-dialog-stale-page").await; let completion = moli_core::page::RendererJavaScriptDialogCompletion::pending(); let dialog = renderer_javascript_dialog_for_test( renderer_document_identity_for_test(1, 1), @@ -3424,12 +3413,12 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn javascript_dialog_prepared_action_dismisses_retired_dialog_scope() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-dialog-generation".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-dialog-generation"); bc.set_active_target_id("TID-dialog-generation"); bc.attach_active_session("SID-dialog-generation"); conn.install_browser_context_fixture_for_test(bc); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-dialog-generation"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-dialog-generation").await; let completion = moli_core::page::RendererJavaScriptDialogCompletion::pending(); let dialog = renderer_javascript_dialog_for_test( renderer_document_identity_for_test(1, 1), @@ -3471,13 +3460,13 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn javascript_dialog_projection_uses_captured_url_and_frame() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-dialog-source".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-dialog-source"); bc.set_active_target_id("TID-dialog-source"); bc.set_target_url("https://example.test/current-before-capture".to_owned()); bc.attach_active_session("SID-dialog-source"); conn.install_browser_context_fixture_for_test(bc); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-dialog-source"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-dialog-source").await; let dialog = RendererPendingJavaScriptDialog::new( RendererJavaScriptDialogId::new(9), renderer_document_identity_for_test(2, 3), @@ -3526,7 +3515,7 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn canonical_activity_drain_order_survives_ordered_typed_event_stream() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_root_target_discovery_enabled(true); conn.configure_download_policy( None, @@ -3537,7 +3526,7 @@ mod producer_tests { Some(true), ) .unwrap(); - let mut bc = BrowserContext::new("BID-activity-order".into()); + let mut bc = conn.new_browser_context_fixture_for_test("BID-activity-order"); bc.set_active_target_id("TID-activity-order"); bc.set_target_url("https://example.test/page".to_owned()); bc.attach_active_session("SID-activity-order"); @@ -3553,7 +3542,8 @@ mod producer_tests { "TID-activity-order", source_document, ); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-activity-order"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-activity-order").await; + let download_owner = CommandOwnerScope::for_session("SID-activity-order"); let mut prepared = ProtocolOutputPayloads::from_slot(InputPreparedOutputSlot::from_outputs( @@ -3570,13 +3560,15 @@ mod producer_tests { )); prepared.extend_payload( InputPreparedOutputSlot::from_outputs( - InputPreparedOutputs::from_download_activations_for_test(vec![ - RendererPendingDownloadActivation { + InputPreparedOutputs::from_download_activations_for_test( + &conn, + &download_owner, + vec![RendererPendingDownloadActivation { url: "https://example.test/report.txt".to_owned(), suggested_filename: Some("report.txt".to_owned()), response: None, - }, - ]), + }], + ), ) .into(), ); @@ -3729,7 +3721,7 @@ mod producer_tests { prepared.extend_payload( super::PagePreparedOutputSlot::from_outputs( super::PagePreparedOutputs::from_same_document_navigations_for_test( - page_residence_identity_for_test(conn, "SID-later-activity-order"), + page_residence_identity_for_test(conn, "SID-later-activity-order").await, vec![document_sourced_same_document_navigation_for_test( source_document, "https://example.test/page#ordered", @@ -3741,7 +3733,7 @@ mod producer_tests { prepared.extend_payload( super::PagePreparedOutputSlot::from_outputs( super::PagePreparedOutputs::from_top_level_location_navigation_for_test( - page_residence_identity_for_test(conn, "SID-later-activity-order"), + page_residence_identity_for_test(conn, "SID-later-activity-order").await, Some(RendererDocumentSourcedTopLevelLocationNavigation::new( source_document, "data:text/html,%3Cmain%3Eordered-location%3C/main%3E".to_owned(), @@ -3860,8 +3852,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn browser_initiated_child_frame_completion_omits_renderer_request_events() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.set_target_url("https://example.test/page".to_owned()); bc.attach_active_session("SID-1"); @@ -3944,8 +3936,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn child_frame_activity_emits_navigation_before_init_before_lifecycle_terminal() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.set_target_url("https://example.test/page".to_owned()); bc.attach_active_session("SID-1"); @@ -4017,8 +4009,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn child_frame_activity_fans_out_page_events_to_enabled_attached_session() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-child-page-fanout".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-child-page-fanout"); bc.set_active_target_id("TID-child-page-fanout"); bc.set_target_url("https://example.test/page".to_owned()); bc.attach_active_session("SID-primary"); @@ -4084,8 +4076,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn child_frame_activity_projects_sandboxed_about_blank_from_document_url() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.set_target_url("https://top.example/page".to_owned()); bc.attach_active_session("SID-1"); @@ -4149,8 +4141,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn child_frame_activity_emits_document_network_events_from_prepared_load() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.set_target_url("https://example.test/page".to_owned()); bc.attach_active_session("SID-1"); @@ -4291,8 +4283,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn stale_child_document_response_emits_network_without_navigation_or_lifecycle() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.set_target_url("https://example.test/page".to_owned()); bc.attach_active_session("SID-1"); @@ -4418,8 +4410,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn child_document_network_without_body_records_known_no_data() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); conn.install_browser_context_fixture_for_test(bc); @@ -4471,8 +4463,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn child_frame_activity_drain_preserves_prepared_attachment_only_token() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.set_target_url("https://example.test/page".to_owned()); bc.attach_active_session("SID-1"); @@ -4557,8 +4549,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_child_frame_activity_does_not_follow_replacement_page_residence() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-child-page-owner".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-child-page-owner"); browser_context.set_active_target_id("TID-child-page-owner"); browser_context.set_target_url("https://example.test/page".to_owned()); browser_context.attach_active_session("SID-child-page-owner"); @@ -4595,8 +4587,9 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_child_frame_activity_does_not_follow_root_document_open_replacement() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-child-root-document".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-child-root-document"); browser_context.set_active_target_id("TID-child-root-document"); browser_context.set_target_url("https://example.test/page".to_owned()); browser_context.attach_active_session("SID-child-root-document"); @@ -4635,8 +4628,9 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_child_frame_activity_keeps_root_document_route_until_delivery() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-child-delivery-route".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-child-delivery-route"); browser_context.set_active_target_id("TID-child-delivery-route"); browser_context.set_target_url("https://example.test/page".to_owned()); browser_context.attach_active_session("SID-child-delivery-route"); @@ -4686,8 +4680,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_child_frame_activity_does_not_follow_detached_protocol_session() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-child-session".into()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-child-session"); browser_context.set_active_target_id("TID-child-session"); browser_context.set_target_url("https://example.test/page".to_owned()); browser_context.attach_active_session("SID-child-session"); @@ -4729,8 +4723,8 @@ mod producer_tests { #[test] fn child_frame_tree_emission_deduplicates_attach_and_removes_owner_state_on_detach() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.set_target_url("about:blank".to_owned()); bc.attach_active_session("SID-1"); @@ -4791,8 +4785,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn child_frame_activity_drain_requires_prepared_output() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.set_target_url("https://example.test/page".to_owned()); bc.attach_active_session("SID-1"); @@ -4813,13 +4807,13 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn popup_activation_creates_target_and_schedules_navigation_without_page_readback() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_root_target_discovery_enabled(true); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active"); bc.attach_active_session("SID-1"); conn.install_browser_context_fixture_for_test(bc); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-1"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-1").await; let source_document = renderer_document_identity_for_test(1, 1); let mut out = Vec::new(); let mut prepared = @@ -4883,7 +4877,7 @@ mod producer_tests { .background_targets() .next() .and_then(|target| context.target_document_url(target.target_id())) - .is_some_and(moli_url::is_about_blank) + .is_some_and(|url| moli_url::is_about_blank(&url)) }, "target creation should install only the initial empty Document" ); @@ -4898,12 +4892,12 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn popup_activation_publishes_automation_lifecycle_without_cdp_discovery() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-automation".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-automation"); bc.set_active_target_id("TID-opener"); bc.attach_active_session("SID-opener"); conn.install_browser_context_fixture_for_test(bc); - let page_owner = page_residence_identity_for_test(&mut conn, "SID-opener"); + let page_owner = page_residence_identity_for_test(&mut conn, "SID-opener").await; let source_document = renderer_document_identity_for_test(1, 1); let mut prepared = ProtocolOutputPayloads::from_slot(super::PagePreparedOutputSlot::from_outputs( @@ -5024,7 +5018,7 @@ mod producer_tests { let mut prepared = ProtocolOutputPayloads::from_slot(super::PagePreparedOutputSlot::from_outputs( super::PagePreparedOutputs::from_same_document_navigations_for_test( - page_residence_identity_for_test(conn, "SID-1"), + page_residence_identity_for_test(conn, "SID-1").await, vec![document_sourced_same_document_navigation_for_test( source_document, "https://example.test/page#prepared", @@ -5182,7 +5176,8 @@ mod producer_tests { .target_root_document_lifecycle_identity_for_owner(&session) .unwrap(); let owner = - page_residence_identity_for_test(&mut ctx.conn, "SID-document-open-same-document"); + page_residence_identity_for_test(&mut ctx.conn, "SID-document-open-same-document") + .await; ctx.process_async(json!({"id": 810, "sessionId": "SID-document-open-same-document", "method": "Runtime.evaluate", "params": {"expression": "document.open(); document.write('replacement'); document.close()"} })).await; @@ -5226,8 +5221,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn stale_page_residence_same_document_navigation_cannot_mutate_replacement() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-stale-page-same-document".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-stale-page-same-document"); bc.set_active_target_id("TID-stale-page-same-document"); bc.set_target_url("https://example.test/replacement".to_owned()); bc.attach_active_session("SID-stale-page-same-document"); @@ -5240,7 +5235,12 @@ mod producer_tests { "TID-stale-page-same-document", source_document, ); - let owner = page_residence_identity_for_test(&mut conn, "SID-stale-page-same-document"); + let owner = + page_residence_identity_for_test(&mut conn, "SID-stale-page-same-document").await; + conn.browser_context + .as_mut() + .unwrap() + .set_target_url("https://example.test/replacement".to_owned()); conn.replace_document_fixture_for_owner_test(&crate::conn::CommandOwnerScope::capture( &conn, Some("SID-stale-page-same-document"), @@ -5276,8 +5276,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn prepared_top_level_location_navigation_waits_for_its_scheduler_turn() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-location".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-location"); bc.set_active_target_id("TID-location"); bc.set_target_url("about:blank".to_owned()); bc.attach_active_session("SID-location"); @@ -5289,7 +5289,7 @@ mod producer_tests { let mut prepared = ProtocolOutputPayloads::from_slot(super::PagePreparedOutputSlot::from_outputs( super::PagePreparedOutputs::from_top_level_location_navigation_for_test( - page_residence_identity_for_test(&mut conn, "SID-location"), + page_residence_identity_for_test(&mut conn, "SID-location").await, Some(RendererDocumentSourcedTopLevelLocationNavigation::new( source_document, target_url.clone(), @@ -5350,8 +5350,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn document_open_replacement_keeps_requested_top_level_navigation() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-document-open-location".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-document-open-location"); bc.set_active_target_id("TID-document-open-location"); bc.set_target_url("https://example.test/source".to_owned()); bc.attach_active_session("SID-document-open-location"); @@ -5365,7 +5365,7 @@ mod producer_tests { "TID-document-open-location", source_document, ); - let owner = page_residence_identity_for_test(&mut conn, "SID-document-open-location"); + let owner = page_residence_identity_for_test(&mut conn, "SID-document-open-location").await; bind_renderer_document_for_test( &mut conn, "SID-document-open-location", @@ -5415,8 +5415,8 @@ mod producer_tests { #[tokio::test(flavor = "multi_thread")] async fn stale_page_residence_top_level_navigation_cannot_replace_current_page() { - let mut conn = CdpConnection::default(); - let mut bc = BrowserContext::new("BID-stale-page-location".into()); + let mut conn = crate::test_support::connection(); + let mut bc = conn.new_browser_context_fixture_for_test("BID-stale-page-location"); bc.set_active_target_id("TID-stale-page-location"); bc.set_target_url("https://example.test/replacement".to_owned()); bc.attach_active_session("SID-stale-page-location"); @@ -5429,7 +5429,11 @@ mod producer_tests { "TID-stale-page-location", source_document, ); - let owner = page_residence_identity_for_test(&mut conn, "SID-stale-page-location"); + let owner = page_residence_identity_for_test(&mut conn, "SID-stale-page-location").await; + conn.browser_context + .as_mut() + .unwrap() + .set_target_url("https://example.test/replacement".to_owned()); conn.replace_document_fixture_for_owner_test(&crate::conn::CommandOwnerScope::capture( &conn, Some("SID-stale-page-location"), @@ -5796,21 +5800,17 @@ fn try_start_page_capture_snapshot_command( "unsupported snapshot format.", )); } - let (page_context_id, page_target_id) = match conn - .resolve_document_command_owner(&CommandOwnerScope::capture(conn, cmd.session_id)) - { - Ok(route) => route, + let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id); + let document = match conn.resolve_browser_document_for_owner(&owner_scope) { + Ok(document) => document, Err(message) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); } }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("admitted document context remains registered"); - match page_context.start_serialize_html_for_target(&page_target_id) { + match conn.start_capture_document_snapshot(document) { Ok(pending) => PageCommandTaskStep::Pending(PendingPageCommandDispatch { command_id: cmd.id, - owner_scope: CommandOwnerScope::capture(conn, cmd.session_id), + owner_scope, kind: Box::new(PendingPageCommandKind::CaptureSnapshot { pending }), }), Err(error) => PageCommandTaskStep::Complete(CommandOutputPlan::error( @@ -6426,9 +6426,7 @@ async fn devtools_frame_tree_for_current_owner_async( Vec::new(), )); } - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(owner) - else { + let Ok(document) = conn.resolve_browser_document_for_owner(owner) else { return Ok(frame_tree_payload( target_id, target_loader_id, @@ -6441,10 +6439,9 @@ async fn devtools_frame_tree_for_current_owner_async( )); }; let target_mime_type = main_document_mime_type( - conn.browser_context_by_id(&page_context_id) - .expect("resolved document context remains registered") - .target_response_headers(&page_target_id) - .expect("loaded document headers"), + &conn + .document_response_headers(document) + .map_err(devtools_frame_tree_error)?, ); let inspector_session_id = conn.target_renderer_runtime_inspector_session_id_for_owner(owner); let pending = conn @@ -6765,25 +6762,20 @@ fn try_start_page_set_document_content_command( if let Err(message) = conn.ensure_document_accessible_for_session_owner(session_id) { return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); } - let Some((page_context_id, page_target_id)) = conn - .loaded_document_owner_identity_for_owner(&CommandOwnerScope::capture(conn, session_id)) - else { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "No Document instance to set HTML for", - )); + let owner_scope = CommandOwnerScope::capture(conn, session_id); + let document = match conn.loaded_browser_document_for_owner(&owner_scope) { + Ok(document) => document, + Err(_) => { + return PageCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, + "No Document instance to set HTML for", + )); + } }; - let page_context = conn - .browser_context_by_id(&page_context_id) - .expect("resolved document context remains registered"); - match page_context.start_set_document_content_for_target( - &page_target_id, - params.frame_id.into(), - params.html, - ) { + match conn.start_set_document_content(document, params.frame_id.into(), params.html) { Ok(pending) => PageCommandTaskStep::Pending(PendingPageCommandDispatch { command_id: cmd.id, - owner_scope: CommandOwnerScope::capture(conn, session_id), + owner_scope, kind: Box::new(PendingPageCommandKind::SetDocumentContent { pending }), }), Err(error) => PageCommandTaskStep::Complete(CommandOutputPlan::error( @@ -6885,16 +6877,12 @@ fn start_devtools_capture_screenshot_command( let session_id = command.context.session_id.as_ref().map(|id| id.as_str()); let owner_scope = CommandOwnerScope::capture(conn, session_id); let base_background_color = conn.default_background_color_for_owner(&owner_scope); - let (page_context_id, page_target_id) = - match conn.resolve_document_command_owner(&CommandOwnerScope::capture(conn, session_id)) { - Ok(route) => route, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("admitted document context remains registered"); + let document = match conn.resolve_browser_document_for_owner(&owner_scope) { + Ok(document) => document, + Err(message) => { + return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); + } + }; let format = match command.format.as_deref() { None | Some("png") => RendererScreenshotFormat::Png, Some("jpeg") => RendererScreenshotFormat::Jpeg, @@ -6938,7 +6926,7 @@ fn start_devtools_capture_screenshot_command( max_width: None, max_height: None, }; - match page_context.start_capture_screenshot_with_request_for_target(&page_target_id, request) { + match conn.start_capture_document_image(document, request) { Ok(pending) => PageCommandTaskStep::Pending(PendingPageCommandDispatch { command_id, owner_scope, @@ -6984,16 +6972,12 @@ fn start_devtools_print_to_pdf_command( .unwrap_or(DevToolsPrintToPdfTransferMode::ReturnAsBase64); let session_id = command.context.session_id.as_ref().map(|id| id.as_str()); let owner_scope = CommandOwnerScope::capture(conn, session_id); - let (page_context_id, page_target_id) = - match conn.resolve_document_command_owner(&CommandOwnerScope::capture(conn, session_id)) { - Ok(route) => route, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("admitted document context remains registered"); + let document = match conn.resolve_browser_document_for_owner(&owner_scope) { + Ok(document) => document, + Err(message) => { + return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); + } + }; let request = RendererCaptureScreenshotRequest { base_background_color: [255; 4], purpose: RendererScreenshotPurpose::Print { @@ -7006,7 +6990,7 @@ fn start_devtools_print_to_pdf_command( max_width: None, max_height: None, }; - match page_context.start_capture_screenshot_with_request_for_target(&page_target_id, request) { + match conn.start_capture_document_image(document, request) { Ok(pending) => PageCommandTaskStep::Pending(PendingPageCommandDispatch { command_id, owner_scope, @@ -7076,9 +7060,7 @@ fn start_devtools_get_frame_tree_command( &[], )); } - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(&owner) - else { + let Ok(document) = conn.resolve_browser_document_for_owner(&owner) else { return PageCommandTaskStep::Complete(get_frame_tree_command_output_plan( output_kind, target_id, @@ -7092,14 +7074,12 @@ fn start_devtools_get_frame_tree_command( &[], )); }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - let target_mime_type = main_document_mime_type( - page_context - .target_response_headers(&page_target_id) - .expect("loaded document headers"), - ); + let target_mime_type = match conn.document_response_headers(document) { + Ok(headers) => main_document_mime_type(&headers), + Err(error) => { + return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, error)); + } + }; let inspector_session_id = conn.target_renderer_runtime_inspector_session_id_for_owner(&owner); let pending = conn .renderer_inspection_binding_for_owner(&owner, RendererInspectorCommandRoute::MainThread) @@ -7116,6 +7096,7 @@ fn start_devtools_get_frame_tree_command( owner_scope: owner, kind: Box::new(PendingPageCommandKind::GetFrameTree { output_kind, + document, target_id, target_loader_id, target_url, @@ -7142,7 +7123,7 @@ mod protocol_neutral_tests { }; use serde_json::{Value, json}; - use crate::conn::{CdpConnection, Cmd}; + use crate::conn::Cmd; use super::{ PageCommandTaskStep, build_cdp_capture_screenshot_command, @@ -7153,7 +7134,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_frame_tree_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(120), @@ -7176,7 +7157,7 @@ mod protocol_neutral_tests { #[test] fn devtools_page_entry_routes_get_frame_tree_command_to_page_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(121), @@ -7202,7 +7183,7 @@ mod protocol_neutral_tests { #[test] fn cdp_get_layout_metrics_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(122), @@ -7225,7 +7206,7 @@ mod protocol_neutral_tests { #[test] fn devtools_page_entry_routes_get_layout_metrics_command_to_page_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(123), @@ -7254,7 +7235,7 @@ mod protocol_neutral_tests { #[test] fn cdp_handle_javascript_dialog_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "accept": false, "promptText": "typed text" @@ -7283,7 +7264,7 @@ mod protocol_neutral_tests { #[test] fn devtools_page_entry_routes_handle_javascript_dialog_command_to_page_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "accept": true }); @@ -7317,7 +7298,7 @@ mod protocol_neutral_tests { #[test] fn cdp_capture_screenshot_builds_requested_capture_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "format": "png", "quality": 100, @@ -7352,7 +7333,7 @@ mod protocol_neutral_tests { #[test] fn cdp_capture_screenshot_preserves_page_clip() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "format": "png", "clip": { @@ -7387,7 +7368,7 @@ mod protocol_neutral_tests { #[test] fn devtools_page_entry_rejects_unsupported_capture_screenshot_format() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "format": "webp" }); @@ -7413,7 +7394,7 @@ mod protocol_neutral_tests { #[test] fn cdp_capture_screenshot_rejects_invalid_quality_and_clip() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); for (id, params, expected_message) in [ ( 132, @@ -7454,7 +7435,7 @@ mod protocol_neutral_tests { #[test] fn devtools_page_entry_validates_capture_screenshot_target_before_unsupported() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let command = DevToolsCaptureScreenshotCommand { context: DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, @@ -7487,7 +7468,7 @@ mod protocol_neutral_tests { #[test] fn cdp_print_to_pdf_builds_protocol_neutral_command() { - let conn = CdpConnection::new(); + let conn = crate::test_support::connection(); let params = json!({ "landscape": true, "printBackground": true, @@ -7537,7 +7518,7 @@ mod protocol_neutral_tests { #[test] fn devtools_page_entry_reports_layout_disabled_without_placeholder_payload() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(130), @@ -7571,7 +7552,7 @@ mod protocol_neutral_tests { #[test] fn devtools_page_entry_validates_print_to_pdf_target_before_unsupported() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let command = DevToolsPrintToPdfCommand { context: DevToolsCommandContext { protocol: DevToolsProtocol::WebDriverBidi, @@ -7769,8 +7750,8 @@ pub(crate) async fn complete_pending_page_command( command_context, ); } - CompletedPageCommandKind::ResetNavigationHistory { page, completed } => { - return navigation::complete_reset_navigation_history_command(conn, &page, *completed); + CompletedPageCommandKind::ResetNavigationHistory { completed } => { + return navigation::complete_reset_navigation_history_command(conn, completed); } CompletedPageCommandKind::AddScriptToEvaluateOnNewDocument(completed) => { return preload::complete_pending_add_script_to_evaluate_on_new_document_command( @@ -7783,28 +7764,7 @@ pub(crate) async fn complete_pending_page_command( .await; } CompletedPageCommandKind::SetBypassContentSecurityPolicy { completed } => { - let completion = match *completed { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, message, - )); - } - }; - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(&owner_scope) - else { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - if let Err(error) = page_context - .finish_set_bypass_content_security_policy_for_target(&page_target_id, completion) - { + if let Err(error) = conn.finish_document_csp_bypass_update(completed) { return PageCommandTaskStep::Complete(CommandOutputPlan::error( -32000, error.to_string(), @@ -7813,37 +7773,13 @@ pub(crate) async fn complete_pending_page_command( CommandOutputPlan::success() } CompletedPageCommandKind::SetDocumentContent { completed } => { - let completion = match *completed { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, message, - )); - } - }; - let (result, output) = { - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(&owner_scope) - else { + let (result, output) = match conn.finish_set_document_content(completed) { + Ok(completed) => completed, + Err(error) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error( -32000, - "No Document instance to set HTML for", + error.to_string(), )); - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - match page_context.finish_set_document_content_command_turn_for_target( - &page_target_id, - completion, - ) { - Ok(completed) => completed, - Err(error) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - error.to_string(), - )); - } } }; command_context.consume_renderer_command_turn_output(output); @@ -7862,7 +7798,6 @@ pub(crate) async fn complete_pending_page_command( CompletedPageCommandKind::SameDocumentNavigate(completed) => { return navigation::complete_pending_same_document_navigate_command( conn, - &owner_scope, *completed, command_context, ) @@ -7870,6 +7805,7 @@ pub(crate) async fn complete_pending_page_command( } CompletedPageCommandKind::GetFrameTree { output_kind, + document, target_id, target_loader_id, target_url, @@ -7896,22 +7832,30 @@ pub(crate) async fn complete_pending_page_command( &[], )); } - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(&owner_scope) - else { - return PageCommandTaskStep::Complete(get_frame_tree_command_output_plan( - output_kind, - target_id, - target_loader_id, - target_url, - target_unreachable_url, - target_security_origin, - target_secure_context_type, - target_mime_type, - Vec::new(), - &[], - )); - }; + match conn.ensure_browser_document_current(document) { + Ok(()) => {} + Err(message) + if matches!(message.as_str(), "Document changed" | "NoDocumentLoaded") => + { + return PageCommandTaskStep::Complete(get_frame_tree_command_output_plan( + output_kind, + target_id, + target_loader_id, + target_url, + target_unreachable_url, + target_security_origin, + target_secure_context_type, + target_mime_type, + Vec::new(), + &[], + )); + } + Err(message) => { + return PageCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, message, + )); + } + } let child_frames = match *completed { Ok(completion) => { if let Err(message) = @@ -7938,9 +7882,14 @@ pub(crate) async fn complete_pending_page_command( )); } }; - let page_context = conn - .browser_context_by_id(&page_context_id) - .expect("resolved document context remains registered"); + let resource_records = match conn.document_subresource_network_records(document) { + Ok(records) => records, + Err(message) => { + return PageCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, message, + )); + } + }; get_frame_tree_command_output_plan( output_kind, target_id, @@ -7951,48 +7900,22 @@ pub(crate) async fn complete_pending_page_command( target_secure_context_type, target_mime_type, child_frames, - page_context - .target_subresource_network_records(&page_target_id) - .expect("loaded document resource records"), + &resource_records, ) } CompletedPageCommandKind::CaptureSnapshot { completed } => { - let completion = match *completed { - Ok(completion) => completion, - Err(message) => { + let snapshot = match conn.finish_capture_document_snapshot(completed) { + Ok(snapshot) => snapshot, + Err(error) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error( -32000, - format!("Failed to serialize page snapshot: {message}"), + format!("Failed to serialize page snapshot: {error}"), )); } }; - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(&owner_scope) - else { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - let html = - match page_context.finish_serialize_html_for_target(&page_target_id, completion) { - Ok(html) => html, - Err(error) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("Failed to serialize page snapshot: {error}"), - )); - } - }; - let url = page_context - .target_document_url(&page_target_id) - .expect("loaded document URL") - .as_str() - .to_owned(); - CommandOutputPlan::result(json!({ "data": build_mhtml_snapshot(&url, &html) })) + CommandOutputPlan::result(json!({ + "data": build_mhtml_snapshot(&snapshot.url, &snapshot.html), + })) } CompletedPageCommandKind::GetLayoutMetrics { completed } => { let completion = match *completed { @@ -8022,28 +7945,7 @@ pub(crate) async fn complete_pending_page_command( } } CompletedPageCommandKind::CaptureScreenshot { completed } => { - let completion = match *completed { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("Failed to capture page screenshot: {message}"), - )); - } - }; - let (page_context_id, page_target_id) = - match conn.resolve_document_command_owner(&owner_scope) { - Ok(route) => route, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, message, - )); - } - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("admitted document context remains registered"); - match page_context.finish_capture_screenshot_for_target(&page_target_id, completion) { + match conn.finish_capture_document_image(completed) { Ok(RendererCaptureScreenshotReply::Captured(image)) => { CommandOutputPlan::from_devtools_result( DevToolsCommandResult::CaptureScreenshot(DevToolsCaptureScreenshotResult { @@ -8062,7 +7964,10 @@ pub(crate) async fn complete_pending_page_command( } Err(error) => CommandOutputPlan::error( -32000, - format!("Failed to capture page screenshot: {error}"), + format!( + "Failed to capture page screenshot: {}", + capture_document_image_error(error) + ), ), } } @@ -8071,30 +7976,7 @@ pub(crate) async fn complete_pending_page_command( options, transfer_mode, } => { - let completion = match *completed { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("Failed to capture PDF content: {message}"), - )); - } - }; - let (page_context_id, page_target_id) = - match conn.resolve_document_command_owner(&owner_scope) { - Ok(route) => route, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, message, - )); - } - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("admitted document context remains registered"); - let image = match page_context - .finish_capture_screenshot_for_target(&page_target_id, completion) - { + let image = match conn.finish_capture_document_image(completed) { Ok(RendererCaptureScreenshotReply::Captured(image)) => image, Ok(RendererCaptureScreenshotReply::LayoutDisabled) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error( @@ -8111,7 +7993,10 @@ pub(crate) async fn complete_pending_page_command( Err(error) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error( -32000, - format!("Failed to capture PDF content: {error}"), + format!( + "Failed to capture PDF content: {}", + capture_document_image_error(error) + ), )); } }; @@ -8198,28 +8083,32 @@ pub(crate) async fn complete_pending_page_command( ) .await; } - CompletedPageCommandKind::StopLoading => { + CompletedPageCommandKind::StopLoading { completed } => { return termination::complete_stop_loading_command_dispatch( conn, command_id, &owner_scope, - ) - .await; - } - CompletedPageCommandKind::Crash => { - return termination::complete_crash_command_dispatch( - conn, - command_id, - &owner_scope, + completed, command_context, ) .await; } - CompletedPageCommandKind::Close => { + CompletedPageCommandKind::Crash { web_contents } => { + return termination::complete_crash_command_dispatch( + conn, + command_id, + &owner_scope, + web_contents, + command_context, + ) + .await; + } + CompletedPageCommandKind::Close { web_contents } => { return termination::complete_close_command_dispatch( conn, command_id, &owner_scope, + web_contents, command_context, ) .await; diff --git a/moli-protocol/src/domains/page/app_manifest.rs b/moli-protocol/src/domains/page/app_manifest.rs index 82c7a1c86b..d38c58ac00 100644 --- a/moli-protocol/src/domains/page/app_manifest.rs +++ b/moli-protocol/src/domains/page/app_manifest.rs @@ -1,16 +1,19 @@ use chromiumoxide_cdp::cdp::browser_protocol::page::GetAppManifestParams; +use moli_core::browser::DocumentHandle; use moli_core::page::{ - CompletedPageCommand, PendingPageCommand, RendererAppManifest, RendererAppManifestDisplayMode, - RendererAppManifestLoadOutcome, RendererAppManifestLoadPreparation, - RendererAppManifestOrientation, RendererAppManifestQueryResult, - RendererPreparedAppManifestLoad, + RendererAppManifest, RendererAppManifestDisplayMode, RendererAppManifestLoadOutcome, + RendererAppManifestLoadPreparation, RendererAppManifestOrientation, + RendererAppManifestQueryResult, RendererPreparedAppManifestLoad, }; use serde_json::{Map, Value, json}; use super::{PageCommandTaskStep, PendingPageCommandDispatch, PendingPageCommandKind}; use crate::{ - conn::CommandDispatchContext, - conn::{CdpConnection, Cmd, CommandOwnerScope}, + conn::{ + BrowserAppManifestLoadPreparation, CdpConnection, Cmd, CommandDispatchContext, + CommandOwnerScope, CompletedAppManifestLoadPreparation, CompletedAppManifestPublication, + PendingAppManifestLoadPreparation, PendingAppManifestPublication, + }, domains::{ command_output::CommandOutputPlan, network::{ @@ -21,19 +24,25 @@ use crate::{ }; enum PendingGetAppManifestWork { - Prepare(PendingPageCommand), - Fetch(Box), + Prepare(PendingAppManifestLoadPreparation), + Fetch { + document: DocumentHandle, + pending: Box, + }, Publish { - pending: PendingPageCommand, + pending: PendingAppManifestPublication, result: Box, }, } enum CompletedGetAppManifestWork { - Prepare(Result), - Fetch(Box), + Prepare(CompletedAppManifestLoadPreparation), + Fetch { + document: DocumentHandle, + outcome: Box, + }, Publish { - completion: Result, + completion: CompletedAppManifestPublication, result: Box, }, } @@ -51,16 +60,13 @@ pub(super) struct CompletedGetAppManifestCommand { impl CompletedGetAppManifestCommand { pub(super) fn renderer_output_predecessor(&self) -> Option { match &self.work { - CompletedGetAppManifestWork::Prepare(Ok(completion)) - | CompletedGetAppManifestWork::Publish { - completion: Ok(completion), - .. - } => completion.renderer_output_predecessor(), - CompletedGetAppManifestWork::Prepare(Err(_)) - | CompletedGetAppManifestWork::Fetch(_) - | CompletedGetAppManifestWork::Publish { - completion: Err(_), .. - } => None, + CompletedGetAppManifestWork::Prepare(completion) => { + completion.renderer_output_predecessor() + } + CompletedGetAppManifestWork::Publish { completion, .. } => { + completion.renderer_output_predecessor() + } + CompletedGetAppManifestWork::Fetch { .. } => None, } } } @@ -68,15 +74,18 @@ impl CompletedGetAppManifestCommand { impl PendingGetAppManifestCommand { pub(super) async fn wait(self) -> CompletedGetAppManifestCommand { let work = match self.work { - PendingGetAppManifestWork::Prepare(pending) => CompletedGetAppManifestWork::Prepare( - pending.wait().await.map_err(|error| error.to_string()), - ), - PendingGetAppManifestWork::Fetch(pending) => { - CompletedGetAppManifestWork::Fetch(Box::new((*pending).execute().await)) + PendingGetAppManifestWork::Prepare(pending) => { + CompletedGetAppManifestWork::Prepare(pending.wait().await) + } + PendingGetAppManifestWork::Fetch { document, pending } => { + CompletedGetAppManifestWork::Fetch { + document, + outcome: Box::new((*pending).execute().await), + } } PendingGetAppManifestWork::Publish { pending, result } => { CompletedGetAppManifestWork::Publish { - completion: pending.wait().await.map_err(|error| error.to_string()), + completion: pending.wait().await, result, } } @@ -99,13 +108,9 @@ pub(super) fn try_start_get_app_manifest_command( return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32602, error)); } }; - let pending = match conn - .resolve_document_command_owner(&CommandOwnerScope::capture(conn, cmd.session_id)) - { - Ok((context_id, target_id)) => conn - .browser_context_by_id(&context_id) - .expect("admitted document context remains registered") - .start_target_app_manifest_load(&target_id), + let owner = CommandOwnerScope::capture(conn, cmd.session_id); + let pending = match conn.resolve_browser_document_for_owner(&owner) { + Ok(document) => conn.start_app_manifest_load_preparation(document), Err(message) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); } @@ -113,7 +118,7 @@ pub(super) fn try_start_get_app_manifest_command( match pending { Ok(pending) => pending_step( cmd.id, - CommandOwnerScope::capture(conn, cmd.session_id), + owner, PendingGetAppManifestCommand { manifest_id: params.manifest_id, work: PendingGetAppManifestWork::Prepare(pending), @@ -135,22 +140,7 @@ pub(super) fn complete_get_app_manifest_command( ) -> PageCommandTaskStep { match completed.work { CompletedGetAppManifestWork::Prepare(completion) => { - let completion = match completion { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("Failed to inspect app manifest: {message}"), - )); - } - }; - let preparation = match conn.resolve_document_command_owner(owner).and_then( - |(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_target_app_manifest_load_preparation(&target_id, completion) - }, - ) { + let preparation = match conn.finish_app_manifest_load_preparation(completion) { Ok(preparation) => preparation, Err(message) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error( @@ -159,6 +149,10 @@ pub(super) fn complete_get_app_manifest_command( )); } }; + let BrowserAppManifestLoadPreparation { + document, + preparation, + } = preparation; match preparation { RendererAppManifestLoadPreparation::Complete(result) => { PageCommandTaskStep::Complete(result_plan( @@ -171,24 +165,14 @@ pub(super) fn complete_get_app_manifest_command( owner.clone(), PendingGetAppManifestCommand { manifest_id: completed.manifest_id, - work: PendingGetAppManifestWork::Fetch(pending), + work: PendingGetAppManifestWork::Fetch { document, pending }, }, ), } } - CompletedGetAppManifestWork::Fetch(outcome) => { + CompletedGetAppManifestWork::Fetch { document, outcome } => { let (result, publication) = (*outcome).into_parts(); - let pending = match conn.resolve_document_command_owner(owner) { - Ok((context_id, target_id)) => conn - .browser_context_by_id(&context_id) - .expect("admitted document context remains registered") - .start_target_app_manifest_publication(&target_id, publication), - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, message, - )); - } - }; + let pending = conn.start_app_manifest_publication(document, publication); match pending { Ok(pending) => pending_step( command_id, @@ -208,22 +192,7 @@ pub(super) fn complete_get_app_manifest_command( } } CompletedGetAppManifestWork::Publish { completion, result } => { - let completion = match completion { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("Failed to publish app manifest result: {message}"), - )); - } - }; - let output = match conn.resolve_document_command_owner(owner).and_then( - |(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("NoDocumentLoaded")? - .finish_target_app_manifest_publication(&target_id, completion) - }, - ) { + let output = match conn.finish_app_manifest_publication(completion) { Ok(output) => output, Err(message) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error( diff --git a/moli-protocol/src/domains/page/navigation.rs b/moli-protocol/src/domains/page/navigation.rs index 29d89532db..5f312b53f8 100644 --- a/moli-protocol/src/domains/page/navigation.rs +++ b/moli-protocol/src/domains/page/navigation.rs @@ -10,7 +10,7 @@ use crate::devtools_runtime::{ use chromiumoxide_cdp::cdp::browser_protocol::page::{ NavigateParams, NavigateToHistoryEntryParams, ReloadParams, }; -use moli_core::page::{ChildFrameDocumentOpenedSnapshot, CompletedPageCommand, PendingPageCommand}; +use moli_core::page::ChildFrameDocumentOpenedSnapshot; use moli_fetch::NET_ERR_ABORTED_ERROR_TEXT; use moli_url_policy::{LocalFileNavigationAccess, route_navigation_url}; use serde_json::{Value, json}; @@ -19,10 +19,13 @@ use url::Url; use crate::conn::{ BackgroundNavigationLoadJob, BackgroundProtocolEvent, CapturedBody, CdpConnection, CdpSessionRoute, ClaimedFetchNavigation, Cmd, CommandDispatchContext, CommandOwnerScope, - FetchRequestStage, NavigationDispatchState, NavigationId, NavigationLoadOutcome, - NavigationRequestInterception, NavigationRequestLoadPolicy, NavigationResultProjection, - NavigationSourceDocumentSecurityContext, PendingFetchNavigation, ResponseStageUrlMatchPolicy, - monotonic_timestamp_seconds, + CompletedChildFrameNavigation, CompletedNavigationHistoryReset, + CompletedTopLevelHistoryTraversal, CompletedTopLevelSameDocumentNavigation, FetchRequestStage, + NavigationDispatchState, NavigationId, NavigationLoadOutcome, NavigationRequestInterception, + NavigationRequestLoadPolicy, NavigationResultProjection, + NavigationSourceDocumentSecurityContext, PendingChildFrameNavigation, PendingFetchNavigation, + PendingTopLevelHistoryTraversal, PendingTopLevelSameDocumentNavigation, + ResponseStageUrlMatchPolicy, monotonic_timestamp_seconds, }; use moli_cookie_jar::{NetworkCookieRequestContext, StoredCookieQueryReport}; @@ -63,7 +66,7 @@ pub(super) struct CompletedNavigateLoadCommand { pub(super) struct PendingChildFrameNavigateCommand { prefix_events: Vec, - pending: PendingPageCommand, + pending: PendingChildFrameNavigation, activity_binding: crate::conn::TargetRootDocumentProtocolAttachmentIdentity, url: String, wait: DevToolsNavigationWait, @@ -71,13 +74,13 @@ pub(super) struct PendingChildFrameNavigateCommand { } pub(super) struct PendingSameDocumentNavigateCommand { - pending: PendingPageCommand, + pending: PendingTopLevelSameDocumentNavigation, result_payload: Value, } pub(super) struct CompletedChildFrameNavigateCommand { prefix_events: Vec, - completed: Result, + completed: CompletedChildFrameNavigation, activity_binding: crate::conn::TargetRootDocumentProtocolAttachmentIdentity, url: String, wait: DevToolsNavigationWait, @@ -85,7 +88,7 @@ pub(super) struct CompletedChildFrameNavigateCommand { } pub(super) struct CompletedSameDocumentNavigateCommand { - completed: Result, + completed: CompletedTopLevelSameDocumentNavigation, result_payload: Value, } @@ -117,39 +120,30 @@ struct HistoryTraversalUrlFallback { } pub(super) struct PendingSameDocumentHistoryTraversalCommand { - pending: PendingPageCommand, + pending: PendingTopLevelHistoryTraversal, fallback: HistoryTraversalUrlFallback, } pub(super) struct CompletedSameDocumentHistoryTraversalCommand { - completed: Result, + completed: CompletedTopLevelHistoryTraversal, fallback: HistoryTraversalUrlFallback, } impl CompletedChildFrameNavigateCommand { pub(super) fn renderer_output_predecessor(&self) -> Option { - self.completed - .as_ref() - .ok() - .and_then(CompletedPageCommand::renderer_output_predecessor) + self.completed.renderer_output_predecessor() } } impl CompletedSameDocumentNavigateCommand { pub(super) fn renderer_output_predecessor(&self) -> Option { - self.completed - .as_ref() - .ok() - .and_then(CompletedPageCommand::renderer_output_predecessor) + self.completed.renderer_output_predecessor() } } impl CompletedSameDocumentHistoryTraversalCommand { pub(super) fn renderer_output_predecessor(&self) -> Option { - self.completed - .as_ref() - .ok() - .and_then(CompletedPageCommand::renderer_output_predecessor) + self.completed.renderer_output_predecessor() } } @@ -169,7 +163,7 @@ impl PendingChildFrameNavigateCommand { pub(super) async fn wait(self) -> CompletedChildFrameNavigateCommand { CompletedChildFrameNavigateCommand { prefix_events: self.prefix_events, - completed: self.pending.wait().await.map_err(|error| error.to_string()), + completed: self.pending.wait().await, activity_binding: self.activity_binding, url: self.url, wait: self.wait, @@ -181,7 +175,7 @@ impl PendingChildFrameNavigateCommand { impl PendingSameDocumentNavigateCommand { pub(super) async fn wait(self) -> CompletedSameDocumentNavigateCommand { CompletedSameDocumentNavigateCommand { - completed: self.pending.wait().await.map_err(|error| error.to_string()), + completed: self.pending.wait().await, result_payload: self.result_payload, } } @@ -199,7 +193,7 @@ impl PendingContinueNavigationWithoutRequestPauseCommand { impl PendingSameDocumentHistoryTraversalCommand { pub(super) async fn wait(self) -> CompletedSameDocumentHistoryTraversalCommand { CompletedSameDocumentHistoryTraversalCommand { - completed: self.pending.wait().await.map_err(|error| error.to_string()), + completed: self.pending.wait().await, fallback: self.fallback, } } @@ -1211,15 +1205,9 @@ fn direct_navigation_result_from_completed( } fn direct_navigation_result_from_completed_same_document( - completed: &CompletedSameDocumentNavigateCommand, + _completed: &CompletedSameDocumentNavigateCommand, result: &mut DirectNavigationResult, ) -> Result<(), DevToolsError> { - if let Err(message) = &completed.completed { - return Err(DevToolsError::new( - DevToolsErrorKind::Internal, - message.clone(), - )); - } result.loader_id = None; result.navigation_id = None; Ok(()) @@ -1229,18 +1217,10 @@ fn direct_navigation_result_from_completed_same_document_traversal( completed: &CompletedSameDocumentHistoryTraversalCommand, result: &mut DirectNavigationResult, ) -> Result<(), DevToolsError> { - match &completed.completed { - Ok(completion) => { - if completion.bool_reply_value() == Some(false) { - result.set_history_traversal_same_document(false); - } - Ok(()) - } - Err(message) => Err(DevToolsError::new( - DevToolsErrorKind::Internal, - message.clone(), - )), + if completed.completed.renderer_accepted() == Some(false) { + result.set_history_traversal_same_document(false); } + Ok(()) } fn direct_navigation_result_from_completed_load( @@ -1280,12 +1260,6 @@ fn direct_navigation_result_from_completed_child_frame( completed: &CompletedChildFrameNavigateCommand, result: &mut DirectNavigationResult, ) -> Result<(), DevToolsError> { - if let Err(message) = &completed.completed { - return Err(DevToolsError::new( - DevToolsErrorKind::Internal, - message.clone(), - )); - } result.set_url(completed.url.clone()); Ok(()) } @@ -1440,18 +1414,13 @@ fn start_top_level_same_document_navigate( url: String, result_payload: Value, ) -> NavigateCommandStart { - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(owner) - else { + let Ok(document) = conn.loaded_browser_document_for_owner(owner) else { return NavigateCommandStart::CompletePlan(CommandOutputPlan::error( -32000, "NoDocumentLoaded", )); }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - match page_context.start_top_level_same_document_navigation_for_target(&page_target_id, url) { + match conn.start_top_level_same_document_navigation(document, url) { Ok(pending) => NavigateCommandStart::PendingSameDocument(Box::new( PendingSameDocumentNavigateCommand { pending, @@ -1500,22 +1469,13 @@ fn start_child_frame_navigate_command( "NoDocumentLoaded", )); }; - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(owner) - else { + let Ok(document) = conn.loaded_browser_document_for_owner(owner) else { return NavigateCommandStart::CompletePlan(CommandOutputPlan::error( -32000, "NoDocumentLoaded", )); }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - match page_context.start_child_frame_navigation_to_url_for_target( - &page_target_id, - frame_id, - url, - ) { + match conn.start_child_frame_navigation(document, frame_id.to_owned(), url.to_owned()) { Ok(pending) => { NavigateCommandStart::PendingChildFrame(Box::new(PendingChildFrameNavigateCommand { prefix_events: Vec::new(), @@ -1702,24 +1662,16 @@ fn start_same_document_history_traversal_command( delta: i64, fallback: HistoryTraversalUrlFallback, ) -> PageCommandTaskStep { - let route = conn - .loaded_document_owner_identity_for_owner(&fallback.owner) - .ok_or_else(|| anyhow::anyhow!("TargetNotLoaded")); - let (page_context_id, page_target_id) = match route { - Ok(route) => route, - Err(error) => { + let document = match conn.loaded_browser_document_for_owner(&fallback.owner) { + Ok(document) => document, + Err(_) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error( -31998, - error.to_string(), + "TargetNotLoaded", )); } }; - let page_context = conn - .browser_context_by_id(&page_context_id) - .expect("resolved document context remains registered"); - let pending = match page_context - .start_top_level_history_traversal_by_delta_for_target(&page_target_id, delta) - { + let pending = match conn.start_top_level_history_traversal(document, delta) { Ok(pending) => pending, Err(error) => { return PageCommandTaskStep::Complete(CommandOutputPlan::error( @@ -1746,20 +1698,7 @@ pub(super) fn complete_pending_same_document_history_traversal_command( completed, fallback, } = completed; - let completion = match completed { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - let result = conn - .loaded_document_owner_identity_for_owner(&fallback.owner) - .ok_or_else(|| "TargetNotLoaded".to_owned()) - .and_then(|(context_id, target_id)| { - conn.browser_context_by_id_mut(&context_id) - .ok_or("TargetNotLoaded")? - .finish_top_level_history_traversal_by_delta_for_target(&target_id, completion) - }); + let result = conn.finish_top_level_history_traversal(completed); match result { Ok(true) => PageCommandTaskStep::Complete(CommandOutputPlan::from_devtools_result( DevToolsCommandResult::Empty, @@ -2414,21 +2353,15 @@ pub(super) fn try_start_reset_navigation_history_command( conn: &mut CdpConnection, cmd: &Cmd<'_>, ) -> PageCommandTaskStep { - let Some(page) = conn.target_page_residence_identity_for_session(cmd.session_id) else { + let owner = CommandOwnerScope::capture(conn, cmd.session_id); + let Ok(document) = conn.loaded_browser_document_for_owner(&owner) else { return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, "NoDocumentLoaded")); }; - let pending = page - .target_id() - .and_then(|target_id| { - conn.browser_context_by_id(page.browser_context_id()) - .map(|context| context.start_reset_navigation_history_for_target(target_id)) - }) - .unwrap_or_else(|| Err("NoDocumentLoaded".into())); - match pending { + match conn.start_navigation_history_reset(document) { Ok(pending) => PageCommandTaskStep::Pending(super::PendingPageCommandDispatch { command_id: cmd.id, - owner_scope: crate::conn::CommandOwnerScope::capture(conn, cmd.session_id), - kind: Box::new(super::PendingPageCommandKind::ResetNavigationHistory { page, pending }), + owner_scope: owner, + kind: Box::new(super::PendingPageCommandKind::ResetNavigationHistory { pending }), }), Err(error) => PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, error)), } @@ -2436,22 +2369,9 @@ pub(super) fn try_start_reset_navigation_history_command( pub(super) fn complete_reset_navigation_history_command( conn: &mut CdpConnection, - page: &crate::conn::TargetPageResidenceIdentity, - completed: Result, + completed: CompletedNavigationHistoryReset, ) -> PageCommandTaskStep { - let completion = match completed { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - let Some((page_context, target_id)) = conn - .browser_context_by_id_mut(page.browser_context_id()) - .zip(page.target_id()) - else { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, "NoDocumentLoaded")); - }; - match page_context.finish_reset_navigation_history_for_target(target_id, completion) { + match conn.finish_navigation_history_reset(completed) { Ok(true) => PageCommandTaskStep::Complete(CommandOutputPlan::success()), Ok(false) => PageCommandTaskStep::Complete(CommandOutputPlan::error( -32000, @@ -2618,6 +2538,15 @@ fn start_navigate_to_url_command_with_background_policy_and_request( referrer, url.starts_with("data:"), ); + let Some(web_contents) = navigation_preflight + .as_ref() + .map(|preflight| preflight.web_contents) + else { + return NavigateCommandStart::CompletePlan(CommandOutputPlan::error( + -31998, + "TargetNotLoaded", + )); + }; let frame_id = navigation_preflight .as_ref() .map(|preflight| preflight.frame_id.clone()) @@ -2648,6 +2577,7 @@ fn start_navigate_to_url_command_with_background_policy_and_request( redirect_headers: None, navigate_id: command_id, owner: owner.clone(), + web_contents, result_projection, frame_id: frame_id.clone(), session_id: None, @@ -3029,32 +2959,13 @@ pub(super) async fn complete_pending_child_frame_navigate_command( if !conn.target_root_document_protocol_attachment_identity_is_current(&activity_binding) { return PageCommandTaskStep::Complete(CommandOutputPlan::error(-31998, "NoSuchTarget")); } - let completion = match completed { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - let (navigated, renderer_output) = { - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(owner) - else { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-31998, "NoSuchTarget")); - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - match page_context.finish_child_frame_navigation_to_url_command_turn_for_target( - &page_target_id, - completion, - ) { - Ok(navigated) => navigated, - Err(error) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - error.to_string(), - )); - } + let (navigated, renderer_output) = match conn.finish_child_frame_navigation(completed) { + Ok(navigated) => navigated, + Err(error) => { + return PageCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, + error.to_string(), + )); } }; command_context.consume_renderer_command_turn_output(renderer_output); @@ -3072,14 +2983,9 @@ pub(super) async fn complete_pending_child_frame_navigate_command( return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); } }; - let child_gate = match child_gate.wait().await { - Ok(completed) => completed, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; + let child_gate = child_gate.wait().await; let (completed, renderer_output) = match conn - .complete_child_frame_lifecycle_work_command_turn_for_session_owner(child_gate) + .finish_document_child_frame_lifecycle_work(child_gate) { Ok(completed) => completed, Err(message) => { @@ -3112,7 +3018,6 @@ pub(super) async fn complete_pending_child_frame_navigate_command( pub(super) async fn complete_pending_same_document_navigate_command( conn: &mut CdpConnection, - owner: &CommandOwnerScope, completed: CompletedSameDocumentNavigateCommand, command_context: &mut CommandDispatchContext, ) -> PageCommandTaskStep { @@ -3120,32 +3025,13 @@ pub(super) async fn complete_pending_same_document_navigate_command( completed, result_payload, } = completed; - let completion = match completed { - Ok(completion) => completion, - Err(message) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); - } - }; - let (navigated, output) = { - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(owner) - else { - return PageCommandTaskStep::Complete(CommandOutputPlan::error(-31998, "NoSuchTarget")); - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - match page_context.finish_top_level_same_document_navigation_command_turn_for_target( - &page_target_id, - completion, - ) { - Ok(navigated) => navigated, - Err(error) => { - return PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - error.to_string(), - )); - } + let (navigated, output) = match conn.finish_top_level_same_document_navigation(completed) { + Ok(navigated) => navigated, + Err(error) => { + return PageCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, + error.to_string(), + )); } }; command_context.consume_renderer_command_turn_output(output); @@ -3360,7 +3246,7 @@ mod child_frame_attachment_tests { }; use serde_json::{Value, json}; - use crate::conn::{CdpConnection, Cmd}; + use crate::conn::Cmd; use super::{ DevToolsNavigationStartOptions, NavigationResultProjection, PageCommandTaskStep, @@ -3437,7 +3323,7 @@ mod child_frame_attachment_tests { #[test] fn devtools_page_entry_routes_navigate_command_to_navigation_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(12), @@ -3510,7 +3396,7 @@ mod child_frame_attachment_tests { #[test] fn devtools_page_entry_routes_reload_command_to_navigation_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(14), @@ -3582,7 +3468,7 @@ mod child_frame_attachment_tests { #[test] fn devtools_page_entry_routes_history_traversal_to_navigation_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(16), diff --git a/moli-protocol/src/domains/page/popup.rs b/moli-protocol/src/domains/page/popup.rs index e4a907c7cb..933ea4b95f 100644 --- a/moli-protocol/src/domains/page/popup.rs +++ b/moli-protocol/src/domains/page/popup.rs @@ -270,7 +270,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn popup_uses_captured_context_and_opener_after_another_context_becomes_active() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.inactive_browser_contexts .push(context("BID-source", "TID-source", "SID-source")); conn.browser_context = Some(context("BID-current", "TID-current", "SID-current")); @@ -314,7 +314,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn noopener_popup_retains_devtools_creator_without_dom_opener_access() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.browser_context = Some(context("BID-1", "TID-opener", "SID-1")); emit( @@ -346,7 +346,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn removed_opener_downgrades_access_without_rebinding_to_current_target() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.browser_context = Some(context("BID-1", "TID-current", "SID-1")); emit( @@ -372,7 +372,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn child_window_popup_preserves_its_exact_opener_frame() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.browser_context = Some(context("BID-1", "TID-root", "SID-1")); emit( @@ -407,7 +407,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn fifo_popup_batch_resolves_a_lightweight_popup_as_the_next_opener() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.browser_context = Some(context("BID-1", "TID-root", "SID-1")); let owner = page_owner("BID-1", "TID-root"); @@ -450,7 +450,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn removed_captured_context_does_not_fall_back_to_the_active_context() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.browser_context = Some(context("BID-current", "TID-current", "SID-current")); emit( diff --git a/moli-protocol/src/domains/page/preload.rs b/moli-protocol/src/domains/page/preload.rs index 2a83b16111..e3a9ac1119 100644 --- a/moli-protocol/src/domains/page/preload.rs +++ b/moli-protocol/src/domains/page/preload.rs @@ -1756,7 +1756,7 @@ mod protocol_neutral_tests { }; use serde_json::json; - use crate::conn::{CdpConnection, Cmd, DocumentStartScript, TargetOwnerState}; + use crate::conn::{Cmd, DocumentStartScript, TargetOwnerState}; use super::{ PageCommandTaskStep, build_cdp_add_preload_script_command, @@ -2056,7 +2056,7 @@ mod protocol_neutral_tests { #[test] fn devtools_preload_entry_routes_add_command_to_owner_error() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({"source": "globalThis.ready = true;"}); let cmd = Cmd::for_test( Some(43), diff --git a/moli-protocol/src/domains/page/resource_search.rs b/moli-protocol/src/domains/page/resource_search.rs index c9e69c7910..441c1f163c 100644 --- a/moli-protocol/src/domains/page/resource_search.rs +++ b/moli-protocol/src/domains/page/resource_search.rs @@ -1,8 +1,8 @@ use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; +use moli_core::browser::DocumentHandle; use moli_core::page::{ - CompletedPageCommand, PendingPageCommand, RendererResourceTextSearchOutcome, - RendererTextSearchMatch, SubresourceNetworkOutcome, SubresourceNetworkRecord, - SubresourceResourceType, + RendererResourceTextSearchOutcome, RendererTextSearchMatch, SubresourceNetworkOutcome, + SubresourceNetworkRecord, SubresourceResourceType, }; use moli_encoding::{ decode_classic_script_source, decode_html_document_with_fallback, decode_text_for_legacy_web, @@ -17,7 +17,10 @@ use serde_json::json; use url::Url; use super::{PageCommandTaskStep, PendingPageCommandDispatch, PendingPageCommandKind}; -use crate::conn::{CdpConnection, Cmd, CommandOwnerScope}; +use crate::conn::{ + CdpConnection, Cmd, CommandOwnerScope, CompletedDocumentResourceTextSearch, + PendingDocumentResourceTextSearch, +}; use crate::domains::command_output::CommandOutputPlan; const AGENT_NOT_ENABLED: &str = "Agent is not enabled."; @@ -46,21 +49,18 @@ enum ResourceSearchSource { pub(super) struct PendingSearchInResourceCommand { params: SearchInResourceParams, source: ResourceSearchSource, - pending: PendingPageCommand, + pending: PendingDocumentResourceTextSearch, } pub(super) struct CompletedSearchInResourceCommand { params: SearchInResourceParams, source: ResourceSearchSource, - completed: Result, + completed: CompletedDocumentResourceTextSearch, } impl CompletedSearchInResourceCommand { pub(super) fn renderer_output_predecessor(&self) -> Option { - self.completed - .as_ref() - .ok() - .and_then(CompletedPageCommand::renderer_output_predecessor) + self.completed.renderer_output_predecessor() } } @@ -69,7 +69,7 @@ impl PendingSearchInResourceCommand { CompletedSearchInResourceCommand { params: self.params, source: self.source, - completed: self.pending.wait().await.map_err(|error| error.to_string()), + completed: self.pending.wait().await, } } } @@ -126,6 +126,9 @@ pub(super) fn try_start_search_in_resource_command( } let materialize_limit = conn.response_body_materialize_limit(); let owner = CommandOwnerScope::capture(conn, cmd.session_id); + let Ok(document) = conn.resolve_browser_document_for_owner(&owner) else { + return complete_error(CONTENT_UNAVAILABLE); + }; if is_root_frame { let main_document = conn.current_main_document_resource_for_session_owner(cmd.session_id); @@ -154,37 +157,22 @@ pub(super) fn try_start_search_in_resource_command( None => SelectedResource::Unavailable, } } else { - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(&owner) - else { + let Ok(records) = conn.document_subresource_network_records(document) else { return complete_error(CONTENT_UNAVAILABLE); }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); select_subresource( - page_context - .target_subresource_network_records(&page_target_id) - .expect("loaded document resource records"), + &records, &root_frame_id, true, ¶ms.url, materialize_limit, ) }; - return start_selected_resource_search(conn, cmd.id, owner, params, selected); + return start_selected_resource_search(conn, cmd.id, owner, document, params, selected); } - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(&owner) - else { - return complete_error(CONTENT_UNAVAILABLE); - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - match page_context.start_child_frame_resource_search_by_lines_for_target( - &page_target_id, + match conn.start_child_frame_document_resource_text_search( + document, params.frame_id.clone(), params.url.clone(), params.query.clone(), @@ -211,21 +199,8 @@ pub(super) fn complete_search_in_resource_command( completed: CompletedSearchInResourceCommand, ) -> PageCommandTaskStep { let materialize_limit = conn.response_body_materialize_limit(); - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(owner) - else { - return complete_error(CONTENT_UNAVAILABLE); - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - let completion = match completed.completed { - Ok(completion) => completion, - Err(message) => return complete_error(format!("Failed to search resource: {message}")), - }; - let outcome = match page_context - .finish_resource_search_by_lines_for_target(&page_target_id, completion) - { + let document = completed.completed.document(); + let outcome = match conn.finish_document_resource_text_search(completed.completed) { Ok(outcome) => outcome, Err(error) => return complete_error(format!("Failed to search resource: {error}")), }; @@ -244,10 +219,11 @@ pub(super) fn complete_search_in_resource_command( ResourceSearchSource::ChildDocument, RendererResourceTextSearchOutcome::ResourceNotFound, ) => { + let Ok(records) = conn.document_subresource_network_records(document) else { + return complete_error(CONTENT_UNAVAILABLE); + }; let selected = select_subresource( - page_context - .target_subresource_network_records(&page_target_id) - .expect("loaded document resource records"), + &records, &completed.params.frame_id, false, &completed.params.url, @@ -257,6 +233,7 @@ pub(super) fn complete_search_in_resource_command( conn, command_id, owner.clone(), + document, completed.params, selected, ) @@ -268,6 +245,7 @@ fn start_selected_resource_search( conn: &mut CdpConnection, command_id: Option, owner: CommandOwnerScope, + document: DocumentHandle, params: SearchInResourceParams, selected: SelectedResource, ) -> PageCommandTaskStep { @@ -276,16 +254,8 @@ fn start_selected_resource_search( SelectedResource::Unavailable => return complete_error(CONTENT_UNAVAILABLE), SelectedResource::Missing => return complete_error(RESOURCE_NOT_FOUND), }; - let Some((page_context_id, page_target_id)) = - conn.loaded_document_owner_identity_for_owner(&owner) - else { - return complete_error(CONTENT_UNAVAILABLE); - }; - let page_context = conn - .browser_context_by_id_mut(&page_context_id) - .expect("resolved document context remains registered"); - match page_context.start_text_search_by_lines_for_target( - &page_target_id, + match conn.start_document_text_search( + document, text, params.query.clone(), params.case_sensitive, diff --git a/moli-protocol/src/domains/page/termination.rs b/moli-protocol/src/domains/page/termination.rs index 27b072fef3..7724ebb34a 100644 --- a/moli-protocol/src/domains/page/termination.rs +++ b/moli-protocol/src/domains/page/termination.rs @@ -1,6 +1,6 @@ use crate::conn::{ - BackgroundProtocolEvent, CdpConnection, Cmd, CommandOwnerScope, DEFAULT_LOADER_ID, - NavigationDispatchState, NavigationId, PendingFetchNavigation, + BackgroundProtocolEvent, CdpConnection, Cmd, CommandOwnerScope, CompletedDocumentLifecycleStop, + DEFAULT_LOADER_ID, NavigationDispatchState, NavigationId, PendingFetchNavigation, PendingSubresourceFetchAuthRequest, PendingSubresourceFetchRequest, PendingSubresourceFetchResponseRequest, monotonic_timestamp_seconds, }; @@ -14,13 +14,19 @@ use crate::domains::command_output::{CommandOutputBuffer, CommandOutputPlan}; pub(crate) struct PageTargetTerminationOwnerAction { owner_scope: CommandOwnerScope, target_id: String, + web_contents: moli_core::browser::WebContentsHandle, } impl PageTargetTerminationOwnerAction { - pub(crate) fn new(owner_scope: CommandOwnerScope, target_id: String) -> Self { + pub(crate) fn new( + owner_scope: CommandOwnerScope, + target_id: String, + web_contents: moli_core::browser::WebContentsHandle, + ) -> Self { Self { owner_scope, target_id, + web_contents, } } @@ -32,8 +38,14 @@ impl PageTargetTerminationOwnerAction { &self.target_id } - fn into_parts(self) -> (CommandOwnerScope, String) { - (self.owner_scope, self.target_id) + fn into_parts( + self, + ) -> ( + CommandOwnerScope, + String, + moli_core::browser::WebContentsHandle, + ) { + (self.owner_scope, self.target_id, self.web_contents) } } @@ -385,10 +397,23 @@ pub(super) fn try_start_stop_loading_command_dispatch( conn: &CdpConnection, cmd: &Cmd<'_>, ) -> PageCommandTaskStep { + let owner_scope = crate::conn::CommandOwnerScope::capture(conn, cmd.session_id); + let pending = conn + .loaded_browser_document_for_owner(&owner_scope) + .ok() + .and_then( + |document| match conn.start_document_lifecycle_stop(document) { + Ok(pending) => Some(pending), + Err(error) => { + tracing::debug!(%error, "failed to start renderer document lifecycle stop"); + None + } + }, + ); PageCommandTaskStep::Pending(super::PendingPageCommandDispatch { command_id: cmd.id, - owner_scope: crate::conn::CommandOwnerScope::capture(conn, cmd.session_id), - kind: Box::new(super::PendingPageCommandKind::StopLoading), + owner_scope, + kind: Box::new(super::PendingPageCommandKind::StopLoading { pending }), }) } @@ -396,15 +421,19 @@ pub(super) async fn complete_stop_loading_command_dispatch( conn: &mut CdpConnection, _command_id: Option, owner: &CommandOwnerScope, + completed: Option, + command_context: &mut crate::conn::CommandDispatchContext, ) -> PageCommandTaskStep { let mut out = Vec::new(); - if let Some((context_id, target_id)) = conn.loaded_document_owner_identity_for_owner(owner) - && let Some(context) = conn.browser_context_by_id_mut(&context_id) - && let Err(error) = context - .stop_target_document_lifecycle_async(&target_id) - .await - { - tracing::debug!(%error, "failed to stop renderer document lifecycle"); + if let Some(completed) = completed { + match conn.finish_document_lifecycle_stop(completed) { + Ok(output) => { + command_context.consume_renderer_command_turn_output(output); + } + Err(error) => { + tracing::debug!(%error, "failed to stop renderer document lifecycle"); + } + } } let ( pending_navigations, @@ -452,10 +481,12 @@ pub(super) fn try_start_crash_command_dispatch( )); } } + let owner_scope = crate::conn::CommandOwnerScope::capture(conn, cmd.session_id); + let web_contents = conn.browser_web_contents_for_owner(&owner_scope).ok(); PageCommandTaskStep::Pending(super::PendingPageCommandDispatch { command_id: cmd.id, - owner_scope: crate::conn::CommandOwnerScope::capture(conn, cmd.session_id), - kind: Box::new(super::PendingPageCommandKind::Crash), + owner_scope, + kind: Box::new(super::PendingPageCommandKind::Crash { web_contents }), }) } @@ -463,6 +494,7 @@ pub(super) async fn complete_crash_command_dispatch( conn: &mut CdpConnection, _command_id: Option, owner: &CommandOwnerScope, + web_contents: Option, command_context: &mut crate::conn::CommandDispatchContext, ) -> PageCommandTaskStep { let mut out = Vec::new(); @@ -493,10 +525,10 @@ pub(super) async fn complete_crash_command_dispatch( // it never enters a V8InspectorSession or the ordinary target IO task FIFO. // Seal both DevTools receivers and interrupt active V8 synchronously so // target retirement cannot wait behind earlier JavaScript or IO work. - if let Some((context_id, target_id)) = conn.loaded_document_owner_identity_for_owner(owner) - && let Some(context) = conn.browser_context_by_id_mut(&context_id) + if let Some(web_contents) = web_contents + && let Err(error) = conn.crash_browser_web_contents_renderer_from_io(web_contents) { - context.crash_target_renderer_from_io(&target_id); + tracing::debug!(%error, "failed to crash exact Browser WebContents renderer"); } // Page.crash retires the target, not merely the DevTools session which @@ -583,10 +615,12 @@ pub(super) fn try_start_close_command_dispatch( conn: &CdpConnection, cmd: &Cmd<'_>, ) -> PageCommandTaskStep { + let owner_scope = crate::conn::CommandOwnerScope::capture(conn, cmd.session_id); + let web_contents = conn.browser_web_contents_for_owner(&owner_scope).ok(); PageCommandTaskStep::Pending(super::PendingPageCommandDispatch { command_id: cmd.id, - owner_scope: crate::conn::CommandOwnerScope::capture(conn, cmd.session_id), - kind: Box::new(super::PendingPageCommandKind::Close), + owner_scope, + kind: Box::new(super::PendingPageCommandKind::Close { web_contents }), }) } @@ -594,6 +628,7 @@ pub(super) async fn complete_close_command_dispatch( conn: &mut CdpConnection, _command_id: Option, owner: &CommandOwnerScope, + web_contents: Option, command_context: &mut crate::conn::CommandDispatchContext, ) -> PageCommandTaskStep { let mut out = Vec::new(); @@ -611,6 +646,18 @@ pub(super) async fn complete_close_command_dispatch( )); }; let target_id = target_id.expect("validated Page target identity"); + let Some(web_contents) = web_contents else { + return PageCommandTaskStep::Complete(CommandOutputPlan::error_without_session( + -31998, + "TargetNotLoaded", + )); + }; + if conn.browser_web_contents_for_owner(owner).ok() != Some(web_contents) { + return PageCommandTaskStep::Complete(CommandOutputPlan::error_without_session( + -31998, + "TargetNotLoaded", + )); + } let ( pending_navigations, @@ -656,6 +703,7 @@ pub(super) async fn complete_close_command_dispatch( conn.publish_page_target_termination_owner_action(PageTargetTerminationOwnerAction::new( owner.clone(), target_id, + web_contents, )); complete_success_with_background_events(out) } @@ -664,7 +712,7 @@ pub(crate) async fn complete_page_target_termination_owner_action_async( conn: &mut CdpConnection, action: PageTargetTerminationOwnerAction, ) -> crate::conn::CdpTurnOutcome { - let (owner_scope, expected_target_id) = action.into_parts(); + let (owner_scope, expected_target_id, web_contents) = action.into_parts(); let mut out = Vec::new(); let current_target_id = conn .target_owner_identity_for_owner(&owner_scope) @@ -675,22 +723,25 @@ pub(crate) async fn complete_page_target_termination_owner_action_async( conn.take_scheduler_events(), ); } + if conn + .browser_web_contents_for_target(&expected_target_id) + .ok() + != Some(web_contents) + { + return crate::conn::CdpTurnOutcome::new_with_protocol_events( + out, + conn.take_scheduler_events(), + ); + } let target_host_closure = conn.prepare_target_host_closure(&expected_target_id); - let is_active_target = conn - .browser_context - .as_ref() - .is_some_and(|browser_context| browser_context.is_active_target(&expected_target_id)); - let closed = if is_active_target { - conn.close_active_page_target_for_target_close_async(&mut out, "Target closed") - .await - } else { - conn.close_background_page_target_for_target_close_async( + let closed = conn + .close_web_contents_for_target_close_async( &expected_target_id, + web_contents, &mut out, "Target closed", ) - .await - }; + .await; let Some(closed) = closed else { return crate::conn::CdpTurnOutcome::new_with_protocol_events( out, @@ -723,6 +774,5 @@ pub(crate) async fn complete_page_target_termination_owner_action_async( ); } out.extend(conn.prepared_target_host_deltas_event_plan(target_destroyed_deltas)); - conn.release_idle_navigation_engine_memory_after_target_close(); crate::conn::CdpTurnOutcome::new_with_protocol_events(out, conn.take_scheduler_events()) } diff --git a/moli-protocol/src/domains/page/tests/capture.rs b/moli-protocol/src/domains/page/tests/capture.rs index b9b2680d16..ad5d283ace 100644 --- a/moli-protocol/src/domains/page/tests/capture.rs +++ b/moli-protocol/src/domains/page/tests/capture.rs @@ -310,18 +310,11 @@ async fn capture_screenshot_rejects_completion_from_replaced_renderer_attachment panic!("captureScreenshot should use the pending renderer page-command lane"); }; - let replacement = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,", - ) - .await - .expect("replacement page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(replacement)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,", + None, + ) + .await; let mut command_context = CommandDispatchContext::default(); let PageCommandTaskStep::Complete(plan) = @@ -393,16 +386,8 @@ async fn capture_snapshot_returns_minimal_mhtml_for_loaded_page() { let mut ctx = TestContext::new(); let page_url = "data:text/html,

Text

"; load_bc_with_target(&mut ctx, "BID-MHTML", "TID-MHTML", page_url); - let page = ctx - .conn - .load_page_via_runtime_async(page_url) - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(page_url, None) + .await; ctx.process_async(json!({ "id": 1110, @@ -443,16 +428,8 @@ async fn capture_snapshot_dispatch_serializes_html_in_renderer_owner() { let page_url = "data:text/html,
renderer
"; load_bc_with_target(&mut ctx, "BID-MHTML-LIVE", "TID-MHTML-LIVE", page_url); - let page = ctx - .conn - .load_page_via_runtime_async(page_url) - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(page_url, None) + .await; let params = serde_json::Value::Null; let cmd = Cmd::for_test( @@ -619,7 +596,9 @@ async fn capture_screenshot_uses_emulated_device_metrics() { #[tokio::test(flavor = "multi_thread")] async fn capture_screenshot_targets_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-screenshot-background".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-screenshot-background".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -689,14 +668,9 @@ async fn capture_screenshot_targets_inactive_loaded_owner_without_activation() { "SID-active", "about:blank", ); - let page = ctx + let mut inactive = ctx .conn - .load_page_via_runtime_async( - "data:text/html,Inactive Screenshot
inactive
", - ) - .await - .expect("inactive page should load"); - let mut inactive = BrowserContext::new("BID-inactive-screenshot".to_owned()); + .new_browser_context_fixture_for_test("BID-inactive-screenshot".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); { @@ -723,9 +697,13 @@ async fn capture_screenshot_targets_inactive_loaded_owner_without_activation() { })), ) }; - inactive.replace_loaded_page(Some(page)); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,Inactive Screenshot
inactive
", + Some("SID-inactive"), + ) + .await; ctx.process_async(json!({ "id": 115, @@ -734,7 +712,8 @@ async fn capture_screenshot_targets_inactive_loaded_owner_without_activation() { })) .await; let response = take_response_by_id(&mut ctx, 115); - assert_png_dimensions(&screenshot_png_bytes(&response), 1920, 1080); + // The inactive Document inherits its own 500x300 viewport at DPR 1.5. + assert_png_dimensions(&screenshot_png_bytes(&response), 750, 450); assert_eq!( ctx.conn .browser_context @@ -1070,16 +1049,8 @@ async fn get_layout_metrics_queries_live_renderer_for_loaded_pages() { "about:blank", ); let page_url = "data:text/html,
"; - let page = ctx - .conn - .load_page_via_runtime_async(page_url) - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(page_url, None) + .await; ctx.process_async(json!({ "id": 125, @@ -1104,7 +1075,9 @@ async fn get_layout_metrics_targets_loaded_background_owner_without_activation() let mut ctx = TestContext::new(); let page_url = "data:text/html,
"; - let mut bc = BrowserContext::new("BID-1".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.set_target_url("data:text/html,active".to_owned()); @@ -1161,7 +1134,9 @@ async fn get_layout_metrics_targets_loaded_background_owner_without_activation() async fn get_layout_metrics_targets_inactive_loaded_owner_without_activation() { let mut ctx = TestContext::new(); let page_url = "data:text/html,
"; - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); inactive.set_target_url("about:blank".to_owned()); diff --git a/moli-protocol/src/domains/page/tests/dialog.rs b/moli-protocol/src/domains/page/tests/dialog.rs index 127f8bc292..dd1b4b6501 100644 --- a/moli-protocol/src/domains/page/tests/dialog.rs +++ b/moli-protocol/src/domains/page/tests/dialog.rs @@ -10,18 +10,25 @@ fn dialog_for_test( } fn page_owner_for_test( - ctx: &mut TestContext, + ctx: &TestContext, session_id: Option<&str>, ) -> crate::conn::TargetPageResidenceIdentity { - let owner = crate::conn::CommandOwnerScope::capture(&ctx.conn, session_id); - if ctx.conn.current_document_id_for_owner(&owner).is_none() { - ctx.conn.replace_document_fixture_for_owner_test(&owner); - } ctx.conn .target_page_residence_identity_for_session(session_id) .expect("target session should expose a Page residence") } +async fn load_dialog_context( + ctx: &mut TestContext, + browser_context_id: &str, + target_id: &str, + session_id: &str, + url: &str, +) { + load_bc_with_session(ctx, browser_context_id, target_id, session_id, url); + ensure_initial_document_for_session(ctx, Some(session_id)).await; +} + fn push_dialog_for_session( ctx: &mut TestContext, session_id: Option<&str>, @@ -48,16 +55,17 @@ fn push_dialog_for_session( )); } -#[test] -fn retiring_page_scope_and_clearing_dialog_state_dismisses_installed_dialog() { +#[tokio::test(flavor = "multi_thread")] +async fn retiring_page_scope_and_clearing_dialog_state_dismisses_installed_dialog() { let mut ctx = TestContext::new(); - load_bc_with_session( + load_dialog_context( &mut ctx, "BID-dialog-clear", "TID-dialog-clear", "SID-dialog-clear", "about:blank", - ); + ) + .await; let completion = RendererJavaScriptDialogCompletion::pending(); push_dialog_for_session( &mut ctx, @@ -112,7 +120,7 @@ fn retiring_page_scope_and_clearing_dialog_state_dismisses_installed_dialog() { #[tokio::test(flavor = "multi_thread")] async fn pending_javascript_dialogs_are_preserved_per_background_target() { let mut ctx = TestContext::new(); - load_bc_with_session(&mut ctx, "BID-1", "TID-A", "SID-A", "about:blank"); + load_dialog_context(&mut ctx, "BID-1", "TID-A", "SID-A", "about:blank").await; push_dialog_for_session( &mut ctx, Some("SID-A"), @@ -133,15 +141,13 @@ async fn pending_javascript_dialogs_are_preserved_per_background_target() { crate::conn::TargetPageSlot::empty_for_test_fixture(), ); ctx.conn.commit_declared_session_fixtures_for_test(); - assert!( - ctx.conn - .browser_context - .as_mut() - .unwrap() - .select_page_target_async("TID-B") - .await - .unwrap() - ); + ctx.install_navigation_fixture_for_session_owner("about:blank", Some("SID-B")) + .await; + let handle = ctx.conn.browser_web_contents_for_target("TID-B").unwrap(); + ctx.conn + .select_browser_web_contents_async(handle) + .await + .unwrap(); assert!( ctx.conn .javascript_dialog_snapshot_for_owner(&crate::conn::CommandOwnerScope::for_session( @@ -156,15 +162,11 @@ async fn pending_javascript_dialogs_are_preserved_per_background_target() { ); for (target, session, message) in [("TID-A", "SID-A", "a"), ("TID-B", "SID-B", "b")] { - assert!( - ctx.conn - .browser_context - .as_mut() - .unwrap() - .select_page_target_async(target) - .await - .unwrap() - ); + let handle = ctx.conn.browser_web_contents_for_target(target).unwrap(); + ctx.conn + .select_browser_web_contents_async(handle) + .await + .unwrap(); let dialog = ctx .conn .javascript_dialog_snapshot_for_owner(&crate::conn::CommandOwnerScope::for_session( @@ -178,17 +180,12 @@ async fn pending_javascript_dialogs_are_preserved_per_background_target() { #[tokio::test(flavor = "multi_thread")] async fn javascript_dialog_events_round_trip_through_page_domain() { let mut ctx = TestContext::new(); - load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(page)); + load_dialog_context(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank").await; + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,", + None, + ) + .await; ctx.process_async(json!({ "id": 2, @@ -238,13 +235,14 @@ async fn javascript_dialog_events_round_trip_through_page_domain() { #[tokio::test(flavor = "multi_thread")] async fn document_open_preserves_dialog_order_and_retires_replaced_document_state() { let mut ctx = TestContext::new(); - load_bc_with_session( + load_dialog_context( &mut ctx, "BID-dialog-open", "TID-dialog-open", "SID-dialog-open", "about:blank", - ); + ) + .await; ctx.install_buffered_navigation_fixture_for_session_owner( url::Url::parse("https://dialog-replacement.example/").unwrap(), "dialog replacement".into(), @@ -292,7 +290,7 @@ async fn document_open_preserves_dialog_order_and_retires_replaced_document_stat #[tokio::test(flavor = "multi_thread")] async fn get_javascript_dialog_text_peeks_without_closing_dialog() { let mut ctx = TestContext::new(); - load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); + load_dialog_context(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank").await; push_dialog_for_session( &mut ctx, Some("SID-1"), @@ -365,7 +363,7 @@ async fn get_javascript_dialog_text_peeks_without_closing_dialog() { #[tokio::test(flavor = "multi_thread")] async fn set_javascript_dialog_prompt_text_is_used_when_accepting_prompt() { let mut ctx = TestContext::new(); - load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); + load_dialog_context(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank").await; push_dialog_for_session( &mut ctx, Some("SID-1"), @@ -429,7 +427,7 @@ async fn set_javascript_dialog_prompt_text_is_used_when_accepting_prompt() { #[tokio::test(flavor = "multi_thread")] async fn handle_javascript_dialog_finishes_renderer_completion() { let mut ctx = TestContext::new(); - load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); + load_dialog_context(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank").await; let completion = RendererJavaScriptDialogCompletion::pending(); push_dialog_for_session( &mut ctx, @@ -468,7 +466,7 @@ async fn handle_javascript_dialog_finishes_renderer_completion() { #[tokio::test(flavor = "multi_thread")] async fn handle_javascript_dialog_rejects_when_no_dialog_is_showing() { let mut ctx = TestContext::new(); - load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); + load_dialog_context(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank").await; ctx.process_async(json!({ "id": 34, @@ -497,13 +495,14 @@ async fn handle_javascript_dialog_rejects_when_no_dialog_is_showing() { #[tokio::test(flavor = "multi_thread")] async fn javascript_dialog_pending_state_is_session_local_for_active_attached_session() { let mut ctx = TestContext::new(); - load_bc_with_session( + load_dialog_context( &mut ctx, "BID-dialog-attached", "TID-dialog-attached", "SID-primary", "about:blank", - ); + ) + .await; let browser_context = ctx.conn.browser_context.as_mut().unwrap(); assert!( browser_context @@ -567,10 +566,11 @@ async fn javascript_dialog_pending_state_is_session_local_for_active_attached_se #[tokio::test(flavor = "multi_thread")] async fn handle_javascript_dialog_rejects_dialog_without_current_page_residence() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-dialog-missing-frame", - "TID-dialog-missing-frame", - )); + ctx.conn.browser_context = + Some(ctx.conn.new_page_target_fixture_for_test( + "BID-dialog-missing-frame", + "TID-dialog-missing-frame", + )); ctx.conn .with_target_devtools_session_state_for_session_mut(None, |state| { state @@ -613,17 +613,12 @@ async fn handle_javascript_dialog_rejects_dialog_without_current_page_residence( #[tokio::test(flavor = "multi_thread")] async fn javascript_dialog_events_are_emitted_after_runtime_call_function_on() { let mut ctx = TestContext::new(); - load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,callFunctionOn") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(page)); + load_dialog_context(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank").await; + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,callFunctionOn", + None, + ) + .await; ctx.process_async(json!({ "id": 4, @@ -674,17 +669,12 @@ async fn javascript_dialog_events_are_emitted_after_runtime_call_function_on() { #[tokio::test(flavor = "multi_thread")] async fn javascript_dialog_events_are_emitted_from_playwright_utility_world_call_function_on() { let mut ctx = TestContext::new(); - load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,utility dialog") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(page)); + load_dialog_context(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank").await; + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,utility dialog", + None, + ) + .await; ctx.process_async(json!({ "id": 7, @@ -759,17 +749,12 @@ async fn javascript_dialog_events_are_emitted_from_playwright_utility_world_call async fn javascript_dialog_events_are_emitted_from_playwright_serialized_utility_call_function_on() { let mut ctx = TestContext::new(); - load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,serialized utility dialog") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(page)); + load_dialog_context(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank").await; + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,serialized utility dialog", + None, + ) + .await; ctx.process_async(json!({ "id": 11, diff --git a/moli-protocol/src/domains/page/tests/document_content.rs b/moli-protocol/src/domains/page/tests/document_content.rs index 6aa5de2a94..b4b9ba5d09 100644 --- a/moli-protocol/src/domains/page/tests/document_content.rs +++ b/moli-protocol/src/domains/page/tests/document_content.rs @@ -2,71 +2,8 @@ use super::*; async fn install_document_content_test_page(ctx: &mut TestContext, url: &str) { load_bc_with_session(ctx, "BID-set-content", "TID-1", "SID-1", "about:blank"); - let committed_document = { - let browser_context = ctx.conn.browser_context.as_mut().expect("browser context"); - browser_context.set_target_url(url.to_owned()); - browser_context - .start_document_navigation_for_active_target(LOADER_ID.to_owned()) - .expect("document-content test navigation should start") - }; - let navigation = ctx - .conn - .load_navigation_via_runtime_for_session_owner_async(Some("SID-1"), url) - .await - .expect("document-content test page should load"); - let artifacts = navigation.page_creation_artifacts; - let committed = { - let browser_context = ctx.conn.browser_context.as_mut().expect("browser context"); - let identity = navigation.main_document_commit.as_ref().unwrap(); - let prepared = browser_context - .start_loaded_document_navigation_for_target( - "TID-1", - committed_document, - navigation.page, - crate::conn::DocumentNavigationDestination { - url: navigation.final_url, - security_origin: identity.security_origin.clone(), - secure_context_type: identity.secure_context_type.clone(), - }, - &artifacts, - &Default::default(), - ) - .unwrap() - .await - .unwrap(); - browser_context - .commit_loaded_navigation(prepared) - .expect("document-content test Document should commit") - }; - let projection_fence = committed - .inspection_projection - .expect("document-content test inspection projection should succeed"); - committed.previous_document_retirement.close().await; - let (binding, _) = ctx.conn.project_committed_document_lifecycle_for_owner( - &crate::conn::CommandOwnerScope::for_session("SID-1"), - committed.lifecycle, - Some(committed_document), - "TID-1".to_owned(), - LOADER_ID.to_owned(), - ); - let binding = binding.expect("renderer lifecycle should bind"); - assert!( - ctx.conn - .publish_document_projection_fence_for_owner( - &crate::conn::CommandOwnerScope::for_session("SID-1"), - &binding, - projection_fence, - ) - .released_output - .is_empty(), - "the fixture should not leave buffered Inspector output behind" - ); - // The fixture commits an already-running renderer Page directly instead - // of going through the production navigation command. Route that exact - // Document's initial lifecycle publication before enabling observers. - // Otherwise Page.enable can replay its old load while a later - // setDocumentContent replacement is running, and a test may mistake the - // previous epoch for the replacement's terminal event. + ctx.install_navigation_fixture_for_session_owner(url, Some("SID-1")) + .await; wait_until_renderer_document_load(ctx, Some("SID-1"), "TID-1", LOADER_ID).await; } @@ -1712,14 +1649,12 @@ async fn root_set_document_content_unloads_descendant_frame_before_clearing_pare .conn .start_child_frame_lifecycle_work_for_owner(owner, std::time::Duration::from_secs(2)) .expect("loaded page should expose child-frame lifecycle work"); - let completed = pending - .wait() - .await - .expect("srcdoc child lifecycle should complete"); + let completed = pending.wait().await; assert!( ctx.conn - .complete_child_frame_lifecycle_work_for_session_owner(completed) + .finish_document_child_frame_lifecycle_work(completed) .expect("srcdoc child lifecycle completion should apply") + .0 ); assert_eq!( diff --git a/moli-protocol/src/domains/page/tests/frame_tree.rs b/moli-protocol/src/domains/page/tests/frame_tree.rs index 6918db687f..4e603d772f 100644 --- a/moli-protocol/src/domains/page/tests/frame_tree.rs +++ b/moli-protocol/src/domains/page/tests/frame_tree.rs @@ -6,14 +6,12 @@ async fn complete_child_frame_lifecycle(ctx: &mut TestContext) { .conn .start_child_frame_lifecycle_work_for_owner(owner, std::time::Duration::from_secs(2)) .expect("loaded page should expose child-frame lifecycle work"); - let completed = pending - .wait() - .await - .expect("child-frame lifecycle work should complete"); + let completed = pending.wait().await; assert!( ctx.conn - .complete_child_frame_lifecycle_work_for_session_owner(completed) - .expect("child-frame lifecycle completion should apply"), + .finish_document_child_frame_lifecycle_work(completed) + .expect("child-frame lifecycle completion should apply") + .0, "child-frame lifecycle should settle before inspecting final frame metadata" ); } @@ -228,16 +226,8 @@ async fn get_frame_tree_uses_owner_element_id_when_frame_name_is_empty() { let mut ctx = TestContext::new(); let page_url = "data:text/html,"; load_bc_with_target(&mut ctx, "BID-ID-FALLBACK", "FID-ID-FALLBACK", page_url); - let page = ctx - .conn - .load_page_via_runtime_async(page_url) - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(page_url, None) + .await; complete_child_frame_lifecycle(&mut ctx).await; ctx.process_async(json!({"id": 15, "method": "Page.getFrameTree"})) @@ -262,7 +252,7 @@ async fn get_frame_tree_projects_sandboxed_about_blank_from_document_url() { .expect("page should load"); { let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(page)); + let _ = bc.commit_active_navigation_for_test(page).await; bc.set_target_security_origin("https://top.example".into()); bc.set_target_secure_context_type("Secure".into()); } @@ -290,7 +280,7 @@ async fn get_frame_tree_recurses_into_nested_child_frames() { .expect("page should load"); { let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(page)); + let _ = bc.commit_active_navigation_for_test(page).await; bc.set_target_security_origin("https://top.example".into()); bc.set_target_secure_context_type("Secure".into()); } @@ -348,7 +338,7 @@ async fn get_frame_tree_projects_nested_sandboxed_srcdoc_from_document_urls() { .expect("page should load"); { let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(page)); + let _ = bc.commit_active_navigation_for_test(page).await; bc.set_target_security_origin("https://top.example".into()); bc.set_target_secure_context_type("Secure".into()); } @@ -431,16 +421,8 @@ async fn get_frame_tree_can_complete_through_pending_command_dispatch() { "SID-PENDING-FRAME-TREE", page_url, ); - let page = ctx - .conn - .load_page_via_runtime_async(page_url) - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(page_url, None) + .await; let raw = json!({ "id": 1203, @@ -483,16 +465,8 @@ async fn pending_get_frame_tree_after_page_unload_returns_empty_target_tree() { "SID-PENDING-FRAME-TREE-UNLOAD", page_url, ); - let page = ctx - .conn - .load_page_via_runtime_async(page_url) - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(page_url, None) + .await; let raw = json!({ "id": 1206, @@ -536,12 +510,71 @@ async fn pending_get_frame_tree_after_page_unload_returns_empty_target_tree() { "unloaded page should match the legacy empty child frame tree path: {response}" ); } + +#[tokio::test(flavor = "multi_thread")] +async fn pending_get_frame_tree_cannot_read_a_replacement_document() { + let mut ctx = TestContext::new(); + let page_url = "data:text/html,"; + load_bc_with_session( + &mut ctx, + "BID-PENDING-FRAME-TREE-REPLACED", + "TID-PENDING-FRAME-TREE-REPLACED", + "SID-PENDING-FRAME-TREE-REPLACED", + page_url, + ); + ctx.install_quiet_navigation_fixture_for_session_owner(page_url, None) + .await; + + let raw = json!({ + "id": 1207, + "method": "Page.getFrameTree", + "sessionId": "SID-PENDING-FRAME-TREE-REPLACED" + }) + .to_string(); + let pending = ctx + .conn + .try_start_pending_command_dispatch(&raw) + .expect("Page.getFrameTree should start against the outgoing Document"); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,replacement", + None, + ) + .await; + + let (messages, scheduler_events) = + complete_pending_command_task_for_test(&mut ctx, pending).await; + assert!(scheduler_events.is_empty()); + let response = messages + .iter() + .find(|message| message["id"] == json!(1207)) + .expect("Page.getFrameTree response"); + assert_eq!( + response["result"]["frameTree"]["frame"]["id"], + json!("TID-PENDING-FRAME-TREE-REPLACED") + ); + assert!( + response["result"]["frameTree"].get("childFrames").is_none(), + "an outgoing snapshot must not be combined with replacement Document state: {response}" + ); + assert_eq!( + ctx.conn + .browser_context + .as_ref() + .unwrap() + .target_document_title("TID-PENDING-FRAME-TREE-REPLACED") + .as_deref(), + Some("replacement") + ); +} + #[tokio::test(flavor = "multi_thread")] async fn get_frame_tree_targets_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); let page_url = "data:text/html,"; - let mut bc = BrowserContext::new("BID-1".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.set_target_url("data:text/html,active".to_owned()); @@ -587,7 +620,9 @@ async fn get_frame_tree_targets_loaded_background_owner_without_activation() { async fn get_frame_tree_targets_inactive_loaded_owner_without_activation() { let mut ctx = TestContext::new(); let page_url = "data:text/html,"; - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); inactive.set_target_url("about:blank".to_owned()); diff --git a/moli-protocol/src/domains/page/tests/lifecycle.rs b/moli-protocol/src/domains/page/tests/lifecycle.rs index 83990df6f0..2a737d8dc1 100644 --- a/moli-protocol/src/domains/page/tests/lifecycle.rs +++ b/moli-protocol/src/domains/page/tests/lifecycle.rs @@ -52,10 +52,10 @@ fn child_frame_security_identity_matches_chromium_cdp_url_projection() { #[test] fn page_owner_state_commands_complete_through_command_dispatch() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-PAGE-COMPLETE", - "TID-PAGE-COMPLETE", - )); + ctx.conn.browser_context = Some( + ctx.conn + .new_page_target_fixture_for_test("BID-PAGE-COMPLETE", "TID-PAGE-COMPLETE"), + ); for (id, method, params) in [ ( @@ -124,9 +124,7 @@ fn page_owner_state_commands_complete_through_command_dispatch() { { let browser_context = ctx.conn.browser_context.as_mut().expect("browser context"); - browser_context - .renderer_runtime() - .set_javascript_dialog_handler_enabled(true); + browser_context.set_javascript_dialog_handler_enabled(true); let page_session_state = &mut browser_context.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .page_session_state; @@ -159,9 +157,7 @@ fn page_owner_state_commands_complete_through_command_dispatch() { .page_session_state, ); assert!( - !browser_context - .renderer_runtime() - .javascript_dialog_handler_enabled(), + !browser_context.javascript_dialog_handler_enabled(), "Page.disable should disengage renderer JavaScript dialog handling" ); } @@ -264,30 +260,11 @@ async fn install_runtime_document_replacement_test_page(ctx: &mut TestContext) - "SID-1", "about:blank", ); - let navigation = ctx - .conn - .load_navigation_via_runtime_async("data:text/html,initial") - .await - .expect("runtime replacement fixture page should load"); - let artifacts = navigation.page_creation_artifacts; - ctx.conn - .browser_context - .as_mut() - .expect("runtime replacement fixture browser context") - .replace_active_page_for_test(Some(navigation.page)); - let (binding, initial_events) = ctx.conn.bind_renderer_document_lifecycle_for_owner( - &crate::conn::CommandOwnerScope::for_session("SID-1"), - artifacts, - None, - "TID-1".to_owned(), - LOADER_ID.to_owned(), - ); - assert!(binding.is_some()); - assert_eq!( - initial_events.len(), - 2, - "prepared data navigation commits at DOMContentLoaded; load remains renderer-owned tail work" - ); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,initial", + Some("SID-1"), + ) + .await; ctx.enable_dom_events_for_test(Some("SID-1")); ctx.process_async(json!({ @@ -409,7 +386,8 @@ async fn lifecycle_events_enable_without_renderer_binding_does_not_synthesize_re .browser_context .as_mut() .expect("browser context") - .replace_active_page_for_test(Some(page)); + .commit_active_navigation_for_test(page) + .await; let raw = json!({ "id": 1213, @@ -679,7 +657,9 @@ async fn bring_to_front_returns_empty_result() { #[tokio::test(flavor = "multi_thread")] async fn page_set_download_behavior_reuses_browser_download_state() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-PAGE-DOWNLOAD".into()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-PAGE-DOWNLOAD"); browser_context.set_active_target_id("TID-PAGE-DOWNLOAD"); ctx.conn .install_browser_context_fixture_for_test(browser_context); @@ -718,7 +698,7 @@ async fn page_set_download_behavior_reuses_browser_download_state() { ); assert_eq!( ctx.conn.download_policy_for_browser_context(None), - &moli_core::browser::DownloadPolicy::default() + moli_core::browser::DownloadPolicy::default() ); assert!( !ctx.conn @@ -729,10 +709,14 @@ async fn page_set_download_behavior_reuses_browser_download_state() { #[tokio::test(flavor = "multi_thread")] async fn page_set_download_behavior_uses_current_page_context_not_param_context() { let mut ctx = TestContext::new(); - let mut active = BrowserContext::new("BID-PAGE-DOWNLOAD-ACTIVE".into()); + let mut active = ctx + .conn + .new_browser_context_fixture_for_test("BID-PAGE-DOWNLOAD-ACTIVE"); active.set_active_target_id("TID-PAGE-DOWNLOAD-ACTIVE"); ctx.conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-PAGE-DOWNLOAD-OTHER".into()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-PAGE-DOWNLOAD-OTHER"); inactive.set_active_target_id("TID-PAGE-DOWNLOAD-OTHER"); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); @@ -768,7 +752,7 @@ async fn page_set_download_behavior_uses_current_page_context_not_param_context( assert_eq!( ctx.conn .download_policy_for_browser_context(Some("BID-PAGE-DOWNLOAD-OTHER")), - &moli_core::browser::DownloadPolicy::default() + moli_core::browser::DownloadPolicy::default() ); } @@ -802,7 +786,7 @@ async fn page_set_download_behavior_rejects_without_page_owner() { #[tokio::test(flavor = "multi_thread")] async fn set_lifecycle_events_enabled_sets_flag() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); ctx.process_async(json!({"id": 1, "method": "Page.setLifecycleEventsEnabled", @@ -840,7 +824,8 @@ async fn set_lifecycle_events_enabled_replays_loaded_page_events() { .browser_context .as_mut() .unwrap() - .replace_active_page_for_test(Some(navigation.page)); + .commit_active_navigation_for_test(navigation.page) + .await; let (binding, initial_events) = ctx.conn.bind_renderer_document_lifecycle_for_owner( &crate::conn::CommandOwnerScope::for_session("SID-1"), navigation.page_creation_artifacts, @@ -907,7 +892,8 @@ async fn set_lifecycle_events_enabled_replays_only_protocol_visible_load_state() .browser_context .as_mut() .unwrap() - .replace_active_page_for_test(Some(navigation.page)); + .commit_active_navigation_for_test(navigation.page) + .await; let (binding, initial_events) = ctx.conn.bind_renderer_document_lifecycle_for_owner( &crate::conn::CommandOwnerScope::for_session("SID-visible"), artifacts, @@ -998,7 +984,9 @@ async fn set_lifecycle_events_enabled_is_session_local_for_active_attached_sessi .expect("page should load"); let artifacts = navigation.page_creation_artifacts; let browser_context = ctx.conn.browser_context.as_mut().unwrap(); - browser_context.replace_active_page_for_test(Some(navigation.page)); + browser_context + .commit_active_navigation_for_test(navigation.page) + .await; assert!( browser_context.assign_attached_session_to_target("TID-active", "SID-attached".to_owned()) ); @@ -1062,7 +1050,9 @@ async fn set_lifecycle_events_enabled_is_session_local_for_active_attached_sessi #[tokio::test(flavor = "multi_thread")] async fn set_lifecycle_events_enabled_is_session_local_for_background_attached_session() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-background-attached-lifecycle".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-background-attached-lifecycle".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -1198,7 +1188,9 @@ async fn page_disable_clears_page_handler_state_for_active_attached_session() { #[tokio::test(flavor = "multi_thread")] async fn page_disable_clears_page_handler_state_for_background_attached_session() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-background-page-disable".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-background-page-disable".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -2031,20 +2023,16 @@ async fn runtime_document_write_preserves_multiple_classic_and_module_script_ord async fn add_script_run_immediately_creates_top_level_world_even_when_child_world_name_matches() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async( - // Keep the child on its initial empty document. A `srcdoc` - // navigation may commit after the preload is registered, in - // which case the new-document script correctly runs in that - // future child document and no longer isolates runImmediately's - // top-level-world behavior. - "data:text/html,parent-frame", - ) - .await - .expect("page should load"); - let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + // Keep the child on its initial empty document. A `srcdoc` + // navigation may commit after the preload is registered, in + // which case the new-document script correctly runs in that + // future child document and no longer isolates runImmediately's + // top-level-world behavior. + "data:text/html,parent-frame", + Some("SID-1"), + ) + .await; ctx.process_async(json!({ "id": 4120, "method": "Runtime.enable", @@ -2236,7 +2224,9 @@ async fn document_start_script_run_immediately_targets_loaded_background_owner_w { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -2322,13 +2312,11 @@ async fn document_start_script_run_immediately_targets_loaded_background_owner_w async fn bare_isolated_worlds_do_not_persist_across_navigation() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,hello") - .await - .expect("page should load"); - let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,hello", + Some("SID-1"), + ) + .await; ctx.process_async(json!({ "id": 49, "method": "Runtime.enable", @@ -2559,14 +2547,13 @@ async fn crash_requires_browser_context() { async fn crash_notifies_all_attached_sessions_and_marks_browser_context_crashed() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,crash-me") - .await - .expect("page should load"); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,crash-me", + Some("SID-1"), + ) + .await; let bc = ctx.conn.browser_context.as_mut().unwrap(); assert!(bc.assign_attached_session_to_target("TID-1", "SID-attached".into())); - let _ = bc.replace_active_page_for_test(Some(page)); let active_document_token = bc .start_document_navigation_for_active_target("LOADER-crash".to_owned()) .expect("active target should start document navigation"); @@ -2641,21 +2628,24 @@ async fn crash_notifies_all_attached_sessions_and_marks_browser_context_crashed( #[tokio::test(flavor = "multi_thread")] async fn crash_targets_background_owner_without_activation() { let mut ctx = TestContext::new(); - let background_page = ctx + let mut bc = ctx .conn - .load_page_via_runtime_async("data:text/html,background-crash") - .await - .expect("background page should load"); - let mut bc = BrowserContext::new("BID-1".to_owned()); + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.set_target_url("data:text/html,Active
active
".to_owned()); bc.register_page_target_url_fixture( "TID-background".to_owned(), Some("SID-background".to_owned()), - background_page.final_url().as_str().to_owned(), + "about:blank".to_owned(), ); - bc.replace_target_page_for_test("TID-background", Some(background_page)); + ctx.conn.install_browser_context_fixture_for_test(bc); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,background-crash", + Some("SID-background"), + ) + .await; + let bc = ctx.conn.browser_context.as_mut().unwrap(); bc.background_target_mut("TID-background") .expect("background target must exist") .devtools_sessions @@ -2670,8 +2660,6 @@ async fn crash_targets_background_owner_without_activation() { .expect("background target should start document navigation"); bc.commit_document_navigation_if_matches(&background_document_token); assert!(bc.accepts_document_body_completion_event(&background_document_token)); - ctx.conn.install_browser_context_fixture_for_test(bc); - ctx.process_async(json!({ "id": 248, "method": "Page.crash", @@ -2833,7 +2821,7 @@ async fn crash_aborts_background_paused_navigation_without_activation() { #[tokio::test(flavor = "multi_thread")] async fn crash_without_target_loaded_errors() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({ "id": 251, @@ -2931,11 +2919,11 @@ async fn crash_aborts_paused_response_stage_navigation() { async fn close_clears_loaded_page_state_and_emits_detached_events() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,close-me") - .await - .expect("page should load"); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,close-me", + Some("SID-1"), + ) + .await; let bc = ctx.conn.browser_context.as_mut().unwrap(); bc.active_page_target_mut().devtools_sessions[moli_page_types::DevToolsSessionKey::Primary] .page_session_state @@ -2982,7 +2970,6 @@ async fn close_clears_loaded_page_state_and_emits_detached_events() { .expect("active fixture target"); context.set_target_crash_state(&target_id, true) }; - let _ = bc.replace_active_page_for_test(Some(page)); let close_document_token = bc .start_document_navigation_for_active_target("LOADER-close".to_owned()) .expect("active target should start document navigation"); @@ -2997,14 +2984,11 @@ async fn close_clears_loaded_page_state_and_emits_detached_events() { .runtime_slot .set_next_subresource_fetch_request_id_for_test(5); assert!(bc.assign_attached_session_to_target("TID-1", "SID-attached".into())); - bc.remember_target_window_name("close-me", "TID-1"); - // An unknown popup cannot retain an orphaned opener side record. - bc.remember_target_opener( - "TID-popup-after-close", - "TID-1".into(), - "FRAME-1".into(), - false, - ); + let handle = bc.web_contents_handle_for_target("TID-1").unwrap(); + bc.set_web_contents_window_name(handle, Some("close-me".into())) + .unwrap(); + // An unknown popup has no WebContents capability on which opener state + // could be installed. bc.record_captured_response_body("REQ-old".into(), "body".into(), [Some("SID-1".into())]); bc.insert_io_stream("STREAM-old".into(), b"body".to_vec(), 0); @@ -3231,7 +3215,7 @@ async fn close_aborts_background_paused_navigation_without_activation() { #[tokio::test(flavor = "multi_thread")] async fn close_without_target_loaded_errors() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({ "id": 2460, @@ -3492,12 +3476,134 @@ async fn close_command_background_events_keep_target_detached_sidecar() { assert_eq!(event.reason.as_deref(), Some("Render process gone.")); } +#[tokio::test(flavor = "multi_thread")] +async fn close_completion_cannot_retarget_a_reused_target_id() { + let mut ctx = TestContext::new(); + load_bc_with_session( + &mut ctx, + "BID-close-completion", + "TID-close-completion", + "SID-close-completion", + "about:blank", + ); + let old_web_contents = ctx + .conn + .browser_web_contents_for_target("TID-close-completion") + .unwrap(); + + let raw = json!({ + "id": 248, + "method": "Page.close", + "sessionId": "SID-close-completion" + }) + .to_string(); + let CdpCommandTaskStep::Pending(pending) = ctx.conn.start_command_dispatch(&raw) else { + panic!("Page.close should start through pending dispatch"); + }; + + load_bc_with_session( + &mut ctx, + "BID-close-completion", + "TID-close-completion", + "SID-close-completion", + "about:blank", + ); + let replacement = ctx + .conn + .browser_web_contents_for_target("TID-close-completion") + .unwrap(); + assert_ne!(replacement, old_web_contents); + + let CdpCommandTaskStep::Complete(outcome) = ctx + .conn + .complete_pending_command_dispatch(pending.wait().await) + .await + else { + panic!("stale Page.close completion should settle with an error"); + }; + let (_, _, _, _, scheduler_events, _) = outcome.into_renderer_owner_turn_parts(); + assert!(scheduler_events.is_empty()); + assert_eq!( + ctx.conn + .browser_web_contents_for_target("TID-close-completion") + .unwrap(), + replacement + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn delayed_close_cannot_retarget_a_reused_target_id() { + let mut ctx = TestContext::new(); + load_bc_with_session( + &mut ctx, + "BID-delayed-close", + "TID-delayed-close", + "SID-delayed-close", + "about:blank", + ); + let old_web_contents = ctx + .conn + .browser_web_contents_for_target("TID-delayed-close") + .unwrap(); + + let raw = json!({ + "id": 248, + "method": "Page.close", + "sessionId": "SID-delayed-close" + }) + .to_string(); + let CdpCommandTaskStep::Pending(pending) = ctx.conn.start_command_dispatch(&raw) else { + panic!("Page.close should publish delayed termination work"); + }; + let CdpCommandTaskStep::Complete(outcome) = ctx + .conn + .complete_pending_command_dispatch(pending.wait().await) + .await + else { + panic!("Page.close pending dispatch should complete"); + }; + let (_, _, _, _, scheduler_events, _) = outcome.into_renderer_owner_turn_parts(); + let [CdpSchedulerEvent::ProtocolWorkPublished { work }] = + <[_; 1]>::try_from(scheduler_events).unwrap() + else { + unreachable!("array pattern fixes the only event kind") + }; + + load_bc_with_session( + &mut ctx, + "BID-delayed-close", + "TID-delayed-close", + "SID-delayed-close", + "about:blank", + ); + let replacement = ctx + .conn + .browser_web_contents_for_target("TID-delayed-close") + .unwrap(); + assert_ne!(replacement, old_web_contents); + + let outcome = ctx + .conn + .complete_ready_protocol_scheduler_work_turn(work) + .await; + let (events, nested_scheduler_events) = outcome.into_protocol_event_parts(); + assert!(events.is_empty()); + assert!(nested_scheduler_events.is_empty()); + assert_eq!( + ctx.conn + .browser_web_contents_for_target("TID-delayed-close") + .unwrap(), + replacement + ); +} + #[tokio::test(flavor = "multi_thread")] async fn set_bypass_csp_accepts_valid_params_and_returns_empty_result() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-PAGE", "TID-PAGE", - )); + ctx.conn.browser_context = Some( + ctx.conn + .new_page_target_fixture_for_test("BID-PAGE", "TID-PAGE"), + ); ctx.process_async(json!({ "id": 2, "method": "Page.setBypassCSP", @@ -3525,9 +3631,10 @@ async fn set_bypass_csp_rejects_invalid_params() { #[tokio::test(flavor = "multi_thread")] async fn set_font_families_accepts_object_params_and_returns_empty_result() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-PAGE", "TID-PAGE", - )); + ctx.conn.browser_context = Some( + ctx.conn + .new_page_target_fixture_for_test("BID-PAGE", "TID-PAGE"), + ); ctx.process_async(json!({ "id": 4, "method": "Page.setFontFamilies", @@ -3579,9 +3686,10 @@ async fn set_font_families_rejects_invalid_params() { #[tokio::test(flavor = "multi_thread")] async fn set_intercept_file_chooser_dialog_accepts_valid_params_and_returns_empty_result() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-PAGE", "TID-PAGE", - )); + ctx.conn.browser_context = Some( + ctx.conn + .new_page_target_fixture_for_test("BID-PAGE", "TID-PAGE"), + ); ctx.process_async(json!({ "id": 51, "method": "Page.setInterceptFileChooserDialog", @@ -3859,6 +3967,35 @@ async fn screencast_capture_materializes_jpeg_frame_and_ack_budget() { "{:?}", &decoded.rgba[..4] ); + assert_eq!( + ctx.conn + .acknowledge_page_screencast_frame_for_session_owner( + Some("SID-screencast-frame"), + registration.generation(), + ), + Some(true), + "the background-color frame must be acknowledged before the next capture", + ); + let PageScreencastCaptureStart::Pending(stale_capture) = ctx + .conn + .start_page_screencast_frame_capture(®istration, None) + else { + panic!("an acknowledged subscription should start another capture"); + }; + ctx.conn + .replace_document_fixture_for_owner_test(&crate::conn::CommandOwnerScope::for_session( + "SID-screencast-frame", + )); + assert!(matches!( + ctx.conn + .complete_page_screencast_frame_capture(stale_capture.wait().await), + PageScreencastCaptureCompletion::Retry + )); + assert_eq!( + ctx.conn.page_screencast_subscription_status(®istration), + PageScreencastSubscriptionStatus::Ready, + "an outgoing Document capture must not finish against its replacement", + ); } #[tokio::test(flavor = "multi_thread")] @@ -4065,7 +4202,9 @@ async fn start_screencast_is_session_local_for_active_attached_session() { #[tokio::test(flavor = "multi_thread")] async fn start_screencast_is_session_local_for_background_attached_session() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-background-screencast".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-background-screencast".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( @@ -4259,7 +4398,7 @@ async fn repeated_start_invalidates_old_screencast_ack_generation() { #[tokio::test(flavor = "multi_thread")] async fn start_screencast_rejects_mock_layout_without_activating_state() { - let conn = crate::conn::CdpConnection::new_with_initial_storage_partition_and_runtime_config( + let conn = crate::test_support::connection_with_config( crate::conn::CdpInitialStoragePartition::memory(), moli_core::runtime::NavigationRuntimeConfig::new( moli_fetch::FetchConfig::default(), diff --git a/moli-protocol/src/domains/page/tests/mod.rs b/moli-protocol/src/domains/page/tests/mod.rs index 5b77251535..f7f79b933c 100644 --- a/moli-protocol/src/domains/page/tests/mod.rs +++ b/moli-protocol/src/domains/page/tests/mod.rs @@ -1,8 +1,8 @@ use super::{LOADER_ID, build_mhtml_snapshot, child_frame_security_identity}; use crate::conn::{ - BrowserContext, CdpCommandTaskStep, CdpSchedulerEvent, EmulatedDeviceMetrics, - FetchInterceptionPattern, FetchRequestStage, NETWORK_ERROR_PAGE_URL, PendingCdpCommandDispatch, - ServiceWorkerTargetState, URL_BASE, + CdpCommandTaskStep, CdpSchedulerEvent, EmulatedDeviceMetrics, FetchInterceptionPattern, + FetchRequestStage, NETWORK_ERROR_PAGE_URL, PendingCdpCommandDispatch, ServiceWorkerTargetState, + URL_BASE, }; use crate::devtools_runtime::{ AutomationEvent, DevToolsCommand, DevToolsCommandContext, DevToolsCommandResult, @@ -74,8 +74,16 @@ fn dialog_projection_for_test( ) -> crate::conn::TargetJavaScriptDialog { // Subscription-only fixture. Actual modal work is installed through the // Browser bridge in dialog tests, never stored in this session projection. + let document = moli_core::browser::DocumentHandle::new( + moli_core::browser::WebContentsHandle::new( + moli_core::browser::BrowserContextId::allocate(), + moli_core::browser::WebContentsId::allocate(), + ), + page_owner.document_id(), + ); crate::conn::TargetJavaScriptDialog::new( frame_id.to_owned(), + document, crate::conn::JavaScriptDialogKey::new( page_owner.document_id(), &renderer_dialog_for_test(Some(frame_id), "alert", "pending", "", None), @@ -152,7 +160,7 @@ fn take_main_document_response_pause_after_extra_info( } fn load_bc_with_target(ctx: &mut TestContext, bc_id: &str, target_id: &str, url: &str) { - let mut bc = BrowserContext::new(bc_id.into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test(bc_id); bc.set_active_target_id(target_id); bc.set_target_url(url.into()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -164,7 +172,7 @@ fn load_bc_with_session( session_id: &str, url: &str, ) { - let mut bc = BrowserContext::new(bc_id.into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test(bc_id); // Use the same target-staging boundary as production. In particular, // about:blank must own an initial-empty-document record before its Page is // materialized; setting only target/session/url metadata creates a Page @@ -187,7 +195,9 @@ fn load_bc_with_session( ); } fn load_bc_with_service_worker_target(ctx: &mut TestContext) { - let mut bc = BrowserContext::new("BID-service-worker-frame-tree".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-service-worker-frame-tree".to_owned()); let target = ServiceWorkerTargetState::new( 3, 7, diff --git a/moli-protocol/src/domains/page/tests/navigation.rs b/moli-protocol/src/domains/page/tests/navigation.rs index 48dd761847..81cdd13964 100644 --- a/moli-protocol/src/domains/page/tests/navigation.rs +++ b/moli-protocol/src/domains/page/tests/navigation.rs @@ -1309,6 +1309,58 @@ async fn navigation_bootstraps_browser_history_length_before_author_scripts() { server.abort(); } +#[tokio::test(flavor = "multi_thread")] +async fn pending_same_document_navigation_cannot_finish_on_a_replacement_document() { + let mut ctx = TestContext::new(); + load_bc_with_session( + &mut ctx, + "BID-stale-same-document", + "TID-stale-same-document", + "SID-stale-same-document", + "about:blank", + ); + let original_url = "data:text/html,navigation-owner"; + ctx.install_navigation_fixture_for_session_owner(original_url, Some("SID-stale-same-document")) + .await; + + let raw = json!({ + "id": 90_121, + "method": "Page.navigate", + "sessionId": "SID-stale-same-document", + "params": { "url": format!("{original_url}#fragment") } + }) + .to_string(); + let CdpCommandTaskStep::Pending(pending) = ctx.conn.start_command_dispatch(&raw) else { + panic!("same-document navigation should start on the original Document"); + }; + let completed = pending.wait().await; + + let replacement_url = "data:text/html,replacement-navigation-owner"; + ctx.install_navigation_fixture_for_session_owner( + replacement_url, + Some("SID-stale-same-document"), + ) + .await; + + let CdpCommandTaskStep::Complete(outcome) = + ctx.conn.complete_pending_command_dispatch(completed).await + else { + panic!("stale same-document navigation must not continue on the replacement Document"); + }; + let (messages, _) = ctx.route_completed_command_outcome_for_test(outcome).await; + let response = messages + .iter() + .find(|message| message["id"] == json!(90_121)) + .expect("same-document navigation response"); + assert_eq!(response["error"]["code"], json!(-32000), "{response}"); + assert_eq!(response["error"]["message"], json!("Document changed")); + assert_eq!( + ctx.conn.browser_context.as_ref().unwrap().target_url(), + replacement_url, + "the outgoing navigation must not mutate its replacement", + ); +} + #[tokio::test(flavor = "multi_thread")] async fn navigation_history_supports_playwright_back_forward_commands() { let mut ctx = TestContext::new(); @@ -1856,16 +1908,8 @@ async fn get_navigation_history_completes_through_command_dispatch() { "SID-HISTORY-COMPLETE", page_url, ); - let page = ctx - .conn - .load_page_via_runtime_async(page_url) - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(page_url, None) + .await; let raw = json!({ "id": 1209, @@ -1919,16 +1963,8 @@ async fn reset_navigation_history_prunes_browser_and_renderer_history() { "SID-RESET-HISTORY", &page_url, ); - let page = ctx - .conn - .load_page_via_runtime_async(&page_url) - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(&page_url, None) + .await; ctx.process_async(json!({ "id": 1210, @@ -2241,7 +2277,9 @@ async fn get_navigation_history_targets_loaded_background_owner_without_activati let mut ctx = TestContext::new(); let background_url = "data:text/html,Background History
background
"; - let mut bc = BrowserContext::new("BID-1".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.set_target_url("data:text/html,Active
active
".to_owned()); @@ -2284,7 +2322,9 @@ async fn reset_navigation_history_targets_loaded_background_owner_without_activa let mut ctx = TestContext::new(); let background_url = "data:text/html,Background Reset History
background
"; - let mut browser_context = BrowserContext::new("BID-reset-background".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-reset-background".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context @@ -2339,7 +2379,9 @@ async fn reset_navigation_history_targets_loaded_background_owner_without_activa #[tokio::test(flavor = "multi_thread")] async fn navigate_to_history_entry_targets_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.set_target_url("data:text/html,Active
active
".to_owned()); @@ -2419,18 +2461,15 @@ async fn get_navigation_history_targets_inactive_loaded_owner_without_activation "about:blank", ); let inactive_url = "data:text/html,Inactive History
inactive
"; - let page = ctx + let mut inactive = ctx .conn - .load_page_via_runtime_async(inactive_url) - .await - .expect("inactive page should load"); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); - inactive.set_target_url(page.final_url().as_str().to_owned()); - inactive.replace_active_page_for_test(Some(page)); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); + ctx.install_navigation_fixture_for_session_owner(inactive_url, Some("SID-inactive")) + .await; ctx.process_async(json!({ "id": 16, @@ -2522,7 +2561,9 @@ async fn navigation_history_marks_reload_as_reload_transition() { #[tokio::test(flavor = "multi_thread")] async fn navigate_targets_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.set_target_url("data:text/html,active".to_owned()); @@ -2565,7 +2606,9 @@ async fn navigate_targets_background_owner_without_activation() { #[tokio::test(flavor = "multi_thread")] async fn navigate_targets_inactive_owner_without_activation() { let mut ctx = TestContext::new(); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.attach_active_session("SID-inactive".to_owned()); inactive.set_target_url("about:blank".to_owned()); @@ -2748,7 +2791,7 @@ async fn reload_requires_browser_context() { #[tokio::test(flavor = "multi_thread")] async fn reload_without_target_loaded_errors() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.process_async(json!({"id": 14, "method": "Page.reload"})) .await; ctx.expect_error(14, -31998, "TargetNotLoaded"); @@ -6990,7 +7033,9 @@ async fn reload_targets_background_owner_without_activation() { let mut ctx = TestContext::new(); let page_url = format!("http://{addr}/page"); - let mut bc = BrowserContext::new("BID-1".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.set_target_url("data:text/html,Active
active
".to_owned()); diff --git a/moli-protocol/src/domains/page/tests/resources.rs b/moli-protocol/src/domains/page/tests/resources.rs index 53fdddebf6..50f31002d0 100644 --- a/moli-protocol/src/domains/page/tests/resources.rs +++ b/moli-protocol/src/domains/page/tests/resources.rs @@ -373,16 +373,8 @@ async fn get_resource_tree_reports_observed_frame_subresources() { "SID-RESOURCE-TREE", &document_url, ); - let page = ctx - .conn - .load_page_via_runtime_async(&document_url) - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(&document_url, None) + .await; ctx.process_async(json!({ "id": 1, @@ -569,16 +561,8 @@ async fn frame_and_resource_trees_report_main_document_response_mime() { "SID-XML-RESOURCE-TREE", &document_url, ); - let page = ctx - .conn - .load_page_via_runtime_async(&document_url) - .await - .expect("XML page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner(&document_url, None) + .await; for (id, method) in [(11, "Page.getResourceTree"), (12, "Page.getFrameTree")] { ctx.process_async(json!({ @@ -991,7 +975,7 @@ async fn search_in_resource_accepts_an_empty_cached_child_document() { let cache_dir = unique_resource_search_cache_dir(); let mut fetch_config = moli_fetch::FetchConfig::default(); fetch_config.set_http_cache_dir(Some(cache_dir.display().to_string())); - let mut ctx = TestContext::from_conn(crate::conn::CdpConnection::new_with_fetch_config( + let mut ctx = TestContext::from_conn(crate::test_support::connection_with_fetch_config( fetch_config, )); load_bc_with_session( diff --git a/moli-protocol/src/domains/page/tests/scripts.rs b/moli-protocol/src/domains/page/tests/scripts.rs index cc3d7a5c9c..30d424ecd3 100644 --- a/moli-protocol/src/domains/page/tests/scripts.rs +++ b/moli-protocol/src/domains/page/tests/scripts.rs @@ -109,16 +109,11 @@ async fn add_script_to_evaluate_on_new_document_validates_params() { async fn add_script_to_evaluate_on_new_document_does_not_mutate_existing_page() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,stable") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,stable", + Some("SID-1"), + ) + .await; ctx.process_async(json!({ "id": 35, @@ -421,16 +416,11 @@ async fn add_script_to_evaluate_on_new_document_world_name_preserves_order_withi async fn add_script_to_evaluate_on_new_document_run_immediately_mutates_existing_page() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,stable") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,stable", + Some("SID-1"), + ) + .await; let raw = json!({ "id": 357, @@ -469,13 +459,12 @@ async fn add_script_to_evaluate_on_new_document_run_immediately_mutates_existing async fn add_script_to_evaluate_on_new_document_run_immediately_world_name_creates_context() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,stable") - .await - .expect("page should load"); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,stable", + Some("SID-1"), + ) + .await; let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(page)); bc.set_target_security_origin("https://stale-top-origin.example".into()); ctx.process_async(json!({ @@ -634,16 +623,11 @@ async fn remove_script_to_evaluate_on_new_document_removes_named_world_preload() async fn remove_script_to_evaluate_on_new_document_does_not_rollback_run_immediately_effect() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,stable") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,stable", + Some("SID-1"), + ) + .await; ctx.process_async(json!({ "id": 367, @@ -707,16 +691,11 @@ async fn create_isolated_world_requires_matching_frame_and_uses_fresh_initial_do .await; ctx.expect_error(40, -32000, "NoFrameForGivenId"); - let iframe_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(iframe_page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-1"), + ) + .await; let child_frame_id = child_frame_id_for_single_iframe(&mut ctx, 400).await; ctx.process_async(json!({ @@ -743,6 +722,11 @@ async fn create_isolated_world_requires_matching_frame_and_uses_fresh_initial_do .as_mut() .unwrap() .clear_loaded_page(); + ctx.conn + .browser_context + .as_mut() + .unwrap() + .begin_active_target_initial_empty_document("about:blank".into()); ensure_initial_document_for_session(&mut ctx, Some("SID-1")).await; ctx.process_async(json!({ "id": 41, @@ -761,8 +745,7 @@ async fn create_isolated_world_requires_matching_frame_and_uses_fresh_initial_do ctx.conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) - .is_some(), + .is_some_and(|bc| bc.has_loaded_page()), "Page.createIsolatedWorld should observe the target-lifecycle initial page" ); @@ -810,8 +793,8 @@ async fn create_isolated_world_requires_matching_frame_and_uses_fresh_initial_do ctx.conn .browser_context .as_ref() - .and_then(|bc| bc.loaded_page()) - .is_some_and(|page| page.final_url().as_str() == "data:text/html,
utility
"), + .and_then(|bc| bc.loaded_document_url_for_test()) + .is_some_and(|url| url.as_str() == "data:text/html,
utility
"), "createIsolatedWorld should load the target initial URL before creating the world" ); } @@ -851,16 +834,11 @@ async fn create_isolated_world_async_accepts_child_frame_from_async_dispatch() { .await; ctx.expect_error(4020, -32000, "NoFrameForGivenId"); - let iframe_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(iframe_page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,", + Some("SID-1"), + ) + .await; ctx.process_async(json!({ "id": 4021, @@ -1117,16 +1095,11 @@ async fn create_isolated_world_for_child_frame_replays_world_scoped_runtime_bind async fn create_isolated_world_installs_matching_bindings_before_document_start_scripts() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,top-frame") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,top-frame", + Some("SID-1"), + ) + .await; ctx.process_async(json!({ "id": 416, @@ -1374,16 +1347,11 @@ async fn create_isolated_world_validates_params() { async fn create_isolated_world_without_runtime_frontend_enabled_only_returns_result() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,hello") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .unwrap() - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,hello", + Some("SID-1"), + ) + .await; ctx.process_async(json!({ "id": 45, @@ -1400,7 +1368,7 @@ async fn create_isolated_world_without_runtime_frontend_enabled_only_returns_res let result = ctx.take_one(); let execution_context_id = result["result"]["executionContextId"] .as_i64() - .expect("executionContextId"); + .unwrap_or_else(|| panic!("missing executionContextId: {result:?}")); assert!(execution_context_id > 0); assert_eq!(result["id"], 45); assert_eq!(result["sessionId"], "SID-1"); @@ -1410,13 +1378,11 @@ async fn create_isolated_world_without_runtime_frontend_enabled_only_returns_res async fn create_isolated_world_accepts_cdp_and_corrected_grant_universal_access_spellings() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,hello") - .await - .expect("page should load"); - let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,hello", + Some("SID-1"), + ) + .await; ctx.process_async(json!({ "id": 47, @@ -1479,13 +1445,12 @@ async fn create_isolated_world_accepts_cdp_and_corrected_grant_universal_access_ async fn create_isolated_world_returns_unique_context_ids_and_emits_runtime_event() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,hello") - .await - .expect("page should load"); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,hello", + Some("SID-1"), + ) + .await; let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(page)); bc.set_target_security_origin("https://stale-top-origin.example".into()); ctx.process_async(json!({ @@ -1573,7 +1538,9 @@ async fn create_isolated_world_returns_unique_context_ids_and_emits_runtime_even async fn create_isolated_world_targets_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -1673,13 +1640,11 @@ async fn create_isolated_world_targets_loaded_background_owner_without_activatio async fn create_isolated_world_does_not_persist_across_navigation() { let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,hello") - .await - .expect("page should load"); - let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,hello", + Some("SID-1"), + ) + .await; ctx.process_async(json!({ "id": 45, "method": "Runtime.enable", @@ -1730,42 +1695,40 @@ async fn create_isolated_world_after_reactivating_browser_context_with_another_l let mut ctx = TestContext::new(); load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); - let first_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,first") - .await - .expect("first page should load"); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,first", + Some("SID-1"), + ) + .await; { let bc = ctx .conn .browser_context .as_mut() .expect("first browser context"); - let _ = bc.replace_active_page_for_test(Some(first_page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state .runtime_frontend_enabled = false; } - let mut second = BrowserContext::new("BID-2".into()); + let mut second = ctx.conn.new_browser_context_fixture_for_test("BID-2"); second.set_active_target_id("TID-2"); second.attach_active_session("SID-2"); second.set_target_url("about:blank".into()); ctx.conn.insert_browser_context(second); assert!(ctx.conn.activate_browser_context_by_id_async("BID-2").await); - let second_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,second") - .await - .expect("second page should load"); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,second", + Some("SID-2"), + ) + .await; { let bc = ctx .conn .browser_context_by_id_mut("BID-2") .expect("second browser context"); - let _ = bc.replace_active_page_for_test(Some(second_page)); bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state diff --git a/moli-protocol/src/domains/performance.rs b/moli-protocol/src/domains/performance.rs index b23511f355..f38d5f04d1 100644 --- a/moli-protocol/src/domains/performance.rs +++ b/moli-protocol/src/domains/performance.rs @@ -450,7 +450,7 @@ fn build_performance_metrics(snapshot: &RendererPerformanceMetricSnapshot) -> Ve #[cfg(test)] mod tests { use super::*; - use crate::conn::{BrowserContext, CdpCommandTaskStep}; + use crate::conn::CdpCommandTaskStep; use crate::testing::TestContext; use serde_json::json; use std::collections::HashMap; @@ -608,7 +608,7 @@ mod tests { } async fn load_document(ctx: &mut TestContext, html: &str) { - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,performance-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); @@ -865,7 +865,9 @@ mod tests { let mut ctx = TestContext::new(); let active_url = "data:text/html,active"; let background_url = "data:text/html,
"; - let mut browser_context = BrowserContext::new("BID-performance-owner".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-performance-owner".to_owned()); browser_context.set_active_target_id("TID-performance-active"); browser_context.attach_active_session("SID-performance-active"); browser_context.set_target_url(active_url.to_owned()); @@ -971,7 +973,7 @@ mod tests { async fn performance_get_metrics_targets_loaded_background_owner_without_activation() { let mut ctx = TestContext::new(); let background_url = "data:text/html,
"; - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.set_target_url("data:text/html,active".to_owned()); @@ -1016,7 +1018,7 @@ mod tests { #[tokio::test] async fn performance_get_metrics_on_unloaded_background_owner_does_not_activate() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.stage_background_target( @@ -1067,7 +1069,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn performance_enable_stages_background_target_session_state() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.stage_background_target( diff --git a/moli-protocol/src/domains/profiler.rs b/moli-protocol/src/domains/profiler.rs index 0f2fa0ae49..425dc5dddc 100644 --- a/moli-protocol/src/domains/profiler.rs +++ b/moli-protocol/src/domains/profiler.rs @@ -29,30 +29,22 @@ mod tests { use tokio::net::TcpListener; use crate::conn::{ - BrowserContext, CdpCommandTaskStep, PendingCdpCommandDispatch, ProfilerAction, - ProfilerInspectorCommand, + CdpCommandTaskStep, PendingCdpCommandDispatch, ProfilerAction, ProfilerInspectorCommand, }; use crate::testing::{TestContext, wait_until_messages, wait_until_scheduler_message}; async fn with_loaded_document_async(ctx: &mut TestContext, html: &str) { - ctx.conn - .insert_browser_context(BrowserContext::new("BID-profiler".into())); - ctx.conn - .browser_context - .as_mut() - .expect("browser context should exist") - .set_active_target_id("TID-profiler"); - let page = ctx + let mut browser_context = ctx .conn - .load_page_via_runtime_async(&format!("data:text/html,{html}")) - .await - .expect("must load document for Profiler domain tests"); - let browser_context = ctx - .conn - .browser_context - .as_mut() - .expect("browser context should exist"); - let _ = browser_context.replace_active_page_for_test(Some(page)); + .new_browser_context_fixture_for_test("BID-profiler"); + browser_context.set_active_target_id("TID-profiler"); + ctx.conn + .install_browser_context_fixture_for_test(browser_context); + ctx.install_quiet_navigation_fixture_for_session_owner( + &format!("data:text/html,{html}"), + None, + ) + .await; } async fn spawn_profiler_navigation_server() -> (String, tokio::task::JoinHandle<()>) { @@ -81,20 +73,17 @@ mod tests { } async fn with_loaded_target_document_async(ctx: &mut TestContext, target_id: &str, html: &str) { - let mut browser_context = BrowserContext::new("BID-profiler".into()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-profiler"); browser_context.set_active_target_id(target_id); - ctx.conn.insert_browser_context(browser_context); - let page = ctx - .conn - .load_page_via_runtime_async(&format!("data:text/html,{html}")) - .await - .expect("must load target document for Profiler domain tests"); - let browser_context = ctx - .conn - .browser_context - .as_mut() - .expect("browser context should exist"); - let _ = browser_context.replace_active_page_for_test(Some(page)); + ctx.conn + .install_browser_context_fixture_for_test(browser_context); + ctx.install_quiet_navigation_fixture_for_session_owner( + &format!("data:text/html,{html}"), + None, + ) + .await; } async fn complete_pending_command_task_for_test( @@ -1976,24 +1965,17 @@ mod tests { async fn active_profiler_recording_survives_http_page_navigation_and_disable() { let (fixture, _server) = spawn_profiler_navigation_server().await; let mut ctx = TestContext::new(); - ctx.conn - .insert_browser_context(BrowserContext::new("BID-profiler-http".into())); - ctx.conn - .browser_context - .as_mut() - .expect("browser context should exist") - .set_active_target_id("TID-profiler-http"); - let page = ctx + let mut browser_context = ctx .conn - .load_page_via_runtime_async(&format!("{fixture}/plain?phase=before")) - .await - .expect("must load initial HTTP document"); - let browser_context = ctx - .conn - .browser_context - .as_mut() - .expect("browser context should exist"); - let _ = browser_context.replace_active_page_for_test(Some(page)); + .new_browser_context_fixture_for_test("BID-profiler-http"); + browser_context.set_active_target_id("TID-profiler-http"); + ctx.conn + .install_browser_context_fixture_for_test(browser_context); + ctx.install_navigation_fixture_for_session_owner( + &format!("{fixture}/plain?phase=before"), + None, + ) + .await; assert_eq!( process_and_take_response(&mut ctx, json!({"id": 200, "method": "Page.enable"}), 200) @@ -2123,6 +2105,7 @@ mod tests { "before target close", ) .await; + ctx.sent.clear(); let enable = process_and_take_response( &mut ctx, @@ -2178,19 +2161,11 @@ mod tests { .expect("browser context should survive target close"); browser_context.set_active_target_id(target_id); } - let page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,after target close", - ) - .await - .expect("same target id should be reusable after close"); - let browser_context = ctx - .conn - .browser_context - .as_mut() - .expect("browser context should exist after reload"); - let _ = browser_context.replace_active_page_for_test(Some(page)); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,after target close", + None, + ) + .await; let stop_after_target_reuse = process_and_take_response(&mut ctx, json!({"id": 125, "method": "Profiler.stop"}), 125) diff --git a/moli-protocol/src/domains/runtime.rs b/moli-protocol/src/domains/runtime.rs index 8311d83f2c..606eb7711b 100644 --- a/moli-protocol/src/domains/runtime.rs +++ b/moli-protocol/src/domains/runtime.rs @@ -71,20 +71,15 @@ pub(in crate::domains) async fn detach_session_inspector_async( browser_context_id, target_id, } => { - let renderer_detach = - conn.browser_context_by_id(browser_context_id) - .and_then(|browser_context| { - browser_context - .shared_worker_target(target_id) - .map(|target| { - ( - browser_context.renderer_runtime(), - target.renderer_instance_id, - ) - }) - }); - if let Some((renderer_runtime, instance_id)) = renderer_detach { - renderer_runtime.detach_shared_worker_runtime_inspector_session( + if let Some((browser_context, instance_id)) = conn + .browser_context_by_id(browser_context_id) + .and_then(|browser_context| { + browser_context + .shared_worker_target(target_id) + .map(|target| (browser_context, target.renderer_instance_id)) + }) + { + browser_context.detach_shared_worker_inspector_session( instance_id, Some(session_id.to_owned()), ); @@ -94,20 +89,15 @@ pub(in crate::domains) async fn detach_session_inspector_async( browser_context_id, target_id, } => { - let renderer_detach = - conn.browser_context_by_id(browser_context_id) - .and_then(|browser_context| { - browser_context - .dedicated_worker_target(target_id) - .map(|target| { - ( - browser_context.renderer_runtime(), - target.renderer_instance_id, - ) - }) - }); - if let Some((renderer_runtime, instance_id)) = renderer_detach { - renderer_runtime.detach_dedicated_worker_runtime_inspector_session( + if let Some((browser_context, instance_id)) = conn + .browser_context_by_id(browser_context_id) + .and_then(|browser_context| { + browser_context + .dedicated_worker_target(target_id) + .map(|target| (browser_context, target.renderer_instance_id)) + }) + { + browser_context.detach_dedicated_worker_inspector_session( instance_id, Some(session_id.to_owned()), ); @@ -117,20 +107,15 @@ pub(in crate::domains) async fn detach_session_inspector_async( browser_context_id, target_id, } => { - let renderer_detach = - conn.browser_context_by_id(browser_context_id) - .and_then(|browser_context| { - browser_context - .service_worker_target(target_id) - .map(|target| { - ( - browser_context.renderer_runtime(), - target.renderer_version_id, - ) - }) - }); - if let Some((renderer_runtime, version_id)) = renderer_detach { - renderer_runtime.detach_service_worker_runtime_inspector_session( + if let Some((browser_context, version_id)) = conn + .browser_context_by_id(browser_context_id) + .and_then(|browser_context| { + browser_context + .service_worker_target(target_id) + .map(|target| (browser_context, target.renderer_version_id)) + }) + { + browser_context.detach_service_worker_inspector_session( version_id, Some(session_id.to_owned()), ); diff --git a/moli-protocol/src/domains/runtime/activity.rs b/moli-protocol/src/domains/runtime/activity.rs index 72e71ebdaa..429d39f3c2 100644 --- a/moli-protocol/src/domains/runtime/activity.rs +++ b/moli-protocol/src/domains/runtime/activity.rs @@ -610,7 +610,7 @@ mod tests { use serde_json::Value; use serde_json::json; - use crate::conn::{BrowserContext, CdpConnection, CommandDispatchContext, CommandOwnerScope}; + use crate::conn::{CdpConnection, CommandDispatchContext, CommandOwnerScope}; use crate::devtools_runtime::AutomationEvent; use crate::domains::activity::{ProtocolOutputPayloads, ProtocolOutputProjectionContext}; use crate::testing::TestContext; @@ -644,7 +644,7 @@ mod tests { } async fn load_document(ctx: &mut TestContext, html: &str) { - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1".to_owned()); bc.set_target_url("data:text/html,runtime-backlog-test".to_owned()); bc.attach_active_session("SID-1".to_owned()); diff --git a/moli-protocol/src/domains/runtime/dispatcher.rs b/moli-protocol/src/domains/runtime/dispatcher.rs index c899b3cce2..d207af5e8e 100644 --- a/moli-protocol/src/domains/runtime/dispatcher.rs +++ b/moli-protocol/src/domains/runtime/dispatcher.rs @@ -9205,9 +9205,7 @@ fn release_service_worker_if_waiting_for_debugger( else { return false; }; - browser_context - .renderer_runtime() - .run_service_worker_if_waiting_for_debugger_for_devtools(version_id) + browser_context.run_service_worker_if_waiting_for_debugger(version_id) } fn is_bidi_runtime_listener_session(session_id: Option<&str>) -> bool { @@ -10673,10 +10671,7 @@ mod protocol_neutral_tests { use moli_shared_worker::SharedWorkerInstanceId; use serde_json::{Value, json}; - use crate::conn::{ - BrowserContext, CdpConnection, CdpSessionRoute, Cmd, CommandOwnerScope, - SharedWorkerTargetState, - }; + use crate::conn::{CdpSessionRoute, Cmd, CommandOwnerScope, SharedWorkerTargetState}; use crate::domains::actions::ConsoleAction; use crate::testing::TestContext; @@ -11619,7 +11614,7 @@ mod protocol_neutral_tests { #[test] fn devtools_runtime_entry_routes_evaluate_command_to_inspector_error_plan() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({"expression": "1 + 1"}); let cmd = Cmd::for_test( Some(15), @@ -11649,8 +11644,9 @@ mod protocol_neutral_tests { #[test] fn duplicate_pending_runtime_id_returns_chromium_error_without_replacing_owner() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-duplicate".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-duplicate".to_owned()); browser_context.set_active_target_id("TID-duplicate".to_owned()); browser_context.attach_active_session("SID-duplicate".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -11707,21 +11703,18 @@ mod protocol_neutral_tests { #[tokio::test] async fn duplicate_non_await_v8_command_preserves_original_completion_owner() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-console-duplicate".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-console-duplicate".to_owned()); browser_context.set_active_target_id("TID-console-duplicate".to_owned()); browser_context.attach_active_session("SID-console-duplicate".to_owned()); ctx.conn .install_browser_context_fixture_for_test(browser_context); - let page = ctx - .conn - .load_page_via_runtime_async("data:text/html,

console duplicate

") - .await - .expect("page should load"); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,

console duplicate

", + None, + ) + .await; let params = json!({}); let enable = Cmd::for_test( @@ -11792,8 +11785,9 @@ mod protocol_neutral_tests { #[test] fn shared_worker_runtime_disable_projection_waits_for_success() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-shared".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-shared".to_owned()); let mut target = SharedWorkerTargetState::new( RendererOwnerLocalHostId::new_for_testing(1), SharedWorkerInstanceId::from_u64(91), diff --git a/moli-protocol/src/domains/runtime/test_support.rs b/moli-protocol/src/domains/runtime/test_support.rs index 925ce9cf8d..0d56fddd58 100644 --- a/moli-protocol/src/domains/runtime/test_support.rs +++ b/moli-protocol/src/domains/runtime/test_support.rs @@ -1,11 +1,10 @@ use serde_json::json; -use crate::conn::BrowserContext; use crate::testing::TestContext; pub(super) async fn with_loaded_document_async(ctx: &mut TestContext, html: &str) { ctx.conn - .insert_browser_context(BrowserContext::new("BID-1".into())); + .insert_browser_context(ctx.conn.new_browser_context_fixture_for_test("BID-1")); ctx.conn .browser_context .as_mut() @@ -23,7 +22,7 @@ pub(super) async fn with_loaded_document_for_active_target_async( target_id: &str, ) { ctx.conn - .insert_browser_context(BrowserContext::new("BID-1".into())); + .insert_browser_context(ctx.conn.new_browser_context_fixture_for_test("BID-1")); { let bc = ctx .conn @@ -45,7 +44,7 @@ pub(super) async fn with_loaded_http_document_async( target_id: &str, ) { ctx.conn - .insert_browser_context(BrowserContext::new("BID-1".into())); + .insert_browser_context(ctx.conn.new_browser_context_fixture_for_test("BID-1")); { let bc = ctx .conn diff --git a/moli-protocol/src/domains/runtime/tests/bindings.rs b/moli-protocol/src/domains/runtime/tests/bindings.rs index 369affcb19..8b4d68d349 100644 --- a/moli-protocol/src/domains/runtime/tests/bindings.rs +++ b/moli-protocol/src/domains/runtime/tests/bindings.rs @@ -1287,7 +1287,7 @@ async fn patchright_style_existing_utility_world_binding_install_persists_withou #[tokio::test] async fn patchright_style_pre_document_add_binding_persists_until_navigation() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(crate::conn::BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); let bc = ctx .conn .browser_context @@ -1358,7 +1358,7 @@ async fn patchright_style_pre_document_add_binding_persists_until_navigation() { #[tokio::test] async fn pre_document_add_binding_completes_through_command_task_without_live_page() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(crate::conn::BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); let bc = ctx .conn .browser_context @@ -1421,7 +1421,7 @@ async fn run_if_waiting_for_debugger_completes_through_command_task() { #[tokio::test] async fn patchright_style_pre_document_remove_binding_succeeds_before_navigation() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(crate::conn::BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); let bc = ctx .conn .browser_context @@ -1498,7 +1498,7 @@ async fn patchright_style_pre_document_remove_binding_succeeds_before_navigation #[tokio::test] async fn pre_document_remove_binding_completes_through_command_task_without_live_page() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(crate::conn::BrowserContext::new("BID-1".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-1")); let bc = ctx .conn .browser_context diff --git a/moli-protocol/src/domains/runtime/tests/console.rs b/moli-protocol/src/domains/runtime/tests/console.rs index 058da40fe1..2b3c684434 100644 --- a/moli-protocol/src/domains/runtime/tests/console.rs +++ b/moli-protocol/src/domains/runtime/tests/console.rs @@ -54,7 +54,9 @@ async fn runtime_discard_console_entries_suppresses_buffered_runtime_events() { async fn runtime_discard_console_entries_is_page_target_local() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id("TID-active"); browser_context.attach_active_session("SID-active"); browser_context.register_page_target_url_fixture( @@ -816,7 +818,7 @@ async fn heap_profiler_moli_diagnostics_reports_dedicated_worker_isolates() { } #[tokio::test] -async fn heap_profiler_moli_reset_idle_engine_only_resets_without_loaded_page() { +async fn heap_profiler_moli_reset_idle_engine_does_not_create_an_unowned_engine() { let mut ctx = TestContext::new(); with_loaded_document_async(&mut ctx, "

loaded

").await; @@ -843,8 +845,16 @@ async fn heap_profiler_moli_reset_idle_engine_only_resets_without_loaded_page() let idle_response = take_response_by_id(&mut ctx, 206_819); assert_eq!( idle_response["result"]["reset"], - json!(true), - "closed target should leave the engine eligible for idle reset: {idle_response:?}" + json!(false), + "closing the WebContents leaves no standalone engine to replace: {idle_response:?}" + ); + assert_eq!( + idle_response["result"]["reason"], + json!("no-standalone-engine") + ); + assert_eq!( + ctx.conn.moli_memory_diagnostics()["isolateScope"]["estimatedRendererOwnerCount"], + json!(0) ); } #[tokio::test(flavor = "multi_thread")] diff --git a/moli-protocol/src/domains/runtime/tests/evaluate.rs b/moli-protocol/src/domains/runtime/tests/evaluate.rs index e8df963d24..795a63f95d 100644 --- a/moli-protocol/src/domains/runtime/tests/evaluate.rs +++ b/moli-protocol/src/domains/runtime/tests/evaluate.rs @@ -1927,7 +1927,9 @@ async fn dom_get_document_rejects_while_main_document_navigation_is_pending() { async fn document_navigation_gate_is_scoped_to_background_target_owner() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id("TID-active"); browser_context.attach_active_session("SID-active"); browser_context.active_page_target_mut().devtools_sessions @@ -2433,13 +2435,13 @@ async fn page_navigate_network_failure_commits_error_document() { .target_url(), failing_url ); - let page = ctx + let page_url = ctx .conn .browser_context .as_ref() - .and_then(BrowserContext::loaded_page) + .and_then(BrowserContext::loaded_document_url_for_test) .expect("network error Document should remain loaded"); - assert_eq!(page.final_url().as_str(), NETWORK_ERROR_PAGE_URL); + assert_eq!(page_url.as_str(), NETWORK_ERROR_PAGE_URL); assert!( !ctx.conn .browser_context @@ -2462,6 +2464,7 @@ async fn page_navigate_network_failure_commits_error_document() { redirect_headers: None, navigate_id: None, owner: crate::conn::CommandOwnerScope::for_session("SID-1"), + web_contents: crate::conn::NavigationDispatchState::detached_web_contents_for_test(), result_projection: crate::conn::NavigationResultProjection::Cdp(json!({})), frame_id: "TID-1".to_owned(), session_id: Some("SID-1".to_owned()), @@ -2509,9 +2512,8 @@ async fn page_navigate_network_failure_commits_error_document() { ctx.conn .browser_context .as_ref() - .and_then(BrowserContext::loaded_page) + .and_then(BrowserContext::loaded_document_url_for_test) .expect("error Document should survive stale body completion") - .final_url() .as_str(), NETWORK_ERROR_PAGE_URL ); @@ -6744,7 +6746,7 @@ async fn registered_named_world_object_handles_remain_callable_after_navigation( } })) .await; - let mut other_context = crate::conn::BrowserContext::new("BID-2".into()); + let mut other_context = ctx.conn.new_browser_context_fixture_for_test("BID-2"); other_context.set_active_target_id("TID-2"); other_context.attach_active_session("SID-2"); ctx.conn @@ -6797,7 +6799,7 @@ async fn call_function_on_rejects_object_id_known_to_different_target_owner() { .unwrap_or_else(|| panic!("Runtime.evaluate should return an object handle: {response:?}")) .to_owned(); - let mut other_context = crate::conn::BrowserContext::new("BID-2".into()); + let mut other_context = ctx.conn.new_browser_context_fixture_for_test("BID-2"); other_context.set_active_target_id("TID-2"); other_context.attach_active_session("SID-2"); ctx.conn @@ -6984,7 +6986,7 @@ async fn call_function_on_rejects_dom_resolve_node_object_id_from_different_targ .unwrap_or_else(|| panic!("DOM.resolveNode should return an object handle: {resolved:?}")) .to_owned(); - let mut other_context = crate::conn::BrowserContext::new("BID-2".into()); + let mut other_context = ctx.conn.new_browser_context_fixture_for_test("BID-2"); other_context.set_active_target_id("TID-2"); other_context.attach_active_session("SID-2"); ctx.conn diff --git a/moli-protocol/src/domains/runtime/tests/mod.rs b/moli-protocol/src/domains/runtime/tests/mod.rs index e1bfc35ac5..1a4f26f5c1 100644 --- a/moli-protocol/src/domains/runtime/tests/mod.rs +++ b/moli-protocol/src/domains/runtime/tests/mod.rs @@ -32,7 +32,9 @@ async fn complete_command_task_step_for_test( ctx.complete_command_task_step_for_test(step).await } fn load_shared_worker_target(ctx: &mut TestContext, session_id: &str) { - let mut bc = BrowserContext::new("BID-shared".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-shared".to_owned()); let mut target = crate::conn::SharedWorkerTargetState::new( moli_core::RendererOwnerLocalHostId::new_for_testing(1), SharedWorkerInstanceId::from_u64(81), @@ -47,7 +49,9 @@ fn load_shared_worker_target(ctx: &mut TestContext, session_id: &str) { } fn load_dedicated_worker_target(ctx: &mut TestContext, session_id: &str) { let browser_context_id = "BID-dedicated".to_owned(); - let mut bc = BrowserContext::new(browser_context_id.clone()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test(browser_context_id.clone()); let mut target = crate::conn::DedicatedWorkerTargetState::new( crate::conn::TargetPageResidenceIdentity::new_for_test(browser_context_id, None, 1), moli_core::RendererOwnerLocalHostId::new_for_testing(1), @@ -140,14 +144,10 @@ async fn push_loaded_runtime_frontend_enabled_background_context_async( session_id: &str, html: &str, ) { - let page = ctx + let mut background_context = ctx .conn - .load_page_via_runtime_async(&format!("data:text/html,{html}")) - .await - .expect("test background page should load"); - let mut background_context = crate::conn::BrowserContext::new(browser_context_id.to_owned()); + .new_browser_context_fixture_for_test(browser_context_id.to_owned()); background_context.set_active_target_id(target_id.to_owned()); - let _ = background_context.replace_active_page_for_test(Some(page)); background_context.attach_active_session(session_id.to_owned()); background_context .active_page_target_mut() @@ -156,6 +156,11 @@ async fn push_loaded_runtime_frontend_enabled_background_context_async( .runtime_frontend_enabled = true; ctx.conn .push_inactive_browser_context_fixture_for_test(background_context); + ctx.install_quiet_navigation_fixture_for_session_owner( + &format!("data:text/html,{html}"), + Some(session_id), + ) + .await; } async fn with_loaded_runtime_frontend_enabled_background_target_async( ctx: &mut TestContext, @@ -165,7 +170,9 @@ async fn with_loaded_runtime_frontend_enabled_background_target_async( background_session_id: &str, html: &str, ) { - let mut browser_context = crate::conn::BrowserContext::new("BID-1".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id(active_target_id.to_owned()); browser_context.attach_active_session(active_session_id.to_owned()); browser_context.register_page_target_url_fixture( diff --git a/moli-protocol/src/domains/runtime/tests/navigation.rs b/moli-protocol/src/domains/runtime/tests/navigation.rs index c3296b6453..55c9a94eb8 100644 --- a/moli-protocol/src/domains/runtime/tests/navigation.rs +++ b/moli-protocol/src/domains/runtime/tests/navigation.rs @@ -3,7 +3,7 @@ use super::*; #[tokio::test] async fn enable_with_background_event_sender_defers_initial_document_page_build() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.set_target_url("about:blank".into()); @@ -92,7 +92,7 @@ async fn enable_uses_fresh_initial_document_without_adapter() { #[tokio::test] async fn enable_reports_no_document_without_legacy_materialization_adapter() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-1".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-1"); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); bc.set_target_url("about:blank".into()); diff --git a/moli-protocol/src/domains/runtime/tests/objects.rs b/moli-protocol/src/domains/runtime/tests/objects.rs index 4fc4854424..c29f2f2af8 100644 --- a/moli-protocol/src/domains/runtime/tests/objects.rs +++ b/moli-protocol/src/domains/runtime/tests/objects.rs @@ -629,7 +629,9 @@ async fn runtime_disable_clears_background_child_default_context_emission_cursor { let mut ctx = TestContext::new(); let html = ""; - let mut browser_context = crate::conn::BrowserContext::new("BID-1".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id("TID-active".to_owned()); browser_context.attach_active_session("SID-active".to_owned()); browser_context.register_page_target_url_fixture( diff --git a/moli-protocol/src/domains/runtime/tests/service_worker.rs b/moli-protocol/src/domains/runtime/tests/service_worker.rs index 3311c9b757..fd696cebd6 100644 --- a/moli-protocol/src/domains/runtime/tests/service_worker.rs +++ b/moli-protocol/src/domains/runtime/tests/service_worker.rs @@ -12,7 +12,9 @@ use moli_core::page::{ }; fn load_service_worker_target(ctx: &mut TestContext, session_id: &str) { - let mut bc = BrowserContext::new("BID-service".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-service".to_owned()); let mut target = service_worker_target(); target.attach_session(session_id.to_owned()); bc.insert_service_worker_target(target); @@ -20,7 +22,9 @@ fn load_service_worker_target(ctx: &mut TestContext, session_id: &str) { } fn load_unattached_service_worker_target(ctx: &mut TestContext) { - let mut bc = BrowserContext::new("BID-service".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-service".to_owned()); bc.insert_service_worker_target(service_worker_target()); ctx.conn.install_browser_context_fixture_for_test(bc); } @@ -98,17 +102,19 @@ fn service_worker_fetch_diagnostic(internal_id: u64) -> RendererServiceWorkerFet } } +fn service_worker_pause_on_start(ctx: &TestContext) -> bool { + ctx.conn + .browser_context + .as_ref() + .expect("browser context should exist") + .service_worker_pause_on_start() +} + #[tokio::test] async fn target_auto_attach_wait_for_debugger_toggles_service_worker_pause_on_start() { let mut ctx = TestContext::new(); load_service_worker_target(&mut ctx, "SID-service-worker"); - let runtime = ctx - .conn - .browser_context - .as_ref() - .expect("browser context should exist") - .renderer_runtime(); - assert!(!runtime.service_worker_pause_on_start_for_devtools()); + assert!(!service_worker_pause_on_start(&ctx)); ctx.process_async(json!({ "id": 78, @@ -120,7 +126,7 @@ async fn target_auto_attach_wait_for_debugger_toggles_service_worker_pause_on_st })) .await; ctx.expect_result(78, json!({}), None); - assert!(runtime.service_worker_pause_on_start_for_devtools()); + assert!(service_worker_pause_on_start(&ctx)); ctx.process_async(json!({ "id": 79, @@ -132,20 +138,14 @@ async fn target_auto_attach_wait_for_debugger_toggles_service_worker_pause_on_st })) .await; ctx.expect_result(79, json!({}), None); - assert!(!runtime.service_worker_pause_on_start_for_devtools()); + assert!(!service_worker_pause_on_start(&ctx)); } #[tokio::test] async fn target_auto_attach_wait_for_debugger_respects_service_worker_filter() { let mut ctx = TestContext::new(); load_service_worker_target(&mut ctx, "SID-service-worker"); - let runtime = ctx - .conn - .browser_context - .as_ref() - .expect("browser context should exist") - .renderer_runtime(); - assert!(!runtime.service_worker_pause_on_start_for_devtools()); + assert!(!service_worker_pause_on_start(&ctx)); ctx.process_async(json!({ "id": 80, @@ -163,7 +163,7 @@ async fn target_auto_attach_wait_for_debugger_respects_service_worker_filter() { ctx.expect_result(80, json!({}), None); assert!( - !runtime.service_worker_pause_on_start_for_devtools(), + !service_worker_pause_on_start(&ctx), "waitForDebuggerOnStart must not pause Service Workers when the target filter excludes them" ); assert_eq!(ctx.conn.service_worker_pause_on_start_owner_count(), 0); @@ -173,17 +173,11 @@ async fn target_auto_attach_wait_for_debugger_respects_service_worker_filter() { async fn service_worker_pause_on_start_waits_for_last_owner() { let mut ctx = TestContext::new(); load_unattached_service_worker_target(&mut ctx); - let runtime = ctx - .conn - .browser_context - .as_ref() - .expect("browser context should exist") - .renderer_runtime(); - assert!(!runtime.service_worker_pause_on_start_for_devtools()); + assert!(!service_worker_pause_on_start(&ctx)); assert_eq!(ctx.conn.service_worker_pause_on_start_owner_count(), 0); crate::domains::target::set_service_worker_pause_on_start_owner(&mut ctx.conn, None, true); - assert!(runtime.service_worker_pause_on_start_for_devtools()); + assert!(service_worker_pause_on_start(&ctx)); assert_eq!(ctx.conn.service_worker_pause_on_start_owner_count(), 1); crate::domains::target::set_service_worker_pause_on_start_owner( @@ -191,12 +185,12 @@ async fn service_worker_pause_on_start_waits_for_last_owner() { Some("SID-service-worker-owner"), true, ); - assert!(runtime.service_worker_pause_on_start_for_devtools()); + assert!(service_worker_pause_on_start(&ctx)); assert_eq!(ctx.conn.service_worker_pause_on_start_owner_count(), 2); crate::domains::target::set_service_worker_pause_on_start_owner(&mut ctx.conn, None, false); assert!( - runtime.service_worker_pause_on_start_for_devtools(), + service_worker_pause_on_start(&ctx), "one remaining DevTools owner must keep new Service Worker targets paused" ); assert_eq!(ctx.conn.service_worker_pause_on_start_owner_count(), 1); @@ -206,7 +200,7 @@ async fn service_worker_pause_on_start_waits_for_last_owner() { Some("SID-service-worker-owner"), false, ); - assert!(!runtime.service_worker_pause_on_start_for_devtools()); + assert!(!service_worker_pause_on_start(&ctx)); assert_eq!(ctx.conn.service_worker_pause_on_start_owner_count(), 0); } diff --git a/moli-protocol/src/domains/runtime/tests/shared_worker.rs b/moli-protocol/src/domains/runtime/tests/shared_worker.rs index 7f325b1e17..e7e8a99c83 100644 --- a/moli-protocol/src/domains/runtime/tests/shared_worker.rs +++ b/moli-protocol/src/domains/runtime/tests/shared_worker.rs @@ -573,11 +573,6 @@ onconnect = event => { .await; let background_url = "data:text/html,background"; - let background_page = ctx - .conn - .load_page_via_runtime_async(background_url) - .await - .expect("background diagnostics page should load"); let browser_context = ctx .conn .browser_context @@ -588,8 +583,11 @@ onconnect = event => { Some("SID-diagnostics-document-worker-bg".to_owned()), background_url.to_owned(), ); - browser_context - .replace_target_page_for_test("TID-diagnostics-document-worker-bg", Some(background_page)); + ctx.install_quiet_navigation_fixture_for_session_owner( + background_url, + Some("SID-diagnostics-document-worker-bg"), + ) + .await; ctx.sent.clear(); ctx.process_async(json!({ @@ -604,8 +602,8 @@ onconnect = event => { assert_eq!(isolate_scope["loadedDocumentPageCount"], json!(2)); assert_eq!( isolate_scope["loadedDocumentRendererOwnerCount"], - json!(1), - "this accounting fixture deliberately reuses the active engine for both document snapshots: {response:?}" + json!(2), + "each WebContents must build its Document on its own renderer owner: {response:?}" ); assert_eq!( isolate_scope["estimatedDocumentIsolateCount"], @@ -3180,8 +3178,8 @@ onconnect = event => { .browser_context .as_ref() .expect("browser context should exist") - .renderer_runtime() - .shared_worker_running_worker_isolate_count_for_diagnostics(), + .shared_worker_runtime_diagnostics_for_diagnostics() + .running_worker_isolate_count, 1, "same-origin pages with the same SharedWorker key should share one running worker isolate" ); diff --git a/moli-protocol/src/domains/runtime_context_events.rs b/moli-protocol/src/domains/runtime_context_events.rs index 7282b3f17f..8ab39caac8 100644 --- a/moli-protocol/src/domains/runtime_context_events.rs +++ b/moli-protocol/src/domains/runtime_context_events.rs @@ -496,7 +496,7 @@ fn mark_child_default_context_event_emitted( #[cfg(test)] mod tests { - use crate::conn::{BrowserContext, CdpConnection, CommandOwnerScope}; + use crate::conn::CommandOwnerScope; use crate::conn::{ServiceWorkerTargetState, SharedWorkerTargetState}; use crate::devtools_runtime::AutomationEvent; use moli_core::{RendererOwnerLocalHostId, page::RendererServiceWorkerVersionStatus}; @@ -505,8 +505,8 @@ mod tests { #[test] fn runtime_context_event_qualification_uses_target_owner_prefix() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id("TID-1".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -550,8 +550,9 @@ mod tests { #[test] fn typed_runtime_context_qualification_tags_shared_worker_target() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-shared".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-shared".to_owned()); let mut target = SharedWorkerTargetState::new( RendererOwnerLocalHostId::new_for_testing(1), SharedWorkerInstanceId::from_u64(9), @@ -601,8 +602,9 @@ mod tests { #[test] fn typed_runtime_context_qualification_classifies_service_worker_target() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-service".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-service".to_owned()); let mut target = ServiceWorkerTargetState::new( 41, 29, @@ -702,8 +704,8 @@ mod tests { #[test] fn live_child_context_delivery_advances_runtime_enable_inventory_cursor() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id("TID-1".to_owned()); browser_context.attach_active_session("SID-1".to_owned()); conn.install_browser_context_fixture_for_test(browser_context); @@ -811,11 +813,10 @@ mod tests { #[test] fn runtime_context_destroyed_clears_matching_remote_object_realm() { - let mut conn = CdpConnection::new(); - conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-runtime-context", - "TID-runtime-context", - )); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some( + conn.new_page_target_fixture_for_test("BID-runtime-context", "TID-runtime-context"), + ); conn.register_runtime_remote_object_ids_for_session_owner_with_realm( None, vec!["object-realm-1".to_owned()], @@ -852,8 +853,9 @@ mod tests { #[test] fn runtime_contexts_cleared_clears_shared_worker_remote_object_tracking() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-shared-runtime-context".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-shared-runtime-context".to_owned()); let mut target = SharedWorkerTargetState::new( RendererOwnerLocalHostId::new_for_testing(7), SharedWorkerInstanceId::from_u64(11), diff --git a/moli-protocol/src/domains/security.rs b/moli-protocol/src/domains/security.rs index 6947970cfa..f8da80d763 100644 --- a/moli-protocol/src/domains/security.rs +++ b/moli-protocol/src/domains/security.rs @@ -2,18 +2,21 @@ use chromiumoxide_cdp::cdp::browser_protocol::security::SetIgnoreCertificateErro use super::actions::SecurityAction; use super::command_output::CommandOutputPlan; -use crate::conn::{CdpConnection, Cmd, CommandOwnerScope}; +use crate::conn::{ + CdpConnection, Cmd, CommandOwnerScope, CompletedDocumentResourceRuntimeUpdate, + PendingDocumentResourceRuntimeUpdate, +}; pub(crate) struct PendingSecurityCommandDispatch { command_id: Option, owner_scope: CommandOwnerScope, - pending: moli_core::page::PendingPageCommand, + pending: PendingDocumentResourceRuntimeUpdate, } pub(crate) struct CompletedSecurityCommandDispatch { command_id: Option, owner_scope: CommandOwnerScope, - completed: Result, + completed: CompletedDocumentResourceRuntimeUpdate, } pub(crate) enum SecurityCommandTaskStep { @@ -26,7 +29,7 @@ impl PendingSecurityCommandDispatch { CompletedSecurityCommandDispatch { command_id: self.command_id, owner_scope: self.owner_scope, - completed: self.pending.wait().await.map_err(|error| error.to_string()), + completed: self.pending.wait().await, } } } @@ -92,12 +95,7 @@ pub(crate) fn complete_pending_security_command( conn: &mut CdpConnection, completed: CompletedSecurityCommandDispatch, ) -> CommandOutputPlan { - let completion = match completed.completed { - Ok(completion) => completion, - Err(error) => return CommandOutputPlan::error(-32000, error), - }; - let owner_scope = completed.owner_scope; - match conn.finish_rebuild_resource_runtime_for_owner(&owner_scope, completion) { + match conn.finish_document_resource_runtime_update(completed.completed) { Ok(()) => CommandOutputPlan::success(), Err(error) => CommandOutputPlan::error(-32000, error), } @@ -107,13 +105,13 @@ pub(crate) fn complete_pending_security_command( mod tests { use serde_json::json; - use crate::{conn::BrowserContext, testing::TestContext}; + use crate::testing::TestContext; #[tokio::test(flavor = "multi_thread")] async fn set_ignore_certificate_errors_toggles_tls_verification() { let mut ctx = TestContext::new(); ctx.conn - .insert_browser_context(BrowserContext::new("BID-9".into())); + .insert_browser_context(ctx.conn.new_browser_context_fixture_for_test("BID-9")); ctx.process_async(json!({ "id": 11, @@ -137,9 +135,9 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn set_ignore_certificate_errors_is_scoped_to_active_browser_context() { let mut ctx = TestContext::new(); - let mut first = BrowserContext::new_with_page_for_test("BID-1", "TID-1"); + let mut first = ctx.conn.new_page_target_fixture_for_test("BID-1", "TID-1"); first.attach_active_session("SID-1"); - let mut second = BrowserContext::new_with_page_for_test("BID-2", "TID-2"); + let mut second = ctx.conn.new_page_target_fixture_for_test("BID-2", "TID-2"); second.attach_active_session("SID-2"); ctx.conn.insert_browser_context(first); ctx.conn.insert_browser_context(second); diff --git a/moli-protocol/src/domains/service_worker.rs b/moli-protocol/src/domains/service_worker.rs index 3be1b6ac2b..1856473af0 100644 --- a/moli-protocol/src/domains/service_worker.rs +++ b/moli-protocol/src/domains/service_worker.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; +use moli_core::browser::ServiceWorkerCommand; use serde::Deserialize; use url::Url; @@ -53,17 +54,23 @@ fn disable_command(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> CommandOutputPlan let Ok(browser_context_id) = browser_context_id_for_command(conn, cmd.session_id) else { return CommandOutputPlan::error(-32001, "Unknown sessionId"); }; - if let Some(context) = conn.browser_context_by_id_mut(&browser_context_id) { + let reset_context = if let Some(context) = conn.browser_context_by_id_mut(&browser_context_id) { let was_enabled = context .service_worker_domain_enabled_sessions() .iter() .any(|session_id| session_id.as_deref() == cmd.session_id); context.set_service_worker_domain_enabled(cmd.session_id, false); - if was_enabled { - context - .renderer_runtime() - .set_service_worker_force_update_on_page_load_for_devtools(false); - } + was_enabled.then(|| context.browser_context_id()) + } else { + None + }; + if let Some(context) = reset_context + && let Err(message) = conn.execute_browser_service_worker_command( + context, + ServiceWorkerCommand::SetForceUpdateOnPageLoad(false), + ) + { + return CommandOutputPlan::error(-32000, message); } CommandOutputPlan::success() } @@ -75,7 +82,7 @@ fn set_force_update_on_page_load_command( let Ok(browser_context_id) = browser_context_id_for_command(conn, cmd.session_id) else { return CommandOutputPlan::error(-32001, "Unknown sessionId"); }; - let Some(context) = conn.browser_context_by_id(&browser_context_id) else { + let Ok(context) = conn.browser_context_handle_for_devtools_id(&browser_context_id) else { return CommandOutputPlan::error(-32000, "No browser context"); }; let Some(force_update) = force_update_on_page_load_param(cmd) else { @@ -84,10 +91,13 @@ fn set_force_update_on_page_load_command( "Invalid ServiceWorker setForceUpdateOnPageLoad params", ); }; - context - .renderer_runtime() - .set_service_worker_force_update_on_page_load_for_devtools(force_update); - CommandOutputPlan::success() + match conn.execute_browser_service_worker_command( + context, + ServiceWorkerCommand::SetForceUpdateOnPageLoad(force_update), + ) { + Ok(()) => CommandOutputPlan::success(), + Err(message) => CommandOutputPlan::error(-32000, message), + } } fn lifecycle_command( @@ -98,60 +108,50 @@ fn lifecycle_command( let Ok(browser_context_id) = browser_context_id_for_command(conn, cmd.session_id) else { return CommandOutputPlan::error(-32001, "Unknown sessionId"); }; - let Some(context) = conn.browser_context_by_id(&browser_context_id) else { + let Some(context_projection) = conn.browser_context_by_id(&browser_context_id) else { return CommandOutputPlan::error(-32000, "No browser context"); }; - if !context + if !context_projection .service_worker_domain_enabled_sessions() .iter() .any(|session_id| session_id.as_deref() == cmd.session_id) { return CommandOutputPlan::error(-32000, "ServiceWorker domain is not enabled"); } - let renderer_runtime = context.renderer_runtime(); - let result = match action { + let Ok(context) = conn.browser_context_handle_for_devtools_id(&browser_context_id) else { + return CommandOutputPlan::error(-32000, "No browser context"); + }; + let command = match action { ServiceWorkerAction::Unregister => { let Some(scope_url) = scope_url_param(cmd) else { return CommandOutputPlan::error(-32602, "Invalid ServiceWorker scopeURL"); }; - renderer_runtime - .unregister_service_worker_scope_for_devtools(&scope_url) - .map(|_| ()) + ServiceWorkerCommand::Unregister { scope: scope_url } } ServiceWorkerAction::StartWorker => { let Some(scope_url) = scope_url_param(cmd) else { return CommandOutputPlan::error(-32602, "Invalid ServiceWorker scopeURL"); }; - renderer_runtime - .start_service_worker_for_devtools(&scope_url) - .map(|_| ()) + ServiceWorkerCommand::Start { scope: scope_url } } ServiceWorkerAction::StopWorker => { let Some(version_id) = version_id_param(cmd) else { return CommandOutputPlan::error(-32602, "Invalid ServiceWorker versionId"); }; - renderer_runtime - .stop_service_worker_for_devtools(version_id) - .map(|_| ()) + ServiceWorkerCommand::StopVersion { version_id } } - ServiceWorkerAction::StopAllWorkers => renderer_runtime - .stop_all_service_workers_for_devtools() - .map(|_| ()), + ServiceWorkerAction::StopAllWorkers => ServiceWorkerCommand::StopAll, ServiceWorkerAction::SkipWaiting => { let Some(scope_url) = scope_url_param(cmd) else { return CommandOutputPlan::error(-32602, "Invalid ServiceWorker scopeURL"); }; - renderer_runtime - .skip_waiting_service_worker_for_devtools(&scope_url) - .map(|_| ()) + ServiceWorkerCommand::SkipWaiting { scope: scope_url } } ServiceWorkerAction::UpdateRegistration => { let Some(scope_url) = scope_url_param(cmd) else { return CommandOutputPlan::error(-32602, "Invalid ServiceWorker scopeURL"); }; - renderer_runtime - .update_service_worker_registration_for_devtools(&scope_url) - .map(|_| ()) + ServiceWorkerCommand::UpdateRegistration { scope: scope_url } } ServiceWorkerAction::DeliverPushMessage => { let Some(params) = deliver_push_message_params(cmd) else { @@ -160,13 +160,11 @@ fn lifecycle_command( "Invalid ServiceWorker deliverPushMessage params", ); }; - renderer_runtime - .deliver_push_message_for_devtools( - ¶ms.origin, - params.registration_id, - params.data, - ) - .map(|_| ()) + ServiceWorkerCommand::DeliverPushMessage { + origin: params.origin, + registration_id: params.registration_id, + data: params.data, + } } ServiceWorkerAction::DispatchSyncEvent => { let Some(params) = sync_event_params(cmd) else { @@ -175,14 +173,12 @@ fn lifecycle_command( "Invalid ServiceWorker dispatchSyncEvent params", ); }; - renderer_runtime - .dispatch_sync_event_for_devtools( - ¶ms.origin, - params.registration_id, - params.tag, - params.last_chance, - ) - .map(|_| ()) + ServiceWorkerCommand::DispatchSyncEvent { + origin: params.origin, + registration_id: params.registration_id, + tag: params.tag, + last_chance: params.last_chance, + } } ServiceWorkerAction::DispatchPeriodicSyncEvent => { let Some(params) = periodic_sync_event_params(cmd) else { @@ -191,21 +187,22 @@ fn lifecycle_command( "Invalid ServiceWorker dispatchPeriodicSyncEvent params", ); }; - renderer_runtime - .dispatch_periodic_sync_event_for_devtools( - ¶ms.origin, - params.registration_id, - params.tag, - ) - .map(|_| ()) + ServiceWorkerCommand::DispatchPeriodicSyncEvent { + origin: params.origin, + registration_id: params.registration_id, + tag: params.tag, + } } ServiceWorkerAction::Enable | ServiceWorkerAction::Disable | ServiceWorkerAction::SetForceUpdateOnPageLoad => { - Err(format!("ServiceWorker.{} is not implemented", cmd.action)) + return CommandOutputPlan::error( + -32000, + format!("ServiceWorker.{} is not implemented", cmd.action), + ); } }; - match result { + match conn.execute_browser_service_worker_command(context, command) { Ok(()) => CommandOutputPlan::success(), Err(message) => CommandOutputPlan::error(-32000, message), } @@ -542,12 +539,10 @@ fn controlled_client_target_ids_for_target( context: &BrowserContext, target: &ServiceWorkerTargetState, ) -> Vec { - let controlled_client_ids = context - .renderer_runtime() - .controlled_service_worker_window_client_ids_for_devtools( - target.renderer_registration_id, - target.renderer_version_id, - ); + let controlled_client_ids = context.controlled_service_worker_window_client_ids( + target.renderer_registration_id, + target.renderer_version_id, + ); page_target_ids_for_controlled_client_ids(context, &controlled_client_ids) } @@ -618,39 +613,44 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn controlled_client_ids_map_to_exact_page_target_ids_even_for_same_url() { let mut ctx = TestContext::new(); - let active_page = ctx + let mut context = ctx .conn - .load_page_via_runtime_async("data:text/html,same-url") - .await - .expect("active page should load"); - let background_page = ctx - .conn - .load_page_via_runtime_async("data:text/html,same-url") - .await - .expect("background page should load"); - let active_client_id = active_page.service_worker_client_id(); - let background_client_id = background_page.service_worker_client_id(); - assert_ne!(active_client_id, background_client_id); - - let mut context = BrowserContext::new("BID-1".to_owned()); + .new_browser_context_fixture_for_test("BID-1".to_owned()); context.set_active_target_id("TID-active".to_owned()); + context.attach_active_session("SID-active".to_owned()); context.set_target_url("data:text/html,same-url".to_owned()); - let _ = context.replace_loaded_page(Some(active_page)); - context.register_page_target_url_fixture( "TID-background".to_owned(), - None, + Some("SID-background".to_owned()), "data:text/html,same-url".to_owned(), ); - let _ = context.replace_target_page_for_test("TID-background", Some(background_page)); + ctx.conn.install_browser_context_fixture_for_test(context); + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,same-url", + Some("SID-active"), + ) + .await; + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,same-url", + Some("SID-background"), + ) + .await; + let context = ctx.conn.browser_context.as_ref().unwrap(); + let active_client_id = context + .target_service_worker_client_id("TID-active") + .expect("active client id"); + let background_client_id = context + .target_service_worker_client_id("TID-background") + .expect("background client id"); + assert_ne!(active_client_id, background_client_id); assert_eq!( - super::page_target_ids_for_controlled_client_ids(&context, &[background_client_id]), + super::page_target_ids_for_controlled_client_ids(context, &[background_client_id]), vec!["TID-background".to_owned()] ); assert_eq!( super::page_target_ids_for_controlled_client_ids( - &context, + context, &[ active_client_id, background_client_id, @@ -858,8 +858,7 @@ mod tests { .browser_context .as_ref() .unwrap() - .renderer_runtime() - .service_worker_force_update_on_page_load_for_devtools() + .service_worker_force_update_on_page_load() ); ctx.process_async(json!({ @@ -883,8 +882,7 @@ mod tests { .browser_context .as_ref() .unwrap() - .renderer_runtime() - .service_worker_force_update_on_page_load_for_devtools() + .service_worker_force_update_on_page_load() ); } diff --git a/moli-protocol/src/domains/storage/ops.rs b/moli-protocol/src/domains/storage/ops.rs index 46c0d12d1c..e23a5ef3ea 100644 --- a/moli-protocol/src/domains/storage/ops.rs +++ b/moli-protocol/src/domains/storage/ops.rs @@ -1,6 +1,8 @@ use crate::conn::{ BrowserContext, BrowserContextCookieManagerSurfaceSnapshot, CdpConnection, Cmd, - CommandOwnerScope, SiteDataClearOptions, + CommandOwnerScope, CompletedChildFrameTreeSnapshot, CompletedDocumentCookieOwnerSnapshot, + CompletedDocumentStorageKeySnapshot, PendingChildFrameTreeSnapshot, + PendingDocumentCookieOwnerSnapshot, PendingDocumentStorageKeySnapshot, SiteDataClearOptions, }; use crate::devtools_runtime::{ DevToolsBrowserContextId, DevToolsCommand, DevToolsCommandResult, DevToolsCookieParam, @@ -11,7 +13,6 @@ use crate::devtools_runtime::{ use crate::domains::actions::StorageAction; use crate::domains::command_output::CommandOutputPlan; use moli_cookie_jar::StoredCookieSetReport; -use moli_core::page::{CompletedPageCommand, PendingPageCommand}; use serde_json::{Value, json}; use url::Url; @@ -41,14 +42,14 @@ pub(crate) struct PendingStorageCommandDispatch { command_id: Option, session_id: Option, kind: PendingStorageCommandKind, - pending: PendingPageCommand, + pending: PendingStorageBrowserCommand, } pub(crate) struct CompletedStorageCommandDispatch { command_id: Option, session_id: Option, kind: PendingStorageCommandKind, - completed: Result, + completed: CompletedStorageBrowserCommand, } pub(crate) enum StorageCommandTaskStep { @@ -62,11 +63,8 @@ enum DevToolsSetCookiesTaskStep { } enum PendingStorageCommandKind { - GetStorageKeyForTopFrame { - owner_scope: CommandOwnerScope, - }, + GetStorageKeyForTopFrame, GetStorageKeyForFrame { - owner_scope: CommandOwnerScope, frame_id: String, }, SetCookies { @@ -75,13 +73,36 @@ enum PendingStorageCommandKind { }, } +enum PendingStorageBrowserCommand { + StorageKey(PendingDocumentStorageKeySnapshot), + ChildFrames(PendingChildFrameTreeSnapshot), + CookieOwner(PendingDocumentCookieOwnerSnapshot), +} + +enum CompletedStorageBrowserCommand { + StorageKey(CompletedDocumentStorageKeySnapshot), + ChildFrames(CompletedChildFrameTreeSnapshot), + CookieOwner(CompletedDocumentCookieOwnerSnapshot), +} + impl PendingStorageCommandDispatch { pub(crate) async fn wait(self) -> CompletedStorageCommandDispatch { + let completed = match self.pending { + PendingStorageBrowserCommand::StorageKey(pending) => { + CompletedStorageBrowserCommand::StorageKey(pending.wait().await) + } + PendingStorageBrowserCommand::ChildFrames(pending) => { + CompletedStorageBrowserCommand::ChildFrames(pending.wait().await) + } + PendingStorageBrowserCommand::CookieOwner(pending) => { + CompletedStorageBrowserCommand::CookieOwner(pending.wait().await) + } + }; CompletedStorageCommandDispatch { command_id: self.command_id, session_id: self.session_id, kind: self.kind, - completed: self.pending.wait().await.map_err(|error| error.to_string()), + completed, } } } @@ -493,16 +514,13 @@ fn start_get_storage_key_for_frame_command( }; if params.frame_id == target_id { - if let Ok((context_id, target_id)) = conn.resolve_document_command_owner(&owner_scope) { - let context = conn - .browser_context_by_id(&context_id) - .expect("admitted document context remains registered"); - return match context.start_document_storage_key_snapshot_for_target(&target_id) { + if let Ok(document) = conn.resolve_browser_document_for_owner(&owner_scope) { + return match conn.start_document_storage_key_snapshot(document) { Ok(pending) => StorageCommandTaskStep::Pending(PendingStorageCommandDispatch { command_id: cmd.id, session_id: cmd.session_id.map(str::to_owned), - kind: PendingStorageCommandKind::GetStorageKeyForTopFrame { owner_scope }, - pending, + kind: PendingStorageCommandKind::GetStorageKeyForTopFrame, + pending: PendingStorageBrowserCommand::StorageKey(pending), }), Err(error) => StorageCommandTaskStep::Complete(CommandOutputPlan::error( -32000, @@ -522,24 +540,20 @@ fn start_get_storage_key_for_frame_command( if let Err(message) = conn.ensure_document_accessible_for_owner(&owner_scope) { return StorageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)); } - let Ok((context_id, target_id)) = conn.resolve_document_command_owner(&owner_scope) else { + let Ok(document) = conn.resolve_browser_document_for_owner(&owner_scope) else { return StorageCommandTaskStep::Complete(CommandOutputPlan::error( -32000, "NoFrameForGivenId", )); }; - let context = conn - .browser_context_by_id(&context_id) - .expect("admitted document context remains registered"); - match context.start_child_frame_tree_snapshot_for_target(&target_id) { + match conn.start_child_frame_tree_snapshot(document) { Ok(pending) => StorageCommandTaskStep::Pending(PendingStorageCommandDispatch { command_id: cmd.id, session_id: cmd.session_id.map(str::to_owned), kind: PendingStorageCommandKind::GetStorageKeyForFrame { - owner_scope, frame_id: params.frame_id, }, - pending, + pending: PendingStorageBrowserCommand::ChildFrames(pending), }), Err(error) => { StorageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, error.to_string())) @@ -672,7 +686,10 @@ fn start_devtools_set_cookies_result( let reports = set_cookies_with_manager_surface(browser_context, &manager_surface, cookies); return DevToolsSetCookiesTaskStep::Complete(Ok(set_cookie_reports_result(&reports))); }; - match browser_context.start_document_cookie_owner_snapshot_for_target(&target_id) { + let document = browser_context + .document_handle_for_target(&target_id) + .expect("loaded document has a Browser handle"); + match conn.start_document_cookie_owner_snapshot(document) { Ok(pending) => DevToolsSetCookiesTaskStep::Pending(PendingStorageCommandDispatch { command_id, session_id: session_id.map(str::to_owned), @@ -680,7 +697,7 @@ fn start_devtools_set_cookies_result( browser_context_id, cookies, }, - pending, + pending: PendingStorageBrowserCommand::CookieOwner(pending), }), Err(error) => DevToolsSetCookiesTaskStep::Complete(Err(DevToolsError::new( DevToolsErrorKind::Internal, @@ -739,47 +756,36 @@ pub(crate) fn complete_pending_storage_command( conn: &mut CdpConnection, completed: CompletedStorageCommandDispatch, ) -> CommandOutputPlan { - match completed.kind { - PendingStorageCommandKind::GetStorageKeyForTopFrame { owner_scope } => { - complete_get_storage_key_for_top_frame_command(conn, &owner_scope, completed.completed) - } - PendingStorageCommandKind::GetStorageKeyForFrame { - owner_scope, - frame_id, - } => complete_get_storage_key_for_frame_command( - conn, - &owner_scope, - completed.completed, - &frame_id, - ), - PendingStorageCommandKind::SetCookies { - browser_context_id, - cookies, - } => devtools_set_cookies_output_plan(complete_set_cookies_result( + match (completed.kind, completed.completed) { + ( + PendingStorageCommandKind::GetStorageKeyForTopFrame, + CompletedStorageBrowserCommand::StorageKey(completed), + ) => complete_get_storage_key_for_top_frame_command(conn, completed), + ( + PendingStorageCommandKind::GetStorageKeyForFrame { frame_id }, + CompletedStorageBrowserCommand::ChildFrames(completed), + ) => complete_get_storage_key_for_frame_command(conn, completed, &frame_id), + ( + PendingStorageCommandKind::SetCookies { + browser_context_id, + cookies, + }, + CompletedStorageBrowserCommand::CookieOwner(completed), + ) => devtools_set_cookies_output_plan(complete_set_cookies_result( conn, &browser_context_id, - completed.completed, + completed, cookies, )), + _ => CommandOutputPlan::error(-32000, "UnexpectedStorageCommandKind"), } } fn complete_get_storage_key_for_top_frame_command( conn: &mut CdpConnection, - owner_scope: &CommandOwnerScope, - completed: Result, + completed: CompletedDocumentStorageKeySnapshot, ) -> CommandOutputPlan { - let completion = match completed { - Ok(completion) => completion, - Err(error) => return CommandOutputPlan::error(-32000, error), - }; - let Ok((context_id, target_id)) = conn.resolve_document_command_owner(owner_scope) else { - return CommandOutputPlan::error(-32000, "NoFrameForGivenId"); - }; - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - match context.finish_document_storage_key_snapshot_for_target(&target_id, completion) { + match conn.finish_document_storage_key_snapshot(completed) { Ok(storage_key) => storage_key_result_plan(&storage_key), Err(error) => CommandOutputPlan::error(-32000, error.to_string()), } @@ -787,30 +793,18 @@ fn complete_get_storage_key_for_top_frame_command( fn complete_get_storage_key_for_frame_command( conn: &mut CdpConnection, - owner_scope: &CommandOwnerScope, - completed: Result, + completed: CompletedChildFrameTreeSnapshot, frame_id: &str, ) -> CommandOutputPlan { - let completion = match completed { - Ok(completion) => completion, - Err(error) => return CommandOutputPlan::error(-32000, error), + let child_frames = match conn.finish_child_frame_tree_snapshot(completed) { + Ok(child_frames) => child_frames, + Err(error) => { + return CommandOutputPlan::error( + -32000, + format!("Failed to snapshot child frame tree: {error}"), + ); + } }; - let Ok((context_id, target_id)) = conn.resolve_document_command_owner(owner_scope) else { - return CommandOutputPlan::error(-32000, "NoFrameForGivenId"); - }; - let context = conn - .browser_context_by_id_mut(&context_id) - .expect("admitted document context remains registered"); - let child_frames = - match context.finish_child_frame_tree_snapshot_for_target(&target_id, completion) { - Ok(child_frames) => child_frames, - Err(error) => { - return CommandOutputPlan::error( - -32000, - format!("Failed to snapshot child frame tree: {error}"), - ); - } - }; let Some(frame) = find_child_frame(&child_frames, frame_id) else { return CommandOutputPlan::error(-32000, "NoFrameForGivenId"); @@ -855,58 +849,37 @@ fn complete_pending_set_cookies_result( conn: &mut CdpConnection, completed: CompletedStorageCommandDispatch, ) -> Result { - let PendingStorageCommandKind::SetCookies { - browser_context_id, - cookies, - } = completed.kind + let ( + PendingStorageCommandKind::SetCookies { + browser_context_id, + cookies, + }, + CompletedStorageBrowserCommand::CookieOwner(completed), + ) = (completed.kind, completed.completed) else { return Err(DevToolsError::new( DevToolsErrorKind::Internal, "UnexpectedStorageCommandKind", )); }; - complete_set_cookies_result(conn, &browser_context_id, completed.completed, cookies) + complete_set_cookies_result(conn, &browser_context_id, completed, cookies) } fn complete_set_cookies_result( conn: &mut CdpConnection, browser_context_id: &str, - completed: Result, + completed: CompletedDocumentCookieOwnerSnapshot, cookies: Vec, ) -> Result { - let completion = match completed { - Ok(completion) => completion, - Err(error) => return Err(DevToolsError::new(DevToolsErrorKind::Internal, error)), - }; + let owner = conn + .finish_document_cookie_owner_snapshot(completed) + .map_err(|error| DevToolsError::new(DevToolsErrorKind::Internal, error))?; let Some(browser_context) = conn.browser_context_by_id_mut(browser_context_id) else { return Err(DevToolsError::new( DevToolsErrorKind::NoSuchTarget, "UnknownBrowserContextId", )); }; - let owner = { - let Some(target_id) = browser_context - .active_target_id() - .filter(|target_id| browser_context.target_has_loaded_page(target_id)) - .map(str::to_owned) - else { - return Err(DevToolsError::new( - DevToolsErrorKind::Internal, - "NoDocumentLoaded", - )); - }; - match browser_context - .finish_document_cookie_owner_snapshot_for_target(&target_id, completion) - { - Ok(owner) => owner, - Err(error) => { - return Err(DevToolsError::new( - DevToolsErrorKind::Internal, - error.to_string(), - )); - } - } - }; let manager_surface = browser_context.cookie_manager_surface_snapshot_with_owner(&owner); let reports = set_cookies_with_manager_surface(browser_context, &manager_surface, cookies); Ok(set_cookie_reports_result(&reports)) @@ -1469,7 +1442,7 @@ mod protocol_neutral_tests { use crate::devtools_runtime::{DevToolsCommand, DevToolsProtocol}; use serde_json::{Value, json}; - use crate::conn::{CdpConnection, Cmd}; + use crate::conn::Cmd; use super::{ StorageCommandTaskStep, build_cdp_storage_clear_cookies_command, @@ -1515,7 +1488,7 @@ mod protocol_neutral_tests { #[test] fn devtools_storage_entry_routes_get_cookies_command_to_cookie_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(141), @@ -1592,7 +1565,7 @@ mod protocol_neutral_tests { #[test] fn devtools_storage_entry_routes_delete_cookies_command_to_cookie_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(146), @@ -1655,7 +1628,7 @@ mod protocol_neutral_tests { #[test] fn devtools_storage_entry_routes_clear_cookies_command_to_cookie_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = Value::Null; let cmd = Cmd::for_test( Some(153), @@ -1726,7 +1699,7 @@ mod protocol_neutral_tests { #[test] fn devtools_storage_entry_routes_set_cookies_command_to_cookie_owner() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let params = json!({ "cookies": [{ "name": "sid", diff --git a/moli-protocol/src/domains/storage/tests.rs b/moli-protocol/src/domains/storage/tests.rs index ee389830e4..99c6a43ce2 100644 --- a/moli-protocol/src/domains/storage/tests.rs +++ b/moli-protocol/src/domains/storage/tests.rs @@ -146,7 +146,7 @@ async fn wait_for_storage_test_completion(ctx: &mut TestContext) -> String { .to_owned() } -fn seed_indexed_db_usage(manager: &moli_core::storage::SharedIndexedDbManager, origin: &str) { +fn seed_indexed_db_usage(manager: moli_core::storage::SharedIndexedDbManager, origin: &str) { let mut manager = manager.lock(); let opened = manager .open(moli_core::storage::IndexedDbOpenOptions { @@ -388,7 +388,7 @@ async fn spawn_partitioned_child_frame_server() -> (String, String, tokio::task: #[tokio::test(flavor = "multi_thread")] async fn storage_get_storage_key_for_frame_returns_top_frame_origin() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-SK-TOP".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-SK-TOP"); bc.set_active_target_id("TID-SK-TOP"); bc.set_target_url("https://top.example/app".into()); bc.set_target_security_origin("https://top.example".into()); @@ -416,7 +416,9 @@ async fn storage_get_storage_key_for_top_frame_uses_loaded_page_storage_key() { .expect("page url should parse") .origin() .ascii_serialization(); - let mut bc = BrowserContext::new("BID-SK-TOP-LIVE".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-SK-TOP-LIVE"); bc.set_active_target_id("TID-SK-TOP-LIVE"); bc.set_target_url("https://stale-target-url.example/app".into()); bc.set_target_security_origin("https://stale-target-url.example".into()); @@ -452,7 +454,9 @@ async fn storage_get_storage_key_for_frame_returns_child_frame_inherited_origin( .expect("page url should parse") .origin(); let top_origin = top_origin.ascii_serialization(); - let mut bc = BrowserContext::new("BID-SK-CHILD".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-SK-CHILD"); bc.set_active_target_id("TID-SK-CHILD"); bc.set_target_url(page_url.clone()); bc.set_target_security_origin(top_origin.clone()); @@ -500,7 +504,9 @@ async fn storage_get_storage_key_for_credentialless_child_uses_page_nonce() { .expect("page url should parse") .origin() .ascii_serialization(); - let mut bc = BrowserContext::new("BID-SK-CREDENTIALLESS".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-SK-CREDENTIALLESS"); bc.set_active_target_id("TID-SK-CREDENTIALLESS"); bc.set_target_url(page_url.clone()); bc.set_target_security_origin(top_origin.clone()); @@ -646,7 +652,9 @@ async fn storage_get_storage_key_for_frame_returns_child_frame_partition_key() { .expect("page url should parse") .origin(); let top_origin = top_origin.ascii_serialization(); - let mut bc = BrowserContext::new("BID-SK-PARTITIONED".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-SK-PARTITIONED"); bc.set_active_target_id("TID-SK-PARTITIONED"); bc.set_target_url(page_url.clone()); bc.set_target_security_origin(top_origin); @@ -703,7 +711,9 @@ async fn storage_get_storage_key_for_frame_rejects_opaque_child_frame() { .expect("page url should parse") .origin(); let top_origin = top_origin.ascii_serialization(); - let mut bc = BrowserContext::new("BID-SK-OPAQUE".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-SK-OPAQUE"); bc.set_active_target_id("TID-SK-OPAQUE"); bc.set_target_url(page_url.clone()); bc.set_target_security_origin(top_origin); @@ -751,7 +761,9 @@ async fn storage_get_storage_key_for_frame_rejects_opaque_child_frame() { #[tokio::test(flavor = "multi_thread")] async fn storage_get_storage_key_for_frame_rejects_unknown_frame() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-SK-MISSING".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-SK-MISSING"); bc.set_active_target_id("TID-SK-MISSING"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -775,7 +787,9 @@ async fn storage_key_targets_loaded_background_owner_without_activation() { .origin() .ascii_serialization(); - let mut bc = BrowserContext::new("BID-SK-BG".to_owned()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-SK-BG".to_owned()); bc.set_active_target_id("TID-active".to_owned()); bc.attach_active_session("SID-active".to_owned()); bc.register_page_target_url_fixture( @@ -846,28 +860,23 @@ async fn pending_storage_key_keeps_background_owner_route_across_completion() { .origin() .ascii_serialization(); - let active_page = ctx + let mut bc = ctx .conn - .load_page_via_runtime_async(&active_url) - .await - .expect("active page should load"); - let background_page = ctx - .conn - .load_page_via_runtime_async(&background_url) - .await - .expect("background page should load"); - - let mut bc = BrowserContext::new("BID-storage-owner-route".to_owned()); + .new_browser_context_fixture_for_test("BID-storage-owner-route".to_owned()); bc.set_active_target_id("TID-storage-active".to_owned()); - bc.set_target_url(active_page.final_url().as_str().to_owned()); - bc.replace_active_page_for_test(Some(active_page)); bc.register_page_target_url_fixture( "TID-storage-background".to_owned(), Some("SID-storage-background".to_owned()), - background_page.final_url().as_str().to_owned(), + "about:blank".to_owned(), ); - bc.replace_target_page_for_test("TID-storage-background", Some(background_page)); ctx.conn.install_browser_context_fixture_for_test(bc); + ctx.install_navigation_fixture_for_session_owner(&active_url, None) + .await; + ctx.install_navigation_fixture_for_session_owner( + &background_url, + Some("SID-storage-background"), + ) + .await; let raw = serde_json::to_string(&json!({ "id": 104, @@ -919,12 +928,16 @@ async fn pending_storage_key_keeps_background_owner_route_across_completion() { #[tokio::test(flavor = "multi_thread")] async fn storage_key_targets_inactive_owner_without_activation() { let mut ctx = TestContext::new(); - let mut active = BrowserContext::new("BID-active".to_owned()); + let mut active = ctx + .conn + .new_browser_context_fixture_for_test("BID-active".to_owned()); active.set_active_target_id("TID-active".to_owned()); active.attach_active_session("SID-active".to_owned()); ctx.conn.install_browser_context_fixture_for_test(active); - let mut inactive = BrowserContext::new("BID-inactive".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-inactive".to_owned()); inactive.set_active_target_id("TID-inactive".to_owned()); inactive.set_target_url("https://inactive.example/app".to_owned()); inactive.set_target_security_origin("https://inactive.example".to_owned()); @@ -953,7 +966,9 @@ async fn storage_key_targets_inactive_owner_without_activation() { #[tokio::test(flavor = "multi_thread")] async fn storage_get_cookies_accepts_attached_page_session_route() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-attached-storage".into()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-attached-storage"); browser_context.set_active_target_id("TID-attached-storage".to_owned()); assert!(browser_context.assign_attached_session_to_target( "TID-attached-storage", @@ -985,7 +1000,9 @@ async fn storage_get_cookies_accepts_attached_page_session_route() { #[tokio::test(flavor = "multi_thread")] async fn storage_get_cookies_accepts_background_page_session_route() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-background-storage".into()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-background-storage"); browser_context.set_active_target_id("TID-active-storage".to_owned()); browser_context.stage_background_target( "TID-background-storage".to_owned(), @@ -1018,7 +1035,7 @@ async fn storage_get_cookies_accepts_background_page_session_route() { #[tokio::test(flavor = "multi_thread")] async fn storage_cookie_metadata_round_trip_includes_priority_and_source() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-M".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-M")); ctx.process_async(json!({ "id": 10, @@ -1062,7 +1079,7 @@ async fn storage_cookie_metadata_round_trip_includes_priority_and_source() { #[tokio::test(flavor = "multi_thread")] async fn storage_get_cookies_omits_same_site_for_unspecified_cookie() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-U".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-U")); ctx.process_async(json!({ "id": 111, @@ -1096,7 +1113,7 @@ async fn storage_get_cookies_omits_same_site_for_unspecified_cookie() { #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_returns_cookie_reports_for_accepted_cookie() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-R".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-R")); ctx.process_async(json!({ "id": 15, @@ -1131,7 +1148,7 @@ async fn storage_set_cookies_returns_cookie_reports_for_accepted_cookie() { #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_reports_secure_access_warning_for_localhost_http() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-RW".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-RW")); ctx.process_async(json!({ "id": 17, @@ -1165,7 +1182,7 @@ async fn storage_set_cookies_reports_secure_access_warning_for_localhost_http() #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_returns_cookie_reports_for_rejected_cookie() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-RJ".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-RJ")); ctx.process_async(json!({ "id": 16, @@ -1203,7 +1220,7 @@ async fn storage_set_cookies_returns_cookie_reports_for_rejected_cookie() { #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_reports_multiple_rejection_reasons() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-RM".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-RM")); ctx.process_async(json!({ "id": 18, @@ -1244,7 +1261,7 @@ async fn storage_set_cookies_reports_multiple_rejection_reasons() { #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_reports_public_suffix_rejection() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-PSL".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-PSL")); ctx.process_async(json!({ "id": 19, @@ -1403,7 +1420,7 @@ fn cookie_query_report_json_projects_facade_exclusion_reasons() { #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_round_trips_partition_key() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-P".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-P")); ctx.process_async(json!({ "id": 12, @@ -1463,7 +1480,7 @@ async fn storage_set_cookies_round_trips_partition_key() { #[tokio::test(flavor = "multi_thread")] async fn storage_delete_cookies_matches_exact_partition_key() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-PD".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-PD")); ctx.process_async(json!({ "id": 14, @@ -1541,7 +1558,7 @@ async fn storage_delete_cookies_matches_exact_partition_key() { #[tokio::test(flavor = "multi_thread")] async fn storage_delete_cookies_rejects_malformed_partition_key() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-PD-BAD".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-PD-BAD")); ctx.process_async(json!({ "id": 17, @@ -1559,7 +1576,10 @@ async fn storage_delete_cookies_rejects_malformed_partition_key() { #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_keeps_cookie_store_available_after_lock_holder_panic() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-store-panic".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-store-panic"), + ); let cookie_store = ctx .conn .browser_context @@ -1626,7 +1646,7 @@ async fn storage_set_cookies_keeps_cookie_store_available_after_lock_holder_pani #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_accepts_leading_dot_domain() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-DOT".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-DOT")); ctx.process_async(json!({ "id": 121, @@ -1663,7 +1683,7 @@ async fn storage_set_cookies_accepts_leading_dot_domain() { #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_reports_invalid_path_and_url_as_facade_rejections() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-PATH".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-PATH")); ctx.process_async(json!({ "id": 122, @@ -1718,7 +1738,7 @@ async fn storage_set_cookies_reports_invalid_path_and_url_as_facade_rejections() #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_reports_structured_name_value_facade_rejections() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-NV".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-NV")); ctx.process_async(json!({ "id": 123, @@ -1785,7 +1805,9 @@ async fn storage_set_cookies_reports_structured_name_value_facade_rejections() { #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_uses_browser_context_default_cookie_url_when_missing() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new_with_page_for_test("BID-DEF", "TID-DEF"); + let mut bc = ctx + .conn + .new_page_target_fixture_for_test("BID-DEF", "TID-DEF"); bc.set_target_url("https://example.com/app".into()); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -1840,11 +1862,13 @@ async fn storage_set_cookies_uses_browser_context_default_cookie_url_when_missin #[tokio::test(flavor = "multi_thread")] async fn storage_cookie_methods_accept_inactive_browser_context_ids() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-ACTIVE", - "TID-ACTIVE", - )); - let mut inactive = BrowserContext::new_with_page_for_test("BID-INACTIVE", "TID-INACTIVE"); + ctx.conn.browser_context = Some( + ctx.conn + .new_page_target_fixture_for_test("BID-ACTIVE", "TID-ACTIVE"), + ); + let mut inactive = ctx + .conn + .new_page_target_fixture_for_test("BID-INACTIVE", "TID-INACTIVE"); inactive.set_target_url("https://inactive.example/app".into()); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); @@ -1929,7 +1953,10 @@ async fn storage_cookie_methods_accept_inactive_browser_context_ids() { #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_origin_clears_cookies_visible_to_origin() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-origin-clear".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-origin-clear"), + ); ctx.process_async(json!({ "id": 12_510, @@ -1975,7 +2002,10 @@ async fn storage_clear_data_for_origin_clears_cookies_visible_to_origin() { #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_origin_non_cookie_types_do_not_clear_cookies() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-origin-noncookie".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-origin-noncookie"), + ); ctx.process_async(json!({ "id": 12_520, @@ -2022,7 +2052,10 @@ async fn storage_clear_data_for_origin_non_cookie_types_do_not_clear_cookies() { #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_origin_clears_local_storage_area() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-origin-local-storage".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-origin-local-storage"), + ); let origin = Url::parse("https://app.example.com/app") .unwrap() @@ -2044,7 +2077,8 @@ async fn storage_clear_data_for_origin_clears_local_storage_area() { ); { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.web_storage_store_for_test().lock(); + let store_handle = bc.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&first_party_key, "local", "1")); assert!(store.set_item(&partitioned_origin_top_a, "local", "1a")); assert!(store.set_item(&partitioned_origin_top_b, "local", "1b")); @@ -2064,7 +2098,8 @@ async fn storage_clear_data_for_origin_clears_local_storage_area() { ctx.expect_result(12_525, json!({}), None); let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.web_storage_store_for_test().lock(); + let store_handle = bc.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert_eq!(store.len(&first_party_key), 0); assert_eq!(store.get_item(&first_party_key, "local"), None); assert_eq!(store.get_item(&partitioned_origin_top_a, "local"), None); @@ -2082,7 +2117,10 @@ async fn storage_clear_data_for_origin_clears_local_storage_area() { #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_storage_key_clears_site_data_for_matching_origin() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-storage-key-clear".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-storage-key-clear"), + ); let origin = Url::parse("https://storage-key-clear.example/app") .unwrap() @@ -2096,13 +2134,15 @@ async fn storage_clear_data_for_storage_key_clears_site_data_for_matching_origin let sibling_storage_key = first_party_storage_key_for_origin(&sibling_origin); { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.web_storage_store_for_test().lock(); + let store_handle = bc.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&storage_key, "local", "1")); assert!(store.set_item(&sibling_storage_key, "local", "2")); } { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.storage_bucket_store_for_test().lock(); + let store_handle = bc.storage_bucket_store_for_test(); + let mut store = store_handle.lock(); store .open_bucket(&storage_key, "bucket-a") .expect("origin bucket should open"); @@ -2123,7 +2163,7 @@ async fn storage_clear_data_for_storage_key_clears_site_data_for_matching_origin seed_indexed_db_usage(bc.indexed_db_manager_for_test(), &sibling_bucket_key); assert!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &bucket_a_key, ) .expect("bucket-a usage should be readable") @@ -2144,20 +2184,22 @@ async fn storage_clear_data_for_storage_key_clears_site_data_for_matching_origin let bc = ctx.conn.browser_context.as_ref().unwrap(); { - let mut store = bc.web_storage_store_for_test().lock(); + let store_handle = bc.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert_eq!(store.get_item(&storage_key, "local"), None); assert_eq!( store.get_item(&sibling_storage_key, "local"), Some("2".to_owned()) ); } - let store = bc.storage_bucket_store_for_test().lock(); + let store_handle = bc.storage_bucket_store_for_test(); + let store = store_handle.lock(); assert_eq!(store.keys(&storage_key), Vec::::new()); assert_eq!(store.keys(&sibling_storage_key), vec!["bucket-b"]); drop(store); assert_eq!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &bucket_a_key ) .expect("bucket-a usage should be readable"), @@ -2165,7 +2207,7 @@ async fn storage_clear_data_for_storage_key_clears_site_data_for_matching_origin ); assert!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &sibling_bucket_key, ) .expect("sibling bucket usage should be readable") @@ -2176,9 +2218,10 @@ async fn storage_clear_data_for_storage_key_clears_site_data_for_matching_origin #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_partitioned_storage_key_clears_exact_partition_only() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new( - "BID-storage-key-partitioned-clear".into(), - )); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-storage-key-partitioned-clear"), + ); let origin = "https://partitioned-storage-key.example"; let first_party_key = first_party_storage_key_for_origin(origin); @@ -2190,14 +2233,16 @@ async fn storage_clear_data_for_partitioned_storage_key_clears_exact_partition_o ); { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.web_storage_store_for_test().lock(); + let store_handle = bc.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&first_party_key, "local", "must-stay")); assert!(store.set_item(&partitioned_key, "local", "must-clear")); assert!(store.set_item(&other_partitioned_key, "local", "other-must-stay")); } { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.storage_bucket_store_for_test().lock(); + let store_handle = bc.storage_bucket_store_for_test(); + let mut store = store_handle.lock(); store .open_bucket(&partitioned_key, "bucket") .expect("partitioned bucket should open"); @@ -2233,7 +2278,8 @@ async fn storage_clear_data_for_partitioned_storage_key_clears_exact_partition_o let bc = ctx.conn.browser_context.as_ref().unwrap(); { - let mut store = bc.web_storage_store_for_test().lock(); + let store_handle = bc.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert_eq!( store.get_item(&first_party_key, "local"), Some("must-stay".to_owned()) @@ -2245,13 +2291,14 @@ async fn storage_clear_data_for_partitioned_storage_key_clears_exact_partition_o ); } { - let store = bc.storage_bucket_store_for_test().lock(); + let store_handle = bc.storage_bucket_store_for_test(); + let store = store_handle.lock(); assert_eq!(store.keys(&partitioned_key), Vec::::new()); assert_eq!(store.keys(&other_partitioned_key), vec!["other-bucket"]); } assert_eq!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &partitioned_key ) .expect("partitioned IndexedDB usage should be readable"), @@ -2259,7 +2306,7 @@ async fn storage_clear_data_for_partitioned_storage_key_clears_exact_partition_o ); assert!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &other_partitioned_key ) .expect("other partitioned IndexedDB usage should be readable") @@ -2267,7 +2314,7 @@ async fn storage_clear_data_for_partitioned_storage_key_clears_exact_partition_o ); assert_eq!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &bucket_key ) .expect("partitioned bucket IndexedDB usage should be readable"), @@ -2275,7 +2322,7 @@ async fn storage_clear_data_for_partitioned_storage_key_clears_exact_partition_o ); assert!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &other_bucket_key ) .expect("other partitioned bucket IndexedDB usage should be readable") @@ -2286,10 +2333,10 @@ async fn storage_clear_data_for_partitioned_storage_key_clears_exact_partition_o #[tokio::test(flavor = "multi_thread")] async fn storage_get_usage_and_quota_reports_local_storage_usage_for_origin() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new_with_page_for_test( - "BID-usage-local", - "TID-usage-local", - )); + ctx.conn.browser_context = Some( + ctx.conn + .new_page_target_fixture_for_test("BID-usage-local", "TID-usage-local"), + ); let origin = Url::parse("https://usage.example/app") .unwrap() @@ -2304,11 +2351,13 @@ async fn storage_get_usage_and_quota_reports_local_storage_usage_for_origin() { ); { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.web_storage_store_for_test().lock(); + let store_handle = bc.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&first_party_key, "local", "abc")); assert!(store.set_item(&partitioned_origin, "partitioned", "de")); assert!(store.set_item(&partitioned_sibling, "sibling", "ignored")); - let mut session_store = bc.session_storage_store_for_test().lock(); + let session_store_handle = bc.session_storage_store_for_test(); + let mut session_store = session_store_handle.lock(); assert!(session_store.set_item(&first_party_key, "session", "session-only")); assert!(session_store.set_item(&partitioned_origin, "session", "also-ignored")); } @@ -2345,7 +2394,9 @@ async fn storage_get_usage_and_quota_reports_local_storage_usage_for_origin() { #[tokio::test(flavor = "multi_thread")] async fn storage_get_usage_and_quota_reports_indexed_db_usage_for_origin() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-usage-indexeddb".into()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-usage-indexeddb"); browser_context.set_active_target_id("TID-usage-indexeddb"); ctx.conn .install_browser_context_fixture_for_test(browser_context); @@ -2423,7 +2474,10 @@ async fn storage_get_usage_and_quota_reports_indexed_db_usage_for_origin() { #[tokio::test(flavor = "multi_thread")] async fn storage_get_usage_and_quota_reports_storage_bucket_usage_for_origin() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-usage-storage-buckets".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-usage-storage-buckets"), + ); let indexed_db_manager = ctx .conn .browser_context @@ -2564,12 +2618,12 @@ async fn storage_get_usage_and_quota_reports_storage_bucket_usage_for_origin() { let bucket_usage = usage_breakdown_value(&result, "storage_buckets"); let bc = ctx.conn.browser_context.as_ref().unwrap(); let bucket_a_usage = moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &bucket_a_key, ) .expect("bucket-a usage should be readable"); let bucket_b_usage = moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &bucket_b_key, ) .expect("bucket-b usage should be readable"); @@ -2594,18 +2648,23 @@ async fn storage_get_usage_and_quota_targets_command_session_browser_context() { .ascii_serialization(); let storage_key = first_party_storage_key_for_origin(&origin); - let mut active = BrowserContext::new_with_page_for_test("BID-usage-active", "TID-usage-active"); + let mut active = ctx + .conn + .new_page_target_fixture_for_test("BID-usage-active", "TID-usage-active"); active.attach_active_session("SID-usage-active"); { - let mut store = active.web_storage_store_for_test().lock(); + let store_handle = active.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&storage_key, "local", "aa")); } - let mut inactive = - BrowserContext::new_with_page_for_test("BID-usage-inactive", "TID-usage-inactive"); + let mut inactive = ctx + .conn + .new_page_target_fixture_for_test("BID-usage-inactive", "TID-usage-inactive"); inactive.attach_active_session("SID-usage-inactive"); { - let mut store = inactive.web_storage_store_for_test().lock(); + let store_handle = inactive.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&storage_key, "local", "bbbb")); } @@ -2635,7 +2694,10 @@ async fn storage_get_usage_and_quota_targets_command_session_browser_context() { #[tokio::test(flavor = "multi_thread")] async fn storage_override_quota_for_origin_affects_get_usage_and_quota() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-quota-override".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-quota-override"), + ); ctx.process_async(json!({ "id": 12_531, @@ -2692,10 +2754,13 @@ async fn storage_override_quota_for_origin_affects_get_usage_and_quota() { async fn storage_override_quota_for_origin_targets_command_session_browser_context() { let mut ctx = TestContext::new(); - let mut active = BrowserContext::new_with_page_for_test("BID-quota-active", "TID-quota-active"); + let mut active = ctx + .conn + .new_page_target_fixture_for_test("BID-quota-active", "TID-quota-active"); active.attach_active_session("SID-quota-active"); - let mut inactive = - BrowserContext::new_with_page_for_test("BID-quota-inactive", "TID-quota-inactive"); + let mut inactive = ctx + .conn + .new_page_target_fixture_for_test("BID-quota-inactive", "TID-quota-inactive"); inactive.attach_active_session("SID-quota-inactive"); ctx.conn.install_browser_context_fixture_for_test(active); @@ -2755,7 +2820,10 @@ async fn storage_override_quota_for_origin_targets_command_session_browser_conte #[tokio::test(flavor = "multi_thread")] async fn storage_get_usage_and_quota_rejects_invalid_origin() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-usage-invalid".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-usage-invalid"), + ); ctx.process_async(json!({ "id": 12_530, @@ -2772,7 +2840,10 @@ async fn storage_get_usage_and_quota_rejects_invalid_origin() { #[tokio::test(flavor = "multi_thread")] async fn storage_get_usage_and_quota_rejects_opaque_origin() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-usage-opaque".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-usage-opaque"), + ); ctx.process_async(json!({ "id": 12_538, @@ -2789,7 +2860,10 @@ async fn storage_get_usage_and_quota_rejects_opaque_origin() { #[tokio::test(flavor = "multi_thread")] async fn storage_override_quota_for_origin_rejects_invalid_params() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-quota-invalid".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-quota-invalid"), + ); ctx.process_async(json!({ "id": 12_539, @@ -2834,23 +2908,24 @@ async fn storage_clear_data_for_origin_targets_command_session_browser_context() .ascii_serialization(); let storage_key = first_party_storage_key_for_origin(&origin); - let mut active = BrowserContext::new_with_page_for_test( - "BID-session-clear-active", - "TID-session-clear-active", - ); + let mut active = ctx + .conn + .new_page_target_fixture_for_test("BID-session-clear-active", "TID-session-clear-active"); active.attach_active_session("SID-session-clear-active"); { - let mut store = active.web_storage_store_for_test().lock(); + let store_handle = active.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&storage_key, "local", "active")); } - let mut inactive = BrowserContext::new_with_page_for_test( + let mut inactive = ctx.conn.new_page_target_fixture_for_test( "BID-session-clear-inactive", "TID-session-clear-inactive", ); inactive.attach_active_session("SID-session-clear-inactive"); { - let mut store = inactive.web_storage_store_for_test().lock(); + let store_handle = inactive.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&storage_key, "local", "inactive")); } @@ -2870,29 +2945,31 @@ async fn storage_clear_data_for_origin_targets_command_session_browser_context() .await; ctx.expect_result(12_527, json!({}), Some("SID-session-clear-inactive")); - let mut active_store = ctx + let active_store_handle = ctx .conn .browser_context .as_ref() .unwrap() - .web_storage_store_for_test() - .lock(); + .web_storage_store_for_test(); + let mut active_store = active_store_handle.lock(); assert_eq!( active_store.get_item(&storage_key, "local"), Some("active".to_owned()) ); drop(active_store); - let mut inactive_store = ctx.conn.inactive_browser_contexts[0] - .web_storage_store_for_test() - .lock(); + let inactive_store_handle = ctx.conn.inactive_browser_contexts[0].web_storage_store_for_test(); + let mut inactive_store = inactive_store_handle.lock(); assert_eq!(inactive_store.get_item(&storage_key, "local"), None); } #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_origin_clears_cookies_and_local_storage_together() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-origin-all".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-origin-all"), + ); let origin = Url::parse("https://app.example.com/app") .unwrap() @@ -2901,7 +2978,8 @@ async fn storage_clear_data_for_origin_clears_cookies_and_local_storage_together let storage_key = first_party_storage_key_for_origin(&origin); { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.web_storage_store_for_test().lock(); + let store_handle = bc.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert!(store.set_item(&storage_key, "local", "1")); } @@ -2931,7 +3009,8 @@ async fn storage_clear_data_for_origin_clears_cookies_and_local_storage_together { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.web_storage_store_for_test().lock(); + let store_handle = bc.web_storage_store_for_test(); + let mut store = store_handle.lock(); assert_eq!(store.len(&storage_key), 0); assert_eq!(store.get_item(&storage_key, "local"), None); } @@ -2960,7 +3039,9 @@ async fn storage_clear_data_for_origin_clears_cookies_and_local_storage_together #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_origin_clears_indexed_db_backend() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-origin-indexeddb".into()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-origin-indexeddb"); browser_context.set_active_target_id("TID-origin-indexeddb"); ctx.conn .install_browser_context_fixture_for_test(browser_context); @@ -3074,7 +3155,10 @@ async fn storage_clear_data_for_origin_clears_indexed_db_backend() { #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_origin_clears_storage_bucket_names_for_origin() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-origin-storage-buckets".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-origin-storage-buckets"), + ); let origin = Url::parse("https://bucket-clear.example/app") .unwrap() @@ -3088,7 +3172,8 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_names_for_origin() let sibling_storage_key = first_party_storage_key_for_origin(&sibling_origin); { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let mut store = bc.storage_bucket_store_for_test().lock(); + let store_handle = bc.storage_bucket_store_for_test(); + let mut store = store_handle.lock(); store .open_bucket(&storage_key, "bucket-a") .expect("bucket-a should open"); @@ -3112,7 +3197,8 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_names_for_origin() ctx.expect_result(12_530, json!({}), None); { let bc = ctx.conn.browser_context.as_ref().unwrap(); - let store = bc.storage_bucket_store_for_test().lock(); + let store_handle = bc.storage_bucket_store_for_test(); + let store = store_handle.lock(); assert_eq!(store.keys(&storage_key), vec!["bucket-a", "bucket-b"]); assert_eq!(store.keys(&sibling_storage_key), vec!["bucket-c"]); } @@ -3129,7 +3215,8 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_names_for_origin() ctx.expect_result(12_531, json!({}), None); let bc = ctx.conn.browser_context.as_ref().unwrap(); - let store = bc.storage_bucket_store_for_test().lock(); + let store_handle = bc.storage_bucket_store_for_test(); + let store = store_handle.lock(); assert_eq!(store.keys(&storage_key), Vec::::new()); assert_eq!(store.keys(&sibling_storage_key), vec!["bucket-c"]); } @@ -3137,7 +3224,10 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_names_for_origin() #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_origin_clears_storage_bucket_indexeddb_for_origin() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-origin-storage-bucket-idb".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-origin-storage-bucket-idb"), + ); let origin = Url::parse("https://bucket-idb-clear.example/app") .unwrap() @@ -3152,7 +3242,8 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_indexeddb_for_origi { let bc = ctx.conn.browser_context.as_ref().unwrap(); { - let mut store = bc.storage_bucket_store_for_test().lock(); + let store_handle = bc.storage_bucket_store_for_test(); + let mut store = store_handle.lock(); store .open_bucket(&storage_key, "bucket-a") .expect("bucket-a should open"); @@ -3172,7 +3263,7 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_indexeddb_for_origi seed_indexed_db_usage(bc.indexed_db_manager_for_test(), &sibling_bucket_key); assert!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &bucket_a_key, ) .expect("bucket-a usage should be readable") @@ -3180,7 +3271,7 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_indexeddb_for_origi ); assert!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &bucket_b_key, ) .expect("bucket-b usage should be readable") @@ -3234,13 +3325,14 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_indexeddb_for_origi ctx.expect_result(12_532, json!({}), None); let bc = ctx.conn.browser_context.as_ref().unwrap(); - let store = bc.storage_bucket_store_for_test().lock(); + let store_handle = bc.storage_bucket_store_for_test(); + let store = store_handle.lock(); assert_eq!(store.keys(&storage_key), Vec::::new()); assert_eq!(store.keys(&sibling_storage_key), vec!["bucket-c"]); drop(store); assert_eq!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &bucket_a_key ) .expect("bucket-a usage should be readable"), @@ -3248,7 +3340,7 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_indexeddb_for_origi ); assert_eq!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &bucket_b_key ) .expect("bucket-b usage should be readable"), @@ -3256,13 +3348,14 @@ async fn storage_clear_data_for_origin_clears_storage_bucket_indexeddb_for_origi ); assert!( moli_core::storage::indexed_db_origin_usage_bytes( - bc.indexed_db_manager_for_test(), + &bc.indexed_db_manager_for_test(), &sibling_bucket_key, ) .expect("sibling bucket usage should be readable") > 0 ); - let storage_service = bc.storage_bucket_store_for_test().lock().storage_service(); + let store_handle = bc.storage_bucket_store_for_test(); + let storage_service = store_handle.lock().storage_service(); assert_eq!( storage_service .opfs_usage(&bucket_a_identity.locator()) @@ -3315,7 +3408,7 @@ async fn storage_clear_data_for_origin_clears_http_cache_entries_for_origin() { assert_eq!(app_hits.load(Ordering::SeqCst), 1); assert_eq!(other_hits.load(Ordering::SeqCst), 1); - let mut ctx = TestContext::from_conn(crate::conn::CdpConnection::new_with_fetch_config( + let mut ctx = TestContext::from_conn(crate::test_support::connection_with_fetch_config( fetch_config.clone(), )); let browser_context = ctx.conn.new_browser_context("BID-cache-clear".into()); @@ -3387,7 +3480,7 @@ async fn storage_clear_data_for_origin_uses_browser_context_http_cache_owner() { let mut clear_config = moli_fetch::FetchConfig::default(); clear_config.set_http_cache_dir(None); - let mut ctx = TestContext::from_conn(crate::conn::CdpConnection::new_with_fetch_config( + let mut ctx = TestContext::from_conn(crate::test_support::connection_with_fetch_config( clear_config, )); let browser_context = BrowserContext::new_with_storage_partition_and_http_cache( @@ -3434,7 +3527,10 @@ async fn storage_clear_data_for_origin_uses_browser_context_http_cache_owner() { #[tokio::test(flavor = "multi_thread")] async fn storage_clear_data_for_origin_rejects_invalid_origin_even_for_noop_types() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-origin-invalid".into())); + ctx.conn.browser_context = Some( + ctx.conn + .new_browser_context_fixture_for_test("BID-origin-invalid"), + ); ctx.process_async(json!({ "id": 12_530, @@ -3451,7 +3547,7 @@ async fn storage_clear_data_for_origin_rejects_invalid_origin_even_for_noop_type #[tokio::test(flavor = "multi_thread")] async fn storage_set_cookies_reports_missing_cookie_url_when_no_default_scope_exists() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-MISS".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-MISS")); ctx.process_async(json!({ "id": 126, @@ -3486,7 +3582,7 @@ async fn storage_set_cookies_reports_missing_cookie_url_when_no_default_scope_ex #[tokio::test(flavor = "multi_thread")] async fn storage_delete_cookies_respects_optional_path_filter() { let mut ctx = TestContext::new(); - ctx.conn.browser_context = Some(BrowserContext::new("BID-D".into())); + ctx.conn.browser_context = Some(ctx.conn.new_browser_context_fixture_for_test("BID-D")); ctx.process_async(json!({ "id": 20, diff --git a/moli-protocol/src/domains/system_info.rs b/moli-protocol/src/domains/system_info.rs index 37d93ffbd4..0d78e31fa1 100644 --- a/moli-protocol/src/domains/system_info.rs +++ b/moli-protocol/src/domains/system_info.rs @@ -55,7 +55,6 @@ fn get_process_info(cmd: &Cmd<'_>) -> CommandOutputPlan { mod tests { use serde_json::json; - use crate::conn::BrowserContext; use crate::testing::TestContext; #[tokio::test(flavor = "multi_thread")] @@ -87,7 +86,9 @@ mod tests { // Chromium source: // content/browser/devtools/protocol/system_info_handler.cc let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-system-info".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-system-info".to_owned()); browser_context.set_active_target_id("TID-system-info".to_owned()); browser_context.attach_active_session("SID-frame".to_owned()); ctx.conn @@ -128,7 +129,9 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn system_info_get_process_info_rejects_frame_target_session() { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-system-info".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-system-info".to_owned()); browser_context.set_active_target_id("TID-system-info".to_owned()); browser_context.attach_active_session("SID-frame".to_owned()); ctx.conn diff --git a/moli-protocol/src/domains/target.rs b/moli-protocol/src/domains/target.rs index a5dca94d2d..e6e67ab0af 100644 --- a/moli-protocol/src/domains/target.rs +++ b/moli-protocol/src/domains/target.rs @@ -164,12 +164,8 @@ pub(in crate::domains) fn set_service_worker_pause_on_start_owner( fn sync_dedicated_worker_pause_on_start_for_devtools(conn: &CdpConnection) { let pause = conn.dedicated_worker_pause_on_start_for_devtools(); - let runtimes = conn - .browser_contexts() - .map(BrowserContext::renderer_runtime) - .collect::>(); - for runtime in runtimes { - runtime.set_dedicated_worker_pause_on_start_for_devtools(pause); + for context in conn.browser_contexts() { + context.set_dedicated_worker_pause_on_start(pause); } } @@ -267,7 +263,7 @@ enum PendingTargetCommandKind { command: DevToolsCloseTargetCommand, }, DisposeBrowserContext { - browser_context_id: String, + disposal: Result, }, SendMessageToTarget { message: String, @@ -311,7 +307,7 @@ enum CompletedTargetCommandKind { command: DevToolsCloseTargetCommand, }, DisposeBrowserContext { - browser_context_id: String, + disposal: Result, }, SendMessageToTarget { message: String, @@ -366,8 +362,8 @@ impl PendingTargetCommandDispatch { PendingTargetCommandKind::CloseTarget { command } => { CompletedTargetCommandKind::CloseTarget { command } } - PendingTargetCommandKind::DisposeBrowserContext { browser_context_id } => { - CompletedTargetCommandKind::DisposeBrowserContext { browser_context_id } + PendingTargetCommandKind::DisposeBrowserContext { disposal } => { + CompletedTargetCommandKind::DisposeBrowserContext { disposal } } PendingTargetCommandKind::SendMessageToTarget { message, @@ -441,9 +437,9 @@ pub(crate) fn try_start_target_command_dispatch( Some(attachment::start_detach_from_target_command(cmd)) } Some(TargetAction::CloseTarget) => Some(closing::start_close_target_command(conn, cmd)), - Some(TargetAction::DisposeBrowserContext) => { - Some(browser_context::start_dispose_browser_context_command(cmd)) - } + Some(TargetAction::DisposeBrowserContext) => Some( + browser_context::start_dispose_browser_context_command(conn, cmd), + ), Some(TargetAction::SendMessageToTarget) => { Some(attachment::start_send_message_to_target_command(cmd)) } @@ -801,11 +797,11 @@ pub(crate) async fn complete_pending_target_command( closing::complete_close_target_command_async(conn, command, command_context).await, ); } - CompletedTargetCommandKind::DisposeBrowserContext { browser_context_id } => { + CompletedTargetCommandKind::DisposeBrowserContext { disposal } => { return TargetCommandTaskStep::Complete( browser_context::complete_dispose_browser_context_command_async( conn, - browser_context_id, + disposal, command_context, ) .await, @@ -887,12 +883,12 @@ fn pending_close_target_command( fn pending_dispose_browser_context_command( command_id: Option, session_id: Option<&str>, - browser_context_id: String, + disposal: Result, ) -> TargetCommandTaskStep { TargetCommandTaskStep::Pending(PendingTargetCommandDispatch { command_id, session_id: session_id.map(str::to_owned), - kind: Box::new(PendingTargetCommandKind::DisposeBrowserContext { browser_context_id }), + kind: Box::new(PendingTargetCommandKind::DisposeBrowserContext { disposal }), }) } @@ -1078,7 +1074,7 @@ mod devtools_runtime_entry_tests { #[tokio::test] async fn devtools_target_entry_routes_create_target_to_initial_document_lifecycle_work() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let step = start_devtools_target_command( &mut conn, Some(41), @@ -1125,7 +1121,7 @@ mod devtools_runtime_entry_tests { #[tokio::test] async fn attach_to_target_completion_plan_preserves_typed_attached_sidecar() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let plan = attachment::complete_attach_to_target_command_async( &mut conn, TargetAttachSessionCommit::direct( @@ -1181,8 +1177,9 @@ mod devtools_runtime_entry_tests { #[tokio::test] async fn devtools_target_close_drains_runtime_ready_events_without_serializing_them() { - let mut conn = CdpConnection::new(); - let mut browser_context = BrowserContext::new("BID-runtime-ready-close".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = + conn.new_browser_context_fixture_for_test("BID-runtime-ready-close".to_owned()); browser_context.set_active_target_id("TID-runtime-ready-close"); browser_context.attach_active_session("SID-runtime-ready-close"); assert!(browser_context.assign_attached_session_to_target( @@ -1190,14 +1187,11 @@ mod devtools_runtime_entry_tests { "SID-runtime-ready-close-attached".to_owned(), )); conn.install_browser_context_fixture_for_test(browser_context); - let page = conn - .load_page_via_runtime_async("data:text/html,

runtime ready close

") - .await - .expect("page should load"); - conn.browser_context - .as_mut() - .expect("browser context") - .replace_active_page_for_test(Some(page)); + conn.install_navigation_fixture_for_session_owner_for_test( + "data:text/html,

runtime ready close

", + None, + ) + .await; conn.register_pending_inspector_await(7101, Some("SID-runtime-ready-close")); assert!( conn.claim_pending_inspector_await_for_scheduler_deferred_reply( @@ -1284,7 +1278,7 @@ mod devtools_runtime_entry_tests { #[test] fn immediate_create_target_staging_does_not_emit_target_created_before_initial_document() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); conn.set_root_target_discovery_enabled(true); let (result, protocol_events) = execute_immediate_devtools_target_command_with_protocol_events( @@ -1319,7 +1313,7 @@ mod devtools_runtime_entry_tests { #[test] fn devtools_target_entry_routes_close_target_to_pending_command() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let step = start_devtools_target_command( &mut conn, Some(42), @@ -1345,7 +1339,7 @@ mod devtools_runtime_entry_tests { #[test] fn devtools_target_entry_routes_activate_target_to_pending_command() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let step = start_devtools_target_command( &mut conn, Some(43), @@ -1371,7 +1365,7 @@ mod devtools_runtime_entry_tests { #[tokio::test] async fn devtools_target_entry_routes_get_targets_to_shared_result() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let create_step = start_devtools_target_command( &mut conn, Some(40), @@ -1409,7 +1403,7 @@ mod devtools_runtime_entry_tests { #[tokio::test] async fn devtools_target_entry_routes_get_client_windows_to_shared_result() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let first_create_step = start_devtools_target_command( &mut conn, Some(46), @@ -1466,7 +1460,7 @@ mod devtools_runtime_entry_tests { #[test] fn devtools_target_entry_routes_get_target_info_to_shared_result() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let step = start_devtools_target_command( &mut conn, Some(45), diff --git a/moli-protocol/src/domains/target/browser_context.rs b/moli-protocol/src/domains/target/browser_context.rs index 35ebebec79..cd200d090e 100644 --- a/moli-protocol/src/domains/target/browser_context.rs +++ b/moli-protocol/src/domains/target/browser_context.rs @@ -221,7 +221,9 @@ pub(in crate::domains) fn devtools_client_window_info_for_target( .as_ref() .map(|context| context.id.as_str()); for browser_context in conn.browser_contexts() { - if let Some(surface) = browser_context.target_window_surface(target_id.as_str()) { + if let Some(handle) = browser_context.web_contents_handle_for_target(target_id.as_str()) + && let Ok(surface) = browser_context.web_contents_window_surface(handle) + { return Some(devtools_client_window_info_from_surface( target_id.clone(), active_browser_context_id == Some(browser_context.id.as_str()) @@ -342,6 +344,14 @@ pub(super) async fn execute_devtools_remove_browser_context_command_async( ); } + let disposal = match super::browser_context_disposal::BrowserContextDisposal::prepare( + conn, + &browser_context_id, + ) { + Ok(disposal) => disposal, + Err(error) => return (Err(error), Vec::new()), + }; + let mut protocol_events = Vec::new(); if should_emit_internal_lifecycle { protocol_events.extend(target_destroyed_automation_events_for_browser_context( @@ -353,7 +363,7 @@ pub(super) async fn execute_devtools_remove_browser_context_command_async( let mut command_context = crate::conn::CommandDispatchContext::default(); if let Err(error) = super::browser_context_disposal::execute_browser_context_disposal_async( conn, - browser_context_id, + disposal, &mut side_effects, &mut command_context, ) @@ -545,7 +555,10 @@ struct DisposeBcParams { browser_context_id: Option, } -pub(super) fn start_dispose_browser_context_command(cmd: &Cmd<'_>) -> TargetCommandTaskStep { +pub(super) fn start_dispose_browser_context_command( + conn: &CdpConnection, + cmd: &Cmd<'_>, +) -> TargetCommandTaskStep { let params: DisposeBcParams = match cmd.get_params() { Ok(Some(p)) => p, _ => { @@ -558,18 +571,24 @@ pub(super) fn start_dispose_browser_context_command(cmd: &Cmd<'_>) -> TargetComm return super::target_command_error(-32602, "InvalidParams"); } }; - pending_dispose_browser_context_command(cmd.id, cmd.session_id, wanted_id) + let disposal = + super::browser_context_disposal::BrowserContextDisposal::prepare(conn, &wanted_id); + pending_dispose_browser_context_command(cmd.id, cmd.session_id, disposal) } pub(super) async fn complete_dispose_browser_context_command_async( conn: &mut CdpConnection, - wanted_id: String, + disposal: Result, command_context: &mut crate::conn::CommandDispatchContext, ) -> CommandOutputPlan { + let disposal = match disposal { + Ok(disposal) => disposal, + Err(error) => return CommandOutputPlan::from_devtools_error(error), + }; let mut side_effects = events::TargetProtocolSideEffects::default(); match super::browser_context_disposal::execute_browser_context_disposal_async( conn, - wanted_id, + disposal, &mut side_effects, command_context, ) diff --git a/moli-protocol/src/domains/target/browser_context_disposal.rs b/moli-protocol/src/domains/target/browser_context_disposal.rs index 1ceea2e9f9..2a4b9cf3b7 100644 --- a/moli-protocol/src/domains/target/browser_context_disposal.rs +++ b/moli-protocol/src/domains/target/browser_context_disposal.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use crate::conn::{CdpConnection, CommandDispatchContext, PreparedTargetHostClosure}; use crate::devtools_runtime::{DevToolsError, DevToolsErrorKind, DevToolsTargetKind}; +use moli_core::browser::BrowserContextId; use super::{events, worker_target}; @@ -10,19 +11,24 @@ const INSPECTOR_DETACHED_REASON: &str = "Render process gone."; struct PageTargetDisposal { target_id: String, + web_contents: moli_core::browser::WebContentsHandle, fetch_owner_session_id: Option>, host_closure: PreparedTargetHostClosure, } -struct BrowserContextDisposal { +pub(super) struct BrowserContextDisposal { browser_context_id: String, + context: BrowserContextId, page_targets: Vec, inspector_session_ids: Vec, pending_inspector_session_owners: Vec>, } impl BrowserContextDisposal { - fn prepare(conn: &CdpConnection, browser_context_id: &str) -> Result { + pub(super) fn prepare( + conn: &CdpConnection, + browser_context_id: &str, + ) -> Result { let Some(browser_context) = conn.browser_context_by_id(browser_context_id) else { return Err(browser_context_not_found(browser_context_id)); }; @@ -56,6 +62,9 @@ impl BrowserContextDisposal { PageTargetDisposal { fetch_owner_session_id, host_closure: conn.prepare_target_host_closure(&target_id), + web_contents: browser_context + .web_contents_handle_for_target(&target_id) + .expect("prepared Page target must retain its exact WebContents"), target_id, } }) @@ -86,6 +95,7 @@ impl BrowserContextDisposal { Ok(Self { browser_context_id: browser_context_id.to_owned(), + context: browser_context.browser_context_id(), page_targets, inspector_session_ids, pending_inspector_session_owners, @@ -122,23 +132,15 @@ fn page_fetch_owner_session_id( pub(super) async fn execute_browser_context_disposal_async( conn: &mut CdpConnection, - browser_context_id: String, + disposal: BrowserContextDisposal, out: &mut events::TargetProtocolSideEffects, command_context: &mut CommandDispatchContext, ) -> Result<(), DevToolsError> { - if !conn.has_browser_context_id(&browser_context_id) { - return Err(browser_context_not_found(&browser_context_id)); + let restore_browser_context = conn.active_browser_context_id(); + if !conn.activate_browser_context_by_browser_id(disposal.context) { + return Err(browser_context_not_found(&disposal.browser_context_id)); } - let restore_browser_context_id = super::previously_active_browser_context_id(conn); - if !conn - .activate_browser_context_by_id_async(&browser_context_id) - .await - { - return Err(browser_context_not_found(&browser_context_id)); - } - let disposal = BrowserContextDisposal::prepare(conn, &browser_context_id)?; - fail_target_pending_work(conn, &disposal, out, command_context).await; out.extend_background_events(command_context.take_protocol_events()); @@ -154,6 +156,7 @@ pub(super) async fn execute_browser_context_disposal_async( worker_target::close_browser_context_worker_targets_for_dispose_async( conn, &disposal.browser_context_id, + disposal.context, DISPOSE_REASON, ) .await, @@ -163,16 +166,22 @@ pub(super) async fn execute_browser_context_disposal_async( close_page_target(conn, out, page_target).await; } - let removed = conn - .remove_browser_context_by_id_restoring_active_async( - &disposal.browser_context_id, - restore_browser_context_id.as_deref(), - ) - .await; - if let Some(mut removed) = removed { - removed.close_all_pages_async().await; + let removed = + conn.remove_browser_context_restoring_active(disposal.context, restore_browser_context); + let Some(mut removed) = removed else { + return Err(browser_context_not_found(&disposal.browser_context_id)); + }; + removed.close_all_pages_async().await; + match removed.remove_from_browser() { + Ok(true) => {} + Ok(false) => return Err(browser_context_not_found(&disposal.browser_context_id)), + Err(error) => { + return Err(DevToolsError::new( + DevToolsErrorKind::Internal, + format!("failed to remove BrowserContext: {error}"), + )); + } } - conn.release_idle_navigation_engine_memory_after_target_close(); Ok(()) } @@ -245,24 +254,14 @@ async fn close_page_target( page_target: PageTargetDisposal, ) { let target_id = page_target.target_id; - let is_active_target = conn - .browser_context - .as_ref() - .is_some_and(|browser_context| browser_context.is_active_target(&target_id)); - let closed = if is_active_target { - conn.close_active_page_target_for_target_close_async( - out.background_events_mut(), - DISPOSE_REASON, - ) - .await - } else { - conn.close_background_page_target_for_target_close_async( + let closed = conn + .close_web_contents_for_target_close_async( &target_id, + page_target.web_contents, out.background_events_mut(), DISPOSE_REASON, ) - .await - }; + .await; let Some(closed) = closed else { tracing::warn!( target_id, diff --git a/moli-protocol/src/domains/target/closing.rs b/moli-protocol/src/domains/target/closing.rs index cdc911548b..81a8f908c3 100644 --- a/moli-protocol/src/domains/target/closing.rs +++ b/moli-protocol/src/domains/target/closing.rs @@ -156,6 +156,9 @@ async fn close_target_inner_async( { return Ok(DevToolsCloseTargetResult { success: true }); } + let web_contents = conn + .browser_web_contents_for_target(&target_id) + .map_err(|message| DevToolsError::new(DevToolsErrorKind::NoSuchTarget, message))?; if conn.browser_context.as_ref().is_some_and(|bc| { !matches!( bc.active_target_identity(), @@ -191,6 +194,7 @@ async fn close_target_inner_async( renderer_output_predecessor, owner_scope, target_id, + web_contents, ) .await; return Ok(DevToolsCloseTargetResult { success: true }); @@ -244,6 +248,7 @@ async fn close_target_inner_async( renderer_output_predecessor, owner_scope, target_id, + web_contents, ) .await; Ok(DevToolsCloseTargetResult { success: true }) @@ -265,9 +270,13 @@ async fn settle_target_close_after_pending_fetches_async( renderer_output_predecessor: Option, owner_scope: crate::conn::CommandOwnerScope, target_id: String, + web_contents: moli_core::browser::WebContentsHandle, ) { - let action = - crate::domains::page::PageTargetTerminationOwnerAction::new(owner_scope, target_id); + let action = crate::domains::page::PageTargetTerminationOwnerAction::new( + owner_scope, + target_id, + web_contents, + ); if let Some(predecessor) = renderer_output_predecessor { command_context.set_renderer_output_predecessor(predecessor); conn.publish_page_target_termination_owner_action(action); diff --git a/moli-protocol/src/domains/target/creation.rs b/moli-protocol/src/domains/target/creation.rs index eb3785f75a..41e3a0cb84 100644 --- a/moli-protocol/src/domains/target/creation.rs +++ b/moli-protocol/src/domains/target/creation.rs @@ -326,6 +326,7 @@ pub(super) fn execute_devtools_create_target_command( let activation = activating_created_target .then(|| TargetActivationTransition::new(target_id.clone(), previous_active_target_id)); let initial_empty_document_url = create_target_initial_empty_document_url(&command.url); + let browser_cache_disabled = conn.browser_global_overrides.cache_disabled; { let bc = conn.browser_context.as_mut().unwrap(); if creating_background_target { @@ -353,6 +354,7 @@ pub(super) fn execute_devtools_create_target_command( bc.begin_active_target_initial_empty_document(initial_empty_document_url.clone()); bc.set_target_crash_state(&target_id, false); } + bc.set_base_cache_disabled_for_target(&target_id, browser_cache_disabled); if command.url != initial_empty_document_url { // Chromium gives Target.createTarget(url) one initial // auto_toplevel history entry for url. The implementation still diff --git a/moli-protocol/src/domains/target/popup.rs b/moli-protocol/src/domains/target/popup.rs index 0c6f53aa8c..df072f63fd 100644 --- a/moli-protocol/src/domains/target/popup.rs +++ b/moli-protocol/src/domains/target/popup.rs @@ -192,15 +192,21 @@ pub(crate) async fn create_popup_target_from_renderer_output_background_events_a session_storage_store, initial_empty_document_storage_key, ); + let popup_handle = browser_context + .web_contents_handle_for_target(&target_id) + .expect("staged popup must own WebContents"); if let Some(opener) = opener { - browser_context.remember_target_opener( - &target_id, - opener.target_id, - opener.frame_id, - can_access_opener, - ); + let opener_handle = browser_context.web_contents_handle_for_target(&opener.target_id); + browser_context + .set_web_contents_opener(popup_handle, opener_handle, can_access_opener) + .expect("staged popup and resolved opener must remain live"); + browser_context.set_target_opener_frame_attribution(&target_id, opener.frame_id); } - browser_context.remember_target_window_name(&target_name, &target_id); + let window_name = + BrowserContext::reusable_window_open_target_name(&target_name).map(str::to_owned); + browser_context + .set_web_contents_window_name(popup_handle, window_name) + .expect("staged popup must remain live"); browser_context.remember_target_popup_id(popup_id, &target_id); } diff --git a/moli-protocol/src/domains/target/tests/test_support.rs b/moli-protocol/src/domains/target/tests/test_support.rs index af5ab833b0..714f90b70e 100644 --- a/moli-protocol/src/domains/target/tests/test_support.rs +++ b/moli-protocol/src/domains/target/tests/test_support.rs @@ -5,12 +5,13 @@ use moli_shared_worker::SharedWorkerInstanceId; // Helper: quickly set up a browser context with a given id, optional // target_id, optional session_id, and optional url. pub(super) fn load_bc(ctx: &mut TestContext, bc_id: &str) { - ctx.conn - .install_browser_context_fixture_for_test(BrowserContext::new(bc_id.into())); + ctx.conn.install_browser_context_fixture_for_test( + ctx.conn.new_browser_context_fixture_for_test(bc_id), + ); } pub(super) fn load_bc_with_target(ctx: &mut TestContext, bc_id: &str, target_id: &str) { - let mut bc = BrowserContext::new(bc_id.into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test(bc_id); bc.set_active_target_id(target_id); ctx.conn.install_browser_context_fixture_for_test(bc); } @@ -202,34 +203,11 @@ pub(super) async fn load_bc_with_titled_page_async( // client setup explicitly in the harness instead of relying on createTarget // to emit discovery events unconditionally. enable_root_target_discovery_for_test(ctx); - let mut bc = BrowserContext::new(bc_id.into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test(bc_id); bc.set_active_target_id(target_id); ctx.conn.insert_browser_context(bc); - let page = ctx - .conn - .load_page_via_runtime_async(&format!("data:text/html,{html}")) - .await - .expect("page should load"); - let renderer_page = crate::conn::RendererPageResidenceIdentity::from_page(&page); - let url = page.final_url().as_str().to_owned(); - let bc = ctx - .conn - .browser_context - .as_mut() - .expect("loaded Target fixture must retain its BrowserContext owner"); - bc.set_target_url(url); - let _ = bc.replace_active_page_for_test(Some(page)); - // Even this lightweight Target-domain fixture owns a real renderer Page - // with a concrete output stream. Bind that stream before any later test - // turn consumes its queued `Opened`/publication records; resolving the - // owner from the then-current Page would be wrong after navigation has - // replaced this initial fixture. - let page_owner = ctx - .conn - .target_page_residence_identity_for_session(None) - .expect("loaded Target fixture must have an exact Page owner"); - ctx.conn - .bind_renderer_page_output_owner(renderer_page, page_owner); + ctx.install_quiet_navigation_fixture_for_session_owner(&format!("data:text/html,{html}"), None) + .await; } pub(super) fn enable_root_target_discovery_for_test(ctx: &mut TestContext) { diff --git a/moli-protocol/src/domains/target/tests/tests_background_staging.rs b/moli-protocol/src/domains/target/tests/tests_background_staging.rs index 6f9d326083..265bdeeac0 100644 --- a/moli-protocol/src/domains/target/tests/tests_background_staging.rs +++ b/moli-protocol/src/domains/target/tests/tests_background_staging.rs @@ -1422,19 +1422,15 @@ async fn same_context_background_session_can_clear_its_own_emulated_media_before "background": true, "browserContextId": "BID-9-PRE-MEDIA-CLEAR", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - assert_eq!(created["method"], "Target.targetCreated"); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - let attached = ctx.take_one(); - assert_eq!(attached["method"], "Target.attachedToTarget"); + let second_target_id = take_created_target_id(&mut ctx, 10419443); + let attached = ctx.take_first_matching("second target attachment", |message| { + message["method"] == json!("Target.attachedToTarget") + && message["params"]["targetInfo"]["targetId"] == json!(second_target_id) + }); let second_session_id = attached["params"]["sessionId"] .as_str() .expect("second target session id") .to_owned(); - ctx.expect_result(10419443, json!({ "targetId": second_target_id }), None); ctx.process_async(json!({ "id": 10419444, @@ -5421,11 +5417,13 @@ async fn same_context_named_popup_reuse_navigates_and_activates_loaded_owner() { ) .await; ctx.enable_background_navigation_scheduler_for_test(); - ctx.conn - .browser_context - .as_mut() - .expect("browser context") - .remember_target_window_name("reportWindow", &owner.target_id); + let browser_context = ctx.conn.browser_context.as_mut().expect("browser context"); + let handle = browser_context + .web_contents_handle_for_target(&owner.target_id) + .unwrap(); + browser_context + .set_web_contents_window_name(handle, Some("reportWindow".into())) + .unwrap(); ctx.process_async(json!({ "id": 1041949446, diff --git a/moli-protocol/src/domains/target/tests/tests_browser_context.rs b/moli-protocol/src/domains/target/tests/tests_browser_context.rs index 23d9d3acc1..199a5c559f 100644 --- a/moli-protocol/src/domains/target/tests/tests_browser_context.rs +++ b/moli-protocol/src/domains/target/tests/tests_browser_context.rs @@ -6,7 +6,7 @@ async fn get_browser_contexts_returns_ids() { let mut ctx = TestContext::new(); load_bc(&mut ctx, "BID-X"); ctx.conn - .insert_browser_context(BrowserContext::new("BID-Y".into())); + .insert_browser_context(ctx.conn.new_browser_context_fixture_for_test("BID-Y")); ctx.process_async(json!({"id": 5, "method": "Target.getBrowserContexts"})) .await; ctx.expect_result(5, json!({ "browserContextIds": ["BID-X", "BID-Y"] }), None); @@ -106,7 +106,7 @@ async fn dispose_browser_context_success() { assert!(ctx.conn.browser_context.is_none()); assert_eq!( ctx.conn.download_policy_for_browser_context(Some("BID-20")), - &moli_core::browser::DownloadPolicy::default() + moli_core::browser::DownloadPolicy::default() ); assert!( !ctx.conn @@ -118,7 +118,7 @@ async fn dispose_browser_context_success() { #[tokio::test(flavor = "multi_thread")] async fn dispose_browser_context_emits_detached_events_for_attached_target() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-20".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-20"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-000000000A"); bc.active_page_target_mut().devtools_sessions[moli_page_types::DevToolsSessionKey::Primary] @@ -790,7 +790,7 @@ async fn dispose_browser_context_aborts_paused_request_stage_navigation() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); bc.active_page_target_mut().devtools_sessions[moli_page_types::DevToolsSessionKey::Primary] @@ -873,7 +873,9 @@ async fn dispose_browser_context_aborts_root_session_navigation_without_target_s }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-root-dispose".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-root-dispose"); bc.set_active_target_id("TID-root-dispose"); bc.active_page_target_mut() .runtime_slot @@ -963,7 +965,7 @@ async fn dispose_browser_context_aborts_paused_runtime_fetch_subresource() { let page_url = format!("http://{addr}/page"); let data_url = format!("http://{addr}/data"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/browser_target.rs b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/browser_target.rs index 65421d14d3..64c241d7c7 100644 --- a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/browser_target.rs +++ b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/browser_target.rs @@ -430,11 +430,14 @@ async fn rust_cdp_capability_target_browser_context_proxy_and_enumeration() { .is_some_and(|ids| ids.iter().any(|id| id == &json!(browser_context_id))), "{contexts}" ); + let policy = ctx + .conn + .browser_context + .as_ref() + .map(BrowserContext::network_policy) + .expect("default BrowserContext"); assert_eq!( - ctx.conn - .browser_context - .as_ref() - .and_then(|context| context.network_policy().http_proxy.as_deref()), + policy.http_proxy.as_deref(), Some("http://proxy.example:8080") ); } diff --git a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_contexts.rs b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_contexts.rs index 988a2c29e2..df57afb644 100644 --- a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_contexts.rs +++ b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_contexts.rs @@ -165,10 +165,13 @@ async fn rust_cdp_chromium_target_create_browser_context_proxy_server_is_recorde .expect("browserContextId") .to_owned(); + let policy = ctx + .conn + .browser_context_by_id(&browser_context_id) + .map(BrowserContext::network_policy) + .expect("created BrowserContext"); assert_eq!( - ctx.conn - .browser_context_by_id(&browser_context_id) - .and_then(|context| context.network_policy().http_proxy.as_deref()), + policy.http_proxy.as_deref(), Some("http://proxy.example:8080") ); } @@ -193,10 +196,13 @@ async fn rust_cdp_chromium_target_create_browser_context_proxy_bypass_normalizes .expect("browserContextId") .to_owned(); + let policy = ctx + .conn + .browser_context_by_id(&browser_context_id) + .map(BrowserContext::network_policy) + .expect("created BrowserContext"); assert_eq!( - ctx.conn - .browser_context_by_id(&browser_context_id) - .and_then(|context| context.network_policy().http_no_proxy.as_deref()), + policy.http_no_proxy.as_deref(), Some("localhost,.example.com") ); } @@ -304,7 +310,7 @@ async fn rust_cdp_chromium_target_dispose_context_clears_context_scoped_download assert_eq!( ctx.conn .download_policy_for_browser_context(Some(&browser_context_id)), - &moli_core::browser::DownloadPolicy::default() + moli_core::browser::DownloadPolicy::default() ); assert!( !ctx.conn diff --git a/moli-protocol/src/domains/target/tests/tests_cdp_smoke_playwright.rs b/moli-protocol/src/domains/target/tests/tests_cdp_smoke_playwright.rs index 3c920ed946..d615ce1c2d 100644 --- a/moli-protocol/src/domains/target/tests/tests_cdp_smoke_playwright.rs +++ b/moli-protocol/src/domains/target/tests/tests_cdp_smoke_playwright.rs @@ -918,7 +918,7 @@ async fn queued_popup_navigation_rechecks_a_late_debugger_barrier() { .conn .browser_context_by_id(&browser_context_id) .and_then(|browser_context| browser_context.target_document_url(&popup_target_id)) - .map(|page| page.as_str()) + .map(|page| page.to_string()) .expect("popup initial Page"); assert_eq!(page_url, "about:blank"); } diff --git a/moli-protocol/src/domains/target/tests/tests_control_plane.rs b/moli-protocol/src/domains/target/tests/tests_control_plane.rs index dc4d8014b0..fabf8bcc3e 100644 --- a/moli-protocol/src/domains/target/tests/tests_control_plane.rs +++ b/moli-protocol/src/domains/target/tests/tests_control_plane.rs @@ -633,7 +633,7 @@ async fn activate_target_then_attach_can_navigate_on_activated_target_without_lo async fn get_target_info_for_inactive_target_keeps_previously_active_context() { let mut ctx = TestContext::new(); load_bc_with_target(&mut ctx, "BID-A", "TID-A"); - let mut inactive = BrowserContext::new_with_page_for_test("BID-B", "TID-B"); + let mut inactive = ctx.conn.new_page_target_fixture_for_test("BID-B", "TID-B"); inactive.set_active_target_id("TID-B"); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); @@ -670,7 +670,7 @@ async fn get_target_info_for_inactive_target_keeps_previously_active_context() { async fn send_message_to_target_restores_previously_active_context() { let mut ctx = TestContext::new(); load_bc_with_target(&mut ctx, "BID-A", "TID-A"); - let mut inactive = BrowserContext::new_with_page_for_test("BID-B", "TID-B"); + let mut inactive = ctx.conn.new_page_target_fixture_for_test("BID-B", "TID-B"); inactive.attach_active_session("SID-B"); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); @@ -700,7 +700,7 @@ async fn send_message_to_target_restores_previously_active_context() { async fn detach_from_target_error_restores_previously_active_context() { let mut ctx = TestContext::new(); load_bc_with_target(&mut ctx, "BID-A", "TID-A"); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = ctx.conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-B"); inactive.attach_active_session("SID-B"); ctx.conn @@ -728,7 +728,7 @@ async fn detach_from_target_error_restores_previously_active_context() { async fn detach_from_inactive_context_cleans_exact_session_without_activating_context() { let mut ctx = TestContext::new(); load_bc_with_target(&mut ctx, "BID-A", "TID-A"); - let mut inactive = BrowserContext::new_with_page_for_test("BID-B", "TID-B"); + let mut inactive = ctx.conn.new_page_target_fixture_for_test("BID-B", "TID-B"); inactive.attach_active_session("SID-B"); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); @@ -858,7 +858,9 @@ async fn detach_from_target_neutrally_resumes_paused_request_stage_navigation() assert!(!bc.has_active_session()); assert_eq!(bc.active_target_id(), Some(target_id.as_str())); assert_eq!( - bc.target_document_url(&target_id).map(url::Url::as_str), + bc.target_document_url(&target_id) + .as_ref() + .map(url::Url::as_str), Some(url.as_str()) ); assert!( @@ -1036,9 +1038,9 @@ async fn production_default_target_auto_attach_exposes_initial_about_blank_page( assert_eq!(bc.active_target_id(), Some(ctx.conn.default_target_id())); assert!(bc.has_active_session()); let loaded_page = bc - .loaded_page() + .loaded_document_url_for_test() .expect("default auto-attached target should install initial about:blank page"); - assert_eq!(loaded_page.final_url().as_str(), "about:blank"); + assert_eq!(loaded_page.as_str(), "about:blank"); } #[tokio::test(flavor = "multi_thread")] @@ -1422,16 +1424,14 @@ async fn set_auto_attach_prefers_existing_background_target_with_background_load None, ) .await; - assert!( - ctx.conn - .browser_context - .as_mut() - .unwrap() - .select_page_target_async("TID-000000000E") - .await - .expect("restoring the original active target should succeed"), - "the original active target should remain background during fixture setup" - ); + let handle = ctx + .conn + .browser_web_contents_for_target("TID-000000000E") + .expect("the original active target should remain background during fixture setup"); + ctx.conn + .select_browser_web_contents_async(handle) + .await + .expect("restoring the original active target should succeed"); ctx.process_async(json!({ "id": 17023, @@ -1749,7 +1749,7 @@ async fn set_auto_attach_false_detaches_existing_background_targets() { async fn set_auto_attach_restores_previously_active_context_after_sweeping_contexts() { let mut ctx = TestContext::new(); load_bc_with_target(&mut ctx, "BID-A", "TID-A"); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = ctx.conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-B"); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); diff --git a/moli-protocol/src/domains/target/tests/tests_playwright_cdp.rs b/moli-protocol/src/domains/target/tests/tests_playwright_cdp.rs index 5f6642ad45..1b310b0c31 100644 --- a/moli-protocol/src/domains/target/tests/tests_playwright_cdp.rs +++ b/moli-protocol/src/domains/target/tests/tests_playwright_cdp.rs @@ -6137,14 +6137,17 @@ async fn playwright_over_cdp_script_execution_disabled_blocks_page_scripts_but_n ) .await; let context = ctx.conn.browser_context.as_mut().unwrap(); - let completion = context - .start_target_page_diagnostics_snapshot(&attached.target_id) + let document = context + .document_handle_for_target(&attached.target_id) + .expect("attached target should have an exact Document"); + let completed = context + .start_document_diagnostics_snapshot(document) .unwrap() .wait() - .await - .unwrap(); - let skipped_script = completion - .page_state() + .await; + let skipped_script = completed + .page_state_for_test() + .expect("diagnostics command should complete") .script_execution .runs() .iter() @@ -6155,9 +6158,8 @@ async fn playwright_over_cdp_script_execution_disabled_blocks_page_scripts_but_n ) }); context - .finish_target_page_diagnostics_snapshot(&attached.target_id, completion) + .finish_document_diagnostics_snapshot(completed) .unwrap(); - let active = ctx .conn .browser_context diff --git a/moli-protocol/src/domains/target/tests/tests_same_context_bindings.rs b/moli-protocol/src/domains/target/tests/tests_same_context_bindings.rs index 4f9bddf2e5..e3fe44b3d0 100644 --- a/moli-protocol/src/domains/target/tests/tests_same_context_bindings.rs +++ b/moli-protocol/src/domains/target/tests/tests_same_context_bindings.rs @@ -32,13 +32,7 @@ async fn same_context_targets_allocate_document_start_script_identifiers_target_ "background": true, "browserContextId": "BID-9-SCRIPT-ID", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - assert_eq!(created["method"], "Target.targetCreated"); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - ctx.expect_result(104195, json!({ "targetId": second_target_id }), None); + let second_target_id = take_created_target_id(&mut ctx, 104195); ctx.process_async(json!({ "id": 104196, @@ -414,13 +408,7 @@ async fn same_context_targets_remove_only_their_own_pre_document_script_identifi "background": true, "browserContextId": "BID-9-SCRIPT-REMOVE", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - assert_eq!(created["method"], "Target.targetCreated"); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - ctx.expect_result(104201, json!({ "targetId": second_target_id }), None); + let second_target_id = take_created_target_id(&mut ctx, 104201); ctx.process_async(json!({ "id": 104202, @@ -576,13 +564,7 @@ async fn same_context_targets_remove_only_their_own_utility_pre_document_script_ "background": true, "browserContextId": "BID-9-UTILITY-SCRIPT-REMOVE", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - assert_eq!(created["method"], "Target.targetCreated"); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - ctx.expect_result(104210, json!({ "targetId": second_target_id }), None); + let second_target_id = take_created_target_id(&mut ctx, 104210); ctx.process_async(json!({ "id": 104211, @@ -759,13 +741,7 @@ async fn same_context_targets_remove_only_their_own_utility_binding_definition_a "background": true, "browserContextId": "BID-9-UTILITY-BINDING-REMOVE", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - assert_eq!(created["method"], "Target.targetCreated"); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - ctx.expect_result(104220, json!({ "targetId": second_target_id }), None); + let second_target_id = take_created_target_id(&mut ctx, 104220); ctx.process_async(json!({ "id": 104221, @@ -955,13 +931,7 @@ async fn same_context_targets_remove_only_their_own_main_world_binding_definitio "background": true, "browserContextId": "BID-9-MAIN-BINDING-REMOVE", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - assert_eq!(created["method"], "Target.targetCreated"); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - ctx.expect_result(104231, json!({ "targetId": second_target_id }), None); + let second_target_id = take_created_target_id(&mut ctx, 104231); ctx.process_async(json!({ "id": 104232, @@ -1157,13 +1127,7 @@ async fn same_context_targets_remove_only_their_own_dual_world_binding_definitio "background": true, "browserContextId": "BID-9-DUAL-BINDING-REMOVE", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - assert_eq!(created["method"], "Target.targetCreated"); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - ctx.expect_result(104243, json!({ "targetId": second_target_id }), None); + let second_target_id = take_created_target_id(&mut ctx, 104243); ctx.process_async(json!({ "id": 104244, diff --git a/moli-protocol/src/domains/target/tests/tests_same_context_overrides.rs b/moli-protocol/src/domains/target/tests/tests_same_context_overrides.rs index 36ee3b7990..a6704518aa 100644 --- a/moli-protocol/src/domains/target/tests/tests_same_context_overrides.rs +++ b/moli-protocol/src/domains/target/tests/tests_same_context_overrides.rs @@ -446,13 +446,7 @@ async fn same_context_targets_restore_their_own_cookie_manager_surface_after_swi "params": {"browserContextId": "BID-9-COOKIE", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - assert_eq!(created["method"], "Target.targetCreated"); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - ctx.expect_result(1041693, json!({ "targetId": second_target_id }), None); + let second_target_id = take_created_target_id(&mut ctx, 1041693); ctx.process_async(json!({ "id": 1041694, @@ -915,9 +909,8 @@ async fn same_context_targets_restore_their_own_loader_overrides_after_switching ); assert!( browser_context - .page_navigation_engine(background.target_id()) + .page_navigation_fetch_config(background.target_id()) .expect("background navigation engine") - .fetch_config() .tls_verify_host(), "the exact background engine must be rebuilt before activation" ); @@ -1102,8 +1095,8 @@ async fn assert_context_proxy_survives_target_selection(close_second: bool) { assert_eq!(context.id, context_id); assert_eq!(context.active_target_id(), Some(targets[0].as_str())); assert_eq!(context.page_target(&targets[1]).is_some(), !close_second); - assert_eq!(ctx.conn.http_proxy(), Some(proxy)); - assert_eq!(ctx.conn.http_no_proxy(), Some("")); + assert_eq!(ctx.conn.http_proxy().as_deref(), Some(proxy)); + assert_eq!(ctx.conn.http_no_proxy().as_deref(), Some("")); let client = ctx .conn .ensure_resource_request_client() diff --git a/moli-protocol/src/domains/target/tests/tests_same_context_state.rs b/moli-protocol/src/domains/target/tests/tests_same_context_state.rs index 93738225d0..102fb904f1 100644 --- a/moli-protocol/src/domains/target/tests/tests_same_context_state.rs +++ b/moli-protocol/src/domains/target/tests/tests_same_context_state.rs @@ -32,12 +32,7 @@ async fn same_context_targets_restore_their_own_script_execution_disabled_after_ "params": {"browserContextId": "BID-9A", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - ctx.expect_result(104171, json!({ "targetId": second_target_id }), None); + let second_target_id = take_created_target_id(&mut ctx, 104171); ctx.process_async(json!({ "id": 104172, diff --git a/moli-protocol/src/domains/target/tests/tests_same_context_target_local.rs b/moli-protocol/src/domains/target/tests/tests_same_context_target_local.rs index fce2e2a5ca..678690a70a 100644 --- a/moli-protocol/src/domains/target/tests/tests_same_context_target_local.rs +++ b/moli-protocol/src/domains/target/tests/tests_same_context_target_local.rs @@ -198,23 +198,11 @@ async fn popup_target_diagnostics_report_distinct_page_vm_document_isolates() { browser_context.attach_active_session("SID-popup-opener"); } - let opener_page = ctx - .conn - .load_page_via_runtime_async( - "data:text/html,openeropener", - ) - .await - .expect("opener page should load"); - let opener_url = opener_page.final_url().as_str().to_owned(); - { - let browser_context = ctx - .conn - .browser_context - .as_mut() - .expect("browser context should remain active"); - browser_context.set_target_url(opener_url); - browser_context.replace_loaded_page(Some(opener_page)); - } + ctx.install_navigation_fixture_for_session_owner( + "data:text/html,openeropener", + Some("SID-popup-opener"), + ) + .await; ctx.sent.clear(); ctx.process_async(json!({ @@ -1837,7 +1825,7 @@ async fn detach_from_target_clears_background_target_session() { "detaching the session should keep the background target itself addressable" ); assert!( - bc.loaded_page().is_some(), + bc.has_loaded_page(), "detaching a background target session should keep the active loaded page active" ); } @@ -1958,7 +1946,7 @@ async fn close_target_aborts_paused_request_stage_navigation() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); bc.active_page_target_mut().devtools_sessions[moli_page_types::DevToolsSessionKey::Primary] @@ -2053,7 +2041,7 @@ async fn close_target_aborts_paused_runtime_fetch_subresource() { let page_url = format!("http://{addr}/page"); let api_url = format!("http://{addr}/api"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2182,7 +2170,7 @@ async fn close_target_aborts_paused_response_stage_runtime_xhr_subresource() { let page_url = format!("http://{addr}/page"); let xhr_url = format!("http://{addr}/xhr"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -2350,7 +2338,7 @@ async fn close_target_aborts_paused_runtime_xhr_auth_subresource() { let page_url = format!("http://{addr}/page"); let protected_url = format!("http://{addr}/protected"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/domains/target/tests/tests_target_attachment.rs b/moli-protocol/src/domains/target/tests/tests_target_attachment.rs index 53bb99c4e0..01ea2733fa 100644 --- a/moli-protocol/src/domains/target/tests/tests_target_attachment.rs +++ b/moli-protocol/src/domains/target/tests/tests_target_attachment.rs @@ -1289,7 +1289,9 @@ async fn session_route_finds_committed_browser_page_and_worker_sessions() { Some("SID-shared-active"), ); - let mut inactive = BrowserContext::new("BID-B".to_owned()); + let mut inactive = ctx + .conn + .new_browser_context_fixture_for_test("BID-B".to_owned()); inactive.set_active_target_id("TID-000000000C".to_owned()); inactive.attach_active_session("SID-inactive"); inactive.register_page_target_fixture( @@ -2410,32 +2412,15 @@ async fn session_cleanup_exception_keeps_peer_alive(primary: bool) { let before = page_renderer_inspector_session_count(&mut ctx, "before failed cleanup").await; // Fail the real resource-owner boundary without terminating this Page or // its peer Inspector. Restoring the live engine makes the same cleanup retryable. - let mut retired_owner = moli_core::runtime::RendererBrowserContextRuntime::new(); - let retired_engine = - moli_core::runtime::NavigationEngine::new_with_fetch_config_and_browser_context_access( - Default::default(), - retired_owner.owner_access(), - moli_page_types::OptionalResourceFetchMask::NONE, - true, - ) + let context = ctx.conn.browser_context.as_ref().unwrap(); + let cleanup_target_id = context.active_target_id_owned().unwrap(); + let handle = context + .web_contents_handle_for_target(&cleanup_target_id) .unwrap(); - retired_owner.shutdown_and_join(); - let cleanup_target_id = ctx + let restore_engine = ctx .conn - .browser_context - .as_ref() - .unwrap() - .active_target_id_owned() + .suspend_navigation_resource_owner_for_test(handle) .unwrap(); - let original_engine = std::mem::replace( - ctx.conn - .browser_context - .as_mut() - .unwrap() - .page_navigation_engine_mut(&cleanup_target_id) - .unwrap(), - retired_engine, - ); ctx.process_async( json!({"id": 7, "method": "Target.detachFromTarget", "params": { @@ -2513,12 +2498,7 @@ async fn session_cleanup_exception_keeps_peer_alive(primary: bool) { json!(true), "failed cleanup retry must preserve later peer policy" ); - *ctx.conn - .browser_context - .as_mut() - .unwrap() - .page_navigation_engine_mut(&cleanup_target_id) - .unwrap() = original_engine; + restore_engine.await; ctx.process_async( json!({"id": 8, "sessionId": "SID-cleanup-peer", "method": "Runtime.evaluate", "params": { "expression": "document.body.textContent", @@ -2536,10 +2516,8 @@ async fn session_cleanup_exception_keeps_peer_alive(primary: bool) { }})).await; ctx.expect_result(73, json!({}), Some("SID-cleanup-peer")); ctx.conn - .browser_context - .as_mut() - .unwrap() .reset_primary_page_session_target_state_async( + "BID-cleanup-exception", "TID-cleanup-exception", "SID-cleanup-exception", ) @@ -2617,11 +2595,16 @@ async fn closed_renderer_session_cleanup(method: &str, params: Value) { // Close the actual receiver before disposal, as the old fail-close tests // did. A cleanup error still is not authority to retire the Browser Page. - ctx.conn + let web_contents = ctx + .conn .browser_context - .as_mut() + .as_ref() .unwrap() - .crash_target_renderer_from_io("TID-closed-cleanup"); + .web_contents_handle_for_target("TID-closed-cleanup") + .expect("target should own WebContents"); + ctx.conn + .crash_browser_web_contents_renderer_from_io(web_contents) + .expect("target WebContents should remain live"); ctx.process_async( json!({"id": 2, "method": "Target.detachFromTarget", "params": { "targetId": "TID-closed-cleanup", "sessionId": "SID-closed-cleanup", @@ -2899,7 +2882,7 @@ async fn detach_from_target_neutrally_resumes_paused_request_stage_navigation() }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); bc.active_page_target_mut() @@ -2967,6 +2950,7 @@ async fn detach_from_target_neutrally_resumes_paused_request_stage_navigation() assert_eq!(bc.active_target_id(), Some("TID-000000000A")); assert_eq!( bc.target_document_url("TID-000000000A") + .as_ref() .map(url::Url::as_str), Some(url.as_str()) ); diff --git a/moli-protocol/src/domains/target/tests/tests_target_basics.rs b/moli-protocol/src/domains/target/tests/tests_target_basics.rs index e20a769aff..39016cd1af 100644 --- a/moli-protocol/src/domains/target/tests/tests_target_basics.rs +++ b/moli-protocol/src/domains/target/tests/tests_target_basics.rs @@ -6,7 +6,7 @@ async fn get_browser_contexts_returns_active_and_inactive_ids() { load_bc(&mut ctx, "BID-A"); ctx.conn .inactive_browser_contexts - .push(BrowserContext::new("BID-B".into())); + .push(ctx.conn.new_browser_context_fixture_for_test("BID-B")); ctx.process_async(json!({"id": 5, "method": "Target.getBrowserContexts"})) .await; @@ -136,7 +136,9 @@ async fn cross_document_navigation_keeps_target_session_and_replaces_page_reside #[tokio::test(flavor = "multi_thread")] async fn close_active_target_fails_only_active_owner_pending_awaits() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-await-owner".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-await-owner"); bc.set_active_target_id("TID-active-await".to_owned()); bc.attach_active_session("SID-active-await".to_owned()); assert!(bc.assign_attached_session_to_target( @@ -253,7 +255,7 @@ async fn create_browser_context_preserves_non_loopback_proxy_bypass_entries() { async fn attach_to_target_selects_inactive_browser_context() { let mut ctx = TestContext::new(); load_bc_with_target(&mut ctx, "BID-A", "TID-A"); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = ctx.conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-B"); ctx.conn .push_inactive_browser_context_fixture_for_test(inactive); @@ -295,7 +297,7 @@ async fn attach_to_target_selects_inactive_browser_context() { async fn attach_to_target_creates_attached_session_and_keeps_target_context_active() { let mut ctx = TestContext::new(); load_bc_with_target(&mut ctx, "BID-A", "TID-A"); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = ctx.conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-B"); inactive.attach_active_session("SID-B"); ctx.conn @@ -361,7 +363,7 @@ async fn dispose_browser_context_aborts_paused_request_stage_navigation() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); bc.active_page_target_mut().devtools_sessions[moli_page_types::DevToolsSessionKey::Primary] @@ -452,7 +454,7 @@ async fn dispose_browser_context_aborts_paused_runtime_fetch_subresource() { let page_url = format!("http://{addr}/page"); let data_url = format!("http://{addr}/data"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -543,8 +545,9 @@ async fn dispose_browser_context_aborts_paused_runtime_fetch_subresource() { async fn create_target_for_inactive_browser_context_keeps_previously_active_context() { let mut ctx = TestContext::new(); load_bc(&mut ctx, "BID-A"); - ctx.conn - .push_inactive_browser_context_fixture_for_test(BrowserContext::new("BID-B".into())); + ctx.conn.push_inactive_browser_context_fixture_for_test( + ctx.conn.new_browser_context_fixture_for_test("BID-B"), + ); ctx.process_async(json!({ "id": 1010, @@ -782,7 +785,7 @@ async fn page_bring_to_front_on_inactive_context_restores_previous_context() { .unwrap() .attach_active_session("SID-active-a"); - let mut inactive = BrowserContext::new("BID-B".into()); + let mut inactive = ctx.conn.new_browser_context_fixture_for_test("BID-B"); inactive.set_active_target_id("TID-active-b".to_owned()); inactive.attach_active_session("SID-active-b".to_owned()); ctx.conn @@ -927,19 +930,15 @@ async fn page_stop_loading_aborts_background_pending_fetch_without_activation() "background": true, "browserContextId": "BID-9", "url": "about:blank#second"} })) .await; - let created = ctx.take_one(); - assert_eq!(created["method"], "Target.targetCreated"); - let second_target_id = created["params"]["targetInfo"]["targetId"] - .as_str() - .expect("second target id") - .to_owned(); - let attached = ctx.take_one(); - assert_eq!(attached["method"], "Target.attachedToTarget"); + let second_target_id = take_created_target_id(&mut ctx, 1034); + let attached = ctx.take_first_matching("background target attachment", |message| { + message["method"] == json!("Target.attachedToTarget") + && message["params"]["targetInfo"]["targetId"] == json!(second_target_id) + }); let session_id = attached["params"]["sessionId"] .as_str() .expect("background session id") .to_owned(); - ctx.expect_result(1034, json!({ "targetId": second_target_id }), None); ctx.process_async(json!({ "id": 1035, @@ -994,8 +993,11 @@ async fn page_stop_loading_aborts_background_pending_fetch_without_activation() let navigation = take_response_by_id(&mut ctx, 1037); assert_eq!(navigation["sessionId"], json!(session_id)); assert_eq!(navigation["error"]["message"], json!("Navigation stopped")); - let failed = ctx.take_one(); - assert_eq!(failed["method"], json!("Network.loadingFailed")); + let failed = ctx.take_first_matching("stopped background navigation failure", |message| { + message["method"] == json!("Network.loadingFailed") + && message["sessionId"] == json!(session_id) + && message["params"]["requestId"] == network_id + }); assert_eq!(failed["sessionId"], json!(session_id)); assert_eq!(failed["params"]["requestId"], network_id); assert_eq!(failed["params"]["errorText"], json!("Navigation stopped")); @@ -1786,7 +1788,7 @@ async fn same_context_targets_do_not_replay_bare_isolated_worlds_after_switching .expect("first target page should initialize"); { let bc = ctx.conn.browser_context.as_mut().expect("browser context"); - let _ = bc.replace_active_page_for_test(Some(first_page)); + let _ = bc.commit_active_navigation_for_test(first_page).await; bc.active_page_target_mut().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] .runtime_session_state 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 5050ac5128..1470380a6f 100644 --- a/moli-protocol/src/domains/target/tests/tests_target_creation.rs +++ b/moli-protocol/src/domains/target/tests/tests_target_creation.rs @@ -1095,16 +1095,8 @@ async fn window_open_hands_off_session_storage_snapshot_and_initial_storage_key( ctx.conn .install_browser_context_fixture_for_test(browser_context); let opener_url = format!("http://{addr}/opener"); - let page = ctx - .conn - .load_page_via_runtime_async(&opener_url) - .await - .expect("opener page should load"); - { - let browser_context = ctx.conn.browser_context.as_mut().unwrap(); - browser_context.set_target_url(page.final_url().as_str().to_owned()); - let _ = browser_context.replace_active_page_for_test(Some(page)); - } + ctx.install_navigation_fixture_for_session_owner(&opener_url, None) + .await; ctx.enable_background_navigation_scheduler_for_test(); tokio::task::LocalSet::new().run_until(async { @@ -2188,7 +2180,7 @@ async fn anchor_left_click_activates_popup_while_initial_navigation_waits_for_de "foreground selection must not wait for initial navigation" ); assert!( - browser_context.target_document_url(browser_context.active_target_id().unwrap()).is_some_and(moli_url::is_about_blank), + browser_context.target_document_url(browser_context.active_target_id().unwrap()).is_some_and(|url| moli_url::is_about_blank(&url)), "waitForDebuggerOnStart should retain the active popup's initial about:blank document" ); @@ -2299,9 +2291,10 @@ async fn anchor_blank_target_with_rel_opener_preserves_exact_opener() { #[tokio::test(flavor = "multi_thread")] async fn create_target_seeds_browser_context_from_connection_cookie_profile() { - let mut ctx = TestContext::from_conn(CdpConnection::new_with_initial_cookies(vec![ - stored_cookie("sid", "seeded"), - ])); + let mut ctx = TestContext::from_conn(crate::test_support::connection_with_config( + crate::CdpInitialStoragePartition::with_cookies(vec![stored_cookie("sid", "seeded")]), + Default::default(), + )); ctx.conn.set_root_target_discovery_enabled(true); ctx.process_async(json!({"id": 11, "method": "Target.createTarget", "params": {"url": "about:blank"}})) @@ -2581,11 +2574,11 @@ async fn tab_auto_attach_does_not_own_browser_level_service_worker_pause() { ctx.expect_result(9034, json!({}), None); assert_eq!(ctx.conn.service_worker_pause_on_start_owner_count(), 0); - assert!(ctx.conn.browser_contexts().all(|browser_context| { - !browser_context - .renderer_runtime() - .service_worker_pause_on_start_for_devtools() - })); + assert!( + ctx.conn + .browser_contexts() + .all(|browser_context| { !browser_context.service_worker_pause_on_start() }) + ); } #[tokio::test(flavor = "multi_thread")] @@ -3057,7 +3050,12 @@ async fn incomplete_popup_rollback_clears_tab_page_sessions_and_target_graph() { page_target_id, "SID-popup-page-attached".to_owned() )); - browser_context.remember_target_window_name("popupName", page_target_id); + let handle = browser_context + .web_contents_handle_for_target(page_target_id) + .unwrap(); + browser_context + .set_web_contents_window_name(handle, Some("popupName".into())) + .unwrap(); browser_context.remember_target_popup_id(Some(42), page_target_id); } ctx.conn diff --git a/moli-protocol/src/domains/target/tests/tests_target_management.rs b/moli-protocol/src/domains/target/tests/tests_target_management.rs index f9e7f49c3d..0eaf31f1ba 100644 --- a/moli-protocol/src/domains/target/tests/tests_target_management.rs +++ b/moli-protocol/src/domains/target/tests/tests_target_management.rs @@ -355,7 +355,7 @@ async fn close_target_emits_detached_events() { #[tokio::test(flavor = "multi_thread")] async fn close_target_without_inspector_enabled_emits_inspector_detached_event() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -383,7 +383,9 @@ async fn close_target_without_inspector_enabled_emits_inspector_detached_event() #[tokio::test(flavor = "multi_thread")] async fn close_target_invalidates_runtime_context_and_object_without_active_page_fallback() { let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-runtime-close".into()); + let mut bc = ctx + .conn + .new_browser_context_fixture_for_test("BID-runtime-close"); bc.set_active_target_id("TID-active"); bc.attach_active_session("SID-active"); bc.register_page_target_url_fixture( @@ -595,7 +597,7 @@ async fn close_target_aborts_paused_request_stage_navigation() { }); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); bc.active_page_target_mut().devtools_sessions[moli_page_types::DevToolsSessionKey::Primary] @@ -690,7 +692,7 @@ async fn close_target_aborts_paused_runtime_fetch_subresource() { let page_url = format!("http://{addr}/page"); let api_url = format!("http://{addr}/api"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -819,7 +821,7 @@ async fn close_target_aborts_paused_response_stage_runtime_xhr_subresource() { let page_url = format!("http://{addr}/page"); let xhr_url = format!("http://{addr}/xhr"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); @@ -987,7 +989,7 @@ async fn close_target_aborts_paused_runtime_xhr_auth_subresource() { let page_url = format!("http://{addr}/page"); let protected_url = format!("http://{addr}/protected"); let mut ctx = TestContext::new(); - let mut bc = BrowserContext::new("BID-9".into()); + let mut bc = ctx.conn.new_browser_context_fixture_for_test("BID-9"); bc.set_active_target_id("TID-000000000A"); bc.attach_active_session("SID-1"); ctx.conn.install_browser_context_fixture_for_test(bc); diff --git a/moli-protocol/src/domains/target/worker_target.rs b/moli-protocol/src/domains/target/worker_target.rs index c6caf61bf0..53915bd392 100644 --- a/moli-protocol/src/domains/target/worker_target.rs +++ b/moli-protocol/src/domains/target/worker_target.rs @@ -10,15 +10,18 @@ //! exact run into a target/session identity before appending output. The //! projection coordinator only validates and consumes the captured value. -use moli_core::page::{ - RendererDedicatedWorkerTargetEvent, RendererDedicatedWorkerTargetInfo, - RendererRuntimeInspectorMessage, RendererServiceWorkerConsoleMessage, - RendererServiceWorkerExceptionMessage, RendererServiceWorkerFetchDiagnostic, - RendererServiceWorkerFetchDiagnosticResult, RendererServiceWorkerRunIdentity, - RendererServiceWorkerTargetEvent, RendererServiceWorkerTargetInfo, - RendererServiceWorkerVersionStatus, RendererSharedWorkerConsoleMessage, - RendererSharedWorkerTargetEvent, RendererSharedWorkerTargetInfo, RuntimeConsoleMessageSnapshot, - SubresourceRequestInitiatorType, +use moli_core::{ + browser::ServiceWorkerCommand, + page::{ + RendererDedicatedWorkerTargetEvent, RendererDedicatedWorkerTargetInfo, + RendererRuntimeInspectorMessage, RendererServiceWorkerConsoleMessage, + RendererServiceWorkerExceptionMessage, RendererServiceWorkerFetchDiagnostic, + RendererServiceWorkerFetchDiagnosticResult, RendererServiceWorkerRunIdentity, + RendererServiceWorkerTargetEvent, RendererServiceWorkerTargetInfo, + RendererServiceWorkerVersionStatus, RendererSharedWorkerConsoleMessage, + RendererSharedWorkerTargetEvent, RendererSharedWorkerTargetInfo, + RuntimeConsoleMessageSnapshot, SubresourceRequestInitiatorType, + }, }; use moli_shared_worker::SharedWorkerInstanceId; use serde_json::json; @@ -1673,9 +1676,7 @@ fn register_service_worker_target_with_active_run( return outputs; }; if should_pause_on_start_for_related_devtools { - context - .renderer_runtime() - .set_service_worker_pause_on_start_for_version_for_devtools(info.version_id, true); + context.set_service_worker_pause_on_start_for_version(info.version_id, true); } context.insert_service_worker_target(ServiceWorkerTargetState::new( info.registration_id, @@ -2260,13 +2261,15 @@ fn remove_service_worker_target_with_reason( pub(super) async fn close_browser_context_worker_targets_for_dispose_async( conn: &mut CdpConnection, browser_context_id: &str, + context: moli_core::browser::BrowserContextId, reason: &'static str, ) -> Vec { - let Some((renderer_runtime, shared_worker_ids, service_worker_ids)) = conn - .browser_context_by_id(browser_context_id) + let Some((context, shared_worker_ids, service_worker_ids)) = conn + .browser_context_by_browser_id(context) + .filter(|candidate| candidate.id == browser_context_id) .map(|context| { ( - context.renderer_runtime(), + context.browser_context_id(), context .shared_worker_targets .keys() @@ -2285,7 +2288,7 @@ pub(super) async fn close_browser_context_worker_targets_for_dispose_async( let mut outputs = TargetPreparedOutputs::default(); for instance_id in shared_worker_ids { - renderer_runtime.close_shared_worker_for_target_close(instance_id); + let _ = conn.close_browser_shared_worker(context, instance_id); outputs.extend(remove_shared_worker_target_with_reason( conn, browser_context_id, @@ -2293,7 +2296,9 @@ pub(super) async fn close_browser_context_worker_targets_for_dispose_async( reason, )); } - if let Err(error) = renderer_runtime.stop_all_service_workers_for_devtools() { + if let Err(error) = + conn.execute_browser_service_worker_command(context, ServiceWorkerCommand::StopAll) + { tracing::warn!( browser_context_id, error, @@ -2335,12 +2340,12 @@ pub(super) async fn close_shared_worker_target_for_target_close_async( target_id: &str, command_context: &mut crate::conn::CommandDispatchContext, ) -> bool { - let Some((browser_context_id, renderer_runtime, instance_id)) = + let Some((browser_context_id, context, instance_id)) = conn.browser_context.as_ref().and_then(|context| { let target = context.shared_worker_target(target_id)?; Some(( context.id.clone(), - context.renderer_runtime(), + context.browser_context_id(), target.renderer_instance_id, )) }) @@ -2348,7 +2353,7 @@ pub(super) async fn close_shared_worker_target_for_target_close_async( return false; }; - renderer_runtime.close_shared_worker_for_target_close(instance_id); + let _ = conn.close_browser_shared_worker(context, instance_id); let outputs = remove_shared_worker_target(conn, &browser_context_id, instance_id); let mut prepared_outputs = ProtocolOutputPayloads::from_slot(TargetPreparedOutputSlot::from_outputs(outputs)); @@ -2367,12 +2372,12 @@ pub(super) async fn close_dedicated_worker_target_for_target_close_async( target_id: &str, command_context: &mut crate::conn::CommandDispatchContext, ) -> bool { - let Some((browser_context_id, renderer_runtime, instance_id)) = + let Some((browser_context_id, context, instance_id)) = conn.browser_context.as_ref().and_then(|context| { let target = context.dedicated_worker_target(target_id)?; Some(( context.id.clone(), - context.renderer_runtime(), + context.browser_context_id(), target.renderer_instance_id, )) }) @@ -2380,7 +2385,7 @@ pub(super) async fn close_dedicated_worker_target_for_target_close_async( return false; }; - renderer_runtime.close_dedicated_worker_for_devtools(instance_id); + let _ = conn.close_browser_dedicated_worker(context, instance_id); let outputs = prepare_dedicated_worker_target_retirement( conn, &browser_context_id, @@ -3631,9 +3636,7 @@ mod worker_target_attachment_tests; #[cfg(test)] mod tests { use super::*; - use crate::conn::{ - BrowserContext, CdpTargetFilter, CdpTargetFilterEntry, DedicatedWorkerTargetState, - }; + use crate::conn::{CdpTargetFilter, CdpTargetFilterEntry, DedicatedWorkerTargetState}; fn runtime_inspector_messages(messages: Vec) -> Vec { messages @@ -3761,8 +3764,8 @@ mod tests { TargetPageResidenceIdentity, RendererPageResidenceIdentity, ) { - let mut conn = CdpConnection::default(); - let mut context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); context.set_active_target_id("TID-page"); let document_id = context.set_active_document_fixture_for_test(1); let owner_page = TargetPageResidenceIdentity::new( @@ -4798,9 +4801,9 @@ mod tests { #[test] fn shared_worker_target_registration_updates_browser_context_and_emits_when_discovered() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let mut outputs = register_shared_worker_target(&mut conn, "BID-1", None, shared_worker_info(7)) @@ -4864,8 +4867,8 @@ mod tests { #[test] fn shared_worker_target_registration_tracks_undiscovered_targets_without_events() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); assert!( register_shared_worker_target(&mut conn, "BID-1", None, shared_worker_info(9)) @@ -4887,9 +4890,9 @@ mod tests { #[test] fn service_worker_target_registration_updates_browser_context_and_emits_when_discovered() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let mut outputs = register_service_worker_target(&mut conn, "BID-1", service_worker_info(7)) @@ -4951,10 +4954,10 @@ mod tests { #[test] fn service_worker_target_registration_auto_attaches_when_enabled() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_root_target_discovery_enabled(true); conn.set_auto_attach_owner(None, true, true, CdpTargetFilter::default_auto_attach()); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let outputs = register_service_worker_target(&mut conn, "BID-1", service_worker_info(9)) .worker_target_lifecycle_outputs; @@ -4988,10 +4991,10 @@ mod tests { #[tokio::test] async fn service_worker_target_lifecycle_drain_preserves_typed_target_sidecars() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_target_discovery_for_owner(None, CdpTargetFilter::default_target_discovery()); conn.set_auto_attach_owner(None, true, true, CdpTargetFilter::default_auto_attach()); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let mut outputs = register_service_worker_target(&mut conn, "BID-1", service_worker_info(11)); @@ -5055,8 +5058,8 @@ mod tests { #[tokio::test] async fn shared_worker_target_lifecycle_drain_routes_discovery_events_to_owner_session() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); conn.set_target_discovery_for_owner( Some("SID-browser"), CdpTargetFilter::default_target_discovery(), @@ -5118,9 +5121,9 @@ mod tests { #[test] fn service_worker_target_registration_respects_auto_attach_filter() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); conn.set_auto_attach_owner(None, true, false, target_filter_excluding("service_worker")); let outputs = register_service_worker_target(&mut conn, "BID-1", service_worker_info(10)) @@ -5142,8 +5145,8 @@ mod tests { #[test] fn service_worker_target_registration_auto_attaches_related_newer_version() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); assert!( register_service_worker_target(&mut conn, "BID-1", service_worker_info(7)).is_empty() ); @@ -5198,10 +5201,10 @@ mod tests { #[test] fn service_worker_auto_attach_related_matching_is_context_scoped_and_preserves_wait() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); conn.inactive_browser_contexts - .push(BrowserContext::new("BID-2".to_owned())); + .push(conn.new_browser_context_fixture_for_test("BID-2".to_owned())); conn.set_service_worker_auto_attach_related_owner( Some("SID-owner"), "BID-1", @@ -5241,8 +5244,8 @@ mod tests { #[test] fn target_creation_projects_starting_only_from_an_exact_live_host() { - let mut conn = CdpConnection::default(); - let mut context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); context.set_active_target_id("TID-page".to_owned()); context.attach_active_session("SID-page".to_owned()); context.set_service_worker_domain_enabled(Some("SID-page"), true); @@ -5284,8 +5287,8 @@ mod tests { #[test] fn service_worker_domain_enabled_session_receives_target_lifecycle_updates() { - let mut conn = CdpConnection::default(); - let mut context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); context.set_active_target_id("TID-page".to_owned()); context.attach_active_session("SID-page".to_owned()); context.set_service_worker_domain_enabled(Some("SID-page"), true); @@ -5391,8 +5394,8 @@ mod tests { #[test] fn service_worker_domain_destroyed_version_keeps_retained_registration() { - let mut conn = CdpConnection::default(); - let mut context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); context.set_active_target_id("TID-page".to_owned()); context.attach_active_session("SID-page".to_owned()); context.set_service_worker_domain_enabled(Some("SID-page"), true); @@ -5433,8 +5436,8 @@ mod tests { #[test] fn service_worker_domain_enabled_session_receives_worker_error_reported() { - let mut conn = CdpConnection::default(); - let mut context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); context.set_active_target_id("TID-page".to_owned()); context.attach_active_session("SID-page".to_owned()); context.set_service_worker_domain_enabled(Some("SID-page"), true); @@ -5475,7 +5478,7 @@ mod tests { #[test] fn service_worker_target_destruction_detaches_session_before_destroyed() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_auto_attach_owner( None, true, @@ -5483,7 +5486,7 @@ mod tests { crate::conn::CdpTargetFilter::default_auto_attach(), ); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let outputs = register_service_worker_target(&mut conn, "BID-1", service_worker_info(11)) .worker_target_lifecycle_outputs; @@ -5527,7 +5530,7 @@ mod tests { #[test] fn service_worker_target_stopped_retains_target_and_clears_runtime_sessions() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_auto_attach_owner( None, true, @@ -5535,7 +5538,7 @@ mod tests { crate::conn::CdpTargetFilter::default_auto_attach(), ); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let outputs = register_service_worker_target(&mut conn, "BID-1", service_worker_info(17)) .worker_target_lifecycle_outputs; @@ -5630,7 +5633,7 @@ mod tests { #[test] fn service_worker_target_started_reloads_after_crash_once() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_auto_attach_owner( None, true, @@ -5638,7 +5641,7 @@ mod tests { crate::conn::CdpTargetFilter::default_auto_attach(), ); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let outputs = register_service_worker_target(&mut conn, "BID-1", service_worker_info(19)) .worker_target_lifecycle_outputs; @@ -5684,7 +5687,7 @@ mod tests { #[tokio::test] async fn service_worker_restart_emits_reload_before_new_runtime_context() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_auto_attach_owner( None, true, @@ -5692,7 +5695,7 @@ mod tests { crate::conn::CdpTargetFilter::default_auto_attach(), ); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let outputs = register_service_worker_target(&mut conn, "BID-1", service_worker_info(29)) .worker_target_lifecycle_outputs; @@ -5845,8 +5848,8 @@ mod tests { #[test] fn service_worker_version_state_updates_without_a_domain_listener() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); assert!( register_service_worker_target(&mut conn, "BID-1", service_worker_info(31)).is_empty(), "an undiscovered target without domain listeners should not manufacture output" @@ -5877,8 +5880,8 @@ mod tests { #[test] fn stale_destroy_cannot_remove_a_restarted_service_worker_version() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let _ = register_service_worker_target(&mut conn, "BID-1", service_worker_info(32)); let old_run = renderer_run(); let old_run_retirement = record_service_worker_target_stopped( @@ -5911,8 +5914,8 @@ mod tests { #[tokio::test] async fn retired_run_output_never_replays_into_the_restarted_worker() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); let mut target = ServiceWorkerTargetState::new( 41, 33, @@ -5989,8 +5992,8 @@ mod tests { #[tokio::test] async fn detached_session_id_reuse_does_not_revive_captured_worker_output() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); let mut target = ServiceWorkerTargetState::new( 41, 34, @@ -6035,14 +6038,14 @@ mod tests { #[test] fn shared_worker_target_registration_auto_attaches_when_enabled() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_auto_attach_owner( None, true, false, crate::conn::CdpTargetFilter::default_auto_attach(), ); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let outputs = register_shared_worker_target(&mut conn, "BID-1", None, shared_worker_info(11)) @@ -6157,9 +6160,9 @@ mod tests { #[test] fn shared_worker_target_registration_respects_auto_attach_filter() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); conn.set_auto_attach_owner(None, true, false, target_filter_excluding("shared_worker")); let outputs = @@ -6181,8 +6184,8 @@ mod tests { #[test] fn shared_worker_runtime_inspector_messages_route_to_tagged_session_only() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); let instance_id = SharedWorkerInstanceId::from_u64(12); let mut target = SharedWorkerTargetState::new( moli_core::RendererOwnerLocalHostId::new_for_testing(1), @@ -6227,8 +6230,8 @@ mod tests { #[test] fn service_worker_console_messages_bind_the_exact_run_and_enabled_sessions() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); let renderer_run = renderer_run(); let mut target = ServiceWorkerTargetState::new( 41, @@ -6300,8 +6303,8 @@ mod tests { #[test] fn service_worker_exception_messages_bind_the_exact_run_and_enabled_sessions() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); let renderer_run = renderer_run(); let mut target = ServiceWorkerTargetState::new( 41, @@ -6406,8 +6409,8 @@ mod tests { #[test] fn service_worker_fetch_diagnostics_bind_the_exact_run_and_enabled_sessions() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); let mut target = ServiceWorkerTargetState::new( 41, 25, @@ -6663,8 +6666,8 @@ mod tests { #[test] fn service_worker_runtime_inspector_messages_bind_and_prepare_the_exact_run() { - let mut conn = CdpConnection::default(); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut conn = crate::test_support::connection(); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); let mut target = ServiceWorkerTargetState::new( 41, 23, @@ -6724,7 +6727,7 @@ mod tests { #[test] fn shared_worker_target_destruction_detaches_session_before_destroyed() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_auto_attach_owner( None, true, @@ -6732,7 +6735,7 @@ mod tests { crate::conn::CdpTargetFilter::default_auto_attach(), ); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let outputs = register_shared_worker_target(&mut conn, "BID-1", None, shared_worker_info(13)) @@ -6774,7 +6777,7 @@ mod tests { #[tokio::test] async fn shared_worker_target_destruction_terminates_all_renderer_calls() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_auto_attach_owner( None, true, @@ -6782,7 +6785,7 @@ mod tests { crate::conn::CdpTargetFilter::default_auto_attach(), ); conn.set_root_target_discovery_enabled(true); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let outputs = register_shared_worker_target(&mut conn, "BID-1", None, shared_worker_info(15)) @@ -6897,7 +6900,7 @@ mod tests { #[test] fn late_shared_worker_messages_after_destroy_are_dropped_without_page_fallback() { - let mut conn = CdpConnection::default(); + let mut conn = crate::test_support::connection(); conn.set_auto_attach_owner( None, true, @@ -6905,7 +6908,7 @@ mod tests { crate::conn::CdpTargetFilter::default_auto_attach(), ); conn.set_root_target_discovery_enabled(true); - let mut browser_context = BrowserContext::new("BID-1".to_owned()); + let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); browser_context.set_active_target_id("TID-page"); browser_context.attach_active_session("SID-page"); conn.install_browser_context_fixture_for_test(browser_context); @@ -6999,8 +7002,8 @@ mod tests { #[tokio::test] async fn shared_worker_target_console_messages_emit_to_target_session_and_advance_cursors() { - let mut conn = CdpConnection::default(); - conn.browser_context = Some(BrowserContext::new("BID-1".to_owned())); + let mut conn = crate::test_support::connection(); + conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); let mut target = SharedWorkerTargetState::new( moli_core::RendererOwnerLocalHostId::new_for_testing(1), SharedWorkerInstanceId::from_u64(17), diff --git a/moli-protocol/src/domains/target/worker_target_attachment_tests.rs b/moli-protocol/src/domains/target/worker_target_attachment_tests.rs index ab0146d4c3..549db883a3 100644 --- a/moli-protocol/src/domains/target/worker_target_attachment_tests.rs +++ b/moli-protocol/src/domains/target/worker_target_attachment_tests.rs @@ -12,7 +12,7 @@ use serde_json::{Value, json}; use super::*; use crate::{ - conn::{BrowserContext, CdpTargetFilter, CommandDispatchContext, CommandOwnerScope}, + conn::{CdpTargetFilter, CommandDispatchContext, CommandOwnerScope}, devtools_runtime::AutomationEvent, domains::activity::{ProtocolOutputPayloads, ProtocolOutputProjectionContext}, }; @@ -93,8 +93,9 @@ fn protocol_messages(events: Vec) -> Vec TestContext { let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-tracing".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-tracing".to_owned()); browser_context.set_active_target_id("TID-tracing".to_owned()); browser_context.set_target_url("https://example.test/page#fragment".to_owned()); browser_context.attach_active_session("SID-owner".to_owned()); diff --git a/moli-protocol/src/domains/webauthn.rs b/moli-protocol/src/domains/webauthn.rs index 02010018fb..7138f1d1d3 100644 --- a/moli-protocol/src/domains/webauthn.rs +++ b/moli-protocol/src/domains/webauthn.rs @@ -13,7 +13,6 @@ pub(crate) fn command_output_plan(cmd: &Cmd<'_>) -> CommandOutputPlan { mod tests { use serde_json::json; - use crate::conn::BrowserContext; use crate::testing::TestContext; #[tokio::test(flavor = "multi_thread")] @@ -21,7 +20,9 @@ mod tests { // Chromium source: // third_party/blink/web_tests/http/tests/inspector-protocol/webauthn/*.js let mut ctx = TestContext::new(); - let mut browser_context = BrowserContext::new("BID-webauthn".to_owned()); + let mut browser_context = ctx + .conn + .new_browser_context_fixture_for_test("BID-webauthn".to_owned()); browser_context.set_active_target_id("TID-webauthn".to_owned()); browser_context.attach_active_session("SID-1".to_owned()); ctx.conn diff --git a/moli-protocol/src/lib.rs b/moli-protocol/src/lib.rs index 5b2cae6514..de19c4779c 100644 --- a/moli-protocol/src/lib.rs +++ b/moli-protocol/src/lib.rs @@ -9,7 +9,7 @@ mod cdp_projection; pub mod conn; pub mod devtools_runtime; pub mod domains; -#[cfg(feature = "test-support")] +#[cfg(any(test, feature = "test-support"))] pub mod test_support; #[cfg(test)] pub mod testing; diff --git a/moli-protocol/src/test_support.rs b/moli-protocol/src/test_support.rs index 7b16474163..764c5d45d0 100644 --- a/moli-protocol/src/test_support.rs +++ b/moli-protocol/src/test_support.rs @@ -14,6 +14,29 @@ use crate::{ conn::{CdpConnection, NavigationId, RendererPageResidenceIdentity}, }; +/// Starts an isolated Browser owner for a protocol test. +pub fn connection() -> CdpConnection { + connection_with_config( + crate::CdpInitialStoragePartition::memory(), + Default::default(), + ) +} + +pub fn connection_with_config( + partition: crate::CdpInitialStoragePartition, + config: moli_core::runtime::NavigationRuntimeConfig, +) -> CdpConnection { + let browser = + moli_core::browser::BrowserService::start().expect("test Browser owner should start"); + CdpConnection::new(browser.handle(), partition, config) +} + +pub fn connection_with_fetch_config(config: moli_fetch::FetchConfig) -> CdpConnection { + let mut runtime_config = moli_core::runtime::NavigationRuntimeConfig::default(); + *runtime_config.fetch_config_mut() = config; + connection_with_config(crate::CdpInitialStoragePartition::memory(), runtime_config) +} + /// Opaque exact-token fixture for scheduler tests that need a real /// target-owned background navigation request. pub struct BackgroundNavigationRequestFixture { diff --git a/moli-protocol/src/testing.rs b/moli-protocol/src/testing.rs index cc6f31f3ca..119dbebe23 100644 --- a/moli-protocol/src/testing.rs +++ b/moli-protocol/src/testing.rs @@ -61,6 +61,89 @@ struct PendingTestRuntimeDeferredReply { renderer_response_permit: Option, } +impl CdpConnection { + /// Builds and commits a test document through the exact Browser owner + /// selected by `session_id`. The returned fence is the last renderer + /// publication that must cross protocol ingress before a later command. + pub(crate) async fn install_navigation_fixture_for_session_owner_for_test( + &mut self, + raw_url: &str, + session_id: Option<&str>, + ) -> Option { + self.commit_declared_session_fixtures_for_test(); + let owner = crate::conn::CommandOwnerScope::capture(self, session_id); + self.install_navigation_fixture_for_owner_for_test(raw_url, &owner) + .await + } + + pub(crate) async fn install_navigation_fixture_for_owner_for_test( + &mut self, + raw_url: &str, + owner: &crate::conn::CommandOwnerScope, + ) -> Option { + let token = self + .start_document_navigation_for_owner(owner, crate::domains::page::LOADER_ID.to_owned()) + .expect("navigation fixture requires a live Browser owner"); + let navigation = self + .load_navigation_via_runtime_for_owner_async(owner, raw_url) + .await + .expect("navigation fixture should load"); + self.install_loaded_navigation_fixture_for_owner_for_test(navigation, owner, token) + .await + } + + async fn install_loaded_navigation_fixture_for_owner_for_test( + &mut self, + navigation: crate::conn::LoadedNavigation, + owner: &crate::conn::CommandOwnerScope, + token: crate::conn::NavigationId, + ) -> Option { + let (_, target_id) = self + .target_owner_identity_for_owner(owner) + .expect("navigation fixture requires an installed browser context"); + let target_id = target_id.expect("navigation fixture requires an exact target"); + let renderer_output_predecessor = navigation.renderer_output_predecessor; + let page_commit = self + .commit_loaded_navigation(navigation.page) + .expect("navigation fixture Page commit must succeed"); + let projection_fence = page_commit + .inspection_projection + .expect("navigation fixture must rebind its renderer inspection endpoint"); + assert!( + page_commit + .committed_document_post_response_continuation + .is_none(), + "lifecycle-target fixture must not retain a DocumentCommit response gate" + ); + page_commit.previous_document_retirement.close().await; + let (binding, _) = self.project_committed_document_lifecycle_for_owner( + owner, + page_commit.lifecycle, + Some(token), + target_id, + crate::domains::page::LOADER_ID.to_owned(), + ); + let binding = + binding.expect("navigation fixture must install its exact renderer Document binding"); + let finished = + self.publish_document_projection_fence_for_owner(owner, &binding, projection_fence); + assert!( + finished.released_output.is_empty(), + "fixture output is ingested after the Document binding" + ); + assert!( + finished.renderer_call_replacements.is_none(), + "fixture must not replace in-flight renderer calls" + ); + assert_eq!( + self.target_root_document_lifecycle_identity_for_owner(owner), + Some(binding.renderer_document_identity()), + "navigation fixture must retain its exact renderer Document binding" + ); + renderer_output_predecessor + } +} + impl PendingTestRuntimeDeferredReply { fn new( pending: PendingCdpCommandDispatch, @@ -132,7 +215,7 @@ fn real_layout_test_runtime_config( } pub(crate) fn real_layout_test_connection() -> CdpConnection { - CdpConnection::new_with_initial_storage_partition_and_runtime_config( + crate::test_support::connection_with_config( CdpInitialStoragePartition::memory(), real_layout_test_runtime_config(OptionalResourceFetchMask::NONE), ) @@ -144,7 +227,7 @@ impl TestContext { /// Historically those tests treated `Target.targetCreated` as a baseline /// event after `Target.createTarget`. Real CDP clients only receive target /// discovery events after enabling discovery, so keep that convenience in - /// `TestContext` instead of changing `CdpConnection::new()`. + /// `TestContext` instead of changing `crate::test_support::connection()`. pub fn new() -> Self { Self::new_with_target_discovery(true) } @@ -162,7 +245,7 @@ impl TestContext { } pub fn new_with_layout_policy(layout_policy: LayoutPolicy) -> Self { - let conn = CdpConnection::new_with_initial_storage_partition_and_runtime_config( + let conn = crate::test_support::connection_with_config( CdpInitialStoragePartition::memory(), NavigationRuntimeConfig::new( FetchConfig::default(), @@ -192,7 +275,7 @@ impl TestContext { target_discovery_enabled: bool, optional_resource_fetch_mask: OptionalResourceFetchMask, ) -> Self { - let mut conn = CdpConnection::new_with_initial_storage_partition_and_runtime_config( + let mut conn = crate::test_support::connection_with_config( CdpInitialStoragePartition::memory(), real_layout_test_runtime_config(optional_resource_fetch_mask), ); @@ -260,18 +343,27 @@ impl TestContext { raw_url: &str, session_id: Option<&str>, ) { - self.conn.commit_declared_session_fixtures_for_test(); - let owner = crate::conn::CommandOwnerScope::capture(&self.conn, session_id); - let token = self + let predecessor = self .conn - .start_document_navigation_for_owner(&owner, crate::domains::page::LOADER_ID.to_owned()) - .expect("navigation fixture requires a live Browser owner"); - let navigation = self - .conn - .load_navigation_via_runtime_for_session_owner_async(session_id, raw_url) - .await - .expect("navigation fixture should load"); - self.install_loaded_navigation_fixture_for_session_owner(navigation, session_id, token) + .install_navigation_fixture_for_session_owner_for_test(raw_url, session_id) + .await; + if let Some(predecessor) = predecessor { + self.route_renderer_output_predecessor_before_command_response(predecessor) + .await; + } + } + + /// Installs a fully routed Document as test setup without exposing that + /// fixture's lifecycle as output of the next command under test. + pub(crate) async fn install_quiet_navigation_fixture_for_session_owner( + &mut self, + raw_url: &str, + session_id: Option<&str>, + ) { + let sent_start = self.sent.len(); + self.install_navigation_fixture_for_session_owner(raw_url, session_id) + .await; + self.fence_and_discard_navigation_fixture_output(sent_start, session_id) .await; } @@ -308,87 +400,12 @@ impl TestContext { ) .await .expect("buffered navigation fixture should build"); - self.install_loaded_navigation_fixture_for_session_owner(navigation, session_id, token) - .await; - } - - async fn install_loaded_navigation_fixture_for_session_owner( - &mut self, - navigation: crate::conn::LoadedNavigation, - session_id: Option<&str>, - token: crate::conn::NavigationId, - ) { let owner = crate::conn::CommandOwnerScope::capture(&self.conn, session_id); - let (_, target_id) = self + let predecessor = self .conn - .target_owner_identity_for_owner(&owner) - .expect("navigation fixture requires an installed browser context"); - let target_id = target_id.expect("navigation fixture requires an exact target"); - let renderer_output_predecessor = navigation.renderer_output_predecessor; - let page_creation_artifacts = navigation.page_creation_artifacts; - let final_url = navigation.final_url; - let main_document_commit = navigation - .main_document_commit - .expect("navigation fixture must retain its frozen Document commit identity"); - let prepared = self - .conn - .start_loaded_document_navigation_for_owner( - &owner, - token, - navigation.page, - crate::conn::DocumentNavigationDestination { - url: final_url, - security_origin: main_document_commit.security_origin.clone(), - secure_context_type: main_document_commit.secure_context_type.clone(), - }, - &page_creation_artifacts, - ) - .expect("navigation fixture must be admitted with matching Page creation artifacts") - .await - .expect("navigation fixture policy must apply"); - let page_commit = self - .conn - .commit_loaded_navigation(prepared) - .expect("navigation fixture Page commit must succeed"); - let projection_fence = page_commit - .inspection_projection - .expect("navigation fixture must rebind its renderer inspection endpoint"); - assert!( - page_commit - .committed_document_post_response_continuation - .is_none(), - "lifecycle-target fixture must not retain a DocumentCommit response gate" - ); - page_commit.previous_document_retirement.close().await; - let (binding, _) = self.conn.project_committed_document_lifecycle_for_owner( - &owner, - page_commit.lifecycle, - Some(token), - target_id, - crate::domains::page::LOADER_ID.to_owned(), - ); - let binding = - binding.expect("navigation fixture must install its exact renderer Document binding"); - let finished = self.conn.publish_document_projection_fence_for_owner( - &owner, - &binding, - projection_fence, - ); - assert!( - finished.released_output.is_empty(), - "fixture output is ingested after the Document binding" - ); - assert!( - finished.renderer_call_replacements.is_none(), - "fixture must not replace in-flight renderer calls" - ); - assert_eq!( - self.conn - .target_root_document_lifecycle_identity_for_owner(&owner,), - Some(binding.renderer_document_identity()), - "navigation fixture must retain its exact renderer Document binding" - ); - if let Some(predecessor) = renderer_output_predecessor { + .install_loaded_navigation_fixture_for_owner_for_test(navigation, &owner, token) + .await; + if let Some(predecessor) = predecessor { // Production does not expose a completed navigation response until // the Page-creation cursor has crossed ordered protocol ingress. // Mirror that boundary here so a later enable command observes @@ -399,6 +416,45 @@ impl TestContext { } } + async fn fence_and_discard_navigation_fixture_output( + &mut self, + sent_start: usize, + session_id: Option<&str>, + ) { + // The build reply may precede the renderer's load continuation. First + // observe load on this exact Document, then fence its published tail; + // a diagnostics reply alone can overtake the load continuation. + let owner = crate::conn::CommandOwnerScope::capture(&self.conn, session_id); + let document = self + .conn + .resolve_browser_document_for_owner(&owner) + .expect("navigation fixture must retain its committed Document"); + self.wait_until_scheduler_state("navigation fixture Document load", |conn| { + assert_eq!( + conn.resolve_browser_document_for_owner(&owner), + Ok(document) + ); + conn.renderer_document_lifecycle_authoritative_state_for_session_owner(session_id) + .is_some_and(|(_, snapshot)| snapshot.load.is_some()) + }) + .await; + let completed = self + .conn + .start_document_diagnostics_snapshot(document) + .expect("navigation fixture Document must accept a diagnostics barrier") + .wait() + .await; + let predecessor = completed.renderer_output_predecessor(); + self.conn + .finish_document_diagnostics_snapshot(completed) + .expect("navigation fixture diagnostics barrier must finish on the same Document"); + if let Some(predecessor) = predecessor { + self.route_renderer_output_predecessor_before_command_response(predecessor) + .await; + } + self.sent.truncate(sent_start); + } + /// Feed a JSON-serialisable message through the async CDP entrypoint and /// route the scheduler work directly requested by that command. Tests /// waiting for later external lifecycle input must use the event-wait @@ -2206,7 +2262,7 @@ mod tests { #[tokio::test] async fn test_context_consumes_selected_task_output_publications_one_per_turn() { - let mut conn = CdpConnection::new(); + let mut conn = crate::test_support::connection(); let (publication_tx, publication_rx) = moli_core::renderer_output_transport_channel(); let (runtime_response_tx, runtime_response_rx) = tokio::sync::mpsc::unbounded_channel(); let (background_event_tx, background_event_rx) = tokio::sync::mpsc::unbounded_channel(); diff --git a/moli-renderer-v8/src/runtime/page.rs b/moli-renderer-v8/src/runtime/page.rs index 98a0b0974e..d1bd5f2357 100644 --- a/moli-renderer-v8/src/runtime/page.rs +++ b/moli-renderer-v8/src/runtime/page.rs @@ -302,13 +302,13 @@ impl JsRuntime { } pub fn document_isolate_model_for_diagnostics(&self) -> &'static str { - "page-vm" + RendererDocumentIsolateAccountingDiagnostics::MODEL } pub fn document_isolate_accounting_for_diagnostics( &self, ) -> RendererDocumentIsolateAccountingDiagnostics { - crate::script_vm::renderer_document_isolate_accounting_diagnostics() + RendererDocumentIsolateAccountingDiagnostics::snapshot() } pub fn renderer_owner_id_for_diagnostics(&self) -> u64 { diff --git a/moli-renderer-v8/src/runtime/page_surface.rs b/moli-renderer-v8/src/runtime/page_surface.rs index c245b17c1a..4b7fabc730 100644 --- a/moli-renderer-v8/src/runtime/page_surface.rs +++ b/moli-renderer-v8/src/runtime/page_surface.rs @@ -1696,6 +1696,15 @@ pub struct RendererDocumentIsolateAccountingDiagnostics { pub reserved: u64, } +impl RendererDocumentIsolateAccountingDiagnostics { + pub const MODEL: &str = "page-vm"; + + /// Process-wide accounting; observing it does not create a renderer owner. + pub fn snapshot() -> Self { + crate::script_vm::renderer_document_isolate_accounting_diagnostics() + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct RendererActivityDiagnostics { pub document_context_count: usize,