1use std::collections::HashMap;
2use std::fmt;
3use std::str::FromStr;
4
5use super::session_types::ErrorEntry;
6use super::session_types::RecordingFrame;
7use super::session_types::RecordingStatus;
8use super::session_types::SessionId;
9use super::session_types::SessionInfo;
10use super::session_types::TraceEntry;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ScrollDirectionError {
15 pub invalid_value: String,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct WaitConditionTypeError {
21 pub invalid_value: String,
22}
23
24impl fmt::Display for WaitConditionTypeError {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 write!(
27 f,
28 "Invalid wait condition type '{}'. Must be one of: text, element, focused, not_visible, stable, text_gone, value",
29 self.invalid_value
30 )
31 }
32}
33
34impl std::error::Error for WaitConditionTypeError {}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum WaitConditionType {
42 Text,
44 Element,
46 Focused,
48 NotVisible,
50 Stable,
52 TextGone,
54 Value,
56}
57
58impl WaitConditionType {
59 pub fn parse(s: &str) -> Result<Self, WaitConditionTypeError> {
61 match s.to_lowercase().as_str() {
62 "text" => Ok(Self::Text),
63 "element" => Ok(Self::Element),
64 "focused" => Ok(Self::Focused),
65 "not_visible" => Ok(Self::NotVisible),
66 "stable" => Ok(Self::Stable),
67 "text_gone" => Ok(Self::TextGone),
68 "value" => Ok(Self::Value),
69 _ => Err(WaitConditionTypeError {
70 invalid_value: s.to_string(),
71 }),
72 }
73 }
74
75 pub fn as_str(&self) -> &'static str {
77 match self {
78 Self::Text => "text",
79 Self::Element => "element",
80 Self::Focused => "focused",
81 Self::NotVisible => "not_visible",
82 Self::Stable => "stable",
83 Self::TextGone => "text_gone",
84 Self::Value => "value",
85 }
86 }
87
88 pub fn requires_target(&self) -> bool {
90 matches!(
91 self,
92 Self::Element | Self::Focused | Self::NotVisible | Self::Value
93 )
94 }
95
96 pub fn requires_text(&self) -> bool {
98 matches!(self, Self::Text | Self::TextGone | Self::Value)
99 }
100}
101
102impl fmt::Display for WaitConditionType {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 write!(f, "{}", self.as_str())
105 }
106}
107
108impl FromStr for WaitConditionType {
109 type Err = WaitConditionTypeError;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 Self::parse(s)
113 }
114}
115
116impl fmt::Display for ScrollDirectionError {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 write!(
119 f,
120 "Invalid scroll direction '{}'. Must be one of: up, down, left, right",
121 self.invalid_value
122 )
123 }
124}
125
126impl std::error::Error for ScrollDirectionError {}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub enum ScrollDirection {
131 Up,
132 Down,
133 Left,
134 Right,
135}
136
137impl ScrollDirection {
138 pub fn parse(s: &str) -> Result<Self, ScrollDirectionError> {
140 match s.to_lowercase().as_str() {
141 "up" => Ok(Self::Up),
142 "down" => Ok(Self::Down),
143 "left" => Ok(Self::Left),
144 "right" => Ok(Self::Right),
145 _ => Err(ScrollDirectionError {
146 invalid_value: s.to_string(),
147 }),
148 }
149 }
150
151 pub fn as_str(&self) -> &'static str {
153 match self {
154 Self::Up => "up",
155 Self::Down => "down",
156 Self::Left => "left",
157 Self::Right => "right",
158 }
159 }
160}
161
162impl fmt::Display for ScrollDirection {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 write!(f, "{}", self.as_str())
165 }
166}
167
168impl FromStr for ScrollDirection {
169 type Err = ScrollDirectionError;
170
171 fn from_str(s: &str) -> Result<Self, Self::Err> {
172 Self::parse(s)
173 }
174}
175
176#[derive(Debug, Clone, Copy)]
177pub struct DomainCursorPosition {
178 pub row: u16,
179 pub col: u16,
180 pub visible: bool,
181}
182
183#[derive(Debug, Clone)]
184pub struct DomainPosition {
185 pub row: u16,
186 pub col: u16,
187 pub width: Option<u16>,
188 pub height: Option<u16>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Hash)]
192pub enum DomainElementType {
193 Button,
194 Input,
195 Checkbox,
196 Radio,
197 Select,
198 MenuItem,
199 ListItem,
200 Spinner,
201 Progress,
202 Link,
203}
204
205impl DomainElementType {
206 pub fn as_str(&self) -> &'static str {
207 match self {
208 DomainElementType::Button => "button",
209 DomainElementType::Input => "input",
210 DomainElementType::Checkbox => "checkbox",
211 DomainElementType::Radio => "radio",
212 DomainElementType::Select => "select",
213 DomainElementType::MenuItem => "menuitem",
214 DomainElementType::ListItem => "listitem",
215 DomainElementType::Spinner => "spinner",
216 DomainElementType::Progress => "progress",
217 DomainElementType::Link => "link",
218 }
219 }
220
221 pub fn is_interactive(&self) -> bool {
223 matches!(
224 self,
225 DomainElementType::Button
226 | DomainElementType::Input
227 | DomainElementType::Checkbox
228 | DomainElementType::Radio
229 | DomainElementType::Select
230 | DomainElementType::MenuItem
231 | DomainElementType::Link
232 )
233 }
234
235 pub fn accepts_input(&self) -> bool {
237 matches!(self, DomainElementType::Input)
238 }
239
240 pub fn is_toggleable(&self) -> bool {
242 matches!(self, DomainElementType::Checkbox | DomainElementType::Radio)
243 }
244}
245
246#[derive(Debug, Clone)]
247pub struct DomainElement {
248 pub element_ref: String,
249 pub element_type: DomainElementType,
250 pub label: Option<String>,
251 pub value: Option<String>,
252 pub position: DomainPosition,
253 pub focused: bool,
254 pub selected: bool,
255 pub checked: Option<bool>,
256 pub disabled: Option<bool>,
257 pub hint: Option<String>,
258}
259
260impl DomainElement {
261 pub fn is_interactive(&self) -> bool {
263 self.element_type.is_interactive()
264 }
265
266 pub fn can_click(&self) -> bool {
270 self.is_interactive() && !self.is_disabled()
271 }
272
273 pub fn can_type(&self) -> bool {
275 self.element_type.accepts_input() && !self.is_disabled()
276 }
277
278 pub fn is_disabled(&self) -> bool {
280 self.disabled.unwrap_or(false)
281 }
282
283 pub fn is_enabled(&self) -> bool {
285 !self.is_disabled()
286 }
287
288 pub fn is_focused(&self) -> bool {
290 self.focused
291 }
292
293 pub fn is_selected(&self) -> bool {
295 self.selected
296 }
297
298 pub fn is_checked(&self) -> Option<bool> {
302 self.checked
303 }
304
305 pub fn display_text(&self) -> Option<&str> {
307 self.label.as_deref().or(self.value.as_deref())
308 }
309}
310
311#[derive(Debug, Clone)]
312pub struct SpawnInput {
313 pub command: String,
314 pub args: Vec<String>,
315 pub cwd: Option<String>,
316 pub env: Option<HashMap<String, String>>,
317 pub session_id: Option<SessionId>,
318 pub cols: u16,
319 pub rows: u16,
320}
321
322#[derive(Debug, Clone)]
323pub struct SpawnOutput {
324 pub session_id: SessionId,
325 pub pid: u32,
326}
327
328#[derive(Debug, Clone)]
329pub struct RestartOutput {
330 pub old_session_id: SessionId,
331 pub new_session_id: SessionId,
332 pub command: String,
333 pub pid: u32,
334}
335
336#[derive(Debug, Clone, Default)]
337pub struct SnapshotInput {
338 pub session_id: Option<SessionId>,
339 pub include_elements: bool,
340 pub region: Option<String>,
341 pub strip_ansi: bool,
342 pub include_cursor: bool,
343}
344
345#[derive(Debug, Clone)]
346pub struct SnapshotOutput {
347 pub session_id: SessionId,
348 pub screen: String,
349 pub elements: Option<Vec<DomainElement>>,
350 pub cursor: Option<DomainCursorPosition>,
351}
352
353#[derive(Debug, Clone, Default)]
354pub struct AccessibilitySnapshotInput {
355 pub session_id: Option<SessionId>,
356 pub interactive_only: bool,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct DomainBoundsError {
362 pub message: String,
363}
364
365impl fmt::Display for DomainBoundsError {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 write!(f, "{}", self.message)
368 }
369}
370
371impl std::error::Error for DomainBoundsError {}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
378pub enum DomainRole {
379 Button,
380 Tab,
381 Input,
382 StaticText,
383 Panel,
384 Checkbox,
385 MenuItem,
386 Status,
387 ToolBlock,
388 PromptMarker,
389 ProgressBar,
390 Link,
391 ErrorMessage,
392 DiffLine,
393 CodeBlock,
394}
395
396impl DomainRole {
397 pub fn as_str(&self) -> &'static str {
399 match self {
400 DomainRole::Button => "button",
401 DomainRole::Tab => "tab",
402 DomainRole::Input => "input",
403 DomainRole::StaticText => "text",
404 DomainRole::Panel => "panel",
405 DomainRole::Checkbox => "checkbox",
406 DomainRole::MenuItem => "menuitem",
407 DomainRole::Status => "status",
408 DomainRole::ToolBlock => "toolblock",
409 DomainRole::PromptMarker => "prompt",
410 DomainRole::ProgressBar => "progressbar",
411 DomainRole::Link => "link",
412 DomainRole::ErrorMessage => "error",
413 DomainRole::DiffLine => "diff",
414 DomainRole::CodeBlock => "codeblock",
415 }
416 }
417
418 pub fn is_interactive(&self) -> bool {
420 matches!(
421 self,
422 DomainRole::Button
423 | DomainRole::Tab
424 | DomainRole::Input
425 | DomainRole::Checkbox
426 | DomainRole::MenuItem
427 | DomainRole::PromptMarker
428 | DomainRole::Link
429 )
430 }
431}
432
433impl fmt::Display for DomainRole {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 write!(f, "{}", self.as_str())
436 }
437}
438
439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub struct DomainBounds {
446 x: u16,
447 y: u16,
448 width: u16,
449 height: u16,
450}
451
452impl DomainBounds {
453 pub fn new(x: u16, y: u16, width: u16, height: u16) -> Result<Self, DomainBoundsError> {
458 if width == 0 {
459 return Err(DomainBoundsError {
460 message: "Width must be at least 1".to_string(),
461 });
462 }
463 if height == 0 {
464 return Err(DomainBoundsError {
465 message: "Height must be at least 1".to_string(),
466 });
467 }
468 Ok(Self {
469 x,
470 y,
471 width,
472 height,
473 })
474 }
475
476 pub fn new_unchecked(x: u16, y: u16, width: u16, height: u16) -> Self {
481 Self {
482 x,
483 y,
484 width,
485 height,
486 }
487 }
488
489 pub fn x(&self) -> u16 {
491 self.x
492 }
493
494 pub fn y(&self) -> u16 {
496 self.y
497 }
498
499 pub fn width(&self) -> u16 {
501 self.width
502 }
503
504 pub fn height(&self) -> u16 {
506 self.height
507 }
508
509 pub fn contains(&self, x: u16, y: u16) -> bool {
511 x >= self.x
512 && x < self.x.saturating_add(self.width)
513 && y >= self.y
514 && y < self.y.saturating_add(self.height)
515 }
516}
517
518#[derive(Debug, Clone)]
519pub struct DomainElementRef {
520 pub role: DomainRole,
521 pub name: Option<String>,
522 pub bounds: DomainBounds,
523 pub visual_hash: u64,
524 pub nth: Option<usize>,
525 pub selected: bool,
526}
527
528#[derive(Debug, Clone, Default)]
529pub struct DomainRefMap {
530 pub refs: HashMap<String, DomainElementRef>,
531}
532
533impl DomainRefMap {
534 pub fn get(&self, ref_id: &str) -> Option<&DomainElementRef> {
535 self.refs.get(ref_id)
536 }
537}
538
539#[derive(Debug, Clone)]
540pub struct DomainSnapshotStats {
541 pub total: usize,
542 pub interactive: usize,
543 pub lines: usize,
544}
545
546#[derive(Debug, Clone)]
547pub struct DomainAccessibilitySnapshot {
548 pub tree: String,
549 pub refs: DomainRefMap,
550 pub stats: DomainSnapshotStats,
551}
552
553#[derive(Debug, Clone)]
554pub struct AccessibilitySnapshotOutput {
555 pub session_id: SessionId,
556 pub snapshot: DomainAccessibilitySnapshot,
557}
558
559#[derive(Debug, Clone)]
560pub struct ClickInput {
561 pub session_id: Option<SessionId>,
562 pub element_ref: String,
563}
564
565#[derive(Debug, Clone)]
566pub struct ClickOutput {
567 pub success: bool,
568 pub message: Option<String>,
569 pub warning: Option<String>,
570}
571
572#[derive(Debug, Clone)]
573pub struct FillInput {
574 pub session_id: Option<SessionId>,
575 pub element_ref: String,
576 pub value: String,
577}
578
579#[derive(Debug, Clone)]
580pub struct FillOutput {
581 pub success: bool,
582 pub message: Option<String>,
583}
584
585#[derive(Debug, Clone)]
586pub struct KeystrokeInput {
587 pub session_id: Option<SessionId>,
588 pub key: String,
589}
590
591#[derive(Debug, Clone)]
592pub struct KeystrokeOutput {
593 pub success: bool,
594}
595
596#[derive(Debug, Clone)]
597pub struct TypeInput {
598 pub session_id: Option<SessionId>,
599 pub text: String,
600}
601
602#[derive(Debug, Clone)]
603pub struct TypeOutput {
604 pub success: bool,
605}
606
607#[derive(Debug, Clone)]
608pub struct KeydownInput {
609 pub session_id: Option<SessionId>,
610 pub key: String,
611}
612
613#[derive(Debug, Clone)]
614pub struct KeydownOutput {
615 pub success: bool,
616}
617
618#[derive(Debug, Clone)]
619pub struct KeyupInput {
620 pub session_id: Option<SessionId>,
621 pub key: String,
622}
623
624#[derive(Debug, Clone)]
625pub struct KeyupOutput {
626 pub success: bool,
627}
628
629#[derive(Debug, Clone)]
630pub struct WaitInput {
631 pub session_id: Option<SessionId>,
632 pub text: Option<String>,
633 pub timeout_ms: u64,
634 pub condition: Option<String>,
635 pub target: Option<String>,
636}
637
638#[derive(Debug, Clone)]
639pub struct WaitOutput {
640 pub found: bool,
641 pub elapsed_ms: u64,
642}
643
644#[derive(Debug, Clone, Default)]
645pub struct FindInput {
646 pub session_id: Option<SessionId>,
647 pub role: Option<String>,
648 pub name: Option<String>,
649 pub text: Option<String>,
650 pub placeholder: Option<String>,
651 pub focused: Option<bool>,
652 pub nth: Option<usize>,
653 pub exact: bool,
654}
655
656#[derive(Debug, Clone)]
657pub struct FindOutput {
658 pub elements: Vec<DomainElement>,
659 pub count: usize,
660}
661
662#[derive(Debug, Clone)]
663pub struct ScrollInput {
664 pub session_id: Option<SessionId>,
665 pub direction: String,
666 pub amount: u16,
667}
668
669#[derive(Debug, Clone)]
670pub struct ScrollOutput {
671 pub success: bool,
672}
673
674#[derive(Debug, Clone)]
675pub struct ResizeInput {
676 pub session_id: Option<SessionId>,
677 pub cols: u16,
678 pub rows: u16,
679}
680
681#[derive(Debug, Clone)]
682pub struct ResizeOutput {
683 pub session_id: SessionId,
684 pub success: bool,
685 pub cols: u16,
686 pub rows: u16,
687}
688
689#[derive(Debug, Clone)]
690pub struct SessionsOutput {
691 pub sessions: Vec<SessionInfo>,
692 pub active_session: Option<SessionId>,
693}
694
695#[derive(Debug, Clone)]
696pub struct KillOutput {
697 pub session_id: SessionId,
698 pub success: bool,
699}
700
701#[derive(Debug, Clone)]
702pub struct ElementStateInput {
703 pub session_id: Option<SessionId>,
704 pub element_ref: String,
705}
706
707#[derive(Debug, Clone)]
708pub struct VisibilityOutput {
709 pub found: bool,
710 pub visible: bool,
711}
712
713#[derive(Debug, Clone)]
714pub struct FocusCheckOutput {
715 pub found: bool,
716 pub focused: bool,
717}
718
719#[derive(Debug, Clone)]
720pub struct IsEnabledOutput {
721 pub found: bool,
722 pub enabled: bool,
723}
724
725#[derive(Debug, Clone)]
726pub struct IsCheckedOutput {
727 pub found: bool,
728 pub checked: bool,
729 pub message: Option<String>,
730}
731
732#[derive(Debug, Clone)]
733pub struct GetTextOutput {
734 pub found: bool,
735 pub text: String,
736}
737
738#[derive(Debug, Clone)]
739pub struct GetValueOutput {
740 pub found: bool,
741 pub value: String,
742}
743
744#[derive(Debug, Clone)]
745pub struct GetFocusedOutput {
746 pub found: bool,
747 pub element: Option<DomainElement>,
748}
749
750#[derive(Debug, Clone)]
751pub struct DoubleClickInput {
752 pub session_id: Option<SessionId>,
753 pub element_ref: String,
754}
755
756#[derive(Debug, Clone)]
757pub struct DoubleClickOutput {
758 pub success: bool,
759}
760
761#[derive(Debug, Clone)]
762pub struct FocusInput {
763 pub session_id: Option<SessionId>,
764 pub element_ref: String,
765}
766
767#[derive(Debug, Clone)]
768pub struct FocusOutput {
769 pub success: bool,
770}
771
772#[derive(Debug, Clone)]
773pub struct ClearInput {
774 pub session_id: Option<SessionId>,
775 pub element_ref: String,
776}
777
778#[derive(Debug, Clone)]
779pub struct ClearOutput {
780 pub success: bool,
781}
782
783#[derive(Debug, Clone)]
784pub struct SelectAllInput {
785 pub session_id: Option<SessionId>,
786 pub element_ref: String,
787}
788
789#[derive(Debug, Clone)]
790pub struct SelectAllOutput {
791 pub success: bool,
792}
793
794#[derive(Debug, Clone)]
795pub struct ToggleInput {
796 pub session_id: Option<SessionId>,
797 pub element_ref: String,
798 pub state: Option<bool>,
799}
800
801#[derive(Debug, Clone)]
802pub struct ToggleOutput {
803 pub success: bool,
804 pub checked: bool,
805 pub message: Option<String>,
806}
807
808#[derive(Debug, Clone)]
809pub struct SelectInput {
810 pub session_id: Option<SessionId>,
811 pub element_ref: String,
812 pub option: String,
813}
814
815#[derive(Debug, Clone)]
816pub struct SelectOutput {
817 pub success: bool,
818 pub selected_option: String,
819 pub message: Option<String>,
820}
821
822#[derive(Debug, Clone)]
823pub struct MultiselectInput {
824 pub session_id: Option<SessionId>,
825 pub element_ref: String,
826 pub options: Vec<String>,
827}
828
829#[derive(Debug, Clone)]
830pub struct MultiselectOutput {
831 pub success: bool,
832 pub selected_options: Vec<String>,
833 pub message: Option<String>,
834}
835
836#[derive(Debug, Clone)]
837pub struct RecordStartInput {
838 pub session_id: Option<SessionId>,
839}
840
841#[derive(Debug, Clone)]
842pub struct RecordStopInput {
843 pub session_id: Option<SessionId>,
844 pub format: Option<String>,
845}
846
847#[derive(Debug, Clone)]
848pub struct RecordStatusInput {
849 pub session_id: Option<SessionId>,
850}
851
852#[derive(Debug, Clone)]
853pub struct RecordStartOutput {
854 pub session_id: SessionId,
855 pub success: bool,
856}
857
858#[derive(Debug, Clone)]
859pub struct RecordStopOutput {
860 pub session_id: SessionId,
861 pub frame_count: usize,
862 pub frames: Vec<RecordingFrame>,
863 pub format: String,
864 pub cols: u16,
865 pub rows: u16,
866}
867
868pub type RecordStatusOutput = RecordingStatus;
869
870#[derive(Debug, Clone)]
871pub struct TraceInput {
872 pub session_id: Option<SessionId>,
873 pub start: bool,
874 pub stop: bool,
875 pub count: usize,
876}
877
878#[derive(Debug, Clone)]
879pub struct TraceOutput {
880 pub tracing: bool,
881 pub entries: Vec<TraceEntry>,
882}
883
884#[derive(Debug, Clone)]
885pub struct ConsoleInput {
886 pub session_id: Option<SessionId>,
887 pub count: usize,
888 pub clear: bool,
889}
890
891#[derive(Debug, Clone)]
892pub struct ConsoleOutput {
893 pub lines: Vec<String>,
894}
895
896#[derive(Debug, Clone)]
897pub struct ErrorsInput {
898 pub session_id: Option<SessionId>,
899 pub count: usize,
900 pub clear: bool,
901}
902
903#[derive(Debug, Clone)]
904pub struct ErrorsOutput {
905 pub errors: Vec<ErrorEntry>,
906 pub total_count: usize,
907}
908
909#[derive(Debug, Clone)]
910pub struct CountInput {
911 pub session_id: Option<SessionId>,
912 pub role: Option<String>,
913 pub name: Option<String>,
914 pub text: Option<String>,
915}
916
917#[derive(Debug, Clone)]
918pub struct CountOutput {
919 pub count: usize,
920}
921
922#[derive(Debug, Clone)]
923pub struct GetTitleOutput {
924 pub session_id: SessionId,
925 pub title: String,
926}
927
928#[derive(Debug, Clone)]
929pub struct ScrollIntoViewInput {
930 pub session_id: Option<SessionId>,
931 pub element_ref: String,
932}
933
934#[derive(Debug, Clone)]
935pub struct ScrollIntoViewOutput {
936 pub success: bool,
937 pub scrolls_needed: usize,
938 pub message: Option<String>,
939}
940
941#[derive(Debug, Clone)]
942pub struct PtyReadInput {
943 pub session_id: Option<SessionId>,
944 pub max_bytes: usize,
945}
946
947#[derive(Debug, Clone)]
948pub struct PtyReadOutput {
949 pub session_id: SessionId,
950 pub data: String,
951 pub bytes_read: usize,
952}
953
954#[derive(Debug, Clone)]
955pub struct PtyWriteInput {
956 pub session_id: Option<SessionId>,
957 pub data: String,
958}
959
960#[derive(Debug, Clone)]
961pub struct PtyWriteOutput {
962 pub session_id: SessionId,
963 pub bytes_written: usize,
964 pub success: bool,
965}
966
967#[derive(Debug, Clone)]
968pub struct SessionInput {
969 pub session_id: Option<SessionId>,
970}
971
972#[derive(Debug, Clone)]
974pub struct AttachInput {
975 pub session_id: SessionId,
977}
978
979#[derive(Debug, Clone)]
981pub struct AttachOutput {
982 pub session_id: SessionId,
984 pub success: bool,
986 pub message: String,
988}
989
990#[derive(Debug, Clone, Default)]
991pub struct HealthInput;
992
993#[derive(Debug, Clone)]
994pub struct HealthOutput {
995 pub status: String,
996 pub pid: u32,
997 pub uptime_ms: u64,
998 pub session_count: usize,
999 pub version: String,
1000 pub active_connections: usize,
1001 pub total_requests: u64,
1002 pub error_count: u64,
1003}
1004
1005#[derive(Debug, Clone, Default)]
1006pub struct MetricsInput;
1007
1008#[derive(Debug, Clone)]
1009pub struct MetricsOutput {
1010 pub requests_total: u64,
1011 pub errors_total: u64,
1012 pub lock_timeouts: u64,
1013 pub poison_recoveries: u64,
1014 pub uptime_ms: u64,
1015 pub active_connections: usize,
1016 pub session_count: usize,
1017}
1018
1019#[derive(Debug, Clone)]
1021pub struct CleanupInput {
1022 pub all: bool,
1024}
1025
1026#[derive(Debug, Clone)]
1028pub struct CleanupFailure {
1029 pub session_id: SessionId,
1031 pub error: String,
1033}
1034
1035#[derive(Debug, Clone)]
1037pub struct CleanupOutput {
1038 pub cleaned: usize,
1040 pub failures: Vec<CleanupFailure>,
1042}
1043
1044#[derive(Debug, Clone, PartialEq, Eq)]
1046pub enum AssertConditionType {
1047 Text,
1049 Element,
1051 Session,
1053}
1054
1055impl AssertConditionType {
1056 pub fn parse(s: &str) -> Result<Self, String> {
1058 match s.to_lowercase().as_str() {
1059 "text" => Ok(Self::Text),
1060 "element" => Ok(Self::Element),
1061 "session" => Ok(Self::Session),
1062 _ => Err(format!(
1063 "Unknown condition type: {}. Use: text, element, or session",
1064 s
1065 )),
1066 }
1067 }
1068
1069 pub fn as_str(&self) -> &'static str {
1071 match self {
1072 Self::Text => "text",
1073 Self::Element => "element",
1074 Self::Session => "session",
1075 }
1076 }
1077}
1078
1079#[derive(Debug, Clone)]
1081pub struct AssertInput {
1082 pub session_id: Option<SessionId>,
1084 pub condition_type: AssertConditionType,
1086 pub value: String,
1088}
1089
1090#[derive(Debug, Clone)]
1092pub struct AssertOutput {
1093 pub passed: bool,
1095 pub condition: String,
1097}
1098
1099#[cfg(test)]
1100mod tests {
1101 use super::*;
1102
1103 mod scroll_direction_tests {
1109 use super::*;
1110
1111 #[test]
1112 fn test_scroll_direction_from_str_up() {
1113 let dir = ScrollDirection::parse("up").expect("Should parse 'up'");
1114 assert_eq!(dir, ScrollDirection::Up);
1115 }
1116
1117 #[test]
1118 fn test_scroll_direction_from_str_down() {
1119 let dir = ScrollDirection::parse("down").expect("Should parse 'down'");
1120 assert_eq!(dir, ScrollDirection::Down);
1121 }
1122
1123 #[test]
1124 fn test_scroll_direction_from_str_left() {
1125 let dir = ScrollDirection::parse("left").expect("Should parse 'left'");
1126 assert_eq!(dir, ScrollDirection::Left);
1127 }
1128
1129 #[test]
1130 fn test_scroll_direction_from_str_right() {
1131 let dir = ScrollDirection::parse("right").expect("Should parse 'right'");
1132 assert_eq!(dir, ScrollDirection::Right);
1133 }
1134
1135 #[test]
1136 fn test_scroll_direction_from_str_invalid() {
1137 let result = ScrollDirection::parse("diagonal");
1138 assert!(result.is_err(), "Invalid direction should be rejected");
1139 }
1140
1141 #[test]
1142 fn test_scroll_direction_from_str_empty() {
1143 let result = ScrollDirection::parse("");
1144 assert!(result.is_err(), "Empty string should be rejected");
1145 }
1146
1147 #[test]
1148 fn test_scroll_direction_case_insensitive() {
1149 assert_eq!(ScrollDirection::parse("UP").unwrap(), ScrollDirection::Up);
1150 assert_eq!(
1151 ScrollDirection::parse("Down").unwrap(),
1152 ScrollDirection::Down
1153 );
1154 assert_eq!(
1155 ScrollDirection::parse("LEFT").unwrap(),
1156 ScrollDirection::Left
1157 );
1158 assert_eq!(
1159 ScrollDirection::parse("RIGHT").unwrap(),
1160 ScrollDirection::Right
1161 );
1162 }
1163
1164 #[test]
1165 fn test_scroll_direction_as_str() {
1166 assert_eq!(ScrollDirection::Up.as_str(), "up");
1167 assert_eq!(ScrollDirection::Down.as_str(), "down");
1168 assert_eq!(ScrollDirection::Left.as_str(), "left");
1169 assert_eq!(ScrollDirection::Right.as_str(), "right");
1170 }
1171
1172 #[test]
1173 fn test_scroll_direction_display() {
1174 assert_eq!(format!("{}", ScrollDirection::Up), "up");
1175 assert_eq!(format!("{}", ScrollDirection::Down), "down");
1176 }
1177 }
1178
1179 mod wait_condition_type_tests {
1184 use super::*;
1185
1186 #[test]
1187 fn test_wait_condition_type_from_str_text() {
1188 let cond = WaitConditionType::parse("text").expect("Should parse 'text'");
1189 assert_eq!(cond, WaitConditionType::Text);
1190 }
1191
1192 #[test]
1193 fn test_wait_condition_type_from_str_element() {
1194 let cond = WaitConditionType::parse("element").expect("Should parse 'element'");
1195 assert_eq!(cond, WaitConditionType::Element);
1196 }
1197
1198 #[test]
1199 fn test_wait_condition_type_from_str_focused() {
1200 let cond = WaitConditionType::parse("focused").expect("Should parse 'focused'");
1201 assert_eq!(cond, WaitConditionType::Focused);
1202 }
1203
1204 #[test]
1205 fn test_wait_condition_type_from_str_not_visible() {
1206 let cond = WaitConditionType::parse("not_visible").expect("Should parse 'not_visible'");
1207 assert_eq!(cond, WaitConditionType::NotVisible);
1208 }
1209
1210 #[test]
1211 fn test_wait_condition_type_from_str_stable() {
1212 let cond = WaitConditionType::parse("stable").expect("Should parse 'stable'");
1213 assert_eq!(cond, WaitConditionType::Stable);
1214 }
1215
1216 #[test]
1217 fn test_wait_condition_type_from_str_text_gone() {
1218 let cond = WaitConditionType::parse("text_gone").expect("Should parse 'text_gone'");
1219 assert_eq!(cond, WaitConditionType::TextGone);
1220 }
1221
1222 #[test]
1223 fn test_wait_condition_type_from_str_value() {
1224 let cond = WaitConditionType::parse("value").expect("Should parse 'value'");
1225 assert_eq!(cond, WaitConditionType::Value);
1226 }
1227
1228 #[test]
1229 fn test_wait_condition_type_from_str_invalid() {
1230 let result = WaitConditionType::parse("invalid");
1231 assert!(result.is_err(), "Invalid condition should be rejected");
1232 }
1233
1234 #[test]
1235 fn test_wait_condition_type_from_str_empty() {
1236 let result = WaitConditionType::parse("");
1237 assert!(result.is_err(), "Empty string should be rejected");
1238 }
1239
1240 #[test]
1241 fn test_wait_condition_type_case_insensitive() {
1242 assert_eq!(
1243 WaitConditionType::parse("TEXT").unwrap(),
1244 WaitConditionType::Text
1245 );
1246 assert_eq!(
1247 WaitConditionType::parse("Element").unwrap(),
1248 WaitConditionType::Element
1249 );
1250 assert_eq!(
1251 WaitConditionType::parse("STABLE").unwrap(),
1252 WaitConditionType::Stable
1253 );
1254 }
1255
1256 #[test]
1257 fn test_wait_condition_type_as_str() {
1258 assert_eq!(WaitConditionType::Text.as_str(), "text");
1259 assert_eq!(WaitConditionType::Element.as_str(), "element");
1260 assert_eq!(WaitConditionType::Focused.as_str(), "focused");
1261 assert_eq!(WaitConditionType::NotVisible.as_str(), "not_visible");
1262 assert_eq!(WaitConditionType::Stable.as_str(), "stable");
1263 assert_eq!(WaitConditionType::TextGone.as_str(), "text_gone");
1264 assert_eq!(WaitConditionType::Value.as_str(), "value");
1265 }
1266
1267 #[test]
1268 fn test_wait_condition_type_display() {
1269 assert_eq!(format!("{}", WaitConditionType::Text), "text");
1270 assert_eq!(format!("{}", WaitConditionType::Stable), "stable");
1271 }
1272
1273 #[test]
1274 fn test_wait_condition_type_requires_target() {
1275 assert!(!WaitConditionType::Text.requires_target());
1276 assert!(WaitConditionType::Element.requires_target());
1277 assert!(WaitConditionType::Focused.requires_target());
1278 assert!(WaitConditionType::NotVisible.requires_target());
1279 assert!(!WaitConditionType::Stable.requires_target());
1280 assert!(!WaitConditionType::TextGone.requires_target());
1281 assert!(WaitConditionType::Value.requires_target());
1282 }
1283
1284 #[test]
1285 fn test_wait_condition_type_requires_text() {
1286 assert!(WaitConditionType::Text.requires_text());
1287 assert!(!WaitConditionType::Element.requires_text());
1288 assert!(!WaitConditionType::Focused.requires_text());
1289 assert!(!WaitConditionType::NotVisible.requires_text());
1290 assert!(!WaitConditionType::Stable.requires_text());
1291 assert!(WaitConditionType::TextGone.requires_text());
1292 assert!(WaitConditionType::Value.requires_text());
1293 }
1294
1295 #[test]
1296 fn test_wait_condition_type_error_message() {
1297 let err = WaitConditionType::parse("invalid").unwrap_err();
1298 assert!(err.to_string().contains("invalid"));
1299 assert!(err.to_string().contains("text"));
1300 }
1301 }
1302
1303 mod domain_bounds_tests {
1308 use super::*;
1309
1310 #[test]
1311 fn test_domain_bounds_valid() {
1312 let bounds = DomainBounds::new(10, 20, 100, 50).expect("Valid bounds");
1313 assert_eq!(bounds.x(), 10);
1314 assert_eq!(bounds.y(), 20);
1315 assert_eq!(bounds.width(), 100);
1316 assert_eq!(bounds.height(), 50);
1317 }
1318
1319 #[test]
1320 fn test_domain_bounds_rejects_zero_width() {
1321 let result = DomainBounds::new(0, 0, 0, 10);
1322 assert!(result.is_err(), "Zero width should be rejected");
1323 assert!(result.unwrap_err().message.contains("Width"));
1324 }
1325
1326 #[test]
1327 fn test_domain_bounds_rejects_zero_height() {
1328 let result = DomainBounds::new(0, 0, 10, 0);
1329 assert!(result.is_err(), "Zero height should be rejected");
1330 assert!(result.unwrap_err().message.contains("Height"));
1331 }
1332
1333 #[test]
1334 fn test_domain_bounds_accepts_minimum() {
1335 let bounds = DomainBounds::new(0, 0, 1, 1).expect("Minimum valid bounds");
1336 assert_eq!(bounds.width(), 1);
1337 assert_eq!(bounds.height(), 1);
1338 }
1339
1340 #[test]
1341 fn test_domain_bounds_at_origin() {
1342 let bounds = DomainBounds::new(0, 0, 10, 10).expect("Bounds at origin");
1343 assert_eq!(bounds.x(), 0);
1344 assert_eq!(bounds.y(), 0);
1345 }
1346
1347 #[test]
1348 fn test_domain_bounds_contains() {
1349 let bounds = DomainBounds::new(10, 10, 20, 10).expect("Valid bounds");
1350 assert!(bounds.contains(10, 10)); assert!(bounds.contains(15, 15)); assert!(bounds.contains(29, 19)); assert!(!bounds.contains(30, 10)); assert!(!bounds.contains(10, 20)); assert!(!bounds.contains(9, 10)); assert!(!bounds.contains(10, 9)); }
1358
1359 #[test]
1360 fn test_domain_bounds_unchecked() {
1361 let bounds = DomainBounds::new_unchecked(0, 0, 0, 0);
1363 assert_eq!(bounds.width(), 0);
1364 assert_eq!(bounds.height(), 0);
1365 }
1366
1367 #[test]
1368 fn test_domain_bounds_error_display() {
1369 let err = DomainBounds::new(0, 0, 0, 10).unwrap_err();
1370 assert!(!err.to_string().is_empty());
1371 }
1372 }
1373
1374 mod domain_element_type_tests {
1379 use super::*;
1380
1381 #[test]
1382 fn test_element_type_is_interactive() {
1383 assert!(DomainElementType::Button.is_interactive());
1385 assert!(DomainElementType::Input.is_interactive());
1386 assert!(DomainElementType::Checkbox.is_interactive());
1387 assert!(DomainElementType::Radio.is_interactive());
1388 assert!(DomainElementType::Select.is_interactive());
1389 assert!(DomainElementType::MenuItem.is_interactive());
1390 assert!(DomainElementType::Link.is_interactive());
1391
1392 assert!(!DomainElementType::ListItem.is_interactive());
1394 assert!(!DomainElementType::Spinner.is_interactive());
1395 assert!(!DomainElementType::Progress.is_interactive());
1396 }
1397
1398 #[test]
1399 fn test_element_type_accepts_input() {
1400 assert!(DomainElementType::Input.accepts_input());
1401 assert!(!DomainElementType::Button.accepts_input());
1402 assert!(!DomainElementType::Checkbox.accepts_input());
1403 }
1404
1405 #[test]
1406 fn test_element_type_is_toggleable() {
1407 assert!(DomainElementType::Checkbox.is_toggleable());
1408 assert!(DomainElementType::Radio.is_toggleable());
1409 assert!(!DomainElementType::Button.is_toggleable());
1410 assert!(!DomainElementType::Input.is_toggleable());
1411 }
1412 }
1413
1414 mod domain_element_tests {
1419 use super::*;
1420
1421 fn make_element(element_type: DomainElementType, disabled: Option<bool>) -> DomainElement {
1422 DomainElement {
1423 element_ref: "test".to_string(),
1424 element_type,
1425 label: Some("Test".to_string()),
1426 value: None,
1427 position: DomainPosition {
1428 row: 0,
1429 col: 0,
1430 width: Some(10),
1431 height: Some(1),
1432 },
1433 focused: false,
1434 selected: false,
1435 checked: None,
1436 disabled,
1437 hint: None,
1438 }
1439 }
1440
1441 #[test]
1442 fn test_element_is_interactive() {
1443 let button = make_element(DomainElementType::Button, None);
1444 assert!(button.is_interactive());
1445
1446 let progress = make_element(DomainElementType::Progress, None);
1447 assert!(!progress.is_interactive());
1448 }
1449
1450 #[test]
1451 fn test_element_can_click_enabled() {
1452 let button = make_element(DomainElementType::Button, None);
1453 assert!(button.can_click());
1454
1455 let disabled_button = make_element(DomainElementType::Button, Some(true));
1456 assert!(!disabled_button.can_click());
1457 }
1458
1459 #[test]
1460 fn test_element_can_click_non_interactive() {
1461 let progress = make_element(DomainElementType::Progress, None);
1462 assert!(!progress.can_click());
1463 }
1464
1465 #[test]
1466 fn test_element_can_type() {
1467 let input = make_element(DomainElementType::Input, None);
1468 assert!(input.can_type());
1469
1470 let disabled_input = make_element(DomainElementType::Input, Some(true));
1471 assert!(!disabled_input.can_type());
1472
1473 let button = make_element(DomainElementType::Button, None);
1474 assert!(!button.can_type());
1475 }
1476
1477 #[test]
1478 fn test_element_is_disabled() {
1479 let enabled = make_element(DomainElementType::Button, None);
1480 assert!(!enabled.is_disabled());
1481 assert!(enabled.is_enabled());
1482
1483 let explicit_enabled = make_element(DomainElementType::Button, Some(false));
1484 assert!(!explicit_enabled.is_disabled());
1485 assert!(explicit_enabled.is_enabled());
1486
1487 let disabled = make_element(DomainElementType::Button, Some(true));
1488 assert!(disabled.is_disabled());
1489 assert!(!disabled.is_enabled());
1490 }
1491
1492 #[test]
1493 fn test_element_display_text() {
1494 let with_label = DomainElement {
1495 element_ref: "test".to_string(),
1496 element_type: DomainElementType::Button,
1497 label: Some("Click Me".to_string()),
1498 value: Some("ignored".to_string()),
1499 position: DomainPosition {
1500 row: 0,
1501 col: 0,
1502 width: None,
1503 height: None,
1504 },
1505 focused: false,
1506 selected: false,
1507 checked: None,
1508 disabled: None,
1509 hint: None,
1510 };
1511 assert_eq!(with_label.display_text(), Some("Click Me"));
1512
1513 let with_value_only = DomainElement {
1514 element_ref: "test".to_string(),
1515 element_type: DomainElementType::Input,
1516 label: None,
1517 value: Some("typed text".to_string()),
1518 position: DomainPosition {
1519 row: 0,
1520 col: 0,
1521 width: None,
1522 height: None,
1523 },
1524 focused: false,
1525 selected: false,
1526 checked: None,
1527 disabled: None,
1528 hint: None,
1529 };
1530 assert_eq!(with_value_only.display_text(), Some("typed text"));
1531
1532 let no_text = DomainElement {
1533 element_ref: "test".to_string(),
1534 element_type: DomainElementType::Button,
1535 label: None,
1536 value: None,
1537 position: DomainPosition {
1538 row: 0,
1539 col: 0,
1540 width: None,
1541 height: None,
1542 },
1543 focused: false,
1544 selected: false,
1545 checked: None,
1546 disabled: None,
1547 hint: None,
1548 };
1549 assert_eq!(no_text.display_text(), None);
1550 }
1551
1552 #[test]
1553 fn test_element_state_methods() {
1554 let focused_selected = DomainElement {
1555 element_ref: "test".to_string(),
1556 element_type: DomainElementType::MenuItem,
1557 label: None,
1558 value: None,
1559 position: DomainPosition {
1560 row: 0,
1561 col: 0,
1562 width: None,
1563 height: None,
1564 },
1565 focused: true,
1566 selected: true,
1567 checked: Some(true),
1568 disabled: None,
1569 hint: None,
1570 };
1571 assert!(focused_selected.is_focused());
1572 assert!(focused_selected.is_selected());
1573 assert_eq!(focused_selected.is_checked(), Some(true));
1574 }
1575 }
1576}