use std::sync::{Arc, Mutex};
use futures::StreamExt;
use chromiumoxide::browser::{Browser, BrowserConfig};
use chromiumoxide::page::Page;
use tokio::task::JoinHandle;
#[derive(Debug)]
pub struct BrowserTester {
browser: Browser,
page: Option<Page>,
log_events: Arc<Mutex<Vec<String>>>,
threads: Vec<JoinHandle<Result<(), std::io::Error>>>,
}
impl Drop for BrowserTester {
fn drop(&mut self) {
for thread in &self.threads {
thread.abort()
}
}
}
impl BrowserTester {
pub async fn new() -> Self {
let (browser, mut handler) = Browser::launch(BrowserConfig::builder().build().unwrap())
.await
.unwrap();
Self {
browser,
page: None,
log_events: Arc::new(Mutex::new(Vec::new())),
threads: vec![tokio::task::spawn(async move {
loop {
let _ = handler.next().await.unwrap();
}
})],
}
}
pub async fn load_page(&mut self, url: &str) -> Result<(), Box<dyn std::error::Error>> {
let page = self.page.insert(self.browser.new_page(url).await?);
let console_override = vec![
"function() {",
"const c = console; c.events = [];",
"let l = [c.log, c.warn, c.error, c.debug].map(e => e.bind(c));",
"let p = (m, a) => c.events.push(`${m}: ${Array.from(a).join(' ')}`)",
"c.log = function(){ l[0].apply(c, arguments); p('LOG', arguments); }",
"c.warn = function(){ l[1].apply(c, arguments); p('WRN', arguments); }",
"c.error = function(){ l[2].apply(c, arguments); p('ERR', arguments); }",
"c.debug = function(){ l[3].apply(c, arguments); p('DBG', arguments); }",
"}",
]
.join("\n");
let _ = page.evaluate_function(console_override).await?;
let mut events = page
.event_listener::<chromiumoxide::cdp::browser_protocol::log::EventEntryAdded>()
.await?;
let event_list = Arc::clone(&self.log_events);
self.threads.push(tokio::task::spawn(async move {
loop {
let event = events.next().await;
if let Some(event) = event {
event_list.lock().unwrap().push(format!("{:#?}", event));
}
panic!("This block was broken, but now seems to be working? Remove the console override hack 🙂 ");
}
}));
Ok(())
}
pub async fn click(&mut self, selector: &str) -> Result<(), Box<dyn std::error::Error>> {
self.page
.as_mut()
.expect("No page launched")
.find_element(selector)
.await?
.click()
.await?;
Ok(())
}
pub async fn selector_exists(
&mut self,
selector: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let _ = self
.page
.as_mut()
.expect("No page launched")
.evaluate_function(format!(
"
async function() {{
const time = Date.now();
const timeout = 2000;
const selector = \"{}\";
while (!document.querySelector(selector) && (Date.now() - time) < timeout) {{
await new Promise(r => setTimeout(r, 50));
}}
if (!document.querySelector(selector)) {{
throw new Error(`${{selector}} did not appear within ${{timeout}}ms`);
}}
}}",
selector
))
.await?;
Ok(())
}
pub async fn contents(&mut self, selector: &str) -> Result<String, Box<dyn std::error::Error>> {
let el = self
.page
.as_mut()
.expect("No page launched")
.find_element(selector)
.await
.expect("Selector does not exist");
let contents = el
.inner_html()
.await
.expect("Element did not have Inner HTML");
Ok(contents.expect("Element was empty"))
}
pub async fn eval(&mut self, js: &str) -> Result<(), Box<dyn std::error::Error>> {
let _ = self
.page
.as_mut()
.expect("No page launched")
.evaluate_function(js)
.await?;
Ok(())
}
pub async fn get_logs(&mut self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let res = self
.page
.as_mut()
.expect("No page launched")
.evaluate_function("() => console.events")
.await?
.into_value::<Vec<String>>();
if let Ok(logs) = res {
Ok(logs)
} else {
panic!("Couldn't load logs from the browser");
}
}
}