use std::cell::RefCell;
use std::rc::Rc;
use crate::connection::Connection;
use super::browser_context::{BrowserContext, ContextOptions};
use super::event_emitter::{EventEmitter, EventEmitterInner};
use super::page::Page;
pub type Pid = u32;
#[derive(Debug, Clone, Default)]
pub struct BrowserOptions {
pub ws_endpoint: String,
pub initial_version: Option<String>,
pub initial_user_agent: Option<String>,
pub initial_pid: Option<Pid>,
}
pub struct Browser {
ws_endpoint: String,
version: RefCell<Option<String>>,
user_agent: RefCell<Option<String>>,
pid: RefCell<Option<Pid>>,
disconnected: RefCell<bool>,
contexts: RefCell<Vec<Rc<BrowserContext>>>,
connection: RefCell<Option<Rc<RefCell<Connection>>>>,
events: Rc<EventEmitterInner>,
}
impl std::fmt::Debug for Browser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Browser")
.field("ws_endpoint", &self.ws_endpoint)
.field("version", &self.version.borrow())
.field("user_agent", &self.user_agent.borrow())
.field("disconnected", &self.disconnected.borrow())
.field("context_count", &self.contexts.borrow().len())
.field("has_connection", &self.connection.borrow().is_some())
.finish()
}
}
impl Browser {
pub fn new(opts: BrowserOptions) -> Self {
Self {
ws_endpoint: opts.ws_endpoint,
version: RefCell::new(opts.initial_version),
user_agent: RefCell::new(opts.initial_user_agent),
pid: RefCell::new(opts.initial_pid),
disconnected: RefCell::new(false),
contexts: RefCell::new(Vec::new()),
connection: RefCell::new(None),
events: Rc::new(EventEmitterInner::new()),
}
}
pub fn new_for_test(ws_endpoint: &str) -> Self {
Self::new(BrowserOptions {
ws_endpoint: ws_endpoint.to_string(),
initial_version: None,
initial_user_agent: None,
initial_pid: None,
})
}
pub fn is_connected(&self) -> bool {
!*self.disconnected.borrow()
}
pub fn set_disconnected(&self) {
*self.disconnected.borrow_mut() = true;
}
pub fn process(&self) -> Option<Pid> {
*self.pid.borrow()
}
pub fn set_pid(&self, pid: Pid) {
*self.pid.borrow_mut() = Some(pid);
}
pub fn ws_endpoint(&self) -> &str {
&self.ws_endpoint
}
pub fn version(&self) -> Option<String> {
self.version.borrow().clone()
}
pub fn set_version(&self, v: impl Into<String>) {
*self.version.borrow_mut() = Some(v.into());
}
pub fn user_agent(&self) -> Option<String> {
self.user_agent.borrow().clone()
}
pub fn set_user_agent(&self, ua: impl Into<String>) {
*self.user_agent.borrow_mut() = Some(ua.into());
}
pub fn contexts(&self) -> Vec<Rc<BrowserContext>> {
self.contexts.borrow().clone()
}
pub fn context_count(&self) -> usize {
self.contexts.borrow().len()
}
pub fn disconnect(&self) {
*self.disconnected.borrow_mut() = true;
}
pub fn close(&self) {
*self.disconnected.borrow_mut() = true;
let contexts = self.contexts.borrow().clone();
for ctx in contexts.iter() {
ctx.close();
}
self.contexts.borrow_mut().clear();
}
pub fn events_inner(&self) -> &Rc<EventEmitterInner> {
&self.events
}
pub fn has_connection(&self) -> bool {
self.connection.borrow().is_some()
}
pub fn set_connection(&self, conn: Rc<RefCell<Connection>>) {
*self.connection.borrow_mut() = Some(conn);
}
pub fn connection(&self) -> Option<Rc<RefCell<Connection>>> {
self.connection.borrow().clone()
}
#[doc(hidden)]
pub fn new_context_for_test(self_rc: &Rc<Browser>) -> Rc<BrowserContext> {
new_context_on_rc(self_rc, ContextOptions::default())
}
}
impl EventEmitter for Browser {
delegate_event_emitter!(self, events);
}
pub fn new_context_on_rc(this: &Rc<Browser>, opts: ContextOptions) -> Rc<BrowserContext> {
let ctx = Rc::new(BrowserContext::new(Rc::downgrade(this), opts));
this.contexts.borrow_mut().push(ctx.clone());
ctx
}
pub fn new_page_on_rc(this: &Rc<Browser>) -> Rc<Page> {
let ctx = if this.contexts.borrow().is_empty() {
new_context_on_rc(this, ContextOptions::default())
} else {
this.contexts.borrow()[0].clone()
};
new_page_on_context(&ctx)
}
pub fn new_page_on_context(ctx: &Rc<BrowserContext>) -> Rc<Page> {
let target_id = format!("TARGET-{}", ctx.pages_count() + 1);
let conn = ctx.browser().connection();
let p = Rc::new(Page::new_with_connection(
target_id,
Rc::downgrade(ctx),
conn,
));
ctx.add_page(p.clone());
p
}
#[cfg(test)]
mod tests {
use super::super::event_emitter::EventHandler;
use super::*;
#[test]
fn new_initial_state() {
let b = Browser::new_for_test("ws://127.0.0.1:9222");
assert!(b.is_connected());
assert_eq!(b.ws_endpoint(), "ws://127.0.0.1:9222");
assert!(b.version().is_none());
assert!(b.user_agent().is_none());
assert!(b.process().is_none());
assert_eq!(b.context_count(), 0);
}
#[test]
fn is_connected_default_true() {
let b = Browser::new_for_test("ws://x");
assert!(b.is_connected());
}
#[test]
fn disconnect_marks_disconnected() {
let b = Browser::new_for_test("ws://x");
b.disconnect();
assert!(!b.is_connected());
}
#[test]
fn set_disconnected_idempotent() {
let b = Browser::new_for_test("ws://x");
b.set_disconnected();
b.set_disconnected();
assert!(!b.is_connected());
}
#[test]
fn version_round_trip() {
let b = Browser::new_for_test("ws://x");
b.set_version("HeadlessChrome/120.0");
assert_eq!(b.version(), Some("HeadlessChrome/120.0".into()));
}
#[test]
fn user_agent_round_trip() {
let b = Browser::new_for_test("ws://x");
b.set_user_agent("Mozilla/5.0 (X11; Linux x86_64)");
assert!(b.user_agent().unwrap().contains("Linux"));
}
#[test]
fn pid_round_trip() {
let b = Browser::new_for_test("ws://x");
b.set_pid(12345);
assert_eq!(b.process(), Some(12345));
}
#[test]
fn close_clears_contexts_and_marks_disconnected() {
let b = Rc::new(Browser::new_for_test("ws://x"));
let _ctx = new_context_on_rc(&b, ContextOptions::default());
assert_eq!(b.context_count(), 1);
b.close();
assert!(!b.is_connected());
assert_eq!(b.context_count(), 0);
}
#[test]
fn new_context_on_rc_incognito() {
let b = Rc::new(Browser::new_for_test("ws://x"));
let ctx = new_context_on_rc(
&b,
ContextOptions {
incognito: true,
..Default::default()
},
);
assert_eq!(b.context_count(), 1);
assert!(ctx.is_incognito());
}
#[test]
fn new_page_on_rc_first_context() {
let b = Rc::new(Browser::new_for_test("ws://x"));
let p = new_page_on_rc(&b);
assert_eq!(b.context_count(), 1);
assert_eq!(p.target_id(), "TARGET-1");
}
#[test]
fn new_page_on_rc_increments_target_id() {
let b = Rc::new(Browser::new_for_test("ws://x"));
let p1 = new_page_on_rc(&b);
let p2 = new_page_on_rc(&b);
assert_eq!(p1.target_id(), "TARGET-1");
assert_eq!(p2.target_id(), "TARGET-2");
}
#[test]
fn event_emitter_via_trait() {
let b = Browser::new_for_test("ws://x");
let counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let c = counter.clone();
let h: EventHandler = std::sync::Arc::new(move |_| {
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
});
b.on("disconnected", h);
b.emit("disconnected", &[]);
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(b.listener_count("disconnected"), 1);
}
#[test]
fn event_emitter_remove_all() {
let b = Browser::new_for_test("ws://x");
let h: EventHandler = std::sync::Arc::new(|_| {});
b.on("a", h.clone());
b.on("b", h);
assert_eq!(b.listener_count("a"), 1);
b.remove_all_listeners(Some("a"));
assert_eq!(b.listener_count("a"), 0);
assert_eq!(b.listener_count("b"), 1);
b.remove_all_listeners(None);
assert_eq!(b.listener_count("b"), 0);
}
#[test]
fn contexts_returns_cloned_list() {
let b = Rc::new(Browser::new_for_test("ws://x"));
new_context_on_rc(&b, ContextOptions::default());
new_context_on_rc(
&b,
ContextOptions {
incognito: true,
..Default::default()
},
);
let list = b.contexts();
assert_eq!(list.len(), 2);
assert!(!list[0].is_incognito());
assert!(list[1].is_incognito());
}
#[test]
fn new_page_on_context_propagates_connection() {
use crate::connection::Connection;
use crate::transport::{InMemoryBridge, InMemoryBridgeResponse, InMemoryTransport};
use std::sync::Arc;
struct MockBridge;
impl InMemoryBridge for MockBridge {
fn dispatch_command(
&self,
_m: &str,
_p: serde_json::Value,
_s: Option<&str>,
) -> InMemoryBridgeResponse {
InMemoryBridgeResponse::Ok(serde_json::Value::Null)
}
}
let b = Rc::new(Browser::new_for_test("ws://x"));
assert!(!b.has_connection());
let bridge: Arc<dyn InMemoryBridge> = Arc::new(MockBridge);
let transport = InMemoryTransport::new(bridge);
let conn = Rc::new(RefCell::new(Connection::from_transport(Box::new(
transport,
))));
b.set_connection(conn);
assert!(b.has_connection());
let p = new_page_on_rc(&b);
assert!(p.has_connection());
}
#[test]
fn new_page_on_context_without_connection_has_no_connection() {
let b = Rc::new(Browser::new_for_test("ws://x"));
assert!(!b.has_connection());
let p = new_page_on_rc(&b);
assert!(!p.has_connection());
}
}