diff --git a/crates/obscura-cdp/tests/dynamic_script_onload_fires.rs b/crates/obscura-cdp/tests/dynamic_script_onload_fires.rs index 457f5f0..2d5a6fe 100644 --- a/crates/obscura-cdp/tests/dynamic_script_onload_fires.rs +++ b/crates/obscura-cdp/tests/dynamic_script_onload_fires.rs @@ -4,6 +4,8 @@ use obscura_cdp::dispatch::{dispatch, CdpContext}; use obscura_cdp::types::CdpRequest; use serde_json::{json, Value}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; @@ -82,6 +84,130 @@ async fn cdp( response.result.unwrap_or_else(|| json!({})) } +async fn serve_dynamic_order_fixture(explicitly_in_order: bool) -> (String, Arc) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let active = Arc::new(AtomicUsize::new(0)); + let peak = Arc::new(AtomicUsize::new(0)); + let server_active = active.clone(); + let server_peak = peak.clone(); + tokio::spawn(async move { + loop { + let (mut socket, _) = listener.accept().await.unwrap(); + let active = server_active.clone(); + let peak = server_peak.clone(); + tokio::spawn(async move { + let current = active.fetch_add(1, Ordering::SeqCst) + 1; + peak.fetch_max(current, Ordering::SeqCst); + let mut buf = [0u8; 2048]; + let read = socket.read(&mut buf).await.unwrap(); + let request = String::from_utf8_lossy(&buf[..read]); + let (content_type, body) = if request.starts_with("GET /slow.js") { + tokio::time::sleep(std::time::Duration::from_millis(700)).await; + ( + "application/javascript", + "window.__dynamicOrder.push('slow');".to_string(), + ) + } else if request.starts_with("GET /fast.js") { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + ( + "application/javascript", + "window.__dynamicOrder.push('fast');".to_string(), + ) + } else { + let ordered = if explicitly_in_order { + "slow.async=false;fast.async=false;" + } else { + "" + }; + let body = format!( + r#""# + ); + ("text/html", body) + }; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + let _ = socket.write_all(response.as_bytes()).await; + active.fetch_sub(1, Ordering::SeqCst); + }); + } + }); + (format!("http://{addr}/"), peak) +} + +async fn navigate_dynamic_order_fixture(explicitly_in_order: bool) -> (Vec, usize, u128) { + let (url, peak) = serve_dynamic_order_fixture(explicitly_in_order).await; + let mut ctx = CdpContext::new(); + let page_id = ctx.create_page(); + let session_id = "script-order-session"; + ctx.sessions.insert(session_id.to_string(), page_id); + + let started = std::time::Instant::now(); + cdp( + &mut ctx, + 1, + "Page.navigate", + json!({"url": url, "waitUntil": "load"}), + session_id, + ) + .await; + let elapsed_ms = started.elapsed().as_millis(); + let result = cdp( + &mut ctx, + 2, + "Runtime.evaluate", + json!({ + "expression": "window.__dynamicOrder", + "returnByValue": true, + }), + session_id, + ) + .await; + let order = result["result"]["value"] + .as_array() + .unwrap() + .iter() + .map(|value| value.as_str().unwrap().to_string()) + .collect(); + (order, peak.load(Ordering::SeqCst), elapsed_ms) +} + +#[tokio::test(flavor = "current_thread")] +async fn dynamic_classic_fetch_concurrency_matches_force_async_state() { + std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1"); + + let (async_order, async_peak, async_elapsed_ms) = + navigate_dynamic_order_fixture(false).await; + assert_eq!(async_order, ["fast", "slow"]); + assert_eq!( + async_peak, 2, + "default dynamic classics must fetch concurrently" + ); + + let (ordered_order, ordered_peak, ordered_elapsed_ms) = + navigate_dynamic_order_fixture(true).await; + assert_eq!(ordered_order, ["slow", "fast"]); + assert_eq!( + ordered_peak, 1, + "explicit async=false scripts must retain their insertion-order queue" + ); + assert!( + async_elapsed_ms + 50 < ordered_elapsed_ms, + "concurrent fetches should avoid the ordered waterfall: async={async_elapsed_ms}ms ordered={ordered_elapsed_ms}ms" + ); +} + #[tokio::test(flavor = "current_thread")] async fn dynamic_external_scripts_execute_and_fire_load() { std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1");