Skip to main content

xcelerate_core/
page.rs

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    /// Finds an element matching the CSS selector.
17    pub async fn find_element(self: Arc<Self>, selector: String) -> XcelerateResult<Arc<Element>> {
18        let js = format!("document.querySelector('{}')", selector);
19        
20        // Evaluate returns complex JSON, we handle it internally
21        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    /// Waits for an element matching the selector to appear in the DOM.
40    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    /// Waits for the page to finish loading.
55    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            // Internal call to evaluate
61            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    /// Reloads the page.
79    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    /// Navigates to a URL.
87    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    /// Returns the page title.
98    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    /// Returns the full HTML content of the page.
110    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    /// Captures a screenshot of the page as a PNG.
122    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    /// Captures a full-page screenshot by overriding device metrics.
132    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    /// Captures a PDF of the page.
173    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    /// Evaluates a script on every new document.
183    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    /// Navigates back in history.
195    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}