fix(cdp): implement Target.attachToBrowserTarget for Playwright (#64) (#81)

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 <noreply@oratelecom.net>
This commit is contained in:
Ben YounesandOra Studio authored and GitHub committed 2026-05-03 17:10:07 +02:00
1 parent 3bb628eaf0
commit 23753405e5
1 file changed
+64
+64
View File
@@ -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"));
}
}