1use crate::connection::Connection;
2use crate::error::{CdpError, Result};
3use serde_json::{Value, json};
4use std::time::Duration;
5use tracing;
6use url::Url;
7
8pub struct Tab {
10 pub session_id: String,
12 pub target_id: String,
14}
15
16impl Tab {
17 pub async fn create(conn: &mut Connection, ws_url: &str, url: &str) -> Result<Self> {
19 let result = conn
20 .call(
21 "Target.createTarget",
22 json!({
23 "url": url,
24 "newWindow": false,
25 }),
26 )
27 .await;
28
29 if let Ok(result) = result {
30 if let Some(session_id) = result.get("sessionId").and_then(|v| v.as_str()) {
31 let target_id = result
32 .get("targetId")
33 .and_then(|v| v.as_str())
34 .ok_or_else(|| CdpError::CommandFailed {
35 method: "Target.createTarget".into(),
36 msg: "no targetId in response".into(),
37 })?
38 .to_string();
39
40 tracing::debug!("Created tab: target={}, session={}", target_id, session_id);
41
42 return Ok(Tab {
43 session_id: session_id.to_string(),
44 target_id,
45 });
46 }
47 tracing::warn!(
49 "Target.createTarget returned targetId without sessionId, falling back to HTTP"
50 );
51 } else {
52 tracing::warn!("Target.createTarget failed, trying HTTP /json/new");
53 }
54
55 Self::create_via_http(conn, ws_url, url).await
57 }
58
59 async fn create_via_http(conn: &mut Connection, ws_url: &str, url: &str) -> Result<Self> {
61 let parsed = Url::parse(ws_url).map_err(|_| CdpError::CommandFailed {
63 method: "create_via_http".into(),
64 msg: "invalid ws_url".into(),
65 })?;
66 let port = parsed.port().ok_or_else(|| CdpError::CommandFailed {
67 method: "create_via_http".into(),
68 msg: "no port in ws_url".into(),
69 })?;
70 let host = parsed.host_str().unwrap_or("127.0.0.1");
71
72 let http_url = format!("http://{}:{}/json/new?{}", host, port, url);
73
74 tracing::debug!("HTTP create target: {}", http_url);
75
76 let client = reqwest::Client::new();
77 let resp = client
78 .put(&http_url)
79 .send()
80 .await
81 .map_err(|e| CdpError::CommandFailed {
82 method: "create_via_http".into(),
83 msg: format!("HTTP request failed: {e}"),
84 })?;
85
86 let target_info: Value = resp.json().await.map_err(|e| CdpError::CommandFailed {
87 method: "create_via_http".into(),
88 msg: format!("HTTP response parse failed: {e}"),
89 })?;
90
91 let target_id = target_info
92 .get("id")
93 .and_then(|v| v.as_str())
94 .ok_or_else(|| CdpError::CommandFailed {
95 method: "create_via_http".into(),
96 msg: "no id in HTTP response".into(),
97 })?
98 .to_string();
99
100 tracing::debug!("Created target via HTTP: target={}", target_id);
101
102 let attach_result = conn
103 .call(
104 "Target.attachToTarget",
105 json!({
106 "targetId": target_id,
107 "flatten": true,
108 }),
109 )
110 .await?;
111
112 let session_id = attach_result
113 .get("sessionId")
114 .and_then(|v| v.as_str())
115 .ok_or_else(|| CdpError::CommandFailed {
116 method: "Target.attachToTarget".into(),
117 msg: "no sessionId in response".into(),
118 })?
119 .to_string();
120
121 tracing::info!(
122 "Attached to created target: {} (session={})",
123 target_id,
124 session_id
125 );
126
127 Ok(Tab {
128 session_id,
129 target_id,
130 })
131 }
132
133 pub async fn navigate(&self, conn: &mut Connection, url: &str) -> Result<()> {
135 tracing::info!("Navigating to: {}", url);
136
137 let result = conn
138 .call_with_session(&self.session_id, "Page.enable", json!({}))
139 .await?;
140 tracing::debug!("Page.enable: {:?}", result);
141
142 let _result = conn
143 .call_with_session(
144 &self.session_id,
145 "Page.navigate",
146 json!({
147 "url": url,
148 }),
149 )
150 .await?;
151
152 self.wait_for_page_load(conn, 10000).await
154 }
155
156 async fn wait_for_page_load(&self, conn: &mut Connection, timeout_ms: u64) -> Result<()> {
158 let start = std::time::Instant::now();
159 let timeout = Duration::from_millis(timeout_ms);
160
161 loop {
162 if start.elapsed() > timeout {
163 tracing::warn!("Page load timed out after {}ms", timeout_ms);
164 return Ok(()); }
166
167 let result = conn
168 .call_with_session(
169 &self.session_id,
170 "Runtime.evaluate",
171 json!({
172 "expression": "document.readyState",
173 "returnByValue": true,
174 }),
175 )
176 .await?;
177
178 let ready_state = result["result"]["value"].as_str().unwrap_or("unknown");
179
180 if ready_state == "complete" {
181 tracing::debug!("Page loaded (readyState=complete)");
182 return Ok(());
183 }
184
185 let has_content = conn.call_with_session(&self.session_id, "Runtime.evaluate", json!({
187 "expression": "document.body ? document.body.innerText.length > 100 : false",
188 "returnByValue": true,
189 })).await?;
190
191 if has_content["result"]["value"].as_bool().unwrap_or(false) {
192 tracing::debug!("Page has sufficient content (readyState={})", ready_state);
193 return Ok(());
194 }
195
196 tokio::time::sleep(Duration::from_millis(200)).await;
197 }
198 }
199
200 pub async fn evaluate(&self, conn: &mut Connection, js: &str) -> Result<Value> {
202 let result = conn
203 .call_with_session(
204 &self.session_id,
205 "Runtime.evaluate",
206 json!({
207 "expression": js,
208 "returnByValue": true,
209 }),
210 )
211 .await?;
212
213 Ok(result)
214 }
215
216 pub async fn extract_text(&self, conn: &mut Connection) -> Result<String> {
218 let result = self
219 .evaluate(conn, "document.body?.innerText || ''")
220 .await?;
221 let text = result["result"]["value"].as_str().unwrap_or("").to_string();
222 Ok(text)
223 }
224
225 pub async fn extract_title(&self, conn: &mut Connection) -> Result<String> {
227 let result = self.evaluate(conn, "document.title || ''").await?;
228 let title = result["result"]["value"].as_str().unwrap_or("").to_string();
229 Ok(title)
230 }
231
232 pub async fn extract_html(&self, conn: &mut Connection) -> Result<String> {
234 let result = self
235 .evaluate(conn, "document.documentElement?.outerHTML || ''")
236 .await?;
237 let html = result["result"]["value"].as_str().unwrap_or("").to_string();
238 Ok(html)
239 }
240
241 pub async fn evaluate_all(
243 &self,
244 conn: &mut Connection,
245 scripts: &[&str],
246 ) -> Result<Vec<Value>> {
247 let mut results = Vec::with_capacity(scripts.len());
248 for script in scripts {
249 let result = self.evaluate(conn, script).await?;
250 results.push(result);
251 }
252 Ok(results)
253 }
254
255 pub async fn close(self, conn: &mut Connection) {
258 let _ = conn
259 .call_with_session(
260 &self.session_id,
261 "Runtime.evaluate",
262 json!({
263 "expression": "window.close()",
264 }),
265 )
266 .await;
267
268 tokio::time::sleep(Duration::from_millis(200)).await;
270
271 let _ = conn
272 .call(
273 "Target.closeTarget",
274 json!({
275 "targetId": self.target_id,
276 }),
277 )
278 .await;
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_tab_struct_creation() {
288 let tab = Tab {
289 session_id: "test-session".into(),
290 target_id: "test-target".into(),
291 };
292 assert_eq!(tab.session_id, "test-session");
293 assert_eq!(tab.target_id, "test-target");
294 }
295}