Cover dynamic classic script scheduling

This commit is contained in:
SGavrl committed 2026-08-04 09:44:07 +00:00
1 parent d6a575a62e
commit 3db63d9125
1 file changed
+126
@@ -4,6 +4,8 @@
use obscura_cdp::dispatch::{dispatch, CdpContext}; use obscura_cdp::dispatch::{dispatch, CdpContext};
use obscura_cdp::types::CdpRequest; use obscura_cdp::types::CdpRequest;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener; use tokio::net::TcpListener;
@@ -82,6 +84,130 @@ async fn cdp(
response.result.unwrap_or_else(|| json!({})) response.result.unwrap_or_else(|| json!({}))
} }
async fn serve_dynamic_order_fixture(explicitly_in_order: bool) -> (String, Arc<AtomicUsize>) {
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#"<script>
window.__dynamicOrder=[];
var slow=document.createElement('script');
var fast=document.createElement('script');
{ordered}
slow.src='/slow.js';
fast.src='/fast.js';
document.head.appendChild(slow);
document.head.appendChild(fast);
</script>"#
);
("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<String>, 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")] #[tokio::test(flavor = "current_thread")]
async fn dynamic_external_scripts_execute_and_fire_load() { async fn dynamic_external_scripts_execute_and_fire_load() {
std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1"); std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1");