use std::{future::Future, pin::Pin, sync::Arc};
use serde::{Deserialize, Serialize};
use crate::Result;
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LocatorQuery {
Node {
id: i32,
},
Css {
selector: String,
},
Text {
text: String,
#[serde(default)]
exact: bool,
},
Placeholder {
text: String,
},
Role {
role: String,
},
BackendNode {
id: i64,
},
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LocatorSpec {
pub query: LocatorQuery,
#[serde(default)]
pub frame_id: Option<String>,
#[serde(default)]
pub index: Option<usize>,
}
impl LocatorSpec {
pub fn css(selector: impl Into<String>) -> Self {
Self {
query: LocatorQuery::Css {
selector: selector.into(),
},
frame_id: None,
index: None,
}
}
pub fn text(text: impl Into<String>) -> Self {
Self {
query: LocatorQuery::Text {
text: text.into(),
exact: false,
},
frame_id: None,
index: None,
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Expectation {
DownloadStarted,
Focused {
target: LocatorSpec,
},
Url {
value: String,
#[serde(default)]
contains: bool,
},
Visible {
target: LocatorSpec,
},
Hidden {
target: LocatorSpec,
},
Text {
target: LocatorSpec,
value: String,
#[serde(default)]
contains: bool,
},
Value {
target: LocatorSpec,
value: String,
},
Checked {
target: LocatorSpec,
checked: bool,
},
Selected {
target: LocatorSpec,
values: Vec<String>,
},
}
impl Expectation {
pub fn kind(&self) -> &'static str {
match self {
Self::DownloadStarted => "download_started",
Self::Focused { .. } => "focused",
Self::Url { .. } => "url",
Self::Visible { .. } => "visible",
Self::Hidden { .. } => "hidden",
Self::Text { .. } => "text",
Self::Value { .. } => "value",
Self::Checked { .. } => "checked",
Self::Selected { .. } => "selected",
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Action {
History {
delta: i32,
},
SelectOption {
target: LocatorSpec,
value: String,
},
NavigateWithHeaders {
url: String,
headers: std::collections::HashMap<String, String>,
},
Drag {
target: LocatorSpec,
dx: f64,
dy: f64,
},
InsertText {
text: String,
},
SelectText {
target: LocatorSpec,
text: String,
},
Click {
target: LocatorSpec,
#[serde(default)]
human: bool,
#[serde(default)]
double: bool,
},
Fill {
target: LocatorSpec,
value: String,
#[serde(default)]
human: bool,
},
Type {
target: LocatorSpec,
text: String,
#[serde(default)]
human: bool,
},
Select {
target: LocatorSpec,
values: Vec<String>,
},
Upload {
target: LocatorSpec,
files: Vec<String>,
},
PressKey {
key: String,
},
Submit {
target: LocatorSpec,
},
Hover {
target: LocatorSpec,
#[serde(default)]
human: bool,
},
Focus {
target: LocatorSpec,
},
ScrollIntoView {
target: LocatorSpec,
},
Navigate {
url: String,
#[serde(default)]
referrer: Option<String>,
},
Reload,
Back,
Forward,
}
impl Action {
pub fn kind(&self) -> &'static str {
match self {
Self::Drag { .. } => "drag",
Self::InsertText { .. } => "insert_text",
Self::SelectText { .. } => "select_text",
Self::SelectOption { .. } => "select_option",
Self::Click { double: true, .. } => "double_click",
Self::Click { .. } => "click",
Self::Fill { .. } => "fill",
Self::Type { .. } => "type",
Self::Select { .. } => "select",
Self::Upload { .. } => "upload",
Self::PressKey { .. } => "press_key",
Self::Submit { .. } => "submit",
Self::Hover { .. } => "hover",
Self::Focus { .. } => "focus",
Self::ScrollIntoView { .. } => "scroll_into_view",
Self::Navigate { .. } | Self::NavigateWithHeaders { .. } => "navigate",
Self::Reload => "reload",
Self::History { .. } => "history",
Self::Back => "back",
Self::Forward => "forward",
}
}
pub fn target(&self) -> Option<&LocatorSpec> {
match self {
Self::Drag { target, .. }
| Self::SelectText { target, .. }
| Self::SelectOption { target, .. }
| Self::Click { target, .. }
| Self::Fill { target, .. }
| Self::Type { target, .. }
| Self::Select { target, .. }
| Self::Upload { target, .. }
| Self::Submit { target }
| Self::Hover { target, .. }
| Self::Focus { target }
| Self::ScrollIntoView { target } => Some(target),
_ => None,
}
}
pub fn requires_expectation(&self) -> bool {
matches!(
self,
Self::Click { .. }
| Self::Type { .. }
| Self::PressKey { .. }
| Self::Submit { .. }
| Self::Drag { .. }
| Self::InsertText { .. }
)
}
}
pub trait DispatchObserver: Send + Sync {
fn before_dispatch(
&self,
preparation: bool,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Serialize, Deserialize)]
pub struct ActionOptions {
#[serde(default = "wait_for_target")]
pub wait_for_target: bool,
#[serde(default = "default_timeout")]
pub timeout_ms: u64,
#[serde(default)]
pub expect: Option<Expectation>,
#[serde(skip)]
pub observer: Option<Arc<dyn DispatchObserver>>,
}
fn wait_for_target() -> bool {
true
}
fn default_timeout() -> u64 {
30_000
}
impl Default for ActionOptions {
fn default() -> Self {
Self {
wait_for_target: true,
timeout_ms: default_timeout(),
expect: None,
observer: None,
}
}
}
impl ActionOptions {
pub fn expecting(expect: Expectation) -> Self {
Self {
expect: Some(expect),
..Self::default()
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ActionOutcome {
NotDispatched,
OutcomeUnknown,
VerifiedComplete,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DispatchState {
NotSent,
MayHaveBeenSent,
Acknowledged,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionReceipt {
pub started_at_ms: u64,
pub finished_at_ms: u64,
pub download_guid: Option<String>,
pub action: String,
pub target_id: String,
pub outcome: ActionOutcome,
pub dispatch: DispatchState,
pub preparation_may_have_executed: bool,
pub verification: Option<String>,
pub elapsed_ms: u64,
pub already_satisfied: bool,
pub cleanup_pending: bool,
}
impl std::fmt::Debug for ActionOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ActionOptions")
.field("timeout_ms", &self.timeout_ms)
.field("expect", &self.expect.as_ref().map(Expectation::kind))
.finish_non_exhaustive()
}
}