From 23753405e5e37e126c130e0bc883db76db0e20ef Mon Sep 17 00:00:00 2001 From: Ben Younes Date: Sun, 3 May 2026 17:10:07 +0200 Subject: [PATCH] fix(cdp): implement Target.attachToBrowserTarget for Playwright (#64) (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playwright (and recent Puppeteer) call Target.attachToBrowserTarget on connect to obtain a session for the implicit "browser" target. Without the handler the dispatcher returns "Unknown Target method: attachToBrowserTarget" and the connect handshake aborts before any user code runs. Add a handler that mirrors Target.attachToTarget but for the browser target: register a "browser-session" → "browser" mapping in ctx.sessions, emit Target.attachedToTarget with type=browser, and return the new session id. Note: this addresses item (1) from #64. The Rocket Loader / Cloudflare JS execution gap and Page.getLayoutMetrics are tracked separately (getLayoutMetrics is fixed in PR #79). Generated by Ora Studio Vibe coded by ousamabenyounes Co-authored-by: Ora Studio --- crates/obscura-cdp/src/domains/target.rs | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/obscura-cdp/src/domains/target.rs b/crates/obscura-cdp/src/domains/target.rs index bd9d1a2..a5207a4 100644 --- a/crates/obscura-cdp/src/domains/target.rs +++ b/crates/obscura-cdp/src/domains/target.rs @@ -104,6 +104,31 @@ pub async fn handle(method: &str, params: &Value, ctx: &mut CdpContext) -> Resul Ok(json!({ "targetId": page_id })) } + "attachToBrowserTarget" => { + // Playwright calls this on connect to obtain a session for the + // implicit "browser" target. Returning Unknown method aborts + // the connect handshake before any user code runs. + let session_id = "browser-session".to_string(); + ctx.sessions.insert(session_id.clone(), "browser".to_string()); + + ctx.pending_events.push(CdpEvent::new( + "Target.attachedToTarget", + json!({ + "sessionId": session_id, + "targetInfo": { + "targetId": "browser", + "type": "browser", + "title": "", + "url": "", + "attached": true, + "browserContextId": "", + }, + "waitingForDebugger": false, + }), + )); + + Ok(json!({ "sessionId": session_id })) + } "attachToTarget" => { let target_id = params.get("targetId").and_then(|v| v.as_str()) .ok_or("targetId required")?; @@ -194,3 +219,42 @@ pub async fn handle(method: &str, params: &Value, ctx: &mut CdpContext) -> Resul _ => Err(format!("Unknown Target method: {}", method)), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn attach_to_browser_target_returns_session_id() { + let mut ctx = CdpContext::new(); + let result = handle("attachToBrowserTarget", &json!({}), &mut ctx) + .await + .expect("attachToBrowserTarget should succeed"); + + assert_eq!(result["sessionId"], "browser-session"); + assert_eq!( + ctx.sessions.get("browser-session").map(String::as_str), + Some("browser") + ); + + // Playwright/Puppeteer expect a Target.attachedToTarget event before + // they finish wiring up the session — without it the connect promise + // hangs. + let attached_evt = ctx + .pending_events + .iter() + .find(|e| e.method == "Target.attachedToTarget") + .expect("attachedToTarget event must be emitted"); + assert_eq!(attached_evt.params["sessionId"], "browser-session"); + assert_eq!(attached_evt.params["targetInfo"]["type"], "browser"); + } + + #[tokio::test] + async fn unknown_target_method_still_errors() { + let mut ctx = CdpContext::new(); + let err = handle("notARealMethod", &json!({}), &mut ctx) + .await + .expect_err("unknown methods must surface as errors"); + assert!(err.contains("Unknown Target method")); + } +}