use crate::browsers::chrome::tab::ChromeTab;
use crate::conduit::cdp::adapter::CdpAdapter;
use crate::error::cdp::{
CreateTabError, EmulateDeviceMetricsError, EvaluateScriptError, LocateError, NavigateError,
NodesFetchError, PreloadScriptError, ScreenshotError,
};
use crate::input::{CdpKeyboard, CdpMouse, CdpTouchscreen, HumanMouse};
use crate::nodes::AXNode;
use rustenium_cdp_definitions::Command;
use rustenium_cdp_definitions::base::CommandResponse;
use rustenium_cdp_definitions::browser_protocol::accessibility::commands::GetFullAxTree;
use rustenium_cdp_definitions::browser_protocol::accessibility::results::GetFullAxTreeResult;
use rustenium_cdp_definitions::browser_protocol::dom::commands::DescribeNode;
use rustenium_cdp_definitions::browser_protocol::dom::types::{
BackendNodeId, Node as DomNode, NodeId,
};
use rustenium_cdp_definitions::browser_protocol::emulation::commands::SetDeviceMetricsOverride;
use rustenium_cdp_definitions::browser_protocol::emulation::types::ScreenOrientation;
use rustenium_cdp_definitions::browser_protocol::page::command_builders::CaptureScreenshotBuilder;
use rustenium_cdp_definitions::browser_protocol::page::commands::{CaptureScreenshotFormat, Navigate};
use rustenium_cdp_definitions::browser_protocol::page::results::NavigateResult;
use rustenium_cdp_definitions::browser_protocol::page::type_builders::ViewportBuilder;
use rustenium_cdp_definitions::browser_protocol::page::types::{
ReferrerPolicy, TransitionType, Viewport,
};
use rustenium_cdp_definitions::browser_protocol::page::command_builders::AddScriptToEvaluateOnNewDocumentBuilder;
use rustenium_cdp_definitions::browser_protocol::page::commands::RemoveScriptToEvaluateOnNewDocument;
use rustenium_cdp_definitions::browser_protocol::page::types::ScriptIdentifier;
use rustenium_cdp_definitions::browser_protocol::target::commands::CreateTarget;
use rustenium_cdp_definitions::js_protocol::runtime::results::EvaluateResult;
use rustenium_cdp_definitions::js_protocol::runtime::types::RemoteObjectId;
use rustenium_core::error::{CdpCommandResultError, CdpSessionSendError};
use rustenium_core::transport::WebsocketConnectionTransport;
use std::time::Duration;
#[derive(Debug, Clone, Default)]
pub struct NavigateOptions {
pub referrer: Option<String>,
pub transition_type: Option<TransitionType>,
pub referrer_policy: Option<ReferrerPolicy>,
}
#[derive(Default, Clone)]
pub struct NavigateOptionsBuilder {
referrer: Option<String>,
transition_type: Option<TransitionType>,
referrer_policy: Option<ReferrerPolicy>,
}
#[derive(Debug, Clone, Default)]
pub struct BrowserScreenshotOptions {
pub full: bool,
pub format: Option<CaptureScreenshotFormat>,
pub viewport: Option<Viewport>,
pub quality: Option<f64>,
pub save_path: Option<String>,
}
#[derive(Clone, Default)]
pub struct BrowserScreenshotOptionsBuilder {
full: bool,
format: Option<CaptureScreenshotFormat>,
viewport: Option<Viewport>,
quality: Option<f64>,
save_path: Option<String>,
}
impl BrowserScreenshotOptionsBuilder {
pub fn full(mut self, full: bool) -> Self {
self.full = full;
self
}
pub fn format(mut self, format: impl Into<CaptureScreenshotFormat>) -> Self {
self.format = Some(format.into());
self
}
pub fn viewport(mut self, viewport: Viewport) -> Self {
self.viewport = Some(viewport);
self
}
pub fn quality(mut self, quality: f64) -> Self {
self.quality = Some(quality);
self
}
pub fn save_path(mut self, save_path: impl Into<String>) -> Self {
self.save_path = Some(save_path.into());
self
}
pub fn build(self) -> BrowserScreenshotOptions {
BrowserScreenshotOptions {
full: self.full,
format: self.format,
viewport: self.viewport,
quality: self.quality,
save_path: self.save_path,
}
}
}
impl NavigateOptionsBuilder {
pub fn referrer(mut self, v: impl Into<String>) -> Self {
self.referrer = Some(v.into());
self
}
pub fn transition_type(mut self, v: impl Into<TransitionType>) -> Self {
self.transition_type = Some(v.into());
self
}
pub fn referrer_policy(mut self, v: impl Into<ReferrerPolicy>) -> Self {
self.referrer_policy = Some(v.into());
self
}
pub fn build(self) -> NavigateOptions {
NavigateOptions {
referrer: self.referrer,
transition_type: self.transition_type,
referrer_policy: self.referrer_policy,
}
}
}
pub trait CdpBrowser: Send + Sync {
type BrowserNode;
fn adapter(&self) -> &CdpAdapter<WebsocketConnectionTransport>;
fn adapter_mut(&mut self) -> &mut CdpAdapter<WebsocketConnectionTransport>;
fn build_node(&self, node: DomNode) -> Self::BrowserNode;
fn navigate(
&mut self,
url: impl Into<String>,
) -> impl Future<Output = Result<NavigateResult, NavigateError>> {
self.navigate_with_options(url, NavigateOptions::default())
}
fn navigate_with_options(
&mut self,
url: impl Into<String>,
options: NavigateOptions,
) -> impl Future<Output = Result<NavigateResult, NavigateError>> {
let mut builder = Navigate::builder().url(url);
if let Some(referrer) = options.referrer {
builder = builder.referrer(referrer);
}
if let Some(transition_type) = options.transition_type {
builder = builder.transition_type(transition_type);
}
if let Some(referrer_policy) = options.referrer_policy {
builder = builder.referrer_policy(referrer_policy);
}
let command = builder.build().unwrap();
let adapter = self.adapter_mut();
async move { adapter.navigate(command).await }
}
fn create_tab(
&mut self,
url: impl Into<String>,
) -> impl Future<Output = Result<ChromeTab, CreateTabError>> {
self.create_tab_with_options(url, CreateTabOptions::default())
}
fn create_tab_with_options(
&mut self,
url: impl Into<String>,
options: CreateTabOptions,
) -> impl Future<Output = Result<ChromeTab, CreateTabError>> {
let mut builder = CreateTarget::builder().url(url);
if let Some(v) = options.left {
builder = builder.left(v);
}
if let Some(v) = options.top {
builder = builder.top(v);
}
if let Some(v) = options.width {
builder = builder.width(v);
}
if let Some(v) = options.height {
builder = builder.height(v);
}
if let Some(v) = options.window_state {
builder = builder.window_state(v);
}
if let Some(v) = options.browser_context_id {
builder = builder.browser_context_id(v);
}
if let Some(v) = options.enable_begin_frame_control {
builder = builder.enable_begin_frame_control(v);
}
if let Some(v) = options.new_window {
builder = builder.new_window(v);
}
if let Some(v) = options.background {
builder = builder.background(v);
}
if let Some(v) = options.for_tab {
builder = builder.for_tab(v);
}
if let Some(v) = options.hidden {
builder = builder.hidden(v);
}
if let Some(v) = options.focus {
builder = builder.focus(v);
}
let command = builder.build().unwrap();
let adapter = self.adapter_mut();
async move {
let target_id = adapter
.create_target(command)
.await
.map_err(CreateTabError::CreateTargetError)?;
Ok(ChromeTab::new(target_id))
}
}
fn emulate_device_metrics(
&mut self,
width: i64,
height: i64,
device_scale_factor: f64,
mobile: bool,
) -> impl Future<Output = Result<(), EmulateDeviceMetricsError>> {
self.emulate_device_metrics_with_options(
width,
height,
device_scale_factor,
mobile,
EmulateDeviceMetricsOptions::default(),
)
}
fn emulate_device_metrics_with_options(
&mut self,
width: i64,
height: i64,
device_scale_factor: f64,
mobile: bool,
options: EmulateDeviceMetricsOptions,
) -> impl Future<Output = Result<(), EmulateDeviceMetricsError>> {
let mut builder = SetDeviceMetricsOverride::builder()
.width(width)
.height(height)
.device_scale_factor(device_scale_factor)
.mobile(mobile);
if let Some(v) = options.scale {
builder = builder.scale(v);
}
if let Some(v) = options.screen_width {
builder = builder.screen_width(v);
}
if let Some(v) = options.screen_height {
builder = builder.screen_height(v);
}
if let Some(v) = options.position_x {
builder = builder.position_x(v);
}
if let Some(v) = options.position_y {
builder = builder.position_y(v);
}
if let Some(v) = options.dont_set_visible_size {
builder = builder.dont_set_visible_size(v);
}
if let Some(v) = options.screen_orientation {
builder = builder.screen_orientation(v);
}
if let Some(v) = options.viewport {
builder = builder.viewport(v);
}
if let Some(v) = options.scrollbar_type {
builder = builder.scrollbar_type(v);
}
let command = builder.build().unwrap();
let adapter = self.adapter_mut();
async move { adapter.emulate_device_metrics(command).await }
}
fn send_command(
&mut self,
command: Command,
) -> impl Future<Output = Result<CommandResponse, CdpSessionSendError>> + Send {
let adapter = self.adapter_mut();
async move { adapter.send_command(command).await }
}
fn get_accessible_nodes(
&mut self,
squash: bool,
) -> impl Future<Output = Result<Vec<AXNode>, NodesFetchError>> + Send {
async move {
let result_value = self
.adapter_mut()
.send_command(GetFullAxTree::builder().build())
.await
.map_err(|e| {
NodesFetchError::CommandResultError(CdpCommandResultError::SessionSendError(e))
})?
.result;
let result = GetFullAxTreeResult::try_from(result_value)
.map_err(|e| NodesFetchError::ParseError(e.to_string()))?;
Ok(AXNode::build_tree(result.nodes, squash))
}
}
fn screenshot(&mut self) -> impl Future<Output = Result<String, ScreenshotError>> + Send {
async move {
self.screenshot_with_options(BrowserScreenshotOptions::default())
.await
}
}
fn screenshot_with_options(
&mut self,
opts: BrowserScreenshotOptions,
) -> impl Future<Output = Result<String, ScreenshotError>> + Send {
let adapter = self.adapter_mut();
let mut cmd = CaptureScreenshotBuilder::default();
if let Some(format) = opts.format {
cmd = cmd.format(format);
}
if let Some(quality) = opts.quality {
cmd = cmd.quality((quality * 100.) as i64)
}
if let Some(viewport) = opts.viewport {
cmd = cmd.clip(viewport);
}
async move {
if opts.full {
let layout_metrics = adapter
.layout_metrics()
.await
.map_err(|e| ScreenshotError::CommandResultError(e))?;
cmd = cmd.capture_beyond_viewport(true);
cmd = cmd.clip(
ViewportBuilder::default()
.x(layout_metrics.css_content_size.x)
.y(layout_metrics.css_content_size.y)
.height(layout_metrics.css_content_size.height)
.width(layout_metrics.css_content_size.width)
.scale(1.)
.build()
.unwrap(),
);
};
adapter.screenshot(cmd.build(), opts.save_path).await
}
}
fn locate(
&mut self,
selector: Selector,
) -> impl Future<Output = Result<Option<Self::BrowserNode>, LocateError>> + Send {
async move {
match self.adapter_mut().locate(selector.as_str()).await? {
Some(node) => Ok(Some(self.build_node(node))),
None => Ok(None),
}
}
}
fn locate_all(
&mut self,
selector: Selector,
) -> impl Future<Output = Result<Vec<Self::BrowserNode>, LocateError>> + Send {
async move {
let nodes = self.adapter_mut().locate_all(selector.as_str()).await?;
Ok(nodes.into_iter().map(|n| self.build_node(n)).collect())
}
}
fn wait_for(
&mut self,
selector: Selector,
timeout: Duration,
) -> impl Future<Output = Result<Self::BrowserNode, LocateError>> + Send {
async move {
let node = self
.adapter_mut()
.wait_for(selector.as_str(), timeout)
.await?;
Ok(self.build_node(node))
}
}
fn wait_for_all(
&mut self,
selector: Selector,
timeout: Duration,
) -> impl Future<Output = Result<Vec<Self::BrowserNode>, LocateError>> + Send {
async move {
let nodes = self
.adapter_mut()
.wait_for_all(selector.as_str(), timeout)
.await?;
Ok(nodes.into_iter().map(|n| self.build_node(n)).collect())
}
}
fn fetch_node(
&mut self,
options: FetchNodeOptions,
) -> impl Future<Output = Result<Self::BrowserNode, NodesFetchError>> + Send {
async move {
let mut builder = DescribeNode::builder();
if let Some(v) = options.node_id {
builder = builder.node_id(v);
}
if let Some(v) = options.backend_node_id {
builder = builder.backend_node_id(v);
}
if let Some(v) = options.object_id {
builder = builder.object_id(v);
}
if let Some(v) = options.depth {
builder = builder.depth(v);
}
if let Some(v) = options.pierce {
builder = builder.pierce(v);
}
let command = builder.build();
let dom_node = self.adapter_mut().fetch_node(command).await?;
Ok(self.build_node(dom_node))
}
}
fn human_mouse(&self) -> &HumanMouse<CdpMouse> {
self.adapter().human_mouse.as_ref()
}
fn mouse(&self) -> &CdpMouse {
self.adapter().mouse.as_ref()
}
fn keyboard(&self) -> &CdpKeyboard<WebsocketConnectionTransport> {
self.adapter().keyboard.as_ref()
}
fn touchscreen(&self) -> &CdpTouchscreen {
self.adapter().touchscreen.as_ref()
}
fn evaluate_script(
&mut self,
expression: impl Into<String>,
await_promise: bool,
) -> impl Future<Output = Result<EvaluateResult, EvaluateScriptError>> + Send {
let expression = expression.into();
let adapter = self.adapter_mut();
async move { adapter.evaluate_script(&expression, await_promise).await }
}
fn add_preload_script(
&mut self,
source: impl Into<String>,
) -> impl Future<Output = Result<ScriptIdentifier, PreloadScriptError>> + Send {
self.add_preload_script_with_options(source, AddPreloadScriptOptions::default())
}
fn add_preload_script_with_options(
&mut self,
source: impl Into<String>,
options: AddPreloadScriptOptions,
) -> impl Future<Output = Result<ScriptIdentifier, PreloadScriptError>> + Send {
let mut builder = AddScriptToEvaluateOnNewDocumentBuilder::default().source(source);
if let Some(v) = options.world_name {
builder = builder.world_name(v);
}
if let Some(v) = options.include_command_line_api {
builder = builder.include_command_line_api(v);
}
if let Some(v) = options.run_immediately {
builder = builder.run_immediately(v);
}
let command = builder.build().unwrap();
let adapter = self.adapter_mut();
async move { adapter.add_preload_script(command).await }
}
fn remove_preload_script(
&mut self,
identifier: impl Into<ScriptIdentifier>,
) -> impl Future<Output = Result<(), PreloadScriptError>> + Send {
let command = RemoveScriptToEvaluateOnNewDocument::builder()
.identifier(identifier)
.build()
.unwrap();
let adapter = self.adapter_mut();
async move { adapter.remove_preload_script(command).await }
}
}
#[derive(Debug, Clone, Default)]
pub struct FetchNodeOptions {
node_id: Option<NodeId>,
backend_node_id: Option<BackendNodeId>,
object_id: Option<RemoteObjectId>,
depth: Option<i64>,
pierce: Option<bool>,
}
impl FetchNodeOptions {
pub fn new() -> Self {
Self::default()
}
pub fn node_id(mut self, node_id: NodeId) -> Self {
self.node_id = Some(node_id);
self
}
pub fn backend_node_id(mut self, backend_node_id: BackendNodeId) -> Self {
self.backend_node_id = Some(backend_node_id);
self
}
pub fn object_id(mut self, object_id: RemoteObjectId) -> Self {
self.object_id = Some(object_id);
self
}
pub fn depth(mut self, depth: i64) -> Self {
self.depth = Some(depth);
self
}
pub fn pierce(mut self, pierce: bool) -> Self {
self.pierce = Some(pierce);
self
}
pub fn build(self) -> Self {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct CreateTabOptions {
pub left: Option<i64>,
pub top: Option<i64>,
pub width: Option<i64>,
pub height: Option<i64>,
pub window_state:
Option<rustenium_cdp_definitions::browser_protocol::target::types::WindowState>,
pub browser_context_id:
Option<rustenium_cdp_definitions::browser_protocol::browser::types::BrowserContextId>,
pub enable_begin_frame_control: Option<bool>,
pub new_window: Option<bool>,
pub background: Option<bool>,
pub for_tab: Option<bool>,
pub hidden: Option<bool>,
pub focus: Option<bool>,
}
#[derive(Default, Clone)]
pub struct CreateTabOptionsBuilder {
left: Option<i64>,
top: Option<i64>,
width: Option<i64>,
height: Option<i64>,
window_state: Option<rustenium_cdp_definitions::browser_protocol::target::types::WindowState>,
browser_context_id:
Option<rustenium_cdp_definitions::browser_protocol::browser::types::BrowserContextId>,
enable_begin_frame_control: Option<bool>,
new_window: Option<bool>,
background: Option<bool>,
for_tab: Option<bool>,
hidden: Option<bool>,
focus: Option<bool>,
}
impl CreateTabOptionsBuilder {
pub fn left(mut self, v: i64) -> Self {
self.left = Some(v);
self
}
pub fn top(mut self, v: i64) -> Self {
self.top = Some(v);
self
}
pub fn width(mut self, v: i64) -> Self {
self.width = Some(v);
self
}
pub fn height(mut self, v: i64) -> Self {
self.height = Some(v);
self
}
pub fn window_state(
mut self,
v: rustenium_cdp_definitions::browser_protocol::target::types::WindowState,
) -> Self {
self.window_state = Some(v);
self
}
pub fn browser_context_id(
mut self,
v: rustenium_cdp_definitions::browser_protocol::browser::types::BrowserContextId,
) -> Self {
self.browser_context_id = Some(v);
self
}
pub fn enable_begin_frame_control(mut self, v: bool) -> Self {
self.enable_begin_frame_control = Some(v);
self
}
pub fn new_window(mut self, v: bool) -> Self {
self.new_window = Some(v);
self
}
pub fn background(mut self, v: bool) -> Self {
self.background = Some(v);
self
}
pub fn for_tab(mut self, v: bool) -> Self {
self.for_tab = Some(v);
self
}
pub fn hidden(mut self, v: bool) -> Self {
self.hidden = Some(v);
self
}
pub fn focus(mut self, v: bool) -> Self {
self.focus = Some(v);
self
}
pub fn build(self) -> CreateTabOptions {
CreateTabOptions {
left: self.left,
top: self.top,
width: self.width,
height: self.height,
window_state: self.window_state,
browser_context_id: self.browser_context_id,
enable_begin_frame_control: self.enable_begin_frame_control,
new_window: self.new_window,
background: self.background,
for_tab: self.for_tab,
hidden: self.hidden,
focus: self.focus,
}
}
}
#[derive(Debug, Clone)]
pub enum Selector {
Css(String),
}
impl Selector {
pub fn css(selector: impl Into<String>) -> Self {
Selector::Css(selector.into())
}
pub fn as_str(&self) -> &str {
match self {
Selector::Css(s) => s.as_str(),
}
}
}
impl<S: Into<String>> From<S> for Selector {
fn from(s: S) -> Self {
Selector::Css(s.into())
}
}
#[derive(Debug, Clone, Default)]
pub struct AddPreloadScriptOptions {
pub world_name: Option<String>,
pub include_command_line_api: Option<bool>,
pub run_immediately: Option<bool>,
}
impl AddPreloadScriptOptions {
pub fn world_name(mut self, v: impl Into<String>) -> Self {
self.world_name = Some(v.into());
self
}
pub fn include_command_line_api(mut self, v: bool) -> Self {
self.include_command_line_api = Some(v);
self
}
pub fn run_immediately(mut self, v: bool) -> Self {
self.run_immediately = Some(v);
self
}
}
use rustenium_cdp_definitions::browser_protocol::emulation::commands::SetDeviceMetricsOverrideScrollbarType;
#[derive(Debug, Clone, Default)]
pub struct EmulateDeviceMetricsOptions {
pub scale: Option<f64>,
pub screen_width: Option<i64>,
pub screen_height: Option<i64>,
pub position_x: Option<i64>,
pub position_y: Option<i64>,
pub dont_set_visible_size: Option<bool>,
pub screen_orientation: Option<ScreenOrientation>,
pub viewport: Option<Viewport>,
pub scrollbar_type: Option<SetDeviceMetricsOverrideScrollbarType>,
}
#[derive(Default, Clone)]
pub struct EmulateDeviceMetricsOptionsBuilder {
scale: Option<f64>,
screen_width: Option<i64>,
screen_height: Option<i64>,
position_x: Option<i64>,
position_y: Option<i64>,
dont_set_visible_size: Option<bool>,
screen_orientation: Option<ScreenOrientation>,
viewport: Option<Viewport>,
scrollbar_type: Option<SetDeviceMetricsOverrideScrollbarType>,
}
impl EmulateDeviceMetricsOptionsBuilder {
pub fn scale(mut self, v: f64) -> Self {
self.scale = Some(v);
self
}
pub fn screen_width(mut self, v: i64) -> Self {
self.screen_width = Some(v);
self
}
pub fn screen_height(mut self, v: i64) -> Self {
self.screen_height = Some(v);
self
}
pub fn position_x(mut self, v: i64) -> Self {
self.position_x = Some(v);
self
}
pub fn position_y(mut self, v: i64) -> Self {
self.position_y = Some(v);
self
}
pub fn dont_set_visible_size(mut self, v: bool) -> Self {
self.dont_set_visible_size = Some(v);
self
}
pub fn screen_orientation(mut self, v: ScreenOrientation) -> Self {
self.screen_orientation = Some(v);
self
}
pub fn viewport(mut self, v: Viewport) -> Self {
self.viewport = Some(v);
self
}
pub fn scrollbar_type(mut self, v: SetDeviceMetricsOverrideScrollbarType) -> Self {
self.scrollbar_type = Some(v);
self
}
pub fn build(self) -> EmulateDeviceMetricsOptions {
EmulateDeviceMetricsOptions {
scale: self.scale,
screen_width: self.screen_width,
screen_height: self.screen_height,
position_x: self.position_x,
position_y: self.position_y,
dont_set_visible_size: self.dont_set_visible_size,
screen_orientation: self.screen_orientation,
viewport: self.viewport,
scrollbar_type: self.scrollbar_type,
}
}
}