Skip to main content

eoka_server/operations/
commands.rs

1use crate::target::{LivePattern, Target};
2use anyhow::{anyhow, Result};
3use eoka::{Action, ActionOptions, LocatorQuery, LocatorSpec};
4use eoka_protocol::{ErrorDetail, Response};
5use serde_json::Value;
6
7pub fn live_locator(target: &str) -> Result<LocatorSpec> {
8    let query = match Target::parse(target) {
9        Target::Live(LivePattern::Css(selector)) => LocatorQuery::Css { selector },
10        Target::Live(LivePattern::Id(id)) => LocatorQuery::Css {
11            selector: format!("[id={}]", serde_json::to_string(&id)?),
12        },
13        Target::Live(LivePattern::Text(text)) => LocatorQuery::Text { text, exact: false },
14        Target::Live(LivePattern::Placeholder(text)) => LocatorQuery::Placeholder { text },
15        Target::Live(LivePattern::Role(role)) => LocatorQuery::Role { role },
16        _ => {
17            return Err(anyhow!(
18                "Snapshot references and indices require the originating observation"
19            ))
20        }
21    };
22    Ok(LocatorSpec {
23        query,
24        frame_id: None,
25        index: None,
26    })
27}
28
29pub fn is_action(cmd: &str) -> bool {
30    matches!(
31        cmd,
32        "click"
33            | "dblclick"
34            | "fill"
35            | "select"
36            | "hover"
37            | "key"
38            | "back"
39            | "forward"
40            | "reload"
41            | "action_perform"
42    )
43}
44
45pub fn action(cmd: &str, args: &Value, target: Option<LocatorSpec>) -> Result<Action> {
46    let required = || target.clone().ok_or_else(|| anyhow!("Target is required"));
47    let string = |key: &str| {
48        args[key]
49            .as_str()
50            .map(str::to_owned)
51            .ok_or_else(|| anyhow!("Missing {key}"))
52    };
53    Ok(match cmd {
54        "click" | "dblclick" => Action::Click {
55            target: required()?,
56            human: false,
57            double: cmd == "dblclick",
58        },
59        "fill" => Action::Fill {
60            target: required()?,
61            value: string("text")?,
62            human: false,
63        },
64        "select" => Action::SelectOption {
65            target: required()?,
66            value: string("value")?,
67        },
68        "hover" => Action::Hover {
69            target: required()?,
70            human: false,
71        },
72        "key" => Action::PressKey {
73            key: string("key")?,
74        },
75        "back" => Action::Back,
76        "forward" => Action::Forward,
77        "reload" => Action::Reload,
78        _ => return Err(anyhow!("Unsupported action")),
79    })
80}
81
82pub fn options(args: &Value) -> Result<ActionOptions> {
83    let mut options: ActionOptions = serde_json::from_value(
84        args.get("options")
85            .cloned()
86            .unwrap_or_else(|| serde_json::json!({})),
87    )?;
88    if let Some(expect) = args.get("expect").filter(|v| !v.is_null()) {
89        options.expect = Some(serde_json::from_value(expect.clone())?);
90    }
91    if let Some(timeout) = args["timeout_ms"].as_u64() {
92        options.timeout_ms = timeout;
93    }
94    Ok(options)
95}
96
97pub fn response(result: Result<impl serde::Serialize>) -> Response {
98    match result {
99        Ok(value) => match serde_json::to_value(value) {
100            Ok(value) => Response::ok(value),
101            Err(_) => Response::err_detail(ErrorDetail::new(
102                "operation_error",
103                "Could not encode operation record",
104            )),
105        },
106        Err(error) => Response::err_detail(
107            ErrorDetail::new("operation_error", error.to_string()).retryable(false),
108        ),
109    }
110}
111
112impl super::OperationManager {
113    pub async fn query(&self, cmd: &str, args: &Value) -> Option<Response> {
114        let id = args["id"].as_str().unwrap_or_default();
115        Some(match cmd {
116            "operation_get" => response(self.get(id).await),
117            "operation_cancel" => response(self.cancel(id).await),
118            "operation_wait" => response(
119                self.wait(id, args["timeout_ms"].as_u64().unwrap_or(30_000))
120                    .await,
121            ),
122            "operation_list" => match serde_json::from_value(args.clone()) {
123                Ok(args) => response(self.list(args).await),
124                Err(error) => Response::err(error.to_string()),
125            },
126            _ => return None,
127        })
128    }
129}