use crate::connection::Connection;
use crate::error::{CdpError, Result};
use serde_json::{Value, json};
use std::time::Duration;
use tracing;
pub struct Tab {
pub session_id: String,
pub target_id: String,
}
impl Tab {
pub async fn create(conn: &mut Connection, _ws_url: &str, url: &str) -> Result<Self> {
let result = conn
.call(
"Target.createTarget",
json!({
"url": url,
"newWindow": false,
}),
)
.await;
if let Ok(result) = result {
if let Some(session_id) = result.get("sessionId").and_then(|v| v.as_str()) {
let target_id = result
.get("targetId")
.and_then(|v| v.as_str())
.ok_or_else(|| CdpError::CommandFailed {
method: "Target.createTarget".into(),
msg: "no targetId in response".into(),
})?
.to_string();
tracing::debug!("Created tab: target={}, session={}", target_id, session_id);
return Ok(Tab {
session_id: session_id.to_string(),
target_id,
});
}
if let Some(target_id) = result.get("targetId").and_then(|v| v.as_str()) {
tracing::warn!(
"Target.createTarget returned targetId without sessionId, attaching..."
);
let attach = conn
.call(
"Target.attachToTarget",
json!({
"targetId": target_id,
"flatten": true,
}),
)
.await
.map_err(|e| CdpError::CommandFailed {
method: "Target.attachToTarget".into(),
msg: format!("attach failed: {e}"),
})?;
let session_id = attach
.get("sessionId")
.and_then(|v| v.as_str())
.ok_or_else(|| CdpError::CommandFailed {
method: "Target.attachToTarget".into(),
msg: "no sessionId in attach response".into(),
})?
.to_string();
tracing::info!(
"Attached to created target: {} (session={})",
target_id,
session_id
);
return Ok(Tab {
session_id,
target_id: target_id.to_string(),
});
}
tracing::warn!(
"Target.createTarget returned neither sessionId nor targetId, cannot attach"
);
} else {
tracing::warn!("Target.createTarget failed");
}
Err(CdpError::CommandFailed {
method: "Target.createTarget".into(),
msg: "could not create tab: all methods exhausted".into(),
})
}
pub async fn navigate(&self, conn: &mut Connection, url: &str) -> Result<()> {
tracing::info!("Navigating to: {}", url);
let result = conn
.call_with_session(&self.session_id, "Page.enable", json!({}))
.await?;
tracing::debug!("Page.enable: {:?}", result);
let _result = conn
.call_with_session(
&self.session_id,
"Page.navigate",
json!({
"url": url,
}),
)
.await?;
self.wait_for_page_load(conn, 10000).await
}
async fn wait_for_page_load(&self, conn: &mut Connection, timeout_ms: u64) -> Result<()> {
let start = std::time::Instant::now();
let timeout = Duration::from_millis(timeout_ms);
loop {
if start.elapsed() > timeout {
tracing::warn!("Page load timed out after {}ms", timeout_ms);
return Ok(()); }
let result = conn
.call_with_session(
&self.session_id,
"Runtime.evaluate",
json!({
"expression": "document.readyState",
"returnByValue": true,
}),
)
.await?;
let ready_state = result["result"]["value"].as_str().unwrap_or("unknown");
if ready_state == "complete" {
tracing::debug!("Page loaded (readyState=complete)");
return Ok(());
}
let has_content = conn.call_with_session(&self.session_id, "Runtime.evaluate", json!({
"expression": "document.body ? document.body.innerText.length > 100 : false",
"returnByValue": true,
})).await?;
if has_content["result"]["value"].as_bool().unwrap_or(false) {
tracing::debug!("Page has sufficient content (readyState={})", ready_state);
return Ok(());
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
pub async fn evaluate(&self, conn: &mut Connection, js: &str) -> Result<Value> {
let result = conn
.call_with_session(
&self.session_id,
"Runtime.evaluate",
json!({
"expression": js,
"returnByValue": true,
}),
)
.await?;
Ok(result)
}
pub async fn extract_text(&self, conn: &mut Connection) -> Result<String> {
let result = self
.evaluate(conn, "document.body?.innerText || ''")
.await?;
let text = result["result"]["value"].as_str().unwrap_or("").to_string();
Ok(text)
}
pub async fn extract_title(&self, conn: &mut Connection) -> Result<String> {
let result = self.evaluate(conn, "document.title || ''").await?;
let title = result["result"]["value"].as_str().unwrap_or("").to_string();
Ok(title)
}
pub async fn extract_html(&self, conn: &mut Connection) -> Result<String> {
let result = self
.evaluate(conn, "document.documentElement?.outerHTML || ''")
.await?;
let html = result["result"]["value"].as_str().unwrap_or("").to_string();
Ok(html)
}
pub async fn evaluate_all(
&self,
conn: &mut Connection,
scripts: &[&str],
) -> Result<Vec<Value>> {
let mut results = Vec::with_capacity(scripts.len());
for script in scripts {
let result = self.evaluate(conn, script).await?;
results.push(result);
}
Ok(results)
}
pub async fn close(self, conn: &mut Connection) {
let _ = conn
.call_with_session(
&self.session_id,
"Runtime.evaluate",
json!({
"expression": "window.close()",
}),
)
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
let _ = conn
.call(
"Target.closeTarget",
json!({
"targetId": self.target_id,
}),
)
.await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tab_struct_creation() {
let tab = Tab {
session_id: "test-session".into(),
target_id: "test-target".into(),
};
assert_eq!(tab.session_id, "test-session");
assert_eq!(tab.target_id, "test-target");
}
}