1use crate::connection::CdpEvent;
2use crate::error::{CdpError, Result};
3use crate::session::Session;
4use serde_json::{Value, json};
5use std::time::Duration;
6use tracing;
7
8#[derive(Debug, Clone)]
10pub struct Tab {
11 pub target_id: String,
12 pub session_id: Option<String>,
13}
14
15impl Tab {
16 pub async fn create(session: &Session, url: &str) -> Result<Self> {
18 let conn = session.connection();
19 let result = conn
20 .call("Target.createTarget", json!({ "url": url }), None)
21 .await?;
22
23 if let Some(session_id) = result.get("sessionId").and_then(|v| v.as_str()) {
25 let target_id = result
26 .get("targetId")
27 .and_then(|v| v.as_str())
28 .ok_or_else(|| CdpError::CdpCallFailed {
29 method: "Target.createTarget".into(),
30 detail: "no targetId in response".into(),
31 })?
32 .to_string();
33
34 tracing::debug!(
35 "Created tab: target={target}, session={session_id}",
36 target = target_id
37 );
38
39 return Ok(Tab {
40 session_id: Some(session_id.to_string()),
41 target_id,
42 });
43 }
44
45 if let Some(target_id) = result.get("targetId").and_then(|v| v.as_str()) {
48 tracing::warn!("Target.createTarget returned targetId without sessionId, attaching...");
49 let attach = conn
50 .call(
51 "Target.attachToTarget",
52 json!({
53 "targetId": target_id,
54 "flatten": true,
55 }),
56 None,
57 )
58 .await
59 .map_err(|e| CdpError::CdpCallFailed {
60 method: "Target.attachToTarget".into(),
61 detail: format!("attach failed: {e}"),
62 })?;
63
64 let session_id = attach
65 .get("sessionId")
66 .and_then(|v| v.as_str())
67 .ok_or_else(|| CdpError::CdpCallFailed {
68 method: "Target.attachToTarget".into(),
69 detail: "no sessionId in attach response".into(),
70 })?
71 .to_string();
72
73 tracing::info!("Attached to created target: {target_id} (session={session_id})",);
74
75 return Ok(Tab {
76 session_id: Some(session_id),
77 target_id: target_id.to_string(),
78 });
79 }
80
81 Err(CdpError::CdpCallFailed {
82 method: "Target.createTarget".into(),
83 detail: "could not create tab: no targetId or sessionId in response".into(),
84 })
85 }
86
87 pub async fn navigate(&self, session: &Session, url: &str) -> Result<()> {
89 session.navigate(self, url).await
90 }
91
92 pub async fn evaluate(&self, session: &Session, js: &str) -> Result<Value> {
94 let conn = session.connection();
95 let sid = self.session_id.as_deref();
96
97 let result = conn
98 .call(
99 "Runtime.evaluate",
100 json!({
101 "expression": js,
102 "returnByValue": true,
103 }),
104 sid,
105 )
106 .await?;
107
108 Ok(result)
109 }
110
111 pub async fn wait_loaded(&self, session: &Session, timeout: Duration) -> Result<()> {
113 let sid = self.session_id.clone();
114
115 session
116 .wait_for(
117 "Page.lifecycleEvent",
118 move |evt: &CdpEvent| {
119 let session_match = match &sid {
121 Some(sid) => evt.session_id.as_deref() == Some(sid.as_str()),
122 None => true,
123 };
124 let name_match =
126 evt.params.get("name").and_then(|v| v.as_str()) == Some("networkIdle");
127 session_match && name_match
128 },
129 timeout,
130 )
131 .await?;
132
133 Ok(())
134 }
135
136 pub async fn title(&self, session: &Session) -> Result<String> {
138 match self.evaluate(session, "document.title || ''").await {
139 Ok(val) => {
140 let title = val
141 .get("result")
142 .and_then(|r| r.get("value"))
143 .and_then(|v| v.as_str())
144 .unwrap_or_else(|| {
145 tracing::warn!("failed to extract title");
146 ""
147 });
148 Ok(title.to_string())
149 }
150 Err(e) => {
151 tracing::warn!("failed to extract title: {e}");
152 Ok(String::new())
153 }
154 }
155 }
156
157 pub async fn close(self, session: &Session) -> Result<()> {
159 let conn = session.connection();
160 let sid = self.session_id.as_deref();
161
162 let _ = conn
164 .call(
165 "Runtime.evaluate",
166 json!({
167 "expression": "window.close()",
168 "userGesture": true,
169 }),
170 sid,
171 )
172 .await;
173
174 tokio::time::sleep(Duration::from_millis(100)).await;
176
177 conn.call(
179 "Target.closeTarget",
180 json!({ "targetId": self.target_id }),
181 None,
182 )
183 .await?;
184 Ok(())
185 }
186}