eoka_server/operations/
commands.rs1use 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}