gbserver/gb-automation/src/web.rs

185 lines
5.1 KiB
Rust
Raw Normal View History

2024-12-22 20:56:52 -03:00
use gb_core::{Result, Error};
use async_recursion::async_recursion;
use chromiumoxide::{
Browser, BrowserConfig,
cdp::browser_protocol::page::ScreenshotFormat,
Page,
};
use std::{sync::Arc, time::Duration};
use tokio::sync::Mutex;
use tracing::{instrument, error};
pub struct WebAutomation {
browser: Arc<Browser>,
pages: Arc<Mutex<Vec<Page>>>,
}
impl WebAutomation {
#[instrument]
pub async fn new() -> Result<Self> {
let config = BrowserConfig::builder()
.with_head()
.window_size(1920, 1080)
.build()?;
let (browser, mut handler) = Browser::launch(config)
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to launch browser: {}", e)))?;
2024-12-22 20:56:52 -03:00
tokio::spawn(async move {
while let Some(h) = handler.next().await {
if let Err(e) = h {
error!("Browser handler error: {}", e);
}
}
});
Ok(Self {
browser: Arc::new(browser),
pages: Arc::new(Mutex::new(Vec::new())),
})
}
#[instrument(skip(self))]
pub async fn new_page(&self) -> Result<Page> {
let page = self.browser.new_page()
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to create page: {}", e)))?;
2024-12-22 20:56:52 -03:00
let mut pages = self.pages.lock().await;
pages.push(page.clone());
Ok(page)
}
#[instrument(skip(self))]
pub async fn navigate(&self, page: &Page, url: &str) -> Result<()> {
page.goto(url)
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to navigate: {}", e)))?;
2024-12-22 20:56:52 -03:00
page.wait_for_navigation()
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to wait for navigation: {}", e)))?;
2024-12-22 20:56:52 -03:00
Ok(())
}
#[instrument(skip(self))]
pub async fn get_element(&self, page: &Page, selector: &str) -> Result<Element> {
let element = page.find_element(selector)
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to find element: {}", e)))?;
2024-12-22 20:56:52 -03:00
Ok(Element { inner: element })
}
#[instrument(skip(self))]
pub async fn click(&self, element: &Element) -> Result<()> {
element.inner.click()
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to click: {}", e)))?;
2024-12-22 20:56:52 -03:00
Ok(())
}
#[instrument(skip(self))]
pub async fn type_text(&self, element: &Element, text: &str) -> Result<()> {
element.inner.type_str(text)
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to type text: {}", e)))?;
2024-12-22 20:56:52 -03:00
Ok(())
}
#[instrument(skip(self))]
pub async fn screenshot(&self, page: &Page, path: &str) -> Result<Vec<u8>> {
let screenshot = page.screenshot(ScreenshotFormat::PNG, None, true)
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to take screenshot: {}", e)))?;
2024-12-22 20:56:52 -03:00
Ok(screenshot)
}
#[instrument(skip(self))]
pub async fn wait_for_selector(&self, page: &Page, selector: &str) -> Result<()> {
page.wait_for_element(selector)
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to wait for selector: {}", e)))?;
2024-12-22 20:56:52 -03:00
Ok(())
}
#[instrument(skip(self))]
#[async_recursion]
pub async fn wait_for_network_idle(&self, page: &Page) -> Result<()> {
let mut retry_count = 0;
let max_retries = 10;
while retry_count < max_retries {
if page.wait_for_network_idle(Duration::from_secs(5))
.await
.is_ok()
{
return Ok(());
}
retry_count += 1;
tokio::time::sleep(Duration::from_secs(1)).await;
}
2024-12-23 00:54:50 -03:00
Err(Error::internal("Network did not become idle".to_string()))
2024-12-22 20:56:52 -03:00
}
}
pub struct Element {
inner: chromiumoxide::Element,
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::*;
#[fixture]
async fn automation() -> WebAutomation {
WebAutomation::new().await.unwrap()
}
#[rstest]
#[tokio::test]
async fn test_navigation(automation: WebAutomation) -> Result<()> {
let page = automation.new_page().await?;
automation.navigate(&page, "https://example.com").await?;
let title = page.title()
.await
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to get title: {}", e)))?;
2024-12-22 20:56:52 -03:00
assert!(title.contains("Example"));
Ok(())
}
#[rstest]
#[tokio::test]
async fn test_element_interaction(automation: WebAutomation) -> Result<()> {
let page = automation.new_page().await?;
automation.navigate(&page, "https://example.com").await?;
let element = automation.get_element(&page, "h1").await?;
automation.click(&element).await?;
Ok(())
}
#[rstest]
#[tokio::test]
async fn test_screenshot(automation: WebAutomation) -> Result<()> {
let page = automation.new_page().await?;
automation.navigate(&page, "https://example.com").await?;
let screenshot = automation.screenshot(&page, "test.png").await?;
Ok(())
}
}