refactor(browser): move physical state behind BrowserService handles

Bind document commands, resource work, Worker controls, downloads and retirement to exact native handles. Remove Context-wide mirrors and transfer physical ownership to the independent Core service.
This commit is contained in:
ldm0
2026-09-25 03:52:08 +08:00
parent 291e2e4473
commit be12a40ec3
288 changed files with 22717 additions and 15008 deletions
Generated
+3
View File
@@ -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",
+3 -3
View File
@@ -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),
}
}
+1 -1
View File
@@ -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.
+6 -6
View File
@@ -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!(
+3
View File
@@ -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" }
+795
View File
@@ -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<WeakIndexedDbManager>,
pub storage_bucket_store: Option<SharedStorageBucketStore>,
}
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<Item = StoredCookie>) -> 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<Item = StoredCookie>,
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<Item = StoredCookie>,
) {
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<Item = StoredCookie>,
) {
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<NavigationRuntimeConfig>,
network_policy: ContextNetworkPolicy,
emulation_defaults: ContextEmulationDefaults,
environment_owner: crate::ProcessEnvironmentOwner,
browser_identity_override: Option<BrowserIdentityProfile>,
permission_overrides: PermissionOverrides,
download_policy: Option<DownloadPolicy>,
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<WebContentsId, WebContents>,
selected_web_contents: Option<WebContentsId>,
// Drop Documents/engines before the runtime root and its storage handles.
renderer_output_transport_sender: Option<crate::RendererOutputTransportSender>,
renderer_runtime_owner: Option<RendererBrowserContextRuntimeOwner>,
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<EmulatedNetworkConditions>,
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<EmulatedNetworkConditions>,
) -> 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<PathBuf>,
http_cache_max_bytes: Option<u64>,
) -> 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<WebContentsId> {
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<ClosingWebContents, String> {
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<ClosingWebContents> {
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<String>,
pub http_no_proxy: Option<String>,
pub tls_verify_host: Option<bool>,
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<EmulatedNetworkConditions>,
pub geolocation: Option<EmulatedGeolocationOverrideState>,
pub device_metrics: Option<EmulatedDeviceMetrics>,
}
#[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>{title}</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,<title>first</title>")
.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,<title>replacement</title>")
.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"
);
}
}
@@ -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<bool, String> {
Ok(!self.web_contents(handle)?.javascript_dialogs.is_empty())
}
pub fn install_document_javascript_dialog(
&mut self,
document: DocumentHandle,
dialog: RendererPendingJavaScriptDialog,
) -> Result<JavaScriptDialogKey, String> {
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<JavaScriptDialogSnapshot> {
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<String>,
) -> Option<JavaScriptDialogClosed> {
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);
}
}
}
@@ -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<CompletedPageCommand, String>,
}
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<crate::RendererOutputFence> {
self.completed
.as_ref()
.ok()
.and_then(CompletedPageCommand::renderer_output_predecessor)
}
fn into_parts(self) -> (DocumentHandle, Result<CompletedPageCommand, String>) {
(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<CompletedPageCommand, String>) {
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<crate::RendererOutputFence> {
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<moli_renderer_v8::RendererPageState>> {
self.0
.completed
.as_ref()
.ok()
.map(CompletedPageCommand::page_state)
}
pub fn into_page_completion_for_test(self) -> Result<CompletedPageCommand, String> {
self.0.completed
}
}
define_document_command!(
PendingChildFrameLifecycleWork,
CompletedChildFrameLifecycleWork
);
define_document_command!(
PendingDocumentResourceRuntimeUpdate,
CompletedDocumentResourceRuntimeUpdate
);
impl CompletedTopLevelHistoryTraversal {
pub fn renderer_accepted(&self) -> Option<bool> {
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<moli_cookie_jar::BrowserCookieFacadeOverrides>,
) -> 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<crate::page::RendererRuntimeHeapUsage, String> {
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<Option<crate::page::EmulatedIdleOverride>, 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<serde_json::Value, String> {
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<Option<DocumentHandle>, 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<PendingSetDocumentContent, String> {
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<PendingTopLevelSameDocumentNavigation, String> {
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<PendingCaptureDocumentScreencastFrame, String> {
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<RendererCaptureScreencastFrameReply, String> {
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<PendingCaptureDocumentSnapshot, String> {
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<DocumentSnapshot, String> {
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<PendingCaptureDocumentImage, String> {
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<RendererCaptureScreenshotReply, String> {
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<PendingTopLevelHistoryTraversal, String> {
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<bool, String> {
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<PendingNavigationHistoryReset, String> {
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<bool, String> {
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<PendingChildFrameNavigation, String> {
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<PendingDocumentResourceTextSearch, String> {
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<PendingDocumentResourceTextSearch, String> {
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<RendererResourceTextSearchOutcome, String> {
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<PendingDocumentCspBypassUpdate, String> {
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<Vec<SubresourceNetworkRecord>, 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<Vec<crate::page::ScriptObservableOutputItem>, 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<PendingDocumentLifecycleStop, String> {
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<RendererCommandTurnOutput, String> {
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<PendingDocumentDiagnosticsSnapshot, String> {
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<RendererPageDiagnosticsSnapshot, String> {
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<PendingChildFrameLifecycleWork, String> {
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(())
}
}
@@ -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<String>,
},
ExtraHttpHeaders(moli_fetch::RequestHeaders),
BlockedUrls(Vec<String>),
BypassServiceWorker(bool),
NetworkOffline(bool),
IdleOverride(Option<EmulatedIdleOverride>),
NavigatorOverrides(moli_page_types::NavigatorOverrides),
EmulatedMedia(EmulatedMediaOverrides),
ViewportSurface(Option<ViewportSurface>),
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<CompletedPageCommand, String>,
}
pub struct PendingDocumentPolicyBatch {
context: crate::browser::BrowserContextId,
document: DocumentHandle,
admission_error: Option<String>,
updates: Vec<PendingDocumentPolicyUpdate>,
}
pub struct CompletedDocumentPolicyBatch {
context: crate::browser::BrowserContextId,
document: DocumentHandle,
admission_error: Option<String>,
updates: Vec<CompletedDocumentPolicyUpdate>,
}
pub struct DocumentRuntimePolicyReconciliation {
pub script_execution_disabled: bool,
pub emulated_media: EmulatedMediaOverrides,
pub network_offline: bool,
pub viewport_surface: Option<ViewportSurface>,
}
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<DocumentPolicyUpdate>,
) -> 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<DocumentPolicyUpdate>,
foreground: bool,
navigator_queries: moli_page_types::NavigatorQueryOverrides,
global_network_conditions: Option<crate::browser::EmulatedNetworkConditions>,
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<PendingDocumentPolicyUpdate, String> {
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<crate::browser::EmulatedNetworkConditions>,
global_geolocation: Option<&crate::browser::EmulatedGeolocationOverrideState>,
) -> Result<PendingDocumentPolicyBatch, String> {
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<crate::browser::EmulatedNetworkConditions>,
global_geolocation: Option<&crate::browser::EmulatedGeolocationOverrideState>,
) -> Result<crate::browser::web_contents::PageSurface, String> {
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(),
))
}
}
@@ -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<u16, String> {
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<crate::page::ScriptExecutionReport, String> {
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<moli_renderer_v8::RendererInspectionEndpoint, String> {
Ok(self.document(document)?.page.renderer_inspection_endpoint())
}
pub fn start_document_storage_key_snapshot(
&self,
document: DocumentHandle,
) -> Result<PendingDocumentStorageKeySnapshot, String> {
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<String, String> {
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<PendingChildFrameTreeSnapshot, String> {
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<Vec<ChildFrameTreeSnapshot>, 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<PendingDocumentCookieOwnerSnapshot, String> {
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<DocumentCookieOwnerSnapshot, String> {
let (document, completion) = completed.into_parts();
self.document_mut(document)?
.page
.finish_document_cookie_owner_snapshot(completion?)
.map_err(|error| error.to_string())
}
}
@@ -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<Url>,
method: Option<String>,
body: Option<Option<String>>,
headers: Option<moli_fetch::RequestHeaderOverride>,
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<u16>,
response_headers: Option<Vec<(String, String)>>,
},
DispatchWebSocketText {
socket_id: u64,
data: String,
},
DispatchWebSocketBinary {
socket_id: u64,
data: Vec<u8>,
},
CloseWebSocket {
socket_id: u64,
code: Option<u16>,
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<CompletedPageCommand, String>,
}
pub enum DocumentFetchCommandOutcome {
Continued(PendingSubresourceContinueOutcome),
Complete,
}
impl DocumentFetchCommandOutcome {
pub fn into_continue_outcome(self) -> Result<PendingSubresourceContinueOutcome, String> {
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<RendererOutputFence> {
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<CompletedPageCommand, String>,
) {
(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<SubresourceResourceType>), String> {
Ok(self
.web_contents(web_contents)?
.fetch_subresource_interception())
}
pub fn start_document_fetch_command(
&self,
document: DocumentHandle,
command: DocumentFetchCommand,
) -> Result<PendingDocumentFetchCommand, String> {
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<SubresourceResourceType>,
accept_stale_completion: bool,
) -> Result<Option<PendingDocumentFetchCommand>, 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<SubresourceResourceType>,
) -> 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<DocumentFetchCommandOutcome, String> {
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<DocumentFetchCommandOutcome, String> {
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<DocumentFetchCommandOutcome, String> {
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)
}
}
@@ -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<i32>,
click_count: i32,
delta_x: f64,
delta_y: f64,
pointer: RendererPointerEventProperties,
modifiers: u8,
},
Touch {
points: Vec<RendererTouchPoint>,
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<DocumentLifetimeObserver, String> {
Ok(self.document_mut(document)?.lifetime.observe())
}
pub fn start_document_autofill_trigger(
&self,
document: DocumentHandle,
request: RendererAutofillTriggerRequest,
) -> Result<PendingDocumentAutofillTrigger, String> {
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<RendererAutofillTriggerOutcome, String> {
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<PendingDocumentInputCommand, String> {
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<RendererCommandTurnOutput, String> {
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),
}
}
}
@@ -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<Url>,
pub requested_url: Url,
pub redirected: bool,
pub redirect_count: usize,
}
impl BrowserContext {
pub fn selected_document_handle(&self) -> Option<DocumentHandle> {
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<DocumentNavigationMetadata> {
self.document_navigation_metadata(self.selected_document_handle()?)
.ok()
}
pub fn document_navigation_metadata(
&self,
handle: DocumentHandle,
) -> Result<DocumentNavigationMetadata, String> {
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<bool, String> {
Ok(self
.web_contents(handle)?
.navigation()
.initial_document_build()
.is_some_and(|build| build.completion.pending()))
}
pub fn pending_document(
&self,
handle: WebContentsHandle,
) -> Result<Option<(NavigationId, DocumentId)>, String> {
Ok(self.web_contents(handle)?.navigation().pending_document())
}
pub fn accepts_document_preparation(
&self,
handle: WebContentsHandle,
navigation: NavigationId,
renderer: RendererPageResidenceIdentity,
) -> Result<bool, String> {
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<RetiringDocument, String> {
Ok(RetiringDocument::from_page(
self.web_contents_mut(handle)?.replace_document(None),
))
}
pub fn document_handle(
&self,
handle: WebContentsHandle,
) -> Result<Option<DocumentHandle>, 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<InitialDocumentAdmission, String> {
self.web_contents_mut(handle)?
.start_initial_document_build(inherited)
}
pub fn commit_initial_document(
&mut self,
built: BuiltInitialDocument,
) -> Result<CommittedInitialDocument, Box<BuiltInitialDocument>> {
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<AdmittedDocumentMaterialization, String> {
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<crate::page::PermissionOverrideRegistration>,
) -> Result<
impl std::future::Future<Output = anyhow::Result<PreparedDocumentNavigation>> + 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<crate::runtime::PreparedDocumentPagePolicy, String> {
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<AdmittedNavigationLoad, String> {
self.web_contents_mut(handle)?
.start_navigation_load(navigation, policy, inherited)
}
pub fn commit_document_navigation(
&mut self,
prepared: PreparedDocumentNavigation,
) -> Result<CommittedDocumentNavigation, String> {
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<NavigationInterceptionPermit, String> {
self.web_contents_mut(handle)?
.pause_navigation_request(navigation, request)
}
pub fn take_navigation_request(
&mut self,
permit: NavigationInterceptionPermit,
) -> Option<ClaimedNavigationRequest> {
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<InterceptedNavigationLoad, String> {
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<AdmittedNavigationLoad, String> {
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<moli_fetch::RawResponse>,
) -> Result<NavigationInterceptionPermit, String> {
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<InterceptedNavigationResponse<moli_fetch::RawResponse>> {
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<NavigationInterceptionPermit, String> {
self.web_contents_mut(handle)?
.pause_navigation_response(navigation, transfer)
}
pub fn take_navigation_response(
&mut self,
permit: NavigationInterceptionPermit,
) -> Option<PausedDocumentTransfer> {
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<PausedDocumentTransfer>> {
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<ResolvedHistoryTraversal, String> {
self.web_contents(handle)?
.resolve_history_traversal(destination)
.map_err(str::to_owned)
}
pub fn document_service_worker_client_id(&self, handle: DocumentHandle) -> Result<u64, String> {
Ok(self.document(handle)?.page.service_worker_client_id())
}
pub fn document_renderer_residence(
&self,
handle: DocumentHandle,
) -> Result<RendererPageResidenceIdentity, String> {
Ok(RendererPageResidenceIdentity::from_page(
&self.document(handle)?.page,
))
}
pub fn document_url(&self, handle: DocumentHandle) -> Result<Url, String> {
Ok(self.document(handle)?.page.final_url().clone())
}
pub fn document_title(&self, handle: DocumentHandle) -> Result<String, String> {
Ok(self.document(handle)?.page.document_title())
}
pub fn commit_document_title(
&mut self,
handle: WebContentsHandle,
change: &crate::RendererDocumentTitleChanged,
) -> Result<Option<bool>, String> {
Ok(self.web_contents_mut(handle)?.commit_document_title(change))
}
pub fn document_lifecycle_snapshot(
&self,
handle: DocumentHandle,
) -> Result<Option<crate::page::RendererDocumentLifecycleSnapshot>, 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<bool, String> {
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<Option<String>, 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<crate::browser::web_contents::InitialDocumentCreator>,
storage_key: Option<moli_storage_key::MoliStorageKey>,
) -> 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<Option<moli_storage_key::MoliStorageKey>, 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<Option<bool>, String> {
Ok(self
.web_contents(handle)?
.navigation()
.is_on_initial_empty_document())
}
pub fn initial_document_has_pending_navigation(
&self,
handle: WebContentsHandle,
) -> Result<bool, String> {
Ok(self
.web_contents(handle)?
.navigation()
.initial_empty_document_pending_cross_document_navigation())
}
pub fn navigation_history_snapshot(
&self,
handle: WebContentsHandle,
) -> Result<(usize, Vec<PageNavigationHistoryEntry>), String> {
Ok(self.web_contents(handle)?.navigation_history_snapshot())
}
pub fn navigation_history_entry_url(
&self,
handle: WebContentsHandle,
entry_id: i32,
) -> Result<Option<String>, 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<Option<SameDocumentNavigationCommitted>, 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<NavigationId, String> {
Ok(self.web_contents_mut(handle)?.start_document_navigation())
}
pub fn accepts_pending_navigation(
&self,
handle: WebContentsHandle,
navigation: &NavigationId,
) -> Result<bool, String> {
Ok(self
.web_contents(handle)?
.navigation()
.accepts_pending_document_navigation_event(navigation))
}
pub fn accepts_document_body_completion(
&self,
handle: WebContentsHandle,
navigation: &NavigationId,
) -> Result<bool, String> {
Ok(self
.web_contents(handle)?
.navigation()
.accepts_document_body_completion_event(navigation))
}
pub fn has_pending_document_navigation(
&self,
handle: WebContentsHandle,
) -> Result<bool, String> {
Ok(self
.web_contents(handle)?
.navigation()
.has_pending_document_navigation())
}
pub fn navigation_is_default(&self, handle: WebContentsHandle) -> Result<bool, String> {
Ok(self.web_contents(handle)?.navigation().is_default())
}
pub fn navigation_retains(
&self,
handle: WebContentsHandle,
navigation: NavigationId,
) -> Result<bool, String> {
Ok(self
.web_contents(handle)?
.navigation()
.retains_navigation(navigation))
}
pub fn current_document_navigation(
&self,
handle: WebContentsHandle,
) -> Result<Option<NavigationId>, String> {
Ok(self
.web_contents(handle)?
.navigation()
.current_document_navigation())
}
pub fn committed_document_navigation(
&self,
handle: WebContentsHandle,
) -> Result<Option<NavigationId>, String> {
Ok(self
.web_contents(handle)?
.navigation()
.committed_document_navigation())
}
pub fn clear_pending_navigation_if_matches(
&mut self,
handle: WebContentsHandle,
navigation: &NavigationId,
) -> Result<bool, String> {
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<crate::browser::web_contents::DocumentHost>,
) -> Result<Option<crate::page::Page>, 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<moli_fetch::FetchCancelHandle> {
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<moli_fetch::FetchCancelHandle>,
) -> 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<bool, String> {
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<moli_fetch::FetchCancelHandle> {
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<moli_fetch::FetchCancelHandle>,
) -> 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<crate::browser::web_contents::DocumentLifecycleEvent> {
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(())
}
}
@@ -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<PendingPermissionPageUpdate>,
}
struct PendingPermissionPageUpdate {
contents_id: WebContentsId,
document_id: DocumentId,
pending: PendingPageCommand,
}
pub struct CompletedContextPermissionUpdate {
context_id: BrowserContextId,
pages: Vec<CompletedPermissionPageUpdate>,
}
struct CompletedPermissionPageUpdate {
contents_id: WebContentsId,
document_id: DocumentId,
completed: Result<CompletedPageCommand, String>,
}
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<Option<PendingContextPermissionUpdate>, 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(())
}
}
@@ -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<crate::RendererOutputTransportSender>,
) {
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<WebContentsHandle> {
Some(WebContentsHandle::new(self.id, self.selected_web_contents?))
}
pub fn web_contents_handle_for_window_id(&self, window_id: u64) -> Option<WebContentsHandle> {
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<WebContentsHandle> {
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<Option<(WebContentsId, bool)>, 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<Option<&str>, String> {
Ok(self.web_contents(handle)?.window.name.as_deref())
}
pub fn clone_session_storage_namespace(
&self,
web_contents: WebContentsId,
) -> Option<crate::browser::web_contents::SessionStorageNamespace> {
self.web_contents
.get(&web_contents)
.map(|contents| contents.session_storage.deep_clone())
}
pub fn web_contents_is_crashed(&self, handle: WebContentsHandle) -> Result<bool, String> {
Ok(self.web_contents(handle)?.crashed)
}
pub fn performance_metric_snapshot(
&self,
handle: WebContentsHandle,
) -> Result<Option<crate::page::RendererPerformanceMetricSnapshot>, String> {
Ok(self.web_contents(handle)?.performance_metric_snapshot())
}
pub fn observe_renderer_page_state(
&mut self,
handle: WebContentsHandle,
snapshot: &std::sync::Arc<moli_renderer_v8::RendererPageState>,
) -> Result<bool, String> {
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<Option<InitialDocumentSnapshot>, String> {
Ok(self
.web_contents(handle)?
.navigation()
.initial_empty_document_state()
.map(Into::into))
}
pub fn web_contents_window_surface(
&self,
handle: WebContentsHandle,
) -> Result<WindowSurface, String> {
Ok(self.web_contents(handle)?.window.surface)
}
pub fn update_web_contents_window_surface(
&mut self,
handle: WebContentsHandle,
state: Option<WindowSurfaceState>,
width: Option<u32>,
height: Option<u32>,
x: Option<i32>,
y: Option<i32>,
) -> 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<String>,
) -> 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<WebContentsHandle>,
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<bool, String> {
Ok(self.web_contents(handle)?.network_offline)
}
pub fn web_contents_tls_verify_host_override(
&self,
handle: WebContentsHandle,
) -> Result<Option<bool>, 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<bool>,
) -> 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<bool, String> {
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<Option<moli_browser_profile::BrowserIdentityProfile>, String> {
Ok(self.web_contents(handle)?.browser_identity_override.clone())
}
pub fn set_web_contents_browser_identity_override(
&mut self,
handle: WebContentsHandle,
identity: Option<moli_browser_profile::BrowserIdentityProfile>,
) -> Result<(), String> {
self.web_contents_mut(handle)?
.set_browser_identity_override(identity);
Ok(())
}
pub fn web_contents_network_request_policy(
&self,
handle: WebContentsHandle,
) -> Result<NetworkRequestPolicy, String> {
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<Option<String>, String> {
Ok(self.web_contents(handle)?.environment_owner.locale())
}
pub fn set_web_contents_locale_override(
&mut self,
handle: WebContentsHandle,
locale: Option<String>,
) -> 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<Option<String>, String> {
Ok(self.web_contents(handle)?.environment_owner.timezone())
}
pub fn set_web_contents_timezone_override(
&mut self,
handle: WebContentsHandle,
timezone: Option<String>,
) -> 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<bool, String> {
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<EmulationPolicy, String> {
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<EmulationPolicyChange>,
) -> 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<crate::browser::web_contents::DocumentHost> {
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<WebContentsId> {
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()
}
}
@@ -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<PendingDocumentBlobRead, String> {
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<Option<Arc<[u8]>>, 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<PendingNetworkResourceLoadPreparation, String> {
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<RendererNetworkResourceLoadPreparation, String> {
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<PendingAppManifestLoadPreparation, String> {
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<BrowserAppManifestLoadPreparation, String> {
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<PendingAppManifestPublication, String> {
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<RendererCommandTurnOutput, String> {
let (document, completion) = completed.into_parts();
self.document_mut(document)?
.page
.finish_publish_app_manifest_load(completion?)
.map_err(|error| error.to_string())
}
}
@@ -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<crate::LayoutPolicy> {
self.web_contents(handle).ok()?.navigation_layout_policy()
}
pub fn web_contents_navigation_renderer_owner_id(
&self,
handle: crate::browser::WebContentsHandle,
) -> Option<u64> {
self.web_contents(handle)
.ok()?
.navigation_renderer_owner_id()
}
pub fn web_contents_navigation_diagnostics(
&self,
handle: crate::browser::WebContentsHandle,
) -> Option<crate::runtime::NavigationEngineDiagnostics> {
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<crate::RendererBrowserContextRuntimeId> {
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<crate::browser::EmulatedNetworkConditions>,
) -> 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<crate::browser::EmulatedNetworkConditions>,
) -> Result<ResourceRequestClient, String> {
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<crate::browser::EmulatedNetworkConditions>,
) -> Result<Option<PendingDocumentResourceRuntimeUpdate>, 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?)
}
}
@@ -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<DownloadPolicy>) {
self.download_policy = policy;
}
pub fn start_download_request(
&mut self,
policy: &DownloadPolicy,
client: ResourceRequestClient,
request: Request,
suggested_filename: Option<String>,
) -> Result<Option<DownloadObservation>, 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<Option<DownloadObservation>, String> {
self.web_contents(web_contents)?;
self.downloads.start_response(policy, url, headers, body)
}
pub fn cancel_download(&self, guid: &str) -> Option<Result<(), DownloadAccessError>> {
self.downloads.cancel(guid)
}
pub fn read_download_artifact(
&self,
guid: &str,
) -> Option<Result<tokio::task::JoinHandle<Result<Vec<u8>, 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<PermissionOverrideRegistration> {
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<WebContentsHandle>,
) -> Result<BrowserContextPageStorageHandles, String> {
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<StoredCookieQueryReport> {
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<OriginStorageUsage, String> {
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<StoredCookie> {
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<BrowserIdentityProfile>) {
self.browser_identity_override = identity;
}
pub fn emulation_defaults(&self) -> &ContextEmulationDefaults {
&self.emulation_defaults
}
pub fn set_default_locale_override(&mut self, locale: Option<String>) -> 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<String>,
) -> Result<(), String> {
self.environment_owner
.set_timezone(timezone.as_deref())
.map_err(str::to_owned)
}
pub fn default_locale_override(&self) -> Option<String> {
self.environment_owner.locale()
}
pub fn default_timezone_override(&self) -> Option<String> {
self.environment_owner.timezone()
}
pub fn set_default_network_conditions(
&mut self,
conditions: Option<EmulatedNetworkConditions>,
) {
self.emulation_defaults.network_conditions = conditions;
}
pub fn set_default_geolocation_override(
&mut self,
geolocation: Option<EmulatedGeolocationOverrideState>,
) {
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<WebContentsHandle> {
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<u64> {
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<u64>) {
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<R>(&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<R>(
&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<String>) {
self.network_policy.http_proxy = proxy;
}
}
@@ -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<String, f64>,
http_cache_root: Option<PathBuf>,
http_cache_max_bytes: Option<u64>,
}
impl StoragePartition {
pub(super) fn new(
pub fn new(
handles: BrowserContextStoragePartitionHandles,
kind: StoragePartitionKind,
http_cache_root: Option<PathBuf>,
@@ -56,15 +56,12 @@ impl StoragePartition {
}
}
pub(super) fn usage_for_origin(
&self,
serialized_origin: &str,
) -> Result<OriginStorageUsage, String> {
pub fn usage_for_origin(&self, serialized_origin: &str) -> Result<OriginStorageUsage, String> {
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<u64>) {
#[cfg(any(test, feature = "test-support"))]
pub fn http_cache_configuration(&self) -> (Option<&std::path::Path>, Option<u64>) {
(self.http_cache_root.as_deref(), self.http_cache_max_bytes)
}
}
@@ -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<u64> {
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<String>,
) -> 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<String>,
) -> 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<String>,
) -> 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<String>,
) -> 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()
}
}
@@ -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<u8>),
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<u8>) -> Self {
pub fn from_bytes(body: Vec<u8>) -> 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<u8>) -> Self {
pub fn from_bytes_spooled(body: Vec<u8>) -> 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<moli_core::page::RendererSyntheticResponseBody>,
pub fn from_optional_renderer_synthetic_response_body(
body: Option<crate::page::RendererSyntheticResponseBody>,
) -> 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<Vec<u8>> {
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn materialize_bytes_limited(&self, limit: usize) -> Result<Vec<u8>> {
ensure_materialize_limit(self.len(), limit)?;
self.materialize_bytes()
}
pub(crate) fn materialize_bytes(&self) -> Result<Vec<u8>> {
pub fn materialize_bytes(&self) -> Result<Vec<u8>> {
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<Vec<u8>> {
pub fn read_range(&self, offset: usize, len: usize) -> Result<Vec<u8>> {
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<String> {
#[cfg(any(test, feature = "test-support"))]
pub fn materialize_lossy_string(&self) -> Result<String> {
self.materialize_bytes()
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
}
pub(crate) fn chunk_reader(&self, chunk_size: usize) -> Result<CapturedBodyChunkReader> {
pub fn chunk_reader(&self, chunk_size: usize) -> Result<CapturedBodyChunkReader> {
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<Option<Vec<u8>>> {
pub fn next_chunk(&mut self) -> Result<Option<Vec<u8>>> {
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<u8>,
@@ -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<CapturedBody> {
pub fn finish(mut self) -> Result<CapturedBody> {
self.finish_in_place()
}
pub(crate) fn finish_in_place(&mut self) -> Result<CapturedBody> {
pub fn finish_in_place(&mut self) -> Result<CapturedBody> {
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!(
@@ -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<String>,
}
impl From<EmulatedMediaOverrides> for moli_core::page::EmulatedMediaOverrides {
impl From<EmulatedMediaOverrides> for crate::page::EmulatedMediaOverrides {
fn from(value: EmulatedMediaOverrides) -> Self {
Self {
media: value.media,
@@ -112,7 +112,7 @@ impl From<EmulatedMediaOverrides> 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);
}
}
+94
View File
@@ -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::<BrowserSequence>()
);
}
#[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);
}
}
+57
View File
@@ -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 {}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+39
View File
@@ -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<Vec<u8>>,
},
DispatchSyncEvent {
origin: Url,
registration_id: u64,
tag: String,
last_chance: bool,
},
DispatchPeriodicSyncEvent {
origin: Url,
registration_id: u64,
tag: String,
},
}
@@ -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<NavigationEngine>,
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<moli_core::page::SubresourceResourceType>),
pub(in crate::conn) network_offline: bool,
pub(in crate::conn) tls_verify_host_override: Option<bool>,
pub(in crate::conn) bypass_content_security_policy: bool,
pub(in crate::conn) browser_identity_override:
Option<moli_browser_profile::BrowserIdentityProfile>,
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<crate::page::SubresourceResourceType>),
pub(crate) network_offline: bool,
pub(crate) tls_verify_host_override: Option<bool>,
pub(crate) bypass_content_security_policy: bool,
pub(crate) browser_identity_override: Option<moli_browser_profile::BrowserIdentityProfile>,
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<moli_core::page::SubresourceResourceType>) {
) -> (bool, Option<crate::page::SubresourceResourceType>) {
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<moli_core::page::SubresourceResourceType>,
) -> Result<Option<moli_core::page::PendingPageCommand>, String> {
resource_type: Option<crate::page::SubresourceResourceType>,
) -> Result<Option<crate::page::PendingPageCommand>, 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<moli_core::page::SubresourceResourceType>,
resource_type: Option<crate::page::SubresourceResourceType>,
) {
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<moli_core::page::RendererPerformanceMetricSnapshot> {
) -> Option<crate::page::RendererPerformanceMetricSnapshot> {
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<moli_renderer_v8::RendererPageState>,
) -> 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<DocumentLifecycleEvent> {
@@ -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<DocumentHost>) -> Option<Page> {
pub fn replace_document(&mut self, next: Option<DocumentHost>) -> Option<Page> {
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<bool>) {
pub fn set_tls_verify_host_override(&mut self, enabled: Option<bool>) {
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<moli_browser_profile::BrowserIdentityProfile>,
) {
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<Page>,
_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<DocumentHost>,
pub current_document: Option<DocumentHost>,
}
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
}
@@ -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<serde_json::Value> {
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<crate::page::RendererRuntimeHeapUsage> {
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<crate::page::PendingPageCommand> {
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<Option<std::sync::Arc<[u8]>>> {
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
}
}
@@ -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<moli_core::browser::WebContentsId>,
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<PermissionOverrideRegistration>,
pub(in crate::conn::state) storage: BrowserContextStoragePartitionHandles,
pub struct InheritedDocumentPolicy {
pub selected_web_contents: Option<crate::browser::WebContentsId>,
pub navigator_queries: moli_page_types::NavigatorQueryOverrides,
pub fetch_config: FetchConfig,
pub extra_headers: moli_fetch::RequestHeaders,
pub emulation: ContextEmulationDefaults,
pub permissions: Vec<PermissionOverrideRegistration>,
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,
@@ -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<EmulatedNetworkConditions>,
pub(crate) geolocation_override: Option<EmulatedGeolocationOverrideState>,
pub(crate) emulated_media: EmulatedMediaOverrides,
pub(crate) emulated_device_metrics: Option<EmulatedDeviceMetrics>,
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<EmulatedNetworkConditions>,
pub geolocation_override: Option<EmulatedGeolocationOverrideState>,
pub emulated_media: EmulatedMediaOverrides,
pub emulated_device_metrics: Option<EmulatedDeviceMetrics>,
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<EmulatedNetworkConditions>),
Geolocation(Option<EmulatedGeolocationOverrideState>),
@@ -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<EmulationPolicyChange>) {
pub fn apply_changes(&mut self, changes: Vec<EmulationPolicyChange>) {
for change in changes {
self.apply(change);
}
@@ -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<Option<Result<(), String>>>,
}
@@ -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<Option<Result<(), String>>>,
}
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<AdmittedInitialDocumentBuild>),
}
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<BuiltInitialDocument> {
pub async fn materialize(self) -> anyhow::Result<BuiltInitialDocument> {
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<InitialDocumentAdmission, String> {
@@ -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<CommittedInitialDocument, Box<BuiltInitialDocument>> {
@@ -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();
@@ -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<JavaScriptDialog>,
}
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<JavaScriptDialogSnapshot> {
pub fn snapshot(&self, key: JavaScriptDialogKey) -> Option<JavaScriptDialogSnapshot> {
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());
}
@@ -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<BuiltDocumentPage<PreparedDocumentNavigation>> {
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<moli_core::page::SubresourceResourceType>),
permissions: &[moli_core::page::PermissionOverrideRegistration],
interception: (bool, Option<crate::page::SubresourceResourceType>),
permissions: &[crate::page::PermissionOverrideRegistration],
) -> anyhow::Result<Self> {
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<DocumentId>,
pub(in crate::conn) previous_renderer: Option<RendererPageResidenceIdentity>,
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<RendererPageCommandPostResponseContinuation>,
pub struct CommittedDocumentNavigation {
pub web_contents: WebContentsId,
pub frame_slot: MainFrameSlotId,
pub navigation: NavigationId,
pub document: DocumentId,
pub previous_document: Option<DocumentId>,
pub previous_renderer: Option<RendererPageResidenceIdentity>,
pub inspection_endpoint: moli_renderer_v8::RendererInspectionEndpoint,
pub lifecycle: CommittedDocumentLifecycle,
pub info: CommittedDocumentInfo,
pub retirement: RetiringDocument,
pub post_response_continuation: Option<RendererPageCommandPostResponseContinuation>,
}
/// Retirement has no commit authority and never retains a Browser registry borrow.
pub(crate) struct RetiringDocument(Option<Page>);
pub struct RetiringDocument(Option<Page>);
/// 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<Page>) -> 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<moli_core::page::PermissionOverrideRegistration>,
permissions: Vec<crate::page::PermissionOverrideRegistration>,
) -> Result<
impl std::future::Future<Output = anyhow::Result<PreparedDocumentNavigation>> + 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<CommittedDocumentNavigation, &'static str> {
@@ -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,
@@ -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<InitialDocumentCreator>,
storage_key: Option<moli_storage_key::MoliStorageKey>,
lifecycle: InitialDocumentLifecycle,
}
/// Owned diagnostic projection of an initial empty Document.
#[derive(Debug, Clone)]
pub struct InitialDocumentSnapshot {
initial_url: String,
creator: Option<InitialDocumentCreator>,
storage_key: Option<moli_storage_key::MoliStorageKey>,
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<PendingNavigationHistoryUpdate>,
paused_interception: Option<super::navigation_interception::PausedNavigationInterception>,
document_preparation: Option<(
moli_core::browser::RendererPageResidenceIdentity,
crate::browser::RendererPageResidenceIdentity,
moli_fetch::FetchCancelHandle,
)>,
cancellation_handles: Vec<moli_fetch::FetchCancelHandle>,
@@ -180,7 +228,7 @@ impl Drop for PendingNavigationRequest {
}
#[derive(Debug, Default)]
pub(in crate::conn) struct NavigationController {
pub struct NavigationController {
pending_navigation_request: Option<PendingNavigationRequest>,
committed_document_navigation: Option<NavigationId>,
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<super::NavigationInterceptionPermit, String> {
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<crate::conn::PausedDocumentTransfer> {
) -> Option<super::PausedDocumentTransfer> {
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<crate::conn::PausedDocumentTransfer>> {
transfer: super::PausedDocumentTransfer,
) -> Result<(), Box<super::PausedDocumentTransfer>> {
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<NavigationId> {
pub fn current_document_navigation(&self) -> Option<NavigationId> {
self.pending_document()
.map(|(navigation, _)| navigation)
.or(self.committed_document_navigation)
}
pub(in crate::conn::state) fn committed_document_navigation(&self) -> Option<NavigationId> {
pub fn committed_document_navigation(&self) -> Option<NavigationId> {
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<moli_fetch::FetchCancelHandle> {
@@ -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<bool> {
pub fn is_on_initial_empty_document(&self) -> Option<bool> {
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<String> {
pub fn navigation_history_entry_url(&self, entry_id: i32) -> Option<String> {
self.history.entry_url(entry_id)
}
pub(crate) fn navigation_history_snapshot(&self) -> (usize, Vec<PageNavigationHistoryEntry>) {
pub fn navigation_history_snapshot(&self) -> (usize, Vec<PageNavigationHistoryEntry>) {
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<moli_fetch::FetchCancelHandle>,
@@ -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<InitialDocumentCreator>,
@@ -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() {
@@ -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,
@@ -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<ResolvedHistoryTraversal, &'static str> {
self.navigation.resolve_history_traversal(destination)
}
pub(in crate::conn::state) fn navigation_history_snapshot(
&self,
) -> (usize, Vec<PageNavigationHistoryEntry>) {
pub fn navigation_history_snapshot(&self) -> (usize, Vec<PageNavigationHistoryEntry>) {
self.navigation.navigation_history_snapshot()
}
pub(in crate::conn::state) fn navigation_history_entry_url(
&self,
entry_id: i32,
) -> Option<String> {
pub fn navigation_history_entry_url(&self, entry_id: i32) -> Option<String> {
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<bool> {
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<PendingPageCommand, String> {
pub fn start_reset_navigation_history(&self) -> Result<PendingPageCommand, String> {
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<bool, String> {
@@ -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 {
@@ -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<Vec<u8>>,
@@ -59,7 +59,7 @@ pub(crate) struct NavigationRequestInterception {
}
impl NavigationRequestInterception {
pub(crate) fn new(
pub fn new(
requested_url: Url,
method: String,
body: Option<Vec<u8>>,
@@ -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<Url>,
method: Option<String>,
@@ -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<Vec<u8>>,
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<SubresourceAuthCredentials>,
) -> anyhow::Result<InterceptedNavigationResponse<StreamingRawResponse>> {
@@ -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<InterceptedNavigationResponse<RawResponse>> {
@@ -221,7 +221,7 @@ impl InterceptedNavigationLoad {
Ok(self.with_response(response))
}
pub(in crate::conn) fn with_response<R>(
pub fn with_response<R>(
mut self,
response: NetworkFetchResult<R>,
) -> InterceptedNavigationResponse<R> {
@@ -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<Vec<u8>>,
moli_fetch::RequestHeaders,
) {
self.into_request_parts()
}
}
#[derive(Debug)]
pub(crate) struct InterceptedNavigationResponse<R> {
pub struct InterceptedNavigationResponse<R> {
work: InterceptedNavigationLoad,
response: NetworkFetchResult<R>,
}
impl<R> InterceptedNavigationResponse<R> {
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<R>) {
pub fn into_parts(self) -> (InterceptedNavigationLoad, NetworkFetchResult<R>) {
(self.work, self.response)
}
@@ -281,48 +293,19 @@ impl<R> InterceptedNavigationResponse<R> {
}
}
impl InterceptedNavigationResponse<StreamingRawResponse> {
pub(crate) async fn materialize(
self,
) -> anyhow::Result<InterceptedNavigationResponse<RawResponse>> {
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<RawResponse> {
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<ClaimedNavigationRequest> {
@@ -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<RawResponse>,
) -> Result<NavigationInterceptionPermit, String> {
@@ -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<InterceptedNavigationResponse<RawResponse>> {
@@ -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<NavigationInterceptionPermit, String> {
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<crate::conn::PausedDocumentTransfer> {
) -> Option<super::PausedDocumentTransfer> {
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<crate::conn::PausedDocumentTransfer>> {
transfer: super::PausedDocumentTransfer,
) -> Result<(), Box<super::PausedDocumentTransfer>> {
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,
@@ -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<DocumentNavigationIdentity>,
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<moli_core::runtime::PreparedDocumentPagePolicy>,
) -> anyhow::Result<moli_core::runtime::BuiltDocumentPage> {
policy: Option<crate::runtime::PreparedDocumentPagePolicy>,
) -> anyhow::Result<crate::runtime::BuiltDocumentPage> {
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<moli_fetch::RequestHeaders>,
chain: Vec<moli_fetch::RedirectInfo>,
@@ -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(),
@@ -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() {
@@ -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<String>,
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<String>,
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();
@@ -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<EmulatedNetworkConditions>,
geolocation_override: Option<EmulatedGeolocationOverrideState>,
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<EmulatedNetworkConditions>,
@@ -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,<p>page surface</p>")
@@ -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<Vec<u8>>,
renderer_completion_tx: oneshot::Sender<anyhow::Result<()>>,
}
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<Vec<u8>>,
renderer_completion_tx: oneshot::Sender<anyhow::Result<()>>,
) -> 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<Vec<u8>>,
oneshot::Sender<anyhow::Result<()>>,
) {
(
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<Box<PausedResponsePreparedDocument>>,
},
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<u8>,
offset: usize,
finished: bool,
}
#[derive(Debug)]
pub struct PendingFetchResponseOpenedBodyStream {
pub handle: String,
pub buffered_bytes: Option<Vec<u8>>,
pub transfer: PausedDocumentTransfer,
}
#[derive(Debug)]
pub enum OpenBodyStreamError {
NotOpenable(Box<PausedDocumentTransfer>),
Failed {
transfer: Box<PausedDocumentTransfer>,
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<Box<PausedResponsePreparedDocument>>,
}
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<StoredCookieQueryReport>,
}
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<crate::page::RendererDevToolsAgentToken> {
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<Self>> {
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<PausedStreamingDocumentResponse, Box<Self>> {
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<PendingFetchResponseOpenedBodyStream, OpenBodyStreamError> {
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<usize> {
match &self.state {
PausedDocumentTransferState::ActiveBodyStream { stream } => Some(stream.offset),
PausedDocumentTransferState::Pending { .. } => None,
}
}
pub async fn read_body_stream_async(
self,
size: Option<usize>,
) -> Result<(Vec<u8>, 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<Vec<u8>>, 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<u8>, 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<usize>) -> Result<(Vec<u8>, 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<u8>, 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<DocumentBodySource> {
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))
}
@@ -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<moli_core::LayoutPolicy> {
pub fn navigation_layout_policy(&self) -> Option<crate::LayoutPolicy> {
self.navigation_engine
.as_ref()
.map(NavigationEngine::layout_policy)
}
pub(in crate::conn::state) fn navigation_renderer_owner_id(&self) -> Option<u64> {
pub fn navigation_renderer_owner_id(&self) -> Option<u64> {
self.navigation_engine
.as_ref()
.map(NavigationEngine::renderer_owner_id_for_diagnostics)
}
pub(in crate::conn::state) fn navigation_diagnostics(
&self,
) -> Option<NavigationEngineDiagnostics> {
pub fn navigation_diagnostics(&self) -> Option<NavigationEngineDiagnostics> {
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<ResourceRequestClient, String> {
@@ -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<Option<PendingPageCommand>, 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(
@@ -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;
@@ -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<SharedWebStorageStore>,
}
@@ -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())),
}
@@ -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();
@@ -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<String>,
pub(in crate::conn) opener: Option<WindowOpener>,
pub(in crate::conn) surface: WindowSurface,
pub struct Window {
pub name: Option<String>,
pub opener: Option<WindowOpener>,
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<Self> {
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<u32>,
height: Option<u32>,
@@ -70,4 +80,18 @@ impl WindowSurface {
self.y = y;
}
}
pub fn update(
&mut self,
state: Option<WindowSurfaceState>,
width: Option<u32>,
height: Option<u32>,
x: Option<i32>,
y: Option<i32>,
) {
if let Some(state) = state {
self.state = state;
}
self.set_geometry(width, height, x, y);
}
}
+19
View File
@@ -227,6 +227,25 @@ impl Page {
)
.await
}
pub fn start_page_surface_override_script(&self, source: &str) -> Result<PendingPageCommand> {
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.
+20
View File
@@ -239,6 +239,26 @@ impl Page {
.await
}
pub fn start_set_script_execution_disabled(
&self,
disabled: bool,
) -> Result<PendingPageCommand> {
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),
+15 -12
View File
@@ -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<PendingPageCommand> {
self.start_page_command(RendererPageCommand::StopDocumentLifecycle)
}
pub fn finish_stop_document_lifecycle(
&mut self,
completion: CompletedPageCommand,
) -> Result<RendererCommandTurnOutput> {
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(
+4 -2
View File
@@ -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"));
}
+14 -16
View File
@@ -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<CdpTargetHostIntegration>,
) -> (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<CdpTargetHostIntegration>,
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<CdpTargetHostIntegration>,
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);
@@ -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,
@@ -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),
}]);
+17 -18
View File
@@ -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);
@@ -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<AtomicU64>,
tab_target_id_allocator: Arc<AtomicU64>,
@@ -62,6 +66,7 @@ struct CdpOwnerRuntimeConfig {
impl SharedCdpOwnerRegistry {
pub(super) fn new(
browser: BrowserHandle,
directory: SharedCdpAgentHostDirectory,
target_id_allocator: Arc<AtomicU64>,
tab_target_id_allocator: Arc<AtomicU64>,
@@ -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,
@@ -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<Body>) -> 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<Self> {
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<StoragePartitionState>,
navigation_runtime_config: NavigationRuntimeConfig,
screencast_interval_ms: u32,
) -> Self {
) -> anyhow::Result<Self> {
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(
@@ -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();
@@ -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,
);
@@ -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(
@@ -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<StoredCookie>,
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<ClassicSessionRuntimeRequest>,
initial_cookie_snapshot: Vec<StoredCookie>,
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,
);
+1 -1
View File
@@ -6,7 +6,7 @@ edition = "2024"
build = "build.rs"
[features]
test-support = []
test-support = ["moli-core/test-support"]
[dependencies]
anyhow = "1.0.100"
+113 -496
View File
@@ -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<PathBuf>,
http_cache_max_bytes: Option<u64>,
) -> 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<NavigationEngine>,
runtime_config: NavigationRuntimeConfig,
renderer_publication_sender: Option<moli_core::RendererOutputTransportSender>,
}
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<NavigationEngine> {
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<NavigationEngine> {
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<EmulatedNetworkConditions>,
pub(crate) geolocation: Option<EmulatedGeolocationOverrideState>,
pub(crate) cache_disabled: bool,
}
/// Persistent per-connection state.
pub struct CdpConnection {
browser: BrowserHandle,
// Browser/session routing state.
pub browser_context: Option<BrowserContext>,
pub inactive_browser_contexts: Vec<BrowserContext>,
@@ -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<moli_browser_profile::BrowserIdentityProfile>,
global_network_conditions: Option<EmulatedNetworkConditions>,
global_geolocation_override: Option<EmulatedGeolocationOverrideState>,
global_cache_disabled: bool,
pub(crate) network_data_collectors: crate::domains::network::NetworkDataCollectorStore,
base_http_proxy: Option<String>,
base_http_no_proxy: Option<String>,
@@ -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::<Vec<_>>();
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<StoredCookie>) -> 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<StoredCookie> {
@@ -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
+12 -107
View File
@@ -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<CompletedChildFrameLifecycleWork, String> {
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<PendingChildFrameLifecycleWork, String> {
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<bool, String> {
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",
+2 -3
View File
@@ -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);
@@ -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<JavaScriptDialogSnapshot> {
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<String>,
) -> 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))
}
}
@@ -53,7 +53,7 @@ impl TargetSessionOwnerMut<'_> {
&mut self,
locale_override: Option<String>,
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<String>,
) -> 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<String>,
) -> 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<String>,
) -> Result<(), &'static str> {
) -> Result<(), String> {
self.target_session_owner_mut_for_owner(owner)
.ok_or("BrowserContextNotLoaded")?
.set_base_timezone_override(timezone_override)
@@ -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<Option<Option<moli_core::page::PendingPageCommand>>, String> {
) -> Result<Option<Option<PendingDocumentFetchCommand>>, String> {
let target_ids = browser_context
.page_targets
.iter()
.map(|target| target.target_id().to_owned())
.collect::<Vec<_>>();
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<crate::conn::NavigationId>,
navigation: crate::conn::NavigationDispatchState,
body: crate::conn::DocumentBodySource,
body_progress_source: crate::domains::network::MainDocumentBodyProgressSource,
prepared_document: Option<Box<crate::conn::PausedResponsePreparedDocument>>,
) -> 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<FetchInterceptionPattern>,
) -> Result<Option<moli_core::page::PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentFetchCommand>, 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<FetchInterceptionPattern>,
) -> Result<Option<moli_core::page::PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentFetchCommand>, 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<String>,
patterns: Vec<FetchInterceptionPattern>,
) -> Result<Option<moli_core::page::PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentFetchCommand>, 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<Option<moli_core::page::PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentFetchCommand>, 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<Option<moli_core::page::PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentFetchCommand>, 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<Option<moli_core::page::PendingPageCommand>, String>,
Result<Option<PendingDocumentFetchCommand>, 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<Option<moli_core::page::PendingPageCommand>, String>,
Result<Option<PendingDocumentFetchCommand>, 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<Option<moli_core::page::PendingPageCommand>, String>,
Result<Option<PendingDocumentFetchCommand>, 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)
@@ -1,6 +1,19 @@
use super::*;
impl CdpConnection {
pub(crate) fn active_browser_context_id(&self) -> Option<moli_core::browser::BrowserContextId> {
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<BrowserContext> {
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<moli_core::browser::BrowserContextId>,
) -> Option<BrowserContext> {
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<moli_core::browser::BrowserContextId>,
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);
}
}
@@ -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<bool> {
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
}
}
@@ -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<Option<PendingPageCommand>, String> {
global_extra_headers: &moli_fetch::RequestHeaders,
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<String>,
) -> Result<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
global_extra_headers: &moli_fetch::RequestHeaders,
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
global_extra_headers: &moli_fetch::RequestHeaders,
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
global_extra_headers: &moli_fetch::RequestHeaders,
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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::<Vec<_>>();
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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<String>,
) -> Result<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<String>,
) -> Result<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentResourceRuntimeUpdate>, 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<moli_browser_profile::BrowserIdentityProfile>,
) -> Option<Result<Option<PendingPageCommand>, String>> {
) -> Option<Result<Option<PendingDocumentResourceRuntimeUpdate>, 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<crate::conn::DevToolsBrowserIdentityOverride>,
) -> Result<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentResourceRuntimeUpdate>, 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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentResourceRuntimeUpdate>, 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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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<Option<PendingPageCommand>, String> {
) -> Result<Option<PendingDocumentPolicyUpdate>, 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")
@@ -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")
@@ -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),
@@ -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(),
@@ -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<String>,
pub(crate) document_fetch_event_session_id: Option<String>,
@@ -147,7 +146,6 @@ pub(crate) struct TargetNavigationLoadInputs {
storage_handles: TargetNavigationStorageHandles,
#[cfg(test)]
pub(crate) root_frame_id: Option<String>,
pub(crate) renderer_runtime: RendererBrowserContextRuntimeOwnerAccess,
/// One identity for navigation request headers and the Document's Navigator.
pub(crate) browser_identity_override: Option<moli_browser_profile::BrowserIdentityProfile>,
pub(crate) http_proxy_override: Option<String>,
@@ -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<String>,
#[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<TargetNavigationRequestPreflight> {
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<Output = anyhow::Result<crate::conn::PreparedDocumentNavigation>>
+ 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<BackgroundProtocolEvent>,
reason: &'static str,
) -> Option<ClosedPageTarget> {
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<BackgroundProtocolEvent>,
reason: &'static str,
) -> Option<ClosedPageTarget> {
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<BackgroundProtocolEvent>,
reason: &'static str,
) -> Option<ClosedPageTarget> {
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<i32>,
y: Option<i32>,
) -> 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<TargetPageResidenceIdentity> {
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<moli_core::browser::DocumentLifetimeObserver> {
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<Result<crate::conn::ResolvedHistoryTraversal, &'static str>> {
) -> Option<Result<crate::conn::ResolvedHistoryTraversal, String>> {
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,<title>background commit</title>")
.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(),
"<!doctype html><title>background commit</title>".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(),
@@ -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<impl std::future::Future<Output = ()> + 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<DocumentHandle, String> {
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<DocumentHandle, String> {
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<DocumentLifetimeObserver, String> {
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<PendingDocumentFetchCommand, String> {
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<DocumentFetchCommandOutcome, String> {
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<PendingDocumentInputCommand, String> {
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<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_input_command(completed)
}
pub(crate) fn start_document_autofill_trigger(
&self,
document: DocumentHandle,
request: RendererAutofillTriggerRequest,
) -> Result<PendingDocumentAutofillTrigger, String> {
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<RendererAutofillTriggerOutcome, String> {
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<PendingDocumentLifecycleStop, String> {
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<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_lifecycle_stop(completed)
}
pub(crate) fn start_document_diagnostics_snapshot(
&self,
document: DocumentHandle,
) -> Result<PendingDocumentDiagnosticsSnapshot, String> {
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<RendererPageDiagnosticsSnapshot, String> {
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<PendingChildFrameLifecycleWork, String> {
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<PendingSetDocumentContent, String> {
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<PendingDocumentPolicyUpdate, String> {
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<PendingDocumentPolicyBatch, String> {
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<PendingTopLevelSameDocumentNavigation, String> {
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<PendingTopLevelHistoryTraversal, String> {
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<bool, 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_history_traversal(completed)
}
pub(crate) fn start_navigation_history_reset(
&self,
document: DocumentHandle,
) -> Result<PendingNavigationHistoryReset, String> {
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<bool, String> {
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<PendingChildFrameNavigation, String> {
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<PendingCaptureDocumentSnapshot, String> {
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<DocumentSnapshot, String> {
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<PendingCaptureDocumentImage, String> {
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<RendererCaptureScreenshotReply, String> {
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<PendingCaptureDocumentScreencastFrame, String> {
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<RendererCaptureScreencastFrameReply, String> {
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<PendingDocumentResourceTextSearch, String> {
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<PendingDocumentResourceTextSearch, String> {
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<RendererResourceTextSearchOutcome, String> {
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<Vec<SubresourceNetworkRecord>, 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<Vec<(String, String)>, 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<PendingDocumentCspBypassUpdate, String> {
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<PendingDocumentStorageKeySnapshot, String> {
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<String, String> {
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<PendingChildFrameTreeSnapshot, String> {
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<Vec<ChildFrameTreeSnapshot>, 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<PendingDocumentCookieOwnerSnapshot, String> {
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<DocumentCookieOwnerSnapshot, String> {
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<PendingDocumentBlobRead, String> {
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<Option<Arc<[u8]>>, 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<PendingNetworkResourceLoadPreparation, String> {
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<RendererNetworkResourceLoadPreparation, String> {
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<PendingAppManifestLoadPreparation, String> {
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<BrowserAppManifestLoadPreparation, String> {
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<PendingAppManifestPublication, String> {
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<RendererCommandTurnOutput, String> {
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)
}
}
@@ -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<WebContentsHandle, String> {
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<WebContentsHandle, String> {
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<WebContentsHandle, String> {
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<WebContentsHandle, String> {
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<WindowSurface, String> {
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<WindowSurfaceState>,
width: Option<u32>,
height: Option<u32>,
x: Option<i32>,
y: Option<i32>,
) -> 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<bool, String> {
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)
}
}
@@ -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<BrowserContextId, String> {
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<bool, String> {
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<bool, String> {
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()
);
}
}
+11 -11
View File
@@ -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(),
@@ -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;
}
}
+18 -27
View File
@@ -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<DocumentCookieOwnerSnapshot> {
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 (
@@ -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<bool> {
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;
}
}
}
File diff suppressed because it is too large Load Diff
+20 -3
View File
@@ -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<DownloadBehavior> {
@@ -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>,
+17 -14
View File
@@ -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
+123 -103
View File
@@ -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<Url>,
policy: DownloadPolicy,
event_route: DownloadEventRoute,
activation: RendererPendingDownloadActivation,
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
struct DownloadEventRoute {
browser_observers: Vec<BrowserDownloadObserver>,
automation_events_enabled: bool,
page_observers: Vec<PageDownloadObserver>,
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
struct BrowserDownloadObserver {
session_id: Option<String>,
subscription_generation: u64,
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
struct PageDownloadObserver {
session_id: Option<String>,
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<BackgroundProtocolEvent>,
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<PreparedDownloadActivation> {
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<BackgroundProtocolEvent>,
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<PreparedDownloadActivation> {
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<BackgroundProtocolEvent>,
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<BackgroundProtocolEvent>,
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<BackgroundProtocolEvent>,
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<PendingDownloadOwnerContext> {
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 {
@@ -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);
}
File diff suppressed because it is too large Load Diff
@@ -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<moli_core::page::RendererRuntimeHeapUsage, String> {
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<Value, String> {
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<moli_core::browser::PendingDocumentBlobRead, String> {
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<Option<std::sync::Arc<[u8]>>, 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,<body><main id='inspected' data-phase='replacement'>new</main></body>",
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 {
@@ -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,<button>Replacement</button>",
None,
@@ -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,<title>replacement</title>",
None,
@@ -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);
}
@@ -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,<title>replacement-document</title>",
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#"<!doctype html><link rel="manifest" href="data:application/manifest+json,%7B%7D"><title>manifest-owner</title>"#
.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,<title>replacement-manifest-owner</title>",
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")
);
}
@@ -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,<main id='inspected'>replacement</main>",
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);
}
+16 -14
View File
@@ -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,<title>inspector-state-source-validation</title>",
)
.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,<title>inspector-state-source-validation</title>",
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);
+3 -56
View File
@@ -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;
+3 -3
View File
@@ -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(
+35 -24
View File
@@ -27,10 +27,11 @@ fn permission_settings(conn: &CdpConnection, context: &str) -> Vec<String> {
#[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::<Vec<_>>(),
@@ -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,<title>first</title>", 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");

Some files were not shown because too many files have changed in this diff Show More