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