1use crate::eoka::{Page, Result};
2use serde::Deserialize;
3
4#[derive(Debug, Clone)]
5pub enum Target {
6 Index(usize),
7 Ref(String),
8 Live(LivePattern),
9}
10
11#[derive(Debug, Clone)]
12pub enum LivePattern {
13 Text(String),
14 Placeholder(String),
15 Role(String),
16 Css(String),
17 Id(String),
18}
19
20impl Target {
21 pub fn parse(s: &str) -> Self {
22 let s = s.trim();
23
24 let index = s
25 .strip_prefix("index:")
26 .or_else(|| s.strip_prefix("idx:"))
27 .unwrap_or(s)
28 .trim();
29
30 let index = index
31 .strip_prefix('[')
32 .and_then(|v| v.strip_suffix(']'))
33 .unwrap_or(index)
34 .trim();
35 if let Ok(idx) = index.parse::<usize>() {
36 return Target::Index(idx);
37 }
38
39 if s.starts_with('@') {
40 return Target::Ref(s.to_string());
41 }
42
43 Target::Live(LivePattern::parse(s))
44 }
45}
46
47impl LivePattern {
48 pub fn parse(s: &str) -> Self {
49 if let Some(v) = s.strip_prefix("text:") {
50 return LivePattern::Text(v.into());
51 }
52 if let Some(v) = s.strip_prefix("placeholder:") {
53 return LivePattern::Placeholder(v.into());
54 }
55 if let Some(v) = s.strip_prefix("role:") {
56 return LivePattern::Role(v.into());
57 }
58 if let Some(v) = s.strip_prefix("css:") {
59 return LivePattern::Css(v.into());
60 }
61 if let Some(v) = s.strip_prefix("id:") {
62 return LivePattern::Id(v.into());
63 }
64 LivePattern::Text(s.into())
65 }
66
67 fn as_js_args(&self) -> (&'static str, &str) {
68 match self {
69 LivePattern::Text(v) => ("text", v),
70 LivePattern::Placeholder(v) => ("placeholder", v),
71 LivePattern::Role(v) => ("role", v),
72 LivePattern::Css(v) => ("css", v),
73 LivePattern::Id(v) => ("id", v),
74 }
75 }
76}
77
78#[derive(Debug, Deserialize, Clone, Default)]
79pub struct BBox {
80 pub x: f64,
81 pub y: f64,
82 pub width: f64,
83 pub height: f64,
84}
85
86#[derive(Debug, Deserialize)]
87pub struct Resolved {
88 pub selector: String,
89 pub tag: String,
90 pub text: String,
91 pub found: bool,
92 #[serde(default)]
93 pub error: Option<String>,
94 #[serde(default)]
95 pub bbox: BBox,
96}
97
98const RESOLVE_JS: &str = include_str!("js/resolve.js");
99
100pub async fn resolve(page: &Page, pattern: &LivePattern) -> Result<Resolved> {
101 let (t, v) = pattern.as_js_args();
102 let js = format!(
103 "{}({},{})",
104 RESOLVE_JS,
105 serde_json::to_string(t).unwrap(),
106 serde_json::to_string(v).unwrap()
107 );
108 page.evaluate(&js).await
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn parse_index() {
117 assert!(matches!(Target::parse("0"), Target::Index(0)));
118 assert!(matches!(Target::parse("15"), Target::Index(15)));
119 assert!(matches!(Target::parse(" 42 "), Target::Index(42)));
120 assert!(matches!(Target::parse("[38]"), Target::Index(38)));
121 assert!(matches!(Target::parse(" [ 38 ] "), Target::Index(38)));
122 assert!(matches!(Target::parse("index:38"), Target::Index(38)));
123 assert!(matches!(Target::parse("index: 38"), Target::Index(38)));
124 assert!(matches!(Target::parse("idx:38"), Target::Index(38)));
125 }
126
127 #[test]
128 fn parse_ref() {
129 assert!(matches!(Target::parse("@e1"), Target::Ref(ref s) if s == "@e1"));
130 assert!(matches!(Target::parse("@e42"), Target::Ref(ref s) if s == "@e42"));
131 assert!(matches!(Target::parse(" @e5 "), Target::Ref(ref s) if s == "@e5"));
132 assert!(matches!(Target::parse("@foo"), Target::Ref(ref s) if s == "@foo"));
133 }
134
135 #[test]
136 fn parse_live_prefixed() {
137 assert!(matches!(
138 Target::parse("text:Submit"),
139 Target::Live(LivePattern::Text(_))
140 ));
141 assert!(matches!(
142 Target::parse("placeholder:Email"),
143 Target::Live(LivePattern::Placeholder(_))
144 ));
145 assert!(matches!(
146 Target::parse("css:button"),
147 Target::Live(LivePattern::Css(_))
148 ));
149 assert!(matches!(
150 Target::parse("id:btn"),
151 Target::Live(LivePattern::Id(_))
152 ));
153 assert!(matches!(
154 Target::parse("role:button"),
155 Target::Live(LivePattern::Role(_))
156 ));
157 }
158
159 #[test]
160 fn parse_live_unprefixed() {
161 assert!(matches!(
162 Target::parse("Submit"),
163 Target::Live(LivePattern::Text(_))
164 ));
165 assert!(matches!(
166 Target::parse("Click Me"),
167 Target::Live(LivePattern::Text(_))
168 ));
169 assert!(matches!(
170 Target::parse("[not-an-index]"),
171 Target::Live(LivePattern::Text(_))
172 ));
173 assert!(matches!(
174 Target::parse("index:abc"),
175 Target::Live(LivePattern::Text(_))
176 ));
177 assert!(matches!(
178 Target::parse("text:[38]"),
179 Target::Live(LivePattern::Text(_))
180 ));
181 }
182
183 #[test]
184 fn parse_preserves_value() {
185 if let Target::Live(LivePattern::Text(v)) = Target::parse("Submit Form") {
186 assert_eq!(v, "Submit Form");
187 } else {
188 panic!("Expected Text");
189 }
190
191 if let Target::Live(LivePattern::Css(v)) = Target::parse("css:button.primary") {
192 assert_eq!(v, "button.primary");
193 } else {
194 panic!("Expected Css");
195 }
196
197 if let Target::Live(LivePattern::Placeholder(v)) = Target::parse("placeholder:Enter email")
198 {
199 assert_eq!(v, "Enter email");
200 } else {
201 panic!("Expected Placeholder");
202 }
203 }
204
205 #[test]
206 fn as_js_args() {
207 assert_eq!(
208 LivePattern::Text("foo".into()).as_js_args(),
209 ("text", "foo")
210 );
211 assert_eq!(
212 LivePattern::Placeholder("bar".into()).as_js_args(),
213 ("placeholder", "bar")
214 );
215 assert_eq!(
216 LivePattern::Css("div.x".into()).as_js_args(),
217 ("css", "div.x")
218 );
219 assert_eq!(LivePattern::Id("myid".into()).as_js_args(), ("id", "myid"));
220 assert_eq!(
221 LivePattern::Role("button".into()).as_js_args(),
222 ("role", "button")
223 );
224 }
225}