use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConsoleLevel {
Log,
Warn,
Error,
Info,
Debug,
Other,
}
impl std::fmt::Display for ConsoleLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConsoleLevel::Log => write!(f, "log"),
ConsoleLevel::Warn => write!(f, "warn"),
ConsoleLevel::Error => write!(f, "error"),
ConsoleLevel::Info => write!(f, "info"),
ConsoleLevel::Debug => write!(f, "debug"),
ConsoleLevel::Other => write!(f, "other"),
}
}
}
#[derive(Debug, Clone)]
pub struct ConsoleEntry {
pub level: ConsoleLevel,
pub text: String,
pub timestamp: f64,
}
#[derive(Debug, Clone)]
pub struct JsException {
pub text: String,
pub url: Option<String>,
pub line: i64,
pub column: i64,
pub stack_trace: Option<String>,
pub timestamp: f64,
}
#[derive(Debug, Clone, Default)]
pub struct ConsoleMonitor {
logs: Arc<Mutex<Vec<ConsoleEntry>>>,
exceptions: Arc<Mutex<Vec<JsException>>>,
}
impl ConsoleMonitor {
pub fn new() -> Self {
Self {
logs: Arc::new(Mutex::new(Vec::new())),
exceptions: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn add_log(&self, entry: ConsoleEntry) {
if let Ok(mut list) = self.logs.lock() {
list.push(entry);
}
}
pub fn add_exception(&self, exc: JsException) {
if let Ok(mut list) = self.exceptions.lock() {
list.push(exc);
}
}
pub fn logs(&self) -> Vec<ConsoleEntry> {
self.logs.lock().map(|l| l.clone()).unwrap_or_default()
}
pub fn exceptions(&self) -> Vec<JsException> {
self.exceptions
.lock()
.map(|l| l.clone())
.unwrap_or_default()
}
pub fn clear(&self) {
if let Ok(mut l) = self.logs.lock() {
l.clear();
}
if let Ok(mut l) = self.exceptions.lock() {
l.clear();
}
}
}
pub async fn enable_runtime(page: &chromiumoxide::Page) -> crate::error::Result<()> {
use chromiumoxide::cdp::js_protocol::runtime::EnableParams;
page.execute(EnableParams::default())
.await
.map_err(|e| crate::error::Error::Browser(format!("enable runtime: {e}")))?;
Ok(())
}
pub fn cdp_type_to_level(
ty: &chromiumoxide::cdp::js_protocol::runtime::ConsoleApiCalledType,
) -> ConsoleLevel {
use chromiumoxide::cdp::js_protocol::runtime::ConsoleApiCalledType;
match ty {
ConsoleApiCalledType::Log => ConsoleLevel::Log,
ConsoleApiCalledType::Debug => ConsoleLevel::Debug,
ConsoleApiCalledType::Info => ConsoleLevel::Info,
ConsoleApiCalledType::Error => ConsoleLevel::Error,
ConsoleApiCalledType::Warning => ConsoleLevel::Warn,
_ => ConsoleLevel::Other,
}
}
pub fn format_stack_trace(st: &chromiumoxide::cdp::js_protocol::runtime::StackTrace) -> String {
let mut lines = Vec::new();
for frame in &st.call_frames {
lines.push(format!(
" at {} ({}:{}:{})",
if frame.function_name.is_empty() {
"<anonymous>"
} else {
&frame.function_name
},
if frame.url.is_empty() {
"<unknown>"
} else {
&frame.url
},
frame.line_number + 1,
frame.column_number + 1,
));
}
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_monitor_add_and_query() {
let monitor = ConsoleMonitor::new();
monitor.add_log(ConsoleEntry {
level: ConsoleLevel::Log,
text: "hello".into(),
timestamp: 1000.0,
});
monitor.add_log(ConsoleEntry {
level: ConsoleLevel::Error,
text: "oops".into(),
timestamp: 1001.0,
});
monitor.add_exception(JsException {
text: "Uncaught TypeError".into(),
url: Some("https://example.com/app.js".into()),
line: 42,
column: 5,
stack_trace: None,
timestamp: 1002.0,
});
assert_eq!(monitor.logs().len(), 2);
assert_eq!(monitor.exceptions().len(), 1);
assert_eq!(monitor.exceptions()[0].line, 42);
monitor.clear();
assert!(monitor.logs().is_empty());
assert!(monitor.exceptions().is_empty());
}
}