use serde::{Deserialize, Deserializer, Serialize};
use crate::bidi::BiDi;
use crate::bidi::command::{BidiCommand, BidiEvent, Empty};
use crate::bidi::error::BidiError;
use crate::bidi::ids::{BrowsingContextId, NavigationId, UserContextId};
use crate::common::protocol::string_enum;
string_enum! {
pub enum ReadinessState {
None = "none",
Interactive = "interactive",
Complete = "complete",
}
}
string_enum! {
pub enum LifecyclePhase {
Load = "load",
DomContentLoaded = "DOMContentLoaded",
}
}
string_enum! {
pub enum CreateType {
Tab = "tab",
Window = "window",
}
}
string_enum! {
pub enum ImageFormatType {
Png = "png",
Jpeg = "jpeg",
}
}
string_enum! {
pub enum TraversalDirection {
Back = "back",
Forward = "forward",
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTree {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_depth: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub root: Option<BrowsingContextId>,
}
impl BidiCommand for GetTree {
const METHOD: &'static str = "browsingContext.getTree";
type Returns = GetTreeResult;
}
#[derive(Debug, Clone, Deserialize)]
pub struct GetTreeResult {
pub contexts: Vec<BrowsingContextInfo>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowsingContextInfo {
pub context: BrowsingContextId,
pub parent: Option<BrowsingContextId>,
#[serde(default)]
pub user_context: Option<UserContextId>,
#[serde(default, deserialize_with = "null_to_default")]
pub children: Vec<BrowsingContextInfo>,
pub url: String,
#[serde(default)]
pub original_opener: Option<BrowsingContextId>,
#[serde(default, rename = "clientWindow")]
pub client_window: Option<String>,
}
fn null_to_default<'de, D, T>(d: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Default + Deserialize<'de>,
{
Ok(Option::<T>::deserialize(d)?.unwrap_or_default())
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Navigate {
pub context: BrowsingContextId,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub wait: Option<ReadinessState>,
}
impl BidiCommand for Navigate {
const METHOD: &'static str = "browsingContext.navigate";
type Returns = NavigateResult;
}
#[derive(Debug, Clone, Deserialize)]
pub struct NavigateResult {
pub navigation: Option<NavigationId>,
pub url: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Reload {
pub context: BrowsingContextId,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_cache: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wait: Option<ReadinessState>,
}
impl BidiCommand for Reload {
const METHOD: &'static str = "browsingContext.reload";
type Returns = NavigateResult;
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Create {
pub r#type: CreateType,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference_context: Option<BrowsingContextId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_context: Option<UserContextId>,
}
impl BidiCommand for Create {
const METHOD: &'static str = "browsingContext.create";
type Returns = CreateResult;
}
#[derive(Debug, Clone, Deserialize)]
pub struct CreateResult {
pub context: BrowsingContextId,
}
#[derive(Debug, Clone, Serialize)]
pub struct Close {
pub context: BrowsingContextId,
#[serde(rename = "promptUnload", skip_serializing_if = "Option::is_none")]
pub prompt_unload: Option<bool>,
}
impl BidiCommand for Close {
const METHOD: &'static str = "browsingContext.close";
type Returns = Empty;
}
#[derive(Debug, Clone, Serialize)]
pub struct Activate {
pub context: BrowsingContextId,
}
impl BidiCommand for Activate {
const METHOD: &'static str = "browsingContext.activate";
type Returns = Empty;
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureScreenshot {
pub context: BrowsingContextId,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<ScreenshotOrigin>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<ImageFormat>,
}
string_enum! {
pub enum ScreenshotOrigin {
Viewport = "viewport",
Document = "document",
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageFormat {
pub r#type: ImageFormatType,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<f64>,
}
impl BidiCommand for CaptureScreenshot {
const METHOD: &'static str = "browsingContext.captureScreenshot";
type Returns = CaptureScreenshotResult;
}
#[derive(Debug, Clone, Deserialize)]
pub struct CaptureScreenshotResult {
pub data: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetViewport {
pub context: BrowsingContextId,
#[serde(skip_serializing_if = "Option::is_none")]
pub viewport: Option<Viewport>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_pixel_ratio: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Viewport {
pub width: u32,
pub height: u32,
}
impl BidiCommand for SetViewport {
const METHOD: &'static str = "browsingContext.setViewport";
type Returns = Empty;
}
#[derive(Debug, Clone, Serialize)]
pub struct TraverseHistory {
pub context: BrowsingContextId,
pub delta: i32,
}
impl BidiCommand for TraverseHistory {
const METHOD: &'static str = "browsingContext.traverseHistory";
type Returns = Empty;
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HandleUserPrompt {
pub context: BrowsingContextId,
#[serde(skip_serializing_if = "Option::is_none")]
pub accept: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_text: Option<String>,
}
impl BidiCommand for HandleUserPrompt {
const METHOD: &'static str = "browsingContext.handleUserPrompt";
type Returns = Empty;
}
string_enum! {
pub enum LocatorType {
Accessibility = "accessibility",
Css = "css",
Context = "context",
InnerText = "innerText",
XPath = "xpath",
}
}
string_enum! {
pub enum InnerTextMatchType {
Full = "full",
Partial = "partial",
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Locator {
#[serde(rename = "type")]
pub locator_type: LocatorType,
pub value: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_case: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub match_type: Option<InnerTextMatchType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_depth: Option<u32>,
}
impl Locator {
pub fn css(selector: impl Into<String>) -> Self {
Self {
locator_type: LocatorType::Css,
value: serde_json::Value::String(selector.into()),
ignore_case: None,
match_type: None,
max_depth: None,
}
}
pub fn xpath(expression: impl Into<String>) -> Self {
Self {
locator_type: LocatorType::XPath,
value: serde_json::Value::String(expression.into()),
ignore_case: None,
match_type: None,
max_depth: None,
}
}
pub fn inner_text(text: impl Into<String>) -> Self {
Self {
locator_type: LocatorType::InnerText,
value: serde_json::Value::String(text.into()),
ignore_case: None,
match_type: None,
max_depth: None,
}
}
pub fn accessibility(role: Option<String>, name: Option<String>) -> Self {
let mut value = serde_json::Map::new();
if let Some(role) = role {
value.insert("role".to_string(), serde_json::Value::String(role));
}
if let Some(name) = name {
value.insert("name".to_string(), serde_json::Value::String(name));
}
Self {
locator_type: LocatorType::Accessibility,
value: serde_json::Value::Object(value),
ignore_case: None,
match_type: None,
max_depth: None,
}
}
pub fn context(context: BrowsingContextId) -> Self {
let mut value = serde_json::Map::new();
value.insert("context".to_string(), serde_json::Value::String(context.0));
Self {
locator_type: LocatorType::Context,
value: serde_json::Value::Object(value),
ignore_case: None,
match_type: None,
max_depth: None,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LocateNodes {
pub context: BrowsingContextId,
pub locator: Locator,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_node_count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub serialization_options: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_nodes: Option<Vec<serde_json::Value>>,
}
impl BidiCommand for LocateNodes {
const METHOD: &'static str = "browsingContext.locateNodes";
type Returns = LocateNodesResult;
}
#[derive(Debug, Clone, Deserialize)]
pub struct LocateNodesResult {
pub nodes: Vec<serde_json::Value>,
}
string_enum! {
pub enum PrintOrientation {
Portrait = "portrait",
Landscape = "landscape",
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PrintMargin {
#[serde(skip_serializing_if = "Option::is_none")]
pub bottom: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub left: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub right: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top: Option<f64>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PrintPage {
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<f64>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Print {
pub context: BrowsingContextId,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub margin: Option<PrintMargin>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orientation: Option<PrintOrientation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<PrintPage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_ranges: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scale: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shrink_to_fit: Option<bool>,
}
impl BidiCommand for Print {
const METHOD: &'static str = "browsingContext.print";
type Returns = PrintResult;
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrintResult {
pub data: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetBypassCsp {
#[serde(serialize_with = "serialize_bypass")]
pub bypass: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contexts: Option<Vec<BrowsingContextId>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_contexts: Option<Vec<UserContextId>>,
}
fn serialize_bypass<S: serde::Serializer>(
value: &Option<bool>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(true) => serializer.serialize_bool(true),
_ => serializer.serialize_none(),
}
}
impl BidiCommand for SetBypassCsp {
const METHOD: &'static str = "browsingContext.setBypassCSP";
type Returns = Empty;
}
pub(crate) mod events {
use super::*;
#[derive(Debug, Clone, Deserialize)]
pub struct ContextCreated(pub BrowsingContextInfo);
impl BidiEvent for ContextCreated {
const METHOD: &'static str = "browsingContext.contextCreated";
}
#[derive(Debug, Clone, Deserialize)]
pub struct ContextDestroyed(pub BrowsingContextInfo);
impl BidiEvent for ContextDestroyed {
const METHOD: &'static str = "browsingContext.contextDestroyed";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigationStarted {
pub context: BrowsingContextId,
pub navigation: Option<NavigationId>,
pub url: String,
pub timestamp: u64,
}
impl BidiEvent for NavigationStarted {
const METHOD: &'static str = "browsingContext.navigationStarted";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Load {
pub context: BrowsingContextId,
pub navigation: Option<NavigationId>,
pub url: String,
pub timestamp: u64,
}
impl BidiEvent for Load {
const METHOD: &'static str = "browsingContext.load";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DomContentLoaded {
pub context: BrowsingContextId,
pub navigation: Option<NavigationId>,
pub url: String,
pub timestamp: u64,
}
impl BidiEvent for DomContentLoaded {
const METHOD: &'static str = "browsingContext.domContentLoaded";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FragmentNavigated {
pub context: BrowsingContextId,
pub navigation: Option<NavigationId>,
pub url: String,
pub timestamp: u64,
}
impl BidiEvent for FragmentNavigated {
const METHOD: &'static str = "browsingContext.fragmentNavigated";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserPromptOpened {
pub context: BrowsingContextId,
#[serde(rename = "type")]
pub prompt_type: String,
pub message: String,
#[serde(default)]
pub default_value: Option<String>,
}
impl BidiEvent for UserPromptOpened {
const METHOD: &'static str = "browsingContext.userPromptOpened";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserPromptClosed {
pub context: BrowsingContextId,
pub accepted: bool,
#[serde(default)]
pub user_text: Option<String>,
}
impl BidiEvent for UserPromptClosed {
const METHOD: &'static str = "browsingContext.userPromptClosed";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HistoryUpdated {
pub context: BrowsingContextId,
pub url: String,
pub timestamp: u64,
#[serde(default)]
pub user_context: Option<UserContextId>,
}
impl BidiEvent for HistoryUpdated {
const METHOD: &'static str = "browsingContext.historyUpdated";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DownloadWillBegin {
pub context: BrowsingContextId,
pub navigation: Option<NavigationId>,
pub url: String,
pub timestamp: u64,
#[serde(default)]
pub user_context: Option<UserContextId>,
pub suggested_filename: String,
}
impl BidiEvent for DownloadWillBegin {
const METHOD: &'static str = "browsingContext.downloadWillBegin";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DownloadEnd {
pub status: DownloadEndStatus,
#[serde(default)]
pub filepath: Option<String>,
pub context: BrowsingContextId,
pub navigation: Option<NavigationId>,
pub url: String,
pub timestamp: u64,
#[serde(default)]
pub user_context: Option<UserContextId>,
}
impl BidiEvent for DownloadEnd {
const METHOD: &'static str = "browsingContext.downloadEnd";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigationAborted {
pub context: BrowsingContextId,
pub navigation: Option<NavigationId>,
pub url: String,
pub timestamp: u64,
#[serde(default)]
pub user_context: Option<UserContextId>,
}
impl BidiEvent for NavigationAborted {
const METHOD: &'static str = "browsingContext.navigationAborted";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigationCommitted {
pub context: BrowsingContextId,
pub navigation: Option<NavigationId>,
pub url: String,
pub timestamp: u64,
#[serde(default)]
pub user_context: Option<UserContextId>,
}
impl BidiEvent for NavigationCommitted {
const METHOD: &'static str = "browsingContext.navigationCommitted";
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigationFailed {
pub context: BrowsingContextId,
pub navigation: Option<NavigationId>,
pub url: String,
pub timestamp: u64,
#[serde(default)]
pub user_context: Option<UserContextId>,
}
impl BidiEvent for NavigationFailed {
const METHOD: &'static str = "browsingContext.navigationFailed";
}
}
string_enum! {
pub enum DownloadEndStatus {
Complete = "complete",
Canceled = "canceled",
}
}
#[derive(Debug)]
pub struct BrowsingContextModule<'a> {
bidi: &'a BiDi,
}
impl<'a> BrowsingContextModule<'a> {
pub(crate) fn new(bidi: &'a BiDi) -> Self {
Self {
bidi,
}
}
pub async fn get_tree(
&self,
root: Option<BrowsingContextId>,
) -> Result<GetTreeResult, BidiError> {
self.bidi
.send(GetTree {
max_depth: None,
root,
})
.await
}
pub async fn top_level(&self) -> Result<BrowsingContextId, BidiError> {
let tree = self.get_tree(None).await?;
tree.contexts.into_iter().next().map(|c| c.context).ok_or_else(|| BidiError {
command: GetTree::METHOD.to_string(),
error: "unknown error".to_string(),
message: "browsingContext.getTree returned no top-level contexts".to_string(),
stacktrace: None,
})
}
pub async fn top_levels(&self) -> Result<Vec<BrowsingContextId>, BidiError> {
let tree = self.get_tree(None).await?;
Ok(tree.contexts.into_iter().map(|c| c.context).collect())
}
pub async fn navigate(
&self,
context: BrowsingContextId,
url: impl Into<String>,
wait: Option<ReadinessState>,
) -> Result<NavigateResult, BidiError> {
self.bidi
.send(Navigate {
context,
url: url.into(),
wait,
})
.await
}
pub async fn reload(
&self,
context: BrowsingContextId,
ignore_cache: bool,
wait: Option<ReadinessState>,
) -> Result<NavigateResult, BidiError> {
self.bidi
.send(Reload {
context,
ignore_cache: Some(ignore_cache),
wait,
})
.await
}
pub async fn create(&self, kind: CreateType) -> Result<CreateResult, BidiError> {
self.bidi
.send(Create {
r#type: kind,
reference_context: None,
background: None,
user_context: None,
})
.await
}
pub async fn close(&self, context: BrowsingContextId) -> Result<(), BidiError> {
self.bidi
.send(Close {
context,
prompt_unload: None,
})
.await?;
Ok(())
}
pub async fn activate(&self, context: BrowsingContextId) -> Result<(), BidiError> {
self.bidi
.send(Activate {
context,
})
.await?;
Ok(())
}
pub async fn capture_screenshot(
&self,
context: BrowsingContextId,
) -> Result<CaptureScreenshotResult, BidiError> {
self.bidi
.send(CaptureScreenshot {
context,
origin: Some(ScreenshotOrigin::Viewport),
format: None,
})
.await
}
pub async fn set_viewport(
&self,
context: BrowsingContextId,
viewport: Option<Viewport>,
) -> Result<(), BidiError> {
self.bidi
.send(SetViewport {
context,
viewport,
device_pixel_ratio: None,
})
.await?;
Ok(())
}
pub async fn traverse_history(
&self,
context: BrowsingContextId,
delta: i32,
) -> Result<(), BidiError> {
self.bidi
.send(TraverseHistory {
context,
delta,
})
.await?;
Ok(())
}
pub async fn handle_user_prompt(
&self,
context: BrowsingContextId,
accept: Option<bool>,
user_text: Option<String>,
) -> Result<(), BidiError> {
self.bidi
.send(HandleUserPrompt {
context,
accept,
user_text,
})
.await?;
Ok(())
}
pub async fn locate_nodes(
&self,
context: BrowsingContextId,
locator: Locator,
) -> Result<LocateNodesResult, BidiError> {
self.bidi
.send(LocateNodes {
context,
locator,
max_node_count: None,
serialization_options: None,
start_nodes: None,
})
.await
}
pub async fn print(&self, context: BrowsingContextId) -> Result<PrintResult, BidiError> {
self.bidi
.send(Print {
context,
background: None,
margin: None,
orientation: None,
page: None,
page_ranges: None,
scale: None,
shrink_to_fit: None,
})
.await
}
pub async fn set_bypass_csp(&self, bypass: Option<bool>) -> Result<(), BidiError> {
self.bidi
.send(SetBypassCsp {
bypass,
contexts: None,
user_contexts: None,
})
.await?;
Ok(())
}
}