1use crate::connection::Connection;
2use crate::error::{CdpError, Result};
3use serde_json::{Value, json};
4use std::time::Duration;
5use tracing;
6
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 if let Some(target_id) = result.get("targetId").and_then(|v| v.as_str()) {
51 tracing::warn!(
52 "Target.createTarget returned targetId without sessionId, attaching..."
53 );
54 let attach = conn
55 .call(
56 "Target.attachToTarget",
57 json!({
58 "targetId": target_id,
59 "flatten": true,
60 }),
61 )
62 .await
63 .map_err(|e| CdpError::CommandFailed {
64 method: "Target.attachToTarget".into(),
65 msg: format!("attach failed: {e}"),
66 })?;
67
68 let session_id = attach
69 .get("sessionId")
70 .and_then(|v| v.as_str())
71 .ok_or_else(|| CdpError::CommandFailed {
72 method: "Target.attachToTarget".into(),
73 msg: "no sessionId in attach response".into(),
74 })?
75 .to_string();
76
77 tracing::info!(
78 "Attached to created target: {} (session={})",
79 target_id,
80 session_id
81 );
82
83 return Ok(Tab {
84 session_id,
85 target_id: target_id.to_string(),
86 });
87 }
88 tracing::warn!(
89 "Target.createTarget returned neither sessionId nor targetId, cannot attach"
90 );
91 } else {
92 tracing::warn!("Target.createTarget failed");
93 }
94
95 Err(CdpError::CommandFailed {
96 method: "Target.createTarget".into(),
97 msg: "could not create tab: all methods exhausted".into(),
98 })
99 }
100
101 pub async fn navigate(&self, conn: &mut Connection, url: &str) -> Result<()> {
103 tracing::info!("Navigating to: {}", url);
104
105 let result = conn
106 .call_with_session(&self.session_id, "Page.enable", json!({}))
107 .await?;
108 tracing::debug!("Page.enable: {:?}", result);
109
110 let _result = conn
111 .call_with_session(
112 &self.session_id,
113 "Page.navigate",
114 json!({
115 "url": url,
116 }),
117 )
118 .await?;
119
120 self.wait_for_page_load(conn, 10000).await
122 }
123
124 async fn wait_for_page_load(&self, conn: &mut Connection, timeout_ms: u64) -> Result<()> {
126 let start = std::time::Instant::now();
127 let timeout = Duration::from_millis(timeout_ms);
128
129 loop {
130 if start.elapsed() > timeout {
131 tracing::warn!("Page load timed out after {}ms", timeout_ms);
132 return Ok(()); }
134
135 let result = conn
136 .call_with_session(
137 &self.session_id,
138 "Runtime.evaluate",
139 json!({
140 "expression": "document.readyState",
141 "returnByValue": true,
142 }),
143 )
144 .await?;
145
146 let ready_state = result["result"]["value"].as_str().unwrap_or("unknown");
147
148 if ready_state == "complete" {
149 tracing::debug!("Page loaded (readyState=complete)");
150 return Ok(());
151 }
152
153 let has_content = conn.call_with_session(&self.session_id, "Runtime.evaluate", json!({
155 "expression": "document.body ? document.body.innerText.length > 100 : false",
156 "returnByValue": true,
157 })).await?;
158
159 if has_content["result"]["value"].as_bool().unwrap_or(false) {
160 tracing::debug!("Page has sufficient content (readyState={})", ready_state);
161 return Ok(());
162 }
163
164 tokio::time::sleep(Duration::from_millis(200)).await;
165 }
166 }
167
168 pub async fn evaluate(&self, conn: &mut Connection, js: &str) -> Result<Value> {
170 let result = conn
171 .call_with_session(
172 &self.session_id,
173 "Runtime.evaluate",
174 json!({
175 "expression": js,
176 "returnByValue": true,
177 }),
178 )
179 .await?;
180
181 Ok(result)
182 }
183
184 pub async fn extract_text(&self, conn: &mut Connection) -> Result<String> {
186 let result = self
187 .evaluate(conn, "document.body?.innerText || ''")
188 .await?;
189 let text = result["result"]["value"].as_str().unwrap_or("").to_string();
190 Ok(text)
191 }
192
193 pub async fn extract_title(&self, conn: &mut Connection) -> Result<String> {
195 let result = self.evaluate(conn, "document.title || ''").await?;
196 let title = result["result"]["value"].as_str().unwrap_or("").to_string();
197 Ok(title)
198 }
199
200 pub async fn extract_html(&self, conn: &mut Connection) -> Result<String> {
202 let result = self
203 .evaluate(conn, "document.documentElement?.outerHTML || ''")
204 .await?;
205 let html = result["result"]["value"].as_str().unwrap_or("").to_string();
206 Ok(html)
207 }
208
209 pub async fn evaluate_all(
211 &self,
212 conn: &mut Connection,
213 scripts: &[&str],
214 ) -> Result<Vec<Value>> {
215 let mut results = Vec::with_capacity(scripts.len());
216 for script in scripts {
217 let result = self.evaluate(conn, script).await?;
218 results.push(result);
219 }
220 Ok(results)
221 }
222
223 pub async fn close(self, conn: &mut Connection) {
226 let _ = conn
227 .call_with_session(
228 &self.session_id,
229 "Runtime.evaluate",
230 json!({
231 "expression": "window.close()",
232 }),
233 )
234 .await;
235
236 tokio::time::sleep(Duration::from_millis(200)).await;
238
239 let _ = conn
240 .call(
241 "Target.closeTarget",
242 json!({
243 "targetId": self.target_id,
244 }),
245 )
246 .await;
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_tab_struct_creation() {
256 let tab = Tab {
257 session_id: "test-session".into(),
258 target_id: "test-target".into(),
259 };
260 assert_eq!(tab.session_id, "test-session");
261 assert_eq!(tab.target_id, "test-target");
262 }
263}