1use crate::connection::CdpClient;
2use crate::element::Element;
3use crate::error::{XcelerateResult, XcelerateError};
4use std::sync::Arc;
5use browser_protocol::page::{GetLayoutMetricsParams, CaptureScreenshotParams, ReloadParams, NavigateParams, EnableParams};
6use browser_protocol::emulation::{SetDeviceMetricsOverrideParams, ClearDeviceMetricsOverrideParams};
7
8#[derive(uniffi::Object)]
9pub struct Page {
10 pub(crate) client: Arc<CdpClient>,
11 pub(crate) session_id: String,
12}
13
14#[uniffi::export(async_runtime = "tokio")]
15impl Page {
16 pub async fn find_element(self: Arc<Self>, selector: String) -> XcelerateResult<Arc<Element>> {
18 let js = format!("document.querySelector('{}')", selector);
19
20 self.client.execute_with_session(
22 Some(&self.session_id),
23 js_protocol::runtime::EvaluateParams {
24 expression: js,
25 ..Default::default()
26 }
27 ).await.and_then(|result| {
28 if let Some(obj_id) = result.result.objectId {
29 Ok(Arc::new(Element {
30 page: self.clone(),
31 object_id: obj_id,
32 }))
33 } else {
34 Err(XcelerateError::NotFound(selector))
35 }
36 })
37 }
38
39 pub async fn wait_for_selector(self: Arc<Self>, selector: String) -> XcelerateResult<Arc<Element>> {
41 let start = std::time::Instant::now();
42 let timeout = std::time::Duration::from_secs(30);
43
44 while start.elapsed() < timeout {
45 if let Ok(element) = self.clone().find_element(selector.clone()).await {
46 return Ok(element);
47 }
48 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
49 }
50
51 Err(XcelerateError::NotFound(format!("Timeout waiting for selector: {}", selector)))
52 }
53
54 pub async fn wait_for_navigation(&self) -> XcelerateResult<()> {
56 let start = std::time::Instant::now();
57 let timeout = std::time::Duration::from_secs(30);
58
59 while start.elapsed() < timeout {
60 let res = self.client.execute_with_session(
62 Some(&self.session_id),
63 js_protocol::runtime::EvaluateParams {
64 expression: "document.readyState".into(),
65 ..Default::default()
66 }
67 ).await?;
68
69 if res.result.value.map_or(false, |v| v.as_str() == Some("complete")) {
70 return Ok(());
71 }
72 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
73 }
74
75 Err(XcelerateError::NotFound("Navigation timeout".into()))
76 }
77
78 pub async fn reload(&self) -> XcelerateResult<()> {
80 self.client.execute_with_session(
81 Some(&self.session_id),
82 ReloadParams { ..Default::default() }
83 ).await.map(|_| ())
84 }
85
86 pub async fn navigate(&self, url: String) -> XcelerateResult<()> {
88 self.client.execute_with_session(
89 Some(&self.session_id),
90 NavigateParams {
91 url,
92 ..Default::default()
93 }
94 ).await.map(|_| ())
95 }
96
97 pub async fn title(&self) -> XcelerateResult<String> {
99 let res = self.client.execute_with_session(
100 Some(&self.session_id),
101 js_protocol::runtime::EvaluateParams {
102 expression: "document.title".into(),
103 ..Default::default()
104 }
105 ).await?;
106 Ok(res.result.value.and_then(|v| v.as_str().map(|s| s.to_string())).unwrap_or_default())
107 }
108
109 pub async fn content(&self) -> XcelerateResult<String> {
111 let res = self.client.execute_with_session(
112 Some(&self.session_id),
113 js_protocol::runtime::EvaluateParams {
114 expression: "document.documentElement.outerHTML".into(),
115 ..Default::default()
116 }
117 ).await?;
118 Ok(res.result.value.and_then(|v| v.as_str().map(|s| s.to_string())).unwrap_or_default())
119 }
120
121 pub async fn screenshot(&self) -> XcelerateResult<Vec<u8>> {
123 use base64::{Engine as _, engine::general_purpose};
124 let res = self.client.execute_with_session(
125 Some(&self.session_id),
126 CaptureScreenshotParams { ..Default::default() }
127 ).await?;
128 Ok(general_purpose::STANDARD.decode(res.data).map_err(|_| XcelerateError::InternalError)?)
129 }
130
131 pub async fn screenshot_full(&self) -> XcelerateResult<Vec<u8>> {
133 use base64::{Engine as _, engine::general_purpose};
134
135 let _ = self.client.execute_with_session(
136 Some(&self.session_id),
137 EnableParams { ..Default::default() }
138 ).await?;
139
140 let metrics = self.client.execute_with_session(
141 Some(&self.session_id),
142 GetLayoutMetricsParams {}
143 ).await?;
144
145 let width = metrics.contentSize.width as u64;
146 let height = metrics.contentSize.height as i64;
147
148 let mut params = SetDeviceMetricsOverrideParams { ..Default::default() };
149 params.width = width;
150 params.height = height;
151 params.deviceScaleFactor = 1.0;
152 params.mobile = false;
153
154 self.client.execute_with_session(
155 Some(&self.session_id),
156 params
157 ).await?;
158
159 let res = self.client.execute_with_session(
160 Some(&self.session_id),
161 CaptureScreenshotParams { ..Default::default() }
162 ).await?;
163
164 let _ = self.client.execute_with_session(
165 Some(&self.session_id),
166 ClearDeviceMetricsOverrideParams {}
167 ).await?;
168
169 Ok(general_purpose::STANDARD.decode(res.data).map_err(|_| XcelerateError::InternalError)?)
170 }
171
172 pub async fn pdf(&self) -> XcelerateResult<Vec<u8>> {
174 use base64::{Engine as _, engine::general_purpose};
175 let res = self.client.execute_with_session(
176 Some(&self.session_id),
177 browser_protocol::page::PrintToPDFParams { ..Default::default() }
178 ).await?;
179 Ok(general_purpose::STANDARD.decode(res.data).map_err(|_| XcelerateError::InternalError)?)
180 }
181
182 pub async fn add_script_to_evaluate_on_new_document(&self, source: String) -> XcelerateResult<String> {
184 let res = self.client.execute_with_session(
185 Some(&self.session_id),
186 browser_protocol::page::AddScriptToEvaluateOnNewDocumentParams {
187 source,
188 ..Default::default()
189 }
190 ).await?;
191 Ok(res.identifier)
192 }
193
194 pub async fn go_back(&self) -> XcelerateResult<()> {
196 let history = self.client.execute_with_session(
197 Some(&self.session_id),
198 browser_protocol::page::GetNavigationHistoryParams {}
199 ).await?;
200
201 if history.currentIndex > 0 {
202 let entry = &history.entries[history.currentIndex as usize - 1];
203 self.client.execute_with_session(
204 Some(&self.session_id),
205 browser_protocol::page::NavigateToHistoryEntryParams { entryId: entry.id }
206 ).await?;
207 }
208 Ok(())
209 }
210}