1#![forbid(unsafe_code)]
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct VerbEntry {
29 pub maestro_name: &'static str,
31 pub smix_name: &'static str,
33 pub category: VerbCategory,
36 pub arg_shape: ArgShape,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum VerbCategory {
44 Tap,
46 Input,
49 Assert,
52 ControlFlow,
55 Lifecycle,
58 Media,
61 Gesture,
64 Device,
67 SmixNative,
70 Utility,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum ArgShape {
79 None,
81 BareString,
83 Selector,
86 Mapping,
89 Path,
91 Coord,
94 Integer,
96}
97
98pub static VERB_TABLE: &[VerbEntry] = &[
101 v("tapOn", "tap", VerbCategory::Tap, ArgShape::Selector),
103 v(
104 "doubleTapOn",
105 "doubleTap",
106 VerbCategory::Tap,
107 ArgShape::Selector,
108 ),
109 v(
110 "longPressOn",
111 "longPress",
112 VerbCategory::Tap,
113 ArgShape::Selector,
114 ),
115 v(
116 "tapByCoord",
117 "tapByCoord",
118 VerbCategory::Tap,
119 ArgShape::Coord,
120 ),
121 v(
123 "inputText",
124 "fill",
125 VerbCategory::Input,
126 ArgShape::BareString,
127 ),
128 v("eraseText", "clear", VerbCategory::Input, ArgShape::Integer),
129 v(
130 "pasteText",
131 "pasteText",
132 VerbCategory::Input,
133 ArgShape::None,
134 ),
135 v(
136 "setClipboard",
137 "setClipboard",
138 VerbCategory::Input,
139 ArgShape::BareString,
140 ),
141 v(
142 "copyTextFrom",
143 "copyTextFrom",
144 VerbCategory::Input,
145 ArgShape::Selector,
146 ),
147 v(
149 "assertVisible",
150 "expect",
151 VerbCategory::Assert,
152 ArgShape::Selector,
153 ),
154 v(
155 "assertNotVisible",
156 "expectNotVisible",
157 VerbCategory::Assert,
158 ArgShape::Selector,
159 ),
160 v(
161 "extendedWaitUntil",
162 "expect",
163 VerbCategory::Assert,
164 ArgShape::Mapping,
165 ),
166 v(
167 "assertTrue",
168 "assertTrue",
169 VerbCategory::Assert,
170 ArgShape::BareString,
171 ),
172 v(
173 "assertScreenshot",
174 "assertScreenshot",
175 VerbCategory::Assert,
176 ArgShape::Mapping,
177 ),
178 v(
180 "runFlow",
181 "runFlow",
182 VerbCategory::ControlFlow,
183 ArgShape::Path,
184 ),
185 v(
186 "retry",
187 "retry",
188 VerbCategory::ControlFlow,
189 ArgShape::Mapping,
190 ),
191 v(
192 "repeat",
193 "repeat",
194 VerbCategory::ControlFlow,
195 ArgShape::Mapping,
196 ),
197 v(
198 "pressKey",
199 "pressKey",
200 VerbCategory::ControlFlow,
201 ArgShape::BareString,
202 ),
203 v(
204 "back",
205 "pressKey",
206 VerbCategory::ControlFlow,
207 ArgShape::None,
208 ),
209 v(
211 "launchApp",
212 "launchApp",
213 VerbCategory::Lifecycle,
214 ArgShape::Mapping,
215 ),
216 v(
217 "stopApp",
218 "terminate",
219 VerbCategory::Lifecycle,
220 ArgShape::None,
221 ),
222 v(
223 "killApp",
224 "terminate",
225 VerbCategory::Lifecycle,
226 ArgShape::BareString,
227 ),
228 v(
229 "clearState",
230 "reset",
231 VerbCategory::Lifecycle,
232 ArgShape::None,
233 ),
234 v(
235 "clearKeychain",
236 "resetKeychain",
237 VerbCategory::Lifecycle,
238 ArgShape::None,
239 ),
240 v(
242 "takeScreenshot",
243 "takeScreenshot",
244 VerbCategory::Media,
245 ArgShape::BareString,
246 ),
247 v(
248 "startRecording",
249 "startRecording",
250 VerbCategory::Media,
251 ArgShape::BareString,
252 ),
253 v(
254 "stopRecording",
255 "stopRecording",
256 VerbCategory::Media,
257 ArgShape::None,
258 ),
259 v(
260 "addMedia",
261 "addMedia",
262 VerbCategory::Media,
263 ArgShape::Mapping,
264 ),
265 v("scroll", "scroll", VerbCategory::Gesture, ArgShape::None),
267 v(
268 "scrollUntilVisible",
269 "scroll",
270 VerbCategory::Gesture,
271 ArgShape::Mapping,
272 ),
273 v("swipe", "swipe", VerbCategory::Gesture, ArgShape::Mapping),
274 v(
275 "hideKeyboard",
276 "hideKeyboard",
277 VerbCategory::Gesture,
278 ArgShape::None,
279 ),
280 v(
282 "openLink",
283 "openUrl",
284 VerbCategory::Device,
285 ArgShape::BareString,
286 ),
287 v(
288 "setLocation",
289 "setLocation",
290 VerbCategory::Device,
291 ArgShape::Mapping,
292 ),
293 v("travel", "travel", VerbCategory::Device, ArgShape::Mapping),
294 v(
295 "setPermissions",
296 "setPermissions",
297 VerbCategory::Device,
298 ArgShape::Mapping,
299 ),
300 v(
301 "setOrientation",
302 "setOrientation",
303 VerbCategory::Device,
304 ArgShape::BareString,
305 ),
306 v(
307 "toggleAirplaneMode",
308 "toggleAirplaneMode",
309 VerbCategory::Device,
310 ArgShape::None,
311 ),
312 v(
314 "webview_eval",
315 "webviewEval",
316 VerbCategory::SmixNative,
317 ArgShape::BareString,
318 ),
319 v(
320 "webviewEval",
321 "webviewEval",
322 VerbCategory::SmixNative,
323 ArgShape::BareString,
324 ),
325 v(
326 "fixture",
327 "fixture",
328 VerbCategory::SmixNative,
329 ArgShape::Mapping,
330 ),
331 v(
332 "tapById",
333 "tapById",
334 VerbCategory::SmixNative,
335 ArgShape::BareString,
336 ),
337 v(
338 "tapAtCoord",
339 "tapAtCoord",
340 VerbCategory::SmixNative,
341 ArgShape::Coord,
342 ),
343 v(
344 "swipeAtCoord",
345 "swipeAtCoord",
346 VerbCategory::SmixNative,
347 ArgShape::Coord,
348 ),
349 v(
350 "doubleTap",
351 "doubleTap",
352 VerbCategory::SmixNative,
353 ArgShape::Selector,
354 ),
355 v(
356 "longPress",
357 "longPress",
358 VerbCategory::SmixNative,
359 ArgShape::Selector,
360 ),
361 v(
362 "ocrText",
363 "ocrText",
364 VerbCategory::SmixNative,
365 ArgShape::BareString,
366 ),
367 v(
368 "anchorRelative",
369 "anchorRelative",
370 VerbCategory::SmixNative,
371 ArgShape::Mapping,
372 ),
373 v(
374 "findTextByOcr",
375 "findTextByOcr",
376 VerbCategory::SmixNative,
377 ArgShape::Mapping,
378 ),
379 v(
380 "expect",
381 "expect",
382 VerbCategory::SmixNative,
383 ArgShape::Mapping,
384 ),
385 v(
387 "waitForAnimationToEnd",
388 "waitForAnimationToEnd",
389 VerbCategory::Utility,
390 ArgShape::None,
391 ),
392 ];
399
400const fn v(
401 maestro_name: &'static str,
402 smix_name: &'static str,
403 category: VerbCategory,
404 arg_shape: ArgShape,
405) -> VerbEntry {
406 VerbEntry {
407 maestro_name,
408 smix_name,
409 category,
410 arg_shape,
411 }
412}
413
414pub fn find_by_maestro(name: &str) -> Option<&'static VerbEntry> {
416 VERB_TABLE.iter().find(|e| e.maestro_name == name)
417}
418
419pub fn find_by_smix(name: &str) -> Option<&'static VerbEntry> {
426 VERB_TABLE.iter().find(|e| e.smix_name == name)
427}
428
429pub fn is_known_verb(name: &str) -> bool {
432 find_by_maestro(name).is_some() || find_by_smix(name).is_some()
433}
434
435pub fn verbs_in_category(category: VerbCategory) -> impl Iterator<Item = &'static VerbEntry> {
438 VERB_TABLE.iter().filter(move |e| e.category == category)
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn find_by_maestro_hits() {
447 let e = find_by_maestro("tapOn").unwrap();
448 assert_eq!(e.smix_name, "tap");
449 assert_eq!(e.category, VerbCategory::Tap);
450 }
451
452 #[test]
453 fn find_by_smix_hits() {
454 let e = find_by_smix("tap").unwrap();
455 assert_eq!(e.maestro_name, "tapOn");
456 }
457
458 #[test]
459 fn find_by_smix_returns_first_when_multiple_match() {
460 let e = find_by_smix("pressKey").unwrap();
465 assert!(e.maestro_name == "pressKey" || e.maestro_name == "back");
466 }
467
468 #[test]
469 fn is_known_covers_both_forms() {
470 assert!(is_known_verb("tapOn"));
471 assert!(is_known_verb("tap"));
472 assert!(is_known_verb("assertVisible"));
473 assert!(is_known_verb("expect"));
474 assert!(!is_known_verb("nonexistent"));
475 }
476
477 #[test]
478 fn extendedwait_maps_to_expect() {
479 let e = find_by_maestro("extendedWaitUntil").unwrap();
480 assert_eq!(e.smix_name, "expect");
481 assert_eq!(e.category, VerbCategory::Assert);
482 assert_eq!(e.arg_shape, ArgShape::Mapping);
483 }
484
485 #[test]
486 fn table_has_no_duplicate_maestro_names() {
487 let mut seen = Vec::new();
488 for e in VERB_TABLE {
489 if seen.contains(&e.maestro_name) {
490 panic!("duplicate maestro_name: {}", e.maestro_name);
491 }
492 seen.push(e.maestro_name);
493 }
494 }
495
496 #[test]
497 fn each_category_has_at_least_one_entry() {
498 for cat in [
499 VerbCategory::Tap,
500 VerbCategory::Input,
501 VerbCategory::Assert,
502 VerbCategory::ControlFlow,
503 VerbCategory::Lifecycle,
504 VerbCategory::Media,
505 VerbCategory::Gesture,
506 VerbCategory::Device,
507 VerbCategory::SmixNative,
508 VerbCategory::Utility,
509 ] {
510 let count = verbs_in_category(cat).count();
511 assert!(count > 0, "category {cat:?} has no entries");
512 }
513 }
514
515 #[test]
516 fn table_covers_v0_2_5_migrate_default_rules() {
517 for name in [
520 "tapOn",
521 "assertVisible",
522 "assertNotVisible",
523 "extendedWaitUntil",
524 "inputText",
525 "eraseText",
526 "clearState",
527 "clearKeychain",
528 "runFlow",
529 "launchApp",
530 "stopApp",
531 "killApp",
532 "openLink",
533 "back",
534 "hideKeyboard",
535 "pressKey",
536 "scroll",
537 "scrollUntilVisible",
538 "swipe",
539 "retry",
540 ] {
541 assert!(
542 find_by_maestro(name).is_some(),
543 "v0.2.5 DEFAULT_RULES verb {name:?} missing from VERB_TABLE"
544 );
545 }
546 }
547
548 #[test]
549 fn table_covers_smix_native_verbs() {
550 for name in ["fixture", "webviewEval"] {
551 assert!(
552 find_by_smix(name).is_some() || find_by_maestro(name).is_some(),
553 "smix-native verb {name:?} missing from VERB_TABLE"
554 );
555 }
556 }
557}