use std::sync::Arc;
pub use chromiumoxide::browser::Browser;
pub use chromiumoxide::error::CdpError;
pub use chromiumoxide::page::Page;
#[derive(Clone)]
pub struct BrowserContext {
browser: Arc<Browser>,
page: Arc<Page>,
}
impl BrowserContext {
pub fn new(browser: Arc<Browser>, page: Arc<Page>) -> Self {
Self { browser, page }
}
pub fn page(&self) -> &Arc<Page> {
&self.page
}
pub fn browser(&self) -> &Arc<Browser> {
&self.browser
}
pub async fn new_page(&self) -> Result<Arc<Page>, CdpError> {
let page = self.browser.new_page("about:blank").await?;
Ok(Arc::new(page))
}
pub async fn new_page_with_url(&self, url: &str) -> Result<Arc<Page>, CdpError> {
let page = self.browser.new_page(url).await?;
Ok(Arc::new(page))
}
pub async fn clone_page(&self) -> Result<BrowserContext, CdpError> {
let url = self
.page
.url()
.await?
.unwrap_or_else(|| "about:blank".to_string());
let new_page = self.browser.new_page(&url).await?;
Ok(BrowserContext {
browser: self.browser.clone(),
page: Arc::new(new_page),
})
}
pub async fn navigate(&self, url: &str) -> Result<(), CdpError> {
self.page.goto(url).await?;
Ok(())
}
pub async fn url(&self) -> Result<Option<String>, CdpError> {
self.page.url().await
}
pub async fn html(&self) -> Result<String, CdpError> {
self.page.content().await
}
pub async fn screenshot(&self) -> Result<Vec<u8>, CdpError> {
self.page
.screenshot(
chromiumoxide::page::ScreenshotParams::builder()
.full_page(true)
.build(),
)
.await
}
pub async fn screenshot_viewport(&self) -> Result<Vec<u8>, CdpError> {
self.page
.screenshot(
chromiumoxide::page::ScreenshotParams::builder()
.full_page(false)
.build(),
)
.await
}
pub async fn click(&self, selector: &str) -> Result<(), CdpError> {
let element = self.page.find_element(selector).await?;
element.click().await?;
Ok(())
}
pub async fn click_all(&self, selector: &str) -> Result<usize, CdpError> {
let elements = self.page.find_elements(selector).await?;
let count = elements.len();
for element in elements {
let _ = element.click().await;
}
Ok(count)
}
pub async fn click_point(&self, x: f64, y: f64) -> Result<(), CdpError> {
use chromiumoxide::layout::Point;
self.page.click_smooth(Point::new(x, y)).await?;
Ok(())
}
pub async fn click_hold(&self, selector: &str, hold_ms: u64) -> Result<(), CdpError> {
let element = self.page.find_element(selector).await?;
let point = element.clickable_point().await?;
self.page.move_mouse_smooth(point).await?;
self.page
.click_and_hold(point, std::time::Duration::from_millis(hold_ms))
.await?;
Ok(())
}
pub async fn click_hold_point(&self, x: f64, y: f64, hold_ms: u64) -> Result<(), CdpError> {
use chromiumoxide::layout::Point;
let point = Point::new(x, y);
self.page.move_mouse_smooth(point).await?;
self.page
.click_and_hold(point, std::time::Duration::from_millis(hold_ms))
.await?;
Ok(())
}
pub async fn click_drag(
&self,
from_selector: &str,
to_selector: &str,
modifier: Option<i64>,
) -> Result<(), CdpError> {
let from_elem = self.page.find_element(from_selector).await?;
let to_elem = self.page.find_element(to_selector).await?;
let from_point = from_elem.clickable_point().await?;
let to_point = to_elem.clickable_point().await?;
self.click_drag_point(
(from_point.x, from_point.y),
(to_point.x, to_point.y),
modifier,
)
.await
}
pub async fn click_drag_point(
&self,
from: (f64, f64),
to: (f64, f64),
modifier: Option<i64>,
) -> Result<(), CdpError> {
use chromiumoxide::layout::Point;
let from_point = Point::new(from.0, from.1);
let to_point = Point::new(to.0, to.1);
match modifier {
Some(m) => {
self.page
.click_and_drag_smooth_with_modifier(from_point, to_point, m)
.await?
}
None => {
self.page
.click_and_drag_smooth(from_point, to_point)
.await?
}
};
Ok(())
}
pub async fn click_all_clickable(&self) -> Result<usize, CdpError> {
let script = r#"
Array.from(document.querySelectorAll('a, button, [onclick], [role="button"], input[type="submit"], input[type="button"]'))
.filter(el => {
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetParent !== null;
})
.length
"#;
let count: usize = self
.page
.evaluate(script)
.await?
.into_value()
.map_err(|e| {
CdpError::ChromeMessage(format!("Failed to count clickable elements: {}", e))
})?;
let click_script = r#"
const elements = Array.from(document.querySelectorAll('a, button, [onclick], [role="button"], input[type="submit"], input[type="button"]'))
.filter(el => {
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetParent !== null;
});
elements.forEach(el => { try { el.click(); } catch(e) {} });
elements.length
"#;
let clicked: usize = self
.page
.evaluate(click_script)
.await?
.into_value()
.unwrap_or(0);
Ok(clicked.min(count))
}
pub async fn type_text(&self, selector: &str, text: &str) -> Result<(), CdpError> {
let element = self.page.find_element(selector).await?;
element.click().await?;
element.type_str(text).await?;
Ok(())
}
pub async fn wait_for(&self, selector: &str) -> Result<(), CdpError> {
self.page.find_element(selector).await?;
Ok(())
}
pub async fn wait_for_timeout(&self, selector: &str, timeout_ms: u64) -> Result<(), CdpError> {
let timeout = std::time::Duration::from_millis(timeout_ms);
tokio::time::timeout(timeout, self.page.find_element(selector))
.await
.map_err(|_| CdpError::Timeout)?
.map(|_| ())
}
pub async fn wait_for_navigation(&self) -> Result<(), CdpError> {
self.page.evaluate("new Promise(r => { if (document.readyState === 'complete') r(); else window.addEventListener('load', r); })").await?;
Ok(())
}
pub async fn wait_for_dom(
&self,
selector: Option<&str>,
timeout_ms: u32,
) -> Result<(), CdpError> {
let sel = selector.unwrap_or("body");
let script = format!(
r#"
new Promise((resolve, reject) => {{
const timeout = {};
const target = document.querySelector('{}');
if (!target) {{ resolve(); return; }}
let timer;
const observer = new MutationObserver(() => {{
clearTimeout(timer);
timer = setTimeout(() => {{
observer.disconnect();
resolve();
}}, 100);
}});
observer.observe(target, {{ childList: true, subtree: true, attributes: true }});
timer = setTimeout(() => {{
observer.disconnect();
resolve();
}}, 100);
setTimeout(() => {{
observer.disconnect();
resolve();
}}, timeout);
}})
"#,
timeout_ms, sel
);
self.page.evaluate(script).await?;
Ok(())
}
pub async fn wait_and_click(&self, selector: &str) -> Result<(), CdpError> {
let element = self.page.find_element(selector).await?;
element.click().await?;
Ok(())
}
pub async fn evaluate<T: serde::de::DeserializeOwned>(
&self,
script: &str,
) -> Result<T, CdpError> {
self.page
.evaluate(script)
.await?
.into_value()
.map_err(|e| CdpError::ChromeMessage(format!("JSON conversion error: {}", e)))
}
pub async fn execute(&self, script: &str) -> Result<(), CdpError> {
self.page.evaluate(script).await?;
Ok(())
}
pub async fn scroll_x(&self, pixels: i32) -> Result<(), CdpError> {
let script = format!("window.scrollBy({}, 0)", pixels);
self.page.evaluate(script).await?;
Ok(())
}
pub async fn scroll_y(&self, pixels: i32) -> Result<(), CdpError> {
let script = format!("window.scrollBy(0, {})", pixels);
self.page.evaluate(script).await?;
Ok(())
}
pub async fn infinite_scroll(&self, max_scrolls: u32) -> Result<usize, CdpError> {
let script = r#"
(async function() {
const maxScrolls = arguments[0];
let lastHeight = document.body.scrollHeight;
let scrollCount = 0;
while (scrollCount < maxScrolls) {
window.scrollTo(0, document.body.scrollHeight);
await new Promise(r => setTimeout(r, 1000));
const newHeight = document.body.scrollHeight;
if (newHeight === lastHeight) break;
lastHeight = newHeight;
scrollCount++;
}
return scrollCount;
})
"#;
let count: usize = self
.page
.evaluate(format!("({script})({max_scrolls})"))
.await?
.into_value()
.unwrap_or(0);
Ok(count)
}
pub async fn fill(&self, selector: &str, value: &str) -> Result<(), CdpError> {
let element = self.page.find_element(selector).await?;
element.click().await?;
element.click().await?;
element.click().await?;
use chromiumoxide::cdp::browser_protocol::input::{
DispatchKeyEventParams, DispatchKeyEventType,
};
self.page
.execute(
DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyDown)
.key("a")
.modifiers(2) .build()
.map_err(|e| CdpError::ChromeMessage(format!("key event build: {e}")))?,
)
.await?;
self.page
.execute(
DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyUp)
.key("a")
.build()
.map_err(|e| CdpError::ChromeMessage(format!("key event build: {e}")))?,
)
.await?;
element.type_str(value).await?;
Ok(())
}
pub async fn find_elements(
&self,
selector: &str,
) -> Result<Vec<chromiumoxide::element::Element>, CdpError> {
self.page.find_elements(selector).await
}
pub async fn get_element_bounds(
&self,
selector: &str,
) -> Result<Option<(f64, f64, f64, f64)>, CdpError> {
let escaped_selector = serde_json::to_string(selector).unwrap_or_else(|_| {
format!(
"\"{}\"",
selector.replace('\\', "\\\\").replace('"', "\\\"")
)
});
let script = format!(
r#"
(function() {{
const el = document.querySelector({});
if (!el) return null;
const rect = el.getBoundingClientRect();
return [rect.x, rect.y, rect.width, rect.height];
}})()
"#,
escaped_selector
);
let result: Option<Vec<f64>> = self
.page
.evaluate(script)
.await?
.into_value()
.map_err(|e| CdpError::ChromeMessage(format!("Failed to get bounds: {}", e)))?;
Ok(result.and_then(|v| {
if v.len() >= 4 {
Some((v[0], v[1], v[2], v[3]))
} else {
None
}
}))
}
pub async fn close(&self) -> Result<(), CdpError> {
self.page.evaluate("window.close()").await?;
Ok(())
}
pub fn set_page(&mut self, page: Arc<Page>) {
self.page = page;
}
pub fn with_page(&self, page: Arc<Page>) -> Self {
Self {
browser: self.browser.clone(),
page,
}
}
}
impl std::fmt::Debug for BrowserContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BrowserContext")
.field("browser", &"Browser { ... }")
.field("page", &"Page { ... }")
.finish()
}
}