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 conn.call("Runtime.evaluate", json!({
96 "expression": js,
97 "returnByValue": true,
98 "awaitPromise": true,
99 "timeout": 10000,
100 }), sid).await
101 }
102
103 pub async fn wait_loaded(&self, session: &Session, timeout: Duration) -> Result<()> {
105 let sid = self.session_id.clone();
106
107 session
108 .wait_for(
109 "Page.lifecycleEvent",
110 move |evt: &CdpEvent| {
111 let session_match = match &sid {
113 Some(sid) => evt.session_id.as_deref() == Some(sid.as_str()),
114 None => true,
115 };
116 let name_match =
118 evt.params.get("name").and_then(|v| v.as_str()) == Some("networkIdle");
119 session_match && name_match
120 },
121 timeout,
122 )
123 .await?;
124
125 Ok(())
126 }
127
128 pub async fn title(&self, session: &Session) -> Result<String> {
130 match self.evaluate(session, "document.title || ''").await {
131 Ok(val) => {
132 let title = val
133 .get("result")
134 .and_then(|r| r.get("value"))
135 .and_then(|v| v.as_str())
136 .unwrap_or_else(|| {
137 tracing::warn!("failed to extract title");
138 ""
139 });
140 Ok(title.to_string())
141 }
142 Err(e) => {
143 tracing::warn!("failed to extract title: {e}");
144 Ok(String::new())
145 }
146 }
147 }
148
149 pub async fn close(self, session: &Session) -> Result<()> {
151 let conn = session.connection();
152 let sid = self.session_id.as_deref();
153
154 let _ = conn
156 .call(
157 "Runtime.evaluate",
158 json!({
159 "expression": "window.close()",
160 "userGesture": true,
161 }),
162 sid,
163 )
164 .await;
165
166 tokio::time::sleep(Duration::from_millis(100)).await;
168
169 conn.call(
171 "Target.closeTarget",
172 json!({ "targetId": self.target_id }),
173 None,
174 )
175 .await?;
176 Ok(())
177 }
178}
179
180