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