Skip to main content

eoka_runner/config/
actions.rs

1use serde::de::{self, MapAccess, Visitor};
2use serde::{Deserialize, Deserializer};
3use std::fmt;
4
5/// A target element - either by CSS selector or visible text.
6#[derive(Debug, Clone, Deserialize, Default)]
7pub struct Target {
8    /// CSS selector.
9    pub selector: Option<String>,
10    /// Visible text to find.
11    pub text: Option<String>,
12}
13
14impl fmt::Display for Target {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match (&self.selector, &self.text) {
17            (Some(s), _) => write!(f, "selector '{}'", s),
18            (_, Some(t)) => write!(f, "text '{}'", t),
19            _ => write!(f, "unknown"),
20        }
21    }
22}
23
24/// An action to execute in the browser.
25#[derive(Debug, Clone)]
26pub enum Action {
27    // Navigation
28    Goto(GotoAction),
29    Back,
30    Forward,
31    Reload,
32
33    // Waiting
34    Wait(WaitAction),
35    WaitForNetworkIdle(WaitForNetworkIdleAction),
36    WaitFor(WaitForAction),
37    WaitForVisible(WaitForAction),
38    WaitForHidden(WaitForAction),
39    WaitForText(WaitForTextAction),
40    WaitForUrl(WaitForUrlAction),
41
42    // Clicking
43    Click(ClickAction),
44    TryClick(TargetAction),
45    TryClickAny(TryClickAnyAction),
46
47    // Input
48    Fill(FillAction),
49    Type(TypeAction),
50    Clear(ClearAction),
51    Select(SelectAction),
52    PressKey(PressKeyAction),
53
54    // Mouse
55    Hover(TargetAction),
56
57    // Cookies
58    SetCookie(SetCookieAction),
59    DeleteCookie(DeleteCookieAction),
60
61    // JavaScript
62    Execute(ExecuteAction),
63
64    // Scrolling
65    Scroll(ScrollAction),
66    ScrollTo(TargetAction),
67
68    // Debug
69    Screenshot(ScreenshotAction),
70    Log(LogAction),
71    AssertText(AssertTextAction),
72    AssertUrl(AssertUrlAction),
73
74    // Control flow
75    IfTextExists(IfTextExistsAction),
76    IfSelectorExists(IfSelectorExistsAction),
77    Repeat(RepeatAction),
78
79    // Composition
80    Include(IncludeAction),
81}
82
83impl Action {
84    /// Short name for logging.
85    pub fn name(&self) -> &'static str {
86        match self {
87            Self::Goto(_) => "goto",
88            Self::Back => "back",
89            Self::Forward => "forward",
90            Self::Reload => "reload",
91            Self::Wait(_) => "wait",
92            Self::WaitForNetworkIdle(_) => "wait_for_network_idle",
93            Self::WaitFor(_) => "wait_for",
94            Self::WaitForVisible(_) => "wait_for_visible",
95            Self::WaitForHidden(_) => "wait_for_hidden",
96            Self::WaitForText(_) => "wait_for_text",
97            Self::WaitForUrl(_) => "wait_for_url",
98            Self::Click(_) => "click",
99            Self::TryClick(_) => "try_click",
100            Self::TryClickAny(_) => "try_click_any",
101            Self::Fill(_) => "fill",
102            Self::Type(_) => "type",
103            Self::Clear(_) => "clear",
104            Self::Select(_) => "select",
105            Self::PressKey(_) => "press_key",
106            Self::Hover(_) => "hover",
107            Self::SetCookie(_) => "set_cookie",
108            Self::DeleteCookie(_) => "delete_cookie",
109            Self::Execute(_) => "execute",
110            Self::Scroll(_) => "scroll",
111            Self::ScrollTo(_) => "scroll_to",
112            Self::Screenshot(_) => "screenshot",
113            Self::Log(_) => "log",
114            Self::AssertText(_) => "assert_text",
115            Self::AssertUrl(_) => "assert_url",
116            Self::IfTextExists(_) => "if_text_exists",
117            Self::IfSelectorExists(_) => "if_selector_exists",
118            Self::Repeat(_) => "repeat",
119            Self::Include(_) => "include",
120        }
121    }
122}
123
124const ACTION_NAMES: &[&str] = &[
125    "goto",
126    "back",
127    "forward",
128    "reload",
129    "wait",
130    "wait_for_network_idle",
131    "wait_for",
132    "wait_for_visible",
133    "wait_for_hidden",
134    "wait_for_text",
135    "wait_for_url",
136    "click",
137    "try_click",
138    "try_click_any",
139    "fill",
140    "type",
141    "clear",
142    "select",
143    "press_key",
144    "hover",
145    "set_cookie",
146    "delete_cookie",
147    "execute",
148    "scroll",
149    "scroll_to",
150    "screenshot",
151    "log",
152    "assert_text",
153    "assert_url",
154    "if_text_exists",
155    "if_selector_exists",
156    "repeat",
157    "include",
158];
159
160impl<'de> Deserialize<'de> for Action {
161    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162    where
163        D: Deserializer<'de>,
164    {
165        deserializer.deserialize_any(ActionVisitor)
166    }
167}
168
169struct ActionVisitor;
170
171impl<'de> Visitor<'de> for ActionVisitor {
172    type Value = Action;
173
174    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
175        formatter.write_str("an action (string for unit variants, or map with single key)")
176    }
177
178    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
179    where
180        E: de::Error,
181    {
182        match value {
183            "back" => Ok(Action::Back),
184            "forward" => Ok(Action::Forward),
185            "reload" => Ok(Action::Reload),
186            other => Err(de::Error::unknown_variant(
187                other,
188                &["back", "forward", "reload"],
189            )),
190        }
191    }
192
193    fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
194    where
195        M: MapAccess<'de>,
196    {
197        let key: String = map
198            .next_key()?
199            .ok_or_else(|| de::Error::custom("expected action type key"))?;
200
201        let action = match key.as_str() {
202            "goto" => Action::Goto(map.next_value()?),
203            "back" => {
204                let _: serde_yaml::Value = map.next_value()?;
205                Action::Back
206            }
207            "forward" => {
208                let _: serde_yaml::Value = map.next_value()?;
209                Action::Forward
210            }
211            "reload" => {
212                let _: serde_yaml::Value = map.next_value()?;
213                Action::Reload
214            }
215            "wait" => Action::Wait(map.next_value()?),
216            "wait_for_network_idle" => Action::WaitForNetworkIdle(map.next_value()?),
217            "wait_for" => Action::WaitFor(map.next_value()?),
218            "wait_for_visible" => Action::WaitForVisible(map.next_value()?),
219            "wait_for_hidden" => Action::WaitForHidden(map.next_value()?),
220            "wait_for_text" => Action::WaitForText(map.next_value()?),
221            "wait_for_url" => Action::WaitForUrl(map.next_value()?),
222            "click" => Action::Click(map.next_value()?),
223            "try_click" => Action::TryClick(map.next_value()?),
224            "try_click_any" => Action::TryClickAny(map.next_value()?),
225            "fill" => Action::Fill(map.next_value()?),
226            "type" => Action::Type(map.next_value()?),
227            "clear" => Action::Clear(map.next_value()?),
228            "select" => Action::Select(map.next_value()?),
229            "press_key" => Action::PressKey(map.next_value()?),
230            "hover" => Action::Hover(map.next_value()?),
231            "set_cookie" => Action::SetCookie(map.next_value()?),
232            "delete_cookie" => Action::DeleteCookie(map.next_value()?),
233            "execute" => Action::Execute(map.next_value()?),
234            "scroll" => Action::Scroll(map.next_value()?),
235            "scroll_to" => Action::ScrollTo(map.next_value()?),
236            "screenshot" => Action::Screenshot(map.next_value()?),
237            "log" => Action::Log(map.next_value()?),
238            "assert_text" => Action::AssertText(map.next_value()?),
239            "assert_url" => Action::AssertUrl(map.next_value()?),
240            "if_text_exists" => Action::IfTextExists(map.next_value()?),
241            "if_selector_exists" => Action::IfSelectorExists(map.next_value()?),
242            "repeat" => Action::Repeat(map.next_value()?),
243            "include" => Action::Include(map.next_value()?),
244            other => return Err(de::Error::unknown_variant(other, ACTION_NAMES)),
245        };
246
247        Ok(action)
248    }
249}
250
251// --- Action payloads ---
252
253#[derive(Debug, Clone, Deserialize)]
254pub struct GotoAction {
255    pub url: String,
256}
257
258#[derive(Debug, Clone, Deserialize)]
259pub struct WaitAction {
260    pub ms: u64,
261}
262
263fn default_idle_ms() -> u64 {
264    500
265}
266fn default_timeout_ms() -> u64 {
267    10000
268}
269
270#[derive(Debug, Clone, Deserialize)]
271pub struct WaitForNetworkIdleAction {
272    #[serde(default = "default_idle_ms")]
273    pub idle_ms: u64,
274    #[serde(default = "default_timeout_ms")]
275    pub timeout_ms: u64,
276}
277
278#[derive(Debug, Clone, Deserialize)]
279pub struct WaitForAction {
280    pub selector: String,
281    #[serde(default = "default_timeout_ms")]
282    pub timeout_ms: u64,
283}
284
285#[derive(Debug, Clone, Deserialize)]
286pub struct WaitForTextAction {
287    pub text: String,
288    #[serde(default = "default_timeout_ms")]
289    pub timeout_ms: u64,
290}
291
292#[derive(Debug, Clone, Deserialize)]
293pub struct WaitForUrlAction {
294    pub contains: String,
295    #[serde(default = "default_timeout_ms")]
296    pub timeout_ms: u64,
297}
298
299#[derive(Debug, Clone, Deserialize)]
300pub struct ClickAction {
301    #[serde(flatten)]
302    pub target: Target,
303    #[serde(default)]
304    pub human: bool,
305    #[serde(default)]
306    pub scroll_into_view: bool,
307}
308
309#[derive(Debug, Clone, Deserialize)]
310pub struct TryClickAnyAction {
311    pub selectors: Option<Vec<String>>,
312    pub texts: Option<Vec<String>>,
313}
314
315#[derive(Debug, Clone, Deserialize)]
316pub struct FillAction {
317    #[serde(flatten)]
318    pub target: Target,
319    pub value: String,
320    #[serde(default)]
321    pub human: bool,
322}
323
324#[derive(Debug, Clone, Deserialize)]
325pub struct TypeAction {
326    #[serde(flatten)]
327    pub target: Target,
328    pub value: String,
329}
330
331#[derive(Debug, Clone, Deserialize)]
332pub struct ClearAction {
333    #[serde(flatten)]
334    pub target: Target,
335}
336
337#[derive(Debug, Clone, Deserialize)]
338pub struct SelectAction {
339    #[serde(flatten)]
340    pub target: Target,
341    pub value: String,
342}
343
344#[derive(Debug, Clone, Deserialize)]
345pub struct PressKeyAction {
346    pub key: String,
347}
348
349/// Generic action that just needs a target element.
350#[derive(Debug, Clone, Deserialize)]
351pub struct TargetAction {
352    #[serde(flatten)]
353    pub target: Target,
354}
355
356#[derive(Debug, Clone, Deserialize)]
357pub struct SetCookieAction {
358    pub name: String,
359    pub value: String,
360    pub domain: Option<String>,
361    pub path: Option<String>,
362}
363
364#[derive(Debug, Clone, Deserialize)]
365pub struct DeleteCookieAction {
366    pub name: String,
367    pub domain: Option<String>,
368}
369
370#[derive(Debug, Clone, Deserialize)]
371pub struct ExecuteAction {
372    pub js: String,
373}
374
375fn default_scroll_amount() -> u32 {
376    1
377}
378
379#[derive(Debug, Clone, Deserialize)]
380pub struct ScrollAction {
381    pub direction: ScrollDirection,
382    #[serde(default = "default_scroll_amount")]
383    pub amount: u32,
384}
385
386#[derive(Debug, Clone, Deserialize)]
387#[serde(rename_all = "snake_case")]
388pub enum ScrollDirection {
389    Up,
390    Down,
391    Left,
392    Right,
393}
394
395#[derive(Debug, Clone, Deserialize)]
396pub struct ScreenshotAction {
397    pub path: String,
398}
399
400#[derive(Debug, Clone, Deserialize)]
401pub struct LogAction {
402    pub message: String,
403}
404
405#[derive(Debug, Clone, Deserialize)]
406pub struct AssertTextAction {
407    pub text: String,
408}
409
410#[derive(Debug, Clone, Deserialize)]
411pub struct AssertUrlAction {
412    pub contains: String,
413}
414
415#[derive(Debug, Clone, Deserialize)]
416pub struct IfTextExistsAction {
417    pub text: String,
418    #[serde(rename = "then")]
419    pub then_actions: Vec<Action>,
420    #[serde(rename = "else", default)]
421    pub else_actions: Vec<Action>,
422}
423
424#[derive(Debug, Clone, Deserialize)]
425pub struct IfSelectorExistsAction {
426    pub selector: String,
427    #[serde(rename = "then")]
428    pub then_actions: Vec<Action>,
429    #[serde(rename = "else", default)]
430    pub else_actions: Vec<Action>,
431}
432
433#[derive(Debug, Clone, Deserialize)]
434pub struct RepeatAction {
435    pub times: u32,
436    pub actions: Vec<Action>,
437}
438
439/// Include another config's actions.
440#[derive(Debug, Clone, Deserialize)]
441pub struct IncludeAction {
442    /// Path to the config file to include.
443    pub path: String,
444
445    /// Parameters to pass to the included config.
446    #[serde(default)]
447    pub params: std::collections::HashMap<String, String>,
448}