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