use rustenium_bidi_definitions::browsing_context::command_builders::{
CaptureScreenshotBuilder, CreateBuilder, LocateNodesBuilder, NavigateBuilder,
};
use rustenium_bidi_definitions::browsing_context::results::NavigateResult;
use rustenium_bidi_definitions::browsing_context::types::{
BrowsingContext, CreateType, Locator,
};
use rustenium_bidi_definitions::browser::types::UserContext;
use rustenium_bidi_definitions::emulation::command_builders::SetTimezoneOverrideBuilder;
use rustenium_bidi_definitions::Event;
use rustenium_bidi_definitions::Command;
use rustenium_bidi_definitions::script::command_builders::{
AddPreloadScriptBuilder, EvaluateBuilder, RemovePreloadScriptBuilder,
};
use rustenium_bidi_definitions::script::types::{
ContextTarget, SerializationOptions,
SerializationOptionsIncludeShadowTree, Target,
};
use rustenium_bidi_definitions::session::results::SubscribeResult;
use rustenium_bidi_definitions::session::types::ProxyConfiguration;
use rustenium_bidi_definitions::base::CommandResponse;
use rustenium_core::error::{CommandResultError, SessionSendError};
use rustenium_core::events::BidiEventManagement;
use rustenium_core::session::SessionConnectionType;
use rustenium_core::transport::{ConnectionTransportConfig, WebsocketConnectionTransport};
use rustenium_core::{find_free_port, BidiSession, NetworkRequest};
use crate::drivers::bidi::drivers::{BidiDriver, BidiDrive, DriverConfiguration};
use crate::error::{
ContextCreationError, ContextIndexError, EmulationError,
EvaluateResultError, FindNodesError, InterceptNetworkError, OpenUrlError, ScreenshotError,
};
use crate::nodes::ChromeNode;
use super::capabilities::ChromeCapabilities;
use std::sync::{Arc, Mutex};
use std::collections::HashSet;
use std::future::Future;
use rustenium_core::process::Process;
pub mod options {
use rustenium_bidi_definitions::browsing_context::commands::CaptureScreenshotOrigin;
use rustenium_bidi_definitions::browsing_context::types::{
BrowsingContext, ClipRectangle, CreateType, ImageFormat, ReadinessState,
};
use rustenium_bidi_definitions::script::types::{
ChannelValue, ResultOwnership, SerializationOptions, SharedReference, Target,
};
use rustenium_bidi_definitions::network::types::UrlPattern;
#[derive(Debug, Clone, Default)]
pub struct BrowserScreenshotOptions {
pub context_id: Option<BrowsingContext>,
pub origin: Option<CaptureScreenshotOrigin>,
pub format: Option<ImageFormat>,
pub clip: Option<ClipRectangle>,
pub save_path: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct NavigateOptions {
pub wait: Option<ReadinessState>,
pub context_id: Option<BrowsingContext>,
}
#[derive(Debug, Clone, Default)]
pub struct CreateContextOptions {
pub context_type: Option<CreateType>,
pub reference_context: Option<BrowsingContext>,
}
#[derive(Debug, Clone, Default)]
pub struct FindNodesOptions {
pub context_id: Option<BrowsingContext>,
pub max_node_count: Option<u64>,
pub serialization_options: Option<SerializationOptions>,
pub start_nodes: Option<Vec<SharedReference>>,
}
#[derive(Debug, Clone, Default)]
pub struct WaitForNodesOptions {
pub context_id: Option<BrowsingContext>,
pub timeout_ms: Option<u64>,
pub poll_interval_ms: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct OnRequestOptions {
pub url_patterns: Option<Vec<UrlPattern>>,
pub contexts: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct SubscribeEventsOptions {
pub browsing_contexts: Option<Vec<String>>,
pub user_contexts: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct EvaluateScriptOptions {
pub target: Option<Target>,
pub result_ownership: Option<ResultOwnership>,
pub serialization_options: Option<SerializationOptions>,
pub user_activation: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct AddPreloadScriptOptions {
pub arguments: Option<Vec<ChannelValue>>,
pub contexts: Option<Vec<BrowsingContext>>,
pub user_contexts: Option<Vec<String>>,
pub sandbox: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct EmulateTimezoneOptions {
pub contexts: Option<Vec<BrowsingContext>>,
pub user_contexts: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct AuthenticateOptions {
pub url_patterns: Option<Vec<UrlPattern>>,
pub contexts: Option<Vec<String>>,
}
}
pub use options::{
BrowserScreenshotOptions, NavigateOptions, CreateContextOptions,
FindNodesOptions, WaitForNodesOptions, OnRequestOptions, SubscribeEventsOptions,
EvaluateScriptOptions, AddPreloadScriptOptions, EmulateTimezoneOptions, AuthenticateOptions,
};
#[derive(Debug, Clone)]
pub struct ChromeConfig {
pub driver_executable_path: String,
pub host: Option<String>,
pub port: Option<u16>,
pub driver_flags: Vec<&'static str>,
pub capabilities: ChromeCapabilities,
pub sandbox: bool,
pub proxy: Option<ProxyConfiguration>,
pub remote_debugging_port: Option<u16>,
pub chrome_executable_path: Option<String>,
pub user_data_dir: Option<String>,
pub browser_flags: Option<Vec<String>>,
}
impl Default for ChromeConfig {
fn default() -> Self {
ChromeConfig {
driver_executable_path: "".to_string(),
host: None,
port: None,
driver_flags: Vec::new(),
capabilities: ChromeCapabilities::default(),
sandbox: false,
proxy: None,
remote_debugging_port: None,
chrome_executable_path: None,
user_data_dir: None,
browser_flags: None,
}
}
}
impl DriverConfiguration for ChromeConfig {
fn exe_path(&self) -> &str {
&self.driver_executable_path
}
fn flags(&self) -> Vec<String> {
let mut flags = vec![
format!(
"--host={}",
self.host.clone().unwrap_or(String::from("localhost"))
),
format!("--port={}", self.port.unwrap_or(find_free_port().unwrap())),
];
flags.extend(self.driver_flags.iter().map(|s| s.to_string()));
flags
}
}
pub struct ChromeBrowser {
config: ChromeConfig,
driver: BidiDriver<WebsocketConnectionTransport>,
chrome_process: Option<Process>
}
impl std::fmt::Debug for ChromeBrowser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChromeBrowser")
.field("config", &self.config)
.field("chrome_process", &self.chrome_process)
.finish_non_exhaustive()
}
}
impl BidiDrive<WebsocketConnectionTransport> for ChromeBrowser {}
impl ChromeBrowser {
pub async fn new(mut config: ChromeConfig) -> ChromeBrowser {
if config.driver_executable_path.is_empty() {
config.driver_executable_path = crate::downloader::ensure_chromedriver()
.to_string_lossy()
.into_owned();
}
let port = find_free_port().unwrap();
config.port = Some(config.port.unwrap_or(port));
let mut ct_config = ConnectionTransportConfig::default();
ct_config.host = config.host.clone().unwrap_or(String::from("localhost"));
ct_config.port = port;
let (debugger_address, chrome_process) = match config.remote_debugging_port {
Some(0) => {
let chrome_port = find_free_port().unwrap();
let chrome_exe = config.chrome_executable_path
.clone()
.unwrap_or_else(|| {
crate::downloader::ensure_chrome()
.to_string_lossy()
.into_owned()
});
let user_data_dir = config.user_data_dir.clone().unwrap_or_else(|| {
std::env::temp_dir()
.join(format!("rustenium-chrome-{}", chrome_port))
.display()
.to_string()
});
let mut chrome_args = vec![
format!("--remote-debugging-port={}", chrome_port),
format!("--user-data-dir={}", user_data_dir),
"--no-first-run".to_string(),
"--no-default-browser-check".to_string(),
];
if let Some(ref flags) = config.browser_flags {
chrome_args.extend(flags.iter().cloned());
}
use rustenium_core::process::Process;
let chrome_proc = Process::create(chrome_exe, chrome_args);
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
(Some(format!("localhost:{}", chrome_port)), Some(chrome_proc))
}
Some(port) => {
(Some(format!("localhost:{}", port)), None)
}
None => {
(None, None)
}
};
if let Some(addr) = debugger_address {
config.capabilities.debugger_address(addr);
}
config.capabilities.add_arg("start-maximized".to_string());
config.capabilities.add_arg("disable-infobars".to_string());
if !config.sandbox {
config.capabilities.add_arg("no-sandbox".to_string());
}
if let Some(proxy) = config.proxy.clone() {
config.capabilities.base_capabilities.proxy = Some(proxy);
}
let capabilities = config.capabilities.clone().build();
let result = Self::start(&config, &ct_config, SessionConnectionType::WebSocket, capabilities).await;
let session = result.0;
let driver_process = result.1;
let mut browser = ChromeBrowser {
config,
driver: BidiDriver::new(
String::from("chromedriver"),
vec![],
session,
0,
Arc::new(Mutex::new(Vec::new())),
driver_process,
),
chrome_process
};
browser.driver.listen_to_context_creation().await.unwrap();
browser
}
pub async fn navigate(&mut self, url: &str) -> Result<NavigateResult, OpenUrlError> {
self.navigate_with_options(url, NavigateOptions::default()).await
}
pub async fn navigate_with_options(
&mut self,
url: &str,
options: NavigateOptions,
) -> Result<NavigateResult, OpenUrlError> {
let context = options.context_id
.unwrap_or_else(|| self.driver.get_active_context_id().unwrap());
let mut builder = NavigateBuilder::default().url(url).context(context);
if let Some(wait) = options.wait {
builder = builder.wait(wait);
}
self.driver.navigate(builder.build().unwrap()).await
}
pub async fn create_context_bidi(
&mut self,
background: bool
) -> Result<rustenium_core::BrowsingContext, ContextCreationError> {
self.create_context_bidi_with_options(background, CreateContextOptions::default()).await
}
pub async fn create_context_bidi_with_options(
&mut self,
background: bool,
options: CreateContextOptions,
) -> Result<rustenium_core::BrowsingContext, ContextCreationError> {
let context_type = options.context_type.unwrap_or(CreateType::Tab);
let mut builder = CreateBuilder::default().r#type(context_type);
if let Some(ref_ctx) = options.reference_context {
builder = builder.reference_context(ref_ctx);
};
builder = builder.background(background);
self.driver.create_context(builder.build().unwrap()).await
}
pub async fn find_nodes(
&mut self,
locator: Locator,
) -> Result<Vec<ChromeNode<WebsocketConnectionTransport>>, FindNodesError> {
self.find_nodes_with_options(locator, FindNodesOptions::default()).await
}
pub async fn find_nodes_with_options(
&mut self,
locator: Locator,
options: FindNodesOptions,
) -> Result<Vec<ChromeNode<WebsocketConnectionTransport>>, FindNodesError> {
let context = options.context_id.clone()
.unwrap_or_else(|| self.driver.get_active_context_id().unwrap());
let serialization_options = options.serialization_options.unwrap_or(SerializationOptions {
max_dom_depth: Some(40),
max_object_depth: Some(0),
include_shadow_tree: Some(SerializationOptionsIncludeShadowTree::None),
});
let mut builder = LocateNodesBuilder::default()
.context(context.clone())
.locator(locator.clone())
.serialization_options(serialization_options);
if let Some(max_count) = options.max_node_count {
builder = builder.max_node_count(max_count);
}
if let Some(start_nodes) = options.start_nodes {
builder = builder.start_nodes(start_nodes);
}
let node_result = self.driver.find_nodes(builder.build().unwrap()).await?;
let mut chrome_nodes = Vec::new();
for node in node_result.nodes.iter() {
let chrome_node = ChromeNode::from_bidi(
node.clone(),
locator.clone(),
self.driver.session.clone(),
context.clone(),
self.driver.mouse.clone(),
);
chrome_nodes.push(chrome_node);
}
Ok(chrome_nodes)
}
pub async fn find_node(
&mut self,
locator: Locator,
) -> Result<Option<ChromeNode<WebsocketConnectionTransport>>, FindNodesError> {
self.find_node_with_options(locator, FindNodesOptions::default()).await
}
pub async fn find_node_with_options(
&mut self,
locator: Locator,
options: FindNodesOptions,
) -> Result<Option<ChromeNode<WebsocketConnectionTransport>>, FindNodesError> {
let nodes = self.find_nodes_with_options(locator, options).await?;
Ok(nodes.into_iter().next())
}
pub async fn wait_for_nodes(
&mut self,
locator: Locator,
) -> Result<Vec<ChromeNode<WebsocketConnectionTransport>>, FindNodesError> {
self.wait_for_nodes_with_options(locator, WaitForNodesOptions::default()).await
}
pub async fn wait_for_nodes_with_options(
&mut self,
locator: Locator,
options: WaitForNodesOptions,
) -> Result<Vec<ChromeNode<WebsocketConnectionTransport>>, FindNodesError> {
let timeout = options.timeout_ms.unwrap_or(4000);
let poll_interval = options.poll_interval_ms.unwrap_or(timeout / 6);
let start = std::time::Instant::now();
loop {
let nodes = self.find_nodes_with_options(
locator.clone(),
FindNodesOptions { context_id: options.context_id.clone(), ..Default::default() },
).await?;
if !nodes.is_empty() {
return Ok(nodes);
}
if start.elapsed().as_millis() as u64 >= timeout {
return Ok(Vec::new());
}
tokio::time::sleep(tokio::time::Duration::from_millis(poll_interval)).await;
}
}
pub async fn wait_for_node(
&mut self,
locator: Locator,
) -> Result<Option<ChromeNode<WebsocketConnectionTransport>>, FindNodesError> {
self.wait_for_node_with_options(locator, WaitForNodesOptions::default()).await
}
pub async fn wait_for_node_with_options(
&mut self,
locator: Locator,
options: WaitForNodesOptions,
) -> Result<Option<ChromeNode<WebsocketConnectionTransport>>, FindNodesError> {
let nodes = self.wait_for_nodes_with_options(locator, options).await?;
Ok(nodes.into_iter().next())
}
pub async fn on_request_bidi<F, Fut>(
&mut self,
handler: F,
) -> Result<(), InterceptNetworkError>
where
F: Fn(NetworkRequest<WebsocketConnectionTransport>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.on_request_bidi_with_options(handler, OnRequestOptions::default()).await
}
pub async fn on_request_bidi_with_options<F, Fut>(
&mut self,
handler: F,
options: OnRequestOptions,
) -> Result<(), InterceptNetworkError>
where
F: Fn(NetworkRequest<WebsocketConnectionTransport>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let mut builder = self.driver.on_request(handler);
if let Some(patterns) = options.url_patterns {
builder = builder.url_patterns(patterns);
}
if let Some(contexts) = options.contexts {
builder = builder.contexts(contexts);
}
builder.execute().await
}
pub async fn subscribe_events<F, R>(
&mut self,
events: HashSet<&str>,
handler: F,
) -> Result<Option<SubscribeResult>, CommandResultError>
where
F: FnMut(Event) -> R + Send + Sync + 'static,
R: Future<Output = ()> + Send + 'static,
{
self.subscribe_events_with_options(events, handler, SubscribeEventsOptions::default()).await
}
pub async fn subscribe_events_with_options<F, R>(
&mut self,
events: HashSet<&str>,
handler: F,
options: SubscribeEventsOptions,
) -> Result<Option<SubscribeResult>, CommandResultError>
where
F: FnMut(Event) -> R + Send + Sync + 'static,
R: Future<Output = ()> + Send + 'static,
{
let mut bidi_event = {
let mut session = self.driver.session.lock().await;
session.create_event::<_, _, BidiSession<WebsocketConnectionTransport>>(events, handler)
};
if let Some(contexts) = options.browsing_contexts {
for ctx in contexts {
bidi_event.add_browsing_context(ctx);
}
}
if let Some(user_ctxs) = options.user_contexts {
for uctx in user_ctxs {
bidi_event.add_user_context(uctx);
}
}
self.driver.session.lock().await.subscribe_events(bidi_event).await
}
pub async fn add_event_handler<F, R>(
&mut self,
events: HashSet<&str>,
handler: F,
) -> String
where
F: FnMut(Event) -> R + Send + Sync + 'static,
R: Future<Output = ()> + Send + 'static,
{
self.driver.add_event_handler(events, handler).await
}
pub async fn evaluate_script_bidi(
&mut self,
expression: String,
await_promise: bool,
) -> Result<rustenium_bidi_definitions::script::types::EvaluateResultSuccess, EvaluateResultError> {
self.evaluate_script_bidi_with_options(expression, await_promise, EvaluateScriptOptions::default()).await
}
pub async fn evaluate_script_bidi_with_options(
&mut self,
expression: String,
await_promise: bool,
options: EvaluateScriptOptions,
) -> Result<rustenium_bidi_definitions::script::types::EvaluateResultSuccess, EvaluateResultError> {
let target = options.target.unwrap_or_else(|| {
let context = self.driver.get_active_context_id().unwrap();
Target::ContextTarget(ContextTarget::new(context))
});
let mut builder = EvaluateBuilder::default()
.expression(expression)
.await_promise(await_promise)
.target(target);
if let Some(ro) = options.result_ownership {
builder = builder.result_ownership(ro);
}
if let Some(so) = options.serialization_options {
builder = builder.serialization_options(so);
}
if let Some(ua) = options.user_activation {
builder = builder.user_activation(ua);
}
self.driver.evaluate_script(builder.build().unwrap()).await
}
pub async fn add_preload_script_bidi(
&mut self,
function_declaration: String,
) -> Result<String, EvaluateResultError> {
self.add_preload_script_bidi_with_options(function_declaration, AddPreloadScriptOptions::default()).await
}
pub async fn add_preload_script_bidi_with_options(
&mut self,
function_declaration: String,
options: AddPreloadScriptOptions,
) -> Result<String, EvaluateResultError> {
let mut builder = AddPreloadScriptBuilder::default()
.function_declaration(function_declaration);
if let Some(args) = options.arguments {
builder = builder.arguments(args);
}
if let Some(contexts) = options.contexts {
builder = builder.contexts(contexts);
}
if let Some(user_contexts) = options.user_contexts {
builder = builder.user_contexts(user_contexts.into_iter().map(UserContext::new));
}
if let Some(sandbox) = options.sandbox {
builder = builder.sandbox(sandbox);
}
self.driver.add_preload_script(builder.build().unwrap()).await.map(|ps| ps.into())
}
pub async fn remove_preload_script_bidi(
&mut self,
script: String,
) -> Result<(), EvaluateResultError> {
let remove_cmd = RemovePreloadScriptBuilder::default().script(script).build().unwrap();
self.driver.remove_preload_script(remove_cmd).await
}
pub async fn screenshot(&mut self) -> Result<String, ScreenshotError> {
self.screenshot_with_options(BrowserScreenshotOptions::default()).await
}
pub async fn screenshot_with_options(&mut self, options: BrowserScreenshotOptions) -> Result<String, ScreenshotError> {
let context = options.context_id
.unwrap_or_else(|| self.driver.get_active_context_id().unwrap());
let mut builder = CaptureScreenshotBuilder::default().context(context);
if let Some(origin) = options.origin {
builder = builder.origin(origin);
}
if let Some(format) = options.format {
builder = builder.format(format);
}
if let Some(clip) = options.clip {
builder = builder.clip(clip);
}
let command = builder.build().unwrap();
self.driver.screenshot(command, options.save_path.as_deref()).await
}
pub async fn emulate_timezone(
&mut self,
timezone: Option<String>,
) -> Result<(), EmulationError> {
self.emulate_timezone_with_options(timezone, EmulateTimezoneOptions::default()).await
}
pub async fn emulate_timezone_with_options(
&mut self,
timezone: Option<String>,
options: EmulateTimezoneOptions,
) -> Result<(), EmulationError> {
let mut builder = SetTimezoneOverrideBuilder::default();
if let Some(tz) = timezone {
builder = builder.timezone(tz);
}
if let Some(contexts) = options.contexts {
builder = builder.contexts(contexts);
}
if let Some(user_contexts) = options.user_contexts {
builder = builder.user_contexts(user_contexts.into_iter().map(UserContext::new));
}
self.driver.set_timezone_override(builder.build()).await
}
pub async fn authenticate(
&mut self,
username: impl Into<String> + Send + 'static,
password: impl Into<String> + Send + 'static,
) -> Result<(), InterceptNetworkError> {
self.authenticate_with_options(username, password, AuthenticateOptions::default()).await
}
pub async fn authenticate_with_options(
&mut self,
username: impl Into<String> + Send + 'static,
password: impl Into<String> + Send + 'static,
options: AuthenticateOptions,
) -> Result<(), InterceptNetworkError> {
let mut builder = self.driver.authenticate(username, password);
if let Some(patterns) = options.url_patterns {
builder = builder.url_patterns(patterns);
}
if let Some(contexts) = options.contexts {
builder = builder.contexts(contexts);
}
builder.execute().await
}
pub fn get_config(&self) -> &ChromeConfig {
&self.config
}
pub fn get_browser_process(&self) -> &Option<Process> {
&self.chrome_process
}
pub fn mouse(&self) -> &crate::input::BidiMouse<WebsocketConnectionTransport> {
self.driver.mouse.as_ref()
}
pub fn human_mouse(&self) -> &crate::input::HumanMouse<crate::input::BidiMouse<WebsocketConnectionTransport>> {
self.driver.human_mouse.as_ref()
}
pub fn keyboard(&self) -> &crate::input::Keyboard<WebsocketConnectionTransport> {
self.driver.keyboard.as_ref()
}
pub fn get_active_context_id(&self) -> Result<BrowsingContext, ContextIndexError> {
self.driver.get_active_context_id()
}
pub async fn send_bidi_command(&mut self, command: Command) -> Result<CommandResponse, SessionSendError> {
self.driver.send_command(command).await
}
pub async fn end_bidi_session(&mut self) -> Result<(), SessionSendError> {
self.driver.end_session().await
}
pub async fn close(mut self) -> Result<(), crate::error::BrowserCloseError> {
tracing::debug!("Closing ChromeBrowser");
self.driver.end_session().await?;
if let Some(mut process) = self.chrome_process.take() {
process.kill()?;
}
tracing::debug!("ChromeBrowser closed");
Ok(())
}
}
pub async fn create_chrome_browser(config: Option<ChromeConfig>) -> ChromeBrowser {
let chrome_browser = ChromeBrowser::new(config.unwrap_or_default()).await;
chrome_browser
}