1use std::sync::Arc;
2use std::thread;
3use std::time::Duration;
4
5use crate::common::mutex_lock_or_recover;
6
7use crate::core::Element;
8
9use crate::daemon::adapters::{core_element_to_domain, core_elements_to_domain};
10use crate::daemon::ansi_keys;
11use crate::daemon::domain::{
12 ClearInput, ClearOutput, ClickInput, ClickOutput, CountInput, CountOutput, DoubleClickInput,
13 DoubleClickOutput, ElementStateInput, FillInput, FillOutput, FindInput, FindOutput,
14 FocusCheckOutput, FocusInput, FocusOutput, GetFocusedOutput, GetTextOutput, GetTitleOutput,
15 GetValueOutput, IsCheckedOutput, IsEnabledOutput, MultiselectInput, MultiselectOutput,
16 ScrollInput, ScrollIntoViewInput, ScrollIntoViewOutput, ScrollOutput, SelectAllInput,
17 SelectAllOutput, SelectInput, SelectOutput, SessionInput, ToggleInput, ToggleOutput,
18 VisibilityOutput,
19};
20use crate::daemon::error::SessionError;
21use crate::daemon::repository::SessionRepository;
22use crate::daemon::select_helpers::navigate_to_option;
23
24#[derive(Debug, Clone, Default)]
30pub struct ElementFilterCriteria {
31 pub role: Option<String>,
33 pub name: Option<String>,
35 pub text: Option<String>,
37 pub focused: Option<bool>,
39 pub exact: bool,
41}
42
43pub fn filter_elements<'a>(
47 elements: impl IntoIterator<Item = &'a Element>,
48 criteria: &ElementFilterCriteria,
49) -> Vec<&'a Element> {
50 elements
51 .into_iter()
52 .filter(|el| {
53 if let Some(ref role) = criteria.role {
55 let el_type = format!("{:?}", el.element_type).to_lowercase();
56 if !el_type.contains(&role.to_lowercase()) {
57 return false;
58 }
59 }
60
61 if let Some(ref name) = criteria.name {
63 let el_label = el.label.as_deref().unwrap_or("");
64 if criteria.exact {
65 if el_label != name {
66 return false;
67 }
68 } else if !el_label.to_lowercase().contains(&name.to_lowercase()) {
69 return false;
70 }
71 }
72
73 if let Some(ref text) = criteria.text {
75 let el_text = el.label.as_deref().unwrap_or("").to_lowercase();
76 if !el_text.contains(&text.to_lowercase()) {
77 return false;
78 }
79 }
80
81 if let Some(focused) = criteria.focused {
83 if el.focused != focused {
84 return false;
85 }
86 }
87
88 true
89 })
90 .collect()
91}
92
93pub trait ClickUseCase: Send + Sync {
94 fn execute(&self, input: ClickInput) -> Result<ClickOutput, SessionError>;
95}
96
97pub struct ClickUseCaseImpl<R: SessionRepository> {
98 repository: Arc<R>,
99}
100
101impl<R: SessionRepository> ClickUseCaseImpl<R> {
102 pub fn new(repository: Arc<R>) -> Self {
103 Self { repository }
104 }
105}
106
107impl<R: SessionRepository> ClickUseCase for ClickUseCaseImpl<R> {
108 fn execute(&self, input: ClickInput) -> Result<ClickOutput, SessionError> {
109 let session = self.repository.resolve(input.session_id.as_deref())?;
110 let mut session_guard = mutex_lock_or_recover(&session);
111
112 session_guard.update()?;
113 session_guard.click(&input.element_ref)?;
114
115 Ok(ClickOutput {
116 success: true,
117 message: None,
118 warning: None,
119 })
120 }
121}
122
123pub trait FillUseCase: Send + Sync {
124 fn execute(&self, input: FillInput) -> Result<FillOutput, SessionError>;
125}
126
127pub struct FillUseCaseImpl<R: SessionRepository> {
128 repository: Arc<R>,
129}
130
131impl<R: SessionRepository> FillUseCaseImpl<R> {
132 pub fn new(repository: Arc<R>) -> Self {
133 Self { repository }
134 }
135}
136
137impl<R: SessionRepository> FillUseCase for FillUseCaseImpl<R> {
138 fn execute(&self, input: FillInput) -> Result<FillOutput, SessionError> {
139 let session = self.repository.resolve(input.session_id.as_deref())?;
140 let mut session_guard = mutex_lock_or_recover(&session);
141
142 session_guard.update()?;
143
144 session_guard.click(&input.element_ref)?;
146
147 session_guard.keystroke("ctrl+a")?;
149 session_guard.type_text(&input.value)?;
150
151 Ok(FillOutput {
152 success: true,
153 message: None,
154 })
155 }
156}
157
158pub trait FindUseCase: Send + Sync {
159 fn execute(&self, input: FindInput) -> Result<FindOutput, SessionError>;
160}
161
162pub struct FindUseCaseImpl<R: SessionRepository> {
163 repository: Arc<R>,
164}
165
166impl<R: SessionRepository> FindUseCaseImpl<R> {
167 pub fn new(repository: Arc<R>) -> Self {
168 Self { repository }
169 }
170}
171
172impl<R: SessionRepository> FindUseCase for FindUseCaseImpl<R> {
173 fn execute(&self, input: FindInput) -> Result<FindOutput, SessionError> {
174 let session = self.repository.resolve(input.session_id.as_deref())?;
175 let mut session_guard = mutex_lock_or_recover(&session);
176
177 session_guard.update()?;
178 let all_elements = session_guard.detect_elements();
179
180 let criteria = ElementFilterCriteria {
181 role: input.role,
182 name: input.name,
183 text: input.text,
184 focused: input.focused,
185 exact: input.exact,
186 };
187
188 let filtered = filter_elements(all_elements.iter(), &criteria);
189
190 let elements: Vec<_> = if let Some(nth) = input.nth {
192 filtered.get(nth).into_iter().cloned().cloned().collect()
193 } else {
194 filtered.into_iter().cloned().collect()
195 };
196
197 let count = elements.len();
198 let domain_elements = core_elements_to_domain(&elements);
199
200 Ok(FindOutput {
201 elements: domain_elements,
202 count,
203 })
204 }
205}
206
207pub trait ScrollUseCase: Send + Sync {
208 fn execute(&self, input: ScrollInput) -> Result<ScrollOutput, SessionError>;
209}
210
211pub struct ScrollUseCaseImpl<R: SessionRepository> {
212 repository: Arc<R>,
213}
214
215impl<R: SessionRepository> ScrollUseCaseImpl<R> {
216 pub fn new(repository: Arc<R>) -> Self {
217 Self { repository }
218 }
219}
220
221impl<R: SessionRepository> ScrollUseCase for ScrollUseCaseImpl<R> {
222 fn execute(&self, input: ScrollInput) -> Result<ScrollOutput, SessionError> {
223 let session = self.repository.resolve(input.session_id.as_deref())?;
224 let session_guard = mutex_lock_or_recover(&session);
225
226 let key_seq: &[u8] = match input.direction.as_str() {
227 "up" => ansi_keys::UP,
228 "down" => ansi_keys::DOWN,
229 "left" => ansi_keys::LEFT,
230 "right" => ansi_keys::RIGHT,
231 _ => {
232 return Err(SessionError::InvalidKey(format!(
233 "Invalid direction: {}",
234 input.direction
235 )));
236 }
237 };
238
239 for _ in 0..input.amount {
240 session_guard.pty_write(key_seq)?;
241 }
242
243 Ok(ScrollOutput { success: true })
244 }
245}
246
247pub trait CountUseCase: Send + Sync {
248 fn execute(&self, input: CountInput) -> Result<CountOutput, SessionError>;
249}
250
251pub struct CountUseCaseImpl<R: SessionRepository> {
252 repository: Arc<R>,
253}
254
255impl<R: SessionRepository> CountUseCaseImpl<R> {
256 pub fn new(repository: Arc<R>) -> Self {
257 Self { repository }
258 }
259}
260
261impl<R: SessionRepository> CountUseCase for CountUseCaseImpl<R> {
262 fn execute(&self, input: CountInput) -> Result<CountOutput, SessionError> {
263 let session = self.repository.resolve(input.session_id.as_deref())?;
264 let mut session_guard = mutex_lock_or_recover(&session);
265
266 session_guard.update()?;
267 let all_elements = session_guard.detect_elements();
268
269 let criteria = ElementFilterCriteria {
270 role: input.role,
271 name: input.name,
272 text: input.text,
273 ..Default::default()
274 };
275
276 let count = filter_elements(all_elements.iter(), &criteria).len();
277
278 Ok(CountOutput { count })
279 }
280}
281
282pub trait DoubleClickUseCase: Send + Sync {
283 fn execute(&self, input: DoubleClickInput) -> Result<DoubleClickOutput, SessionError>;
284}
285
286pub struct DoubleClickUseCaseImpl<R: SessionRepository> {
287 repository: Arc<R>,
288}
289
290impl<R: SessionRepository> DoubleClickUseCaseImpl<R> {
291 pub fn new(repository: Arc<R>) -> Self {
292 Self { repository }
293 }
294}
295
296impl<R: SessionRepository> DoubleClickUseCase for DoubleClickUseCaseImpl<R> {
297 fn execute(&self, input: DoubleClickInput) -> Result<DoubleClickOutput, SessionError> {
298 let session = self.repository.resolve(input.session_id.as_deref())?;
299 {
300 let mut session_guard = mutex_lock_or_recover(&session);
301 session_guard.update()?;
302 session_guard.click(&input.element_ref)?;
303 }
304
305 thread::sleep(Duration::from_millis(50));
306
307 {
308 let mut session_guard = mutex_lock_or_recover(&session);
309 session_guard.click(&input.element_ref)?;
310 }
311
312 Ok(DoubleClickOutput { success: true })
313 }
314}
315
316pub trait FocusUseCase: Send + Sync {
317 fn execute(&self, input: FocusInput) -> Result<FocusOutput, SessionError>;
318}
319
320pub struct FocusUseCaseImpl<R: SessionRepository> {
321 repository: Arc<R>,
322}
323
324impl<R: SessionRepository> FocusUseCaseImpl<R> {
325 pub fn new(repository: Arc<R>) -> Self {
326 Self { repository }
327 }
328}
329
330impl<R: SessionRepository> FocusUseCase for FocusUseCaseImpl<R> {
331 fn execute(&self, input: FocusInput) -> Result<FocusOutput, SessionError> {
332 let session = self.repository.resolve(input.session_id.as_deref())?;
333 let mut session_guard = mutex_lock_or_recover(&session);
334
335 session_guard.update()?;
336 session_guard.detect_elements();
337
338 if session_guard.find_element(&input.element_ref).is_none() {
340 return Err(SessionError::ElementNotFound(input.element_ref));
341 }
342
343 session_guard.pty_write(b"\t")?;
345
346 Ok(FocusOutput { success: true })
347 }
348}
349
350pub trait ClearUseCase: Send + Sync {
351 fn execute(&self, input: ClearInput) -> Result<ClearOutput, SessionError>;
352}
353
354pub struct ClearUseCaseImpl<R: SessionRepository> {
355 repository: Arc<R>,
356}
357
358impl<R: SessionRepository> ClearUseCaseImpl<R> {
359 pub fn new(repository: Arc<R>) -> Self {
360 Self { repository }
361 }
362}
363
364impl<R: SessionRepository> ClearUseCase for ClearUseCaseImpl<R> {
365 fn execute(&self, input: ClearInput) -> Result<ClearOutput, SessionError> {
366 let session = self.repository.resolve(input.session_id.as_deref())?;
367 let mut session_guard = mutex_lock_or_recover(&session);
368
369 session_guard.update()?;
370 session_guard.detect_elements();
371
372 if session_guard.find_element(&input.element_ref).is_none() {
374 return Err(SessionError::ElementNotFound(input.element_ref));
375 }
376
377 session_guard.pty_write(b"\x15")?;
379
380 Ok(ClearOutput { success: true })
381 }
382}
383
384pub trait SelectAllUseCase: Send + Sync {
385 fn execute(&self, input: SelectAllInput) -> Result<SelectAllOutput, SessionError>;
386}
387
388pub struct SelectAllUseCaseImpl<R: SessionRepository> {
389 repository: Arc<R>,
390}
391
392impl<R: SessionRepository> SelectAllUseCaseImpl<R> {
393 pub fn new(repository: Arc<R>) -> Self {
394 Self { repository }
395 }
396}
397
398impl<R: SessionRepository> SelectAllUseCase for SelectAllUseCaseImpl<R> {
399 fn execute(&self, input: SelectAllInput) -> Result<SelectAllOutput, SessionError> {
400 let session = self.repository.resolve(input.session_id.as_deref())?;
401 let mut session_guard = mutex_lock_or_recover(&session);
402
403 session_guard.update()?;
404 session_guard.detect_elements();
405
406 if session_guard.find_element(&input.element_ref).is_none() {
408 return Err(SessionError::ElementNotFound(input.element_ref));
409 }
410
411 session_guard.pty_write(b"\x01")?;
413
414 Ok(SelectAllOutput { success: true })
415 }
416}
417
418pub trait ToggleUseCase: Send + Sync {
419 fn execute(&self, input: ToggleInput) -> Result<ToggleOutput, SessionError>;
420}
421
422pub struct ToggleUseCaseImpl<R: SessionRepository> {
423 repository: Arc<R>,
424}
425
426impl<R: SessionRepository> ToggleUseCaseImpl<R> {
427 pub fn new(repository: Arc<R>) -> Self {
428 Self { repository }
429 }
430}
431
432impl<R: SessionRepository> ToggleUseCase for ToggleUseCaseImpl<R> {
433 fn execute(&self, input: ToggleInput) -> Result<ToggleOutput, SessionError> {
434 let session = self.repository.resolve(input.session_id.as_deref())?;
435 let mut session_guard = mutex_lock_or_recover(&session);
436
437 session_guard.update()?;
438 session_guard.detect_elements();
439
440 let current_checked = match session_guard.find_element(&input.element_ref) {
441 Some(el) => {
442 let el_type = el.element_type.as_str();
443 if el_type != "checkbox" && el_type != "radio" {
444 return Err(SessionError::WrongElementType {
445 element_ref: input.element_ref.clone(),
446 expected: "checkbox/radio".to_string(),
447 actual: el_type.to_string(),
448 });
449 }
450 el.checked.unwrap_or(false)
451 }
452 None => return Err(SessionError::ElementNotFound(input.element_ref)),
453 };
454
455 let should_toggle = input.state != Some(current_checked);
456 let new_checked = if should_toggle {
457 session_guard.pty_write(b" ")?;
458 !current_checked
459 } else {
460 current_checked
461 };
462
463 Ok(ToggleOutput {
464 success: true,
465 checked: new_checked,
466 message: None,
467 })
468 }
469}
470
471pub trait SelectUseCase: Send + Sync {
472 fn execute(&self, input: SelectInput) -> Result<SelectOutput, SessionError>;
473}
474
475pub struct SelectUseCaseImpl<R: SessionRepository> {
476 repository: Arc<R>,
477}
478
479impl<R: SessionRepository> SelectUseCaseImpl<R> {
480 pub fn new(repository: Arc<R>) -> Self {
481 Self { repository }
482 }
483}
484
485impl<R: SessionRepository> SelectUseCase for SelectUseCaseImpl<R> {
486 fn execute(&self, input: SelectInput) -> Result<SelectOutput, SessionError> {
487 let session = self.repository.resolve(input.session_id.as_deref())?;
488 let mut session_guard = mutex_lock_or_recover(&session);
489
490 session_guard.update()?;
491 session_guard.detect_elements();
492
493 match session_guard.find_element(&input.element_ref) {
494 Some(el) if el.element_type.as_str() != "select" => {
495 return Err(SessionError::WrongElementType {
496 element_ref: input.element_ref.clone(),
497 expected: "select".to_string(),
498 actual: el.element_type.as_str().to_string(),
499 });
500 }
501 None => return Err(SessionError::ElementNotFound(input.element_ref.clone())),
502 _ => {}
503 }
504
505 let screen_text = session_guard.screen_text();
506 navigate_to_option(&mut *session_guard, &input.option, &screen_text)?;
507 session_guard.pty_write(b"\r")?;
508
509 Ok(SelectOutput {
510 success: true,
511 selected_option: input.option,
512 message: None,
513 })
514 }
515}
516
517pub trait MultiselectUseCase: Send + Sync {
518 fn execute(&self, input: MultiselectInput) -> Result<MultiselectOutput, SessionError>;
519}
520
521pub struct MultiselectUseCaseImpl<R: SessionRepository> {
522 repository: Arc<R>,
523}
524
525impl<R: SessionRepository> MultiselectUseCaseImpl<R> {
526 pub fn new(repository: Arc<R>) -> Self {
527 Self { repository }
528 }
529}
530
531impl<R: SessionRepository> MultiselectUseCase for MultiselectUseCaseImpl<R> {
532 fn execute(&self, input: MultiselectInput) -> Result<MultiselectOutput, SessionError> {
533 let session = self.repository.resolve(input.session_id.as_deref())?;
534 let mut session_guard = mutex_lock_or_recover(&session);
535
536 session_guard.update()?;
537 session_guard.detect_elements();
538
539 if session_guard.find_element(&input.element_ref).is_none() {
541 return Err(SessionError::ElementNotFound(input.element_ref));
542 }
543
544 let mut selected = Vec::new();
545 for option in &input.options {
546 session_guard.pty_write(option.as_bytes())?;
547 thread::sleep(Duration::from_millis(50));
548 session_guard.pty_write(b" ")?; session_guard.pty_write(&[0x15])?; selected.push(option.clone());
551 }
552
553 session_guard.pty_write(b"\r")?; Ok(MultiselectOutput {
556 success: true,
557 selected_options: selected,
558 message: None,
559 })
560 }
561}
562
563pub trait GetTextUseCase: Send + Sync {
568 fn execute(&self, input: ElementStateInput) -> Result<GetTextOutput, SessionError>;
569}
570
571pub struct GetTextUseCaseImpl<R: SessionRepository> {
572 repository: Arc<R>,
573}
574
575impl<R: SessionRepository> GetTextUseCaseImpl<R> {
576 pub fn new(repository: Arc<R>) -> Self {
577 Self { repository }
578 }
579}
580
581impl<R: SessionRepository> GetTextUseCase for GetTextUseCaseImpl<R> {
582 fn execute(&self, input: ElementStateInput) -> Result<GetTextOutput, SessionError> {
583 let session = self.repository.resolve(input.session_id.as_deref())?;
584 let mut session_guard = mutex_lock_or_recover(&session);
585
586 session_guard.update()?;
587 session_guard.detect_elements();
588
589 match session_guard.find_element(&input.element_ref) {
590 Some(el) => {
591 let text = el
592 .label
593 .clone()
594 .or_else(|| el.value.clone())
595 .unwrap_or_default();
596 Ok(GetTextOutput { found: true, text })
597 }
598 None => Ok(GetTextOutput {
599 found: false,
600 text: String::new(),
601 }),
602 }
603 }
604}
605
606pub trait GetValueUseCase: Send + Sync {
607 fn execute(&self, input: ElementStateInput) -> Result<GetValueOutput, SessionError>;
608}
609
610pub struct GetValueUseCaseImpl<R: SessionRepository> {
611 repository: Arc<R>,
612}
613
614impl<R: SessionRepository> GetValueUseCaseImpl<R> {
615 pub fn new(repository: Arc<R>) -> Self {
616 Self { repository }
617 }
618}
619
620impl<R: SessionRepository> GetValueUseCase for GetValueUseCaseImpl<R> {
621 fn execute(&self, input: ElementStateInput) -> Result<GetValueOutput, SessionError> {
622 let session = self.repository.resolve(input.session_id.as_deref())?;
623 let mut session_guard = mutex_lock_or_recover(&session);
624
625 session_guard.update()?;
626 session_guard.detect_elements();
627
628 match session_guard.find_element(&input.element_ref) {
629 Some(el) => Ok(GetValueOutput {
630 found: true,
631 value: el.value.clone().unwrap_or_default(),
632 }),
633 None => Ok(GetValueOutput {
634 found: false,
635 value: String::new(),
636 }),
637 }
638 }
639}
640
641pub trait IsVisibleUseCase: Send + Sync {
642 fn execute(&self, input: ElementStateInput) -> Result<VisibilityOutput, SessionError>;
643}
644
645pub struct IsVisibleUseCaseImpl<R: SessionRepository> {
646 repository: Arc<R>,
647}
648
649impl<R: SessionRepository> IsVisibleUseCaseImpl<R> {
650 pub fn new(repository: Arc<R>) -> Self {
651 Self { repository }
652 }
653}
654
655impl<R: SessionRepository> IsVisibleUseCase for IsVisibleUseCaseImpl<R> {
656 fn execute(&self, input: ElementStateInput) -> Result<VisibilityOutput, SessionError> {
657 let session = self.repository.resolve(input.session_id.as_deref())?;
658 let mut session_guard = mutex_lock_or_recover(&session);
659
660 session_guard.update()?;
661 session_guard.detect_elements();
662
663 let visible = session_guard.find_element(&input.element_ref).is_some();
664 Ok(VisibilityOutput {
665 found: visible,
666 visible,
667 })
668 }
669}
670
671pub trait IsFocusedUseCase: Send + Sync {
672 fn execute(&self, input: ElementStateInput) -> Result<FocusCheckOutput, SessionError>;
673}
674
675pub struct IsFocusedUseCaseImpl<R: SessionRepository> {
676 repository: Arc<R>,
677}
678
679impl<R: SessionRepository> IsFocusedUseCaseImpl<R> {
680 pub fn new(repository: Arc<R>) -> Self {
681 Self { repository }
682 }
683}
684
685impl<R: SessionRepository> IsFocusedUseCase for IsFocusedUseCaseImpl<R> {
686 fn execute(&self, input: ElementStateInput) -> Result<FocusCheckOutput, SessionError> {
687 let session = self.repository.resolve(input.session_id.as_deref())?;
688 let mut session_guard = mutex_lock_or_recover(&session);
689
690 session_guard.update()?;
691 session_guard.detect_elements();
692
693 match session_guard.find_element(&input.element_ref) {
694 Some(el) => Ok(FocusCheckOutput {
695 found: true,
696 focused: el.focused,
697 }),
698 None => Ok(FocusCheckOutput {
699 found: false,
700 focused: false,
701 }),
702 }
703 }
704}
705
706pub trait IsEnabledUseCase: Send + Sync {
707 fn execute(&self, input: ElementStateInput) -> Result<IsEnabledOutput, SessionError>;
708}
709
710pub struct IsEnabledUseCaseImpl<R: SessionRepository> {
711 repository: Arc<R>,
712}
713
714impl<R: SessionRepository> IsEnabledUseCaseImpl<R> {
715 pub fn new(repository: Arc<R>) -> Self {
716 Self { repository }
717 }
718}
719
720impl<R: SessionRepository> IsEnabledUseCase for IsEnabledUseCaseImpl<R> {
721 fn execute(&self, input: ElementStateInput) -> Result<IsEnabledOutput, SessionError> {
722 let session = self.repository.resolve(input.session_id.as_deref())?;
723 let mut session_guard = mutex_lock_or_recover(&session);
724
725 session_guard.update()?;
726 session_guard.detect_elements();
727
728 match session_guard.find_element(&input.element_ref) {
729 Some(el) => Ok(IsEnabledOutput {
730 found: true,
731 enabled: !el.disabled.unwrap_or(false),
732 }),
733 None => Ok(IsEnabledOutput {
734 found: false,
735 enabled: false,
736 }),
737 }
738 }
739}
740
741pub trait IsCheckedUseCase: Send + Sync {
742 fn execute(&self, input: ElementStateInput) -> Result<IsCheckedOutput, SessionError>;
743}
744
745pub struct IsCheckedUseCaseImpl<R: SessionRepository> {
746 repository: Arc<R>,
747}
748
749impl<R: SessionRepository> IsCheckedUseCaseImpl<R> {
750 pub fn new(repository: Arc<R>) -> Self {
751 Self { repository }
752 }
753}
754
755impl<R: SessionRepository> IsCheckedUseCase for IsCheckedUseCaseImpl<R> {
756 fn execute(&self, input: ElementStateInput) -> Result<IsCheckedOutput, SessionError> {
757 let session = self.repository.resolve(input.session_id.as_deref())?;
758 let mut session_guard = mutex_lock_or_recover(&session);
759
760 session_guard.update()?;
761 session_guard.detect_elements();
762
763 match session_guard.find_element(&input.element_ref) {
764 Some(el) => {
765 let el_type = el.element_type.as_str();
766 if el_type != "checkbox" && el_type != "radio" {
767 Ok(IsCheckedOutput {
768 found: true,
769 checked: false,
770 message: Some(format!(
771 "Element {} is a {} not a checkbox/radio.",
772 input.element_ref, el_type
773 )),
774 })
775 } else {
776 Ok(IsCheckedOutput {
777 found: true,
778 checked: el.checked.unwrap_or(false),
779 message: None,
780 })
781 }
782 }
783 None => Ok(IsCheckedOutput {
784 found: false,
785 checked: false,
786 message: None,
787 }),
788 }
789 }
790}
791
792pub trait GetFocusedUseCase: Send + Sync {
793 fn execute(&self, input: SessionInput) -> Result<GetFocusedOutput, SessionError>;
794}
795
796pub struct GetFocusedUseCaseImpl<R: SessionRepository> {
797 repository: Arc<R>,
798}
799
800impl<R: SessionRepository> GetFocusedUseCaseImpl<R> {
801 pub fn new(repository: Arc<R>) -> Self {
802 Self { repository }
803 }
804}
805
806impl<R: SessionRepository> GetFocusedUseCase for GetFocusedUseCaseImpl<R> {
807 fn execute(&self, input: SessionInput) -> Result<GetFocusedOutput, SessionError> {
808 let session = self.repository.resolve(input.session_id.as_deref())?;
809 let mut session_guard = mutex_lock_or_recover(&session);
810
811 session_guard.update()?;
812 session_guard.detect_elements();
813
814 let focused_el = session_guard
815 .cached_elements()
816 .iter()
817 .find(|e| e.focused)
818 .map(core_element_to_domain);
819
820 Ok(GetFocusedOutput {
821 found: focused_el.is_some(),
822 element: focused_el,
823 })
824 }
825}
826
827pub trait GetTitleUseCase: Send + Sync {
828 fn execute(&self, input: SessionInput) -> Result<GetTitleOutput, SessionError>;
829}
830
831pub struct GetTitleUseCaseImpl<R: SessionRepository> {
832 repository: Arc<R>,
833}
834
835impl<R: SessionRepository> GetTitleUseCaseImpl<R> {
836 pub fn new(repository: Arc<R>) -> Self {
837 Self { repository }
838 }
839}
840
841impl<R: SessionRepository> GetTitleUseCase for GetTitleUseCaseImpl<R> {
842 fn execute(&self, input: SessionInput) -> Result<GetTitleOutput, SessionError> {
843 let session = self.repository.resolve(input.session_id.as_deref())?;
844 let session_guard = mutex_lock_or_recover(&session);
845
846 Ok(GetTitleOutput {
847 session_id: session_guard.id.clone(),
848 title: session_guard.command.clone(),
849 })
850 }
851}
852
853pub trait ScrollIntoViewUseCase: Send + Sync {
854 fn execute(&self, input: ScrollIntoViewInput) -> Result<ScrollIntoViewOutput, SessionError>;
855}
856
857pub struct ScrollIntoViewUseCaseImpl<R: SessionRepository> {
858 repository: Arc<R>,
859}
860
861impl<R: SessionRepository> ScrollIntoViewUseCaseImpl<R> {
862 pub fn new(repository: Arc<R>) -> Self {
863 Self { repository }
864 }
865}
866
867impl<R: SessionRepository> ScrollIntoViewUseCase for ScrollIntoViewUseCaseImpl<R> {
868 fn execute(&self, input: ScrollIntoViewInput) -> Result<ScrollIntoViewOutput, SessionError> {
869 let session = self.repository.resolve(input.session_id.as_deref())?;
870 let max_scrolls = 50;
871
872 for scroll_count in 0..max_scrolls {
873 {
874 let mut session_guard = mutex_lock_or_recover(&session);
875 let _ = session_guard.update();
876 session_guard.detect_elements();
877
878 if session_guard.find_element(&input.element_ref).is_some() {
879 return Ok(ScrollIntoViewOutput {
880 success: true,
881 scrolls_needed: scroll_count,
882 message: None,
883 });
884 }
885
886 session_guard.pty_write(ansi_keys::DOWN)?;
887 }
888 thread::sleep(Duration::from_millis(50));
889 }
890
891 Ok(ScrollIntoViewOutput {
892 success: false,
893 scrolls_needed: max_scrolls,
894 message: Some(format!(
895 "Element '{}' not found after {} scroll attempts.",
896 input.element_ref, max_scrolls
897 )),
898 })
899 }
900}
901
902#[cfg(test)]
903mod tests {
904 use super::*;
905 use crate::daemon::domain::SessionId;
906 use crate::daemon::test_support::{MockError, MockSessionRepository};
907
908 #[test]
913 fn test_click_usecase_returns_error_when_no_active_session() {
914 let repo = Arc::new(MockSessionRepository::new());
915 let usecase = ClickUseCaseImpl::new(repo);
916
917 let input = ClickInput {
918 session_id: None,
919 element_ref: "@e1".to_string(),
920 };
921
922 let result = usecase.execute(input);
923 assert!(matches!(result, Err(SessionError::NoActiveSession)));
924 }
925
926 #[test]
927 fn test_click_usecase_returns_error_when_session_not_found() {
928 let repo = Arc::new(
929 MockSessionRepository::builder()
930 .with_resolve_error(MockError::NotFound("missing".to_string()))
931 .build(),
932 );
933 let usecase = ClickUseCaseImpl::new(repo);
934
935 let input = ClickInput {
936 session_id: Some(SessionId::new("missing")),
937 element_ref: "@e1".to_string(),
938 };
939
940 let result = usecase.execute(input);
941 assert!(matches!(result, Err(SessionError::NotFound(_))));
942 }
943
944 #[test]
949 fn test_fill_usecase_returns_error_when_no_active_session() {
950 let repo = Arc::new(MockSessionRepository::new());
951 let usecase = FillUseCaseImpl::new(repo);
952
953 let input = FillInput {
954 session_id: None,
955 element_ref: "@inp1".to_string(),
956 value: "test value".to_string(),
957 };
958
959 let result = usecase.execute(input);
960 assert!(matches!(result, Err(SessionError::NoActiveSession)));
961 }
962
963 #[test]
964 fn test_fill_usecase_returns_error_when_session_not_found() {
965 let repo = Arc::new(
966 MockSessionRepository::builder()
967 .with_resolve_error(MockError::NotFound("nonexistent".to_string()))
968 .build(),
969 );
970 let usecase = FillUseCaseImpl::new(repo);
971
972 let input = FillInput {
973 session_id: Some(SessionId::new("nonexistent")),
974 element_ref: "@inp1".to_string(),
975 value: "test".to_string(),
976 };
977
978 let result = usecase.execute(input);
979 assert!(matches!(result, Err(SessionError::NotFound(_))));
980 }
981
982 #[test]
987 fn test_find_usecase_returns_error_when_no_active_session() {
988 let repo = Arc::new(MockSessionRepository::new());
989 let usecase = FindUseCaseImpl::new(repo);
990
991 let input = FindInput::default();
992
993 let result = usecase.execute(input);
994 assert!(matches!(result, Err(SessionError::NoActiveSession)));
995 }
996
997 #[test]
998 fn test_find_usecase_returns_error_when_session_not_found() {
999 let repo = Arc::new(
1000 MockSessionRepository::builder()
1001 .with_resolve_error(MockError::NotFound("unknown".to_string()))
1002 .build(),
1003 );
1004 let usecase = FindUseCaseImpl::new(repo);
1005
1006 let input = FindInput {
1007 session_id: Some(SessionId::new("unknown")),
1008 ..Default::default()
1009 };
1010
1011 let result = usecase.execute(input);
1012 assert!(matches!(result, Err(SessionError::NotFound(_))));
1013 }
1014
1015 #[test]
1020 fn test_toggle_usecase_returns_error_when_no_active_session() {
1021 let repo = Arc::new(MockSessionRepository::new());
1022 let usecase = ToggleUseCaseImpl::new(repo);
1023
1024 let input = ToggleInput {
1025 session_id: None,
1026 element_ref: "@cb1".to_string(),
1027 state: None,
1028 };
1029
1030 let result = usecase.execute(input);
1031 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1032 }
1033
1034 #[test]
1035 fn test_toggle_usecase_returns_error_when_session_not_found() {
1036 let repo = Arc::new(
1037 MockSessionRepository::builder()
1038 .with_resolve_error(MockError::NotFound("missing".to_string()))
1039 .build(),
1040 );
1041 let usecase = ToggleUseCaseImpl::new(repo);
1042
1043 let input = ToggleInput {
1044 session_id: Some(SessionId::new("missing")),
1045 element_ref: "@cb1".to_string(),
1046 state: Some(true),
1047 };
1048
1049 let result = usecase.execute(input);
1050 assert!(matches!(result, Err(SessionError::NotFound(_))));
1051 }
1052
1053 #[test]
1058 fn test_select_usecase_returns_error_when_no_active_session() {
1059 let repo = Arc::new(MockSessionRepository::new());
1060 let usecase = SelectUseCaseImpl::new(repo);
1061
1062 let input = SelectInput {
1063 session_id: None,
1064 element_ref: "@sel1".to_string(),
1065 option: "Option A".to_string(),
1066 };
1067
1068 let result = usecase.execute(input);
1069 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1070 }
1071
1072 #[test]
1073 fn test_select_usecase_returns_error_when_session_not_found() {
1074 let repo = Arc::new(
1075 MockSessionRepository::builder()
1076 .with_resolve_error(MockError::NotFound("missing".to_string()))
1077 .build(),
1078 );
1079 let usecase = SelectUseCaseImpl::new(repo);
1080
1081 let input = SelectInput {
1082 session_id: Some(SessionId::new("missing")),
1083 element_ref: "@sel1".to_string(),
1084 option: "Option A".to_string(),
1085 };
1086
1087 let result = usecase.execute(input);
1088 assert!(matches!(result, Err(SessionError::NotFound(_))));
1089 }
1090
1091 #[test]
1096 fn test_multiselect_usecase_returns_error_when_no_active_session() {
1097 let repo = Arc::new(MockSessionRepository::new());
1098 let usecase = MultiselectUseCaseImpl::new(repo);
1099
1100 let input = MultiselectInput {
1101 session_id: None,
1102 element_ref: "@msel1".to_string(),
1103 options: vec!["A".to_string(), "B".to_string()],
1104 };
1105
1106 let result = usecase.execute(input);
1107 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1108 }
1109
1110 #[test]
1111 fn test_multiselect_usecase_returns_error_when_session_not_found() {
1112 let repo = Arc::new(
1113 MockSessionRepository::builder()
1114 .with_resolve_error(MockError::NotFound("missing".to_string()))
1115 .build(),
1116 );
1117 let usecase = MultiselectUseCaseImpl::new(repo);
1118
1119 let input = MultiselectInput {
1120 session_id: Some(SessionId::new("missing")),
1121 element_ref: "@msel1".to_string(),
1122 options: vec!["A".to_string()],
1123 };
1124
1125 let result = usecase.execute(input);
1126 assert!(matches!(result, Err(SessionError::NotFound(_))));
1127 }
1128
1129 #[test]
1134 fn test_scroll_into_view_usecase_returns_error_when_no_active_session() {
1135 let repo = Arc::new(MockSessionRepository::new());
1136 let usecase = ScrollIntoViewUseCaseImpl::new(repo);
1137
1138 let input = ScrollIntoViewInput {
1139 session_id: None,
1140 element_ref: "@e1".to_string(),
1141 };
1142
1143 let result = usecase.execute(input);
1144 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1145 }
1146
1147 #[test]
1148 fn test_scroll_into_view_usecase_returns_error_when_session_not_found() {
1149 let repo = Arc::new(
1150 MockSessionRepository::builder()
1151 .with_resolve_error(MockError::NotFound("missing".to_string()))
1152 .build(),
1153 );
1154 let usecase = ScrollIntoViewUseCaseImpl::new(repo);
1155
1156 let input = ScrollIntoViewInput {
1157 session_id: Some(SessionId::new("missing")),
1158 element_ref: "@e1".to_string(),
1159 };
1160
1161 let result = usecase.execute(input);
1162 assert!(matches!(result, Err(SessionError::NotFound(_))));
1163 }
1164
1165 #[test]
1170 fn test_scroll_usecase_returns_error_when_no_active_session() {
1171 let repo = Arc::new(MockSessionRepository::new());
1172 let usecase = ScrollUseCaseImpl::new(repo);
1173
1174 let input = ScrollInput {
1175 session_id: None,
1176 direction: "down".to_string(),
1177 amount: 5,
1178 };
1179
1180 let result = usecase.execute(input);
1181 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1182 }
1183
1184 #[test]
1185 fn test_scroll_usecase_returns_error_when_session_not_found() {
1186 let repo = Arc::new(
1187 MockSessionRepository::builder()
1188 .with_resolve_error(MockError::NotFound("missing".to_string()))
1189 .build(),
1190 );
1191 let usecase = ScrollUseCaseImpl::new(repo);
1192
1193 let input = ScrollInput {
1194 session_id: Some(SessionId::new("missing")),
1195 direction: "up".to_string(),
1196 amount: 3,
1197 };
1198
1199 let result = usecase.execute(input);
1200 assert!(matches!(result, Err(SessionError::NotFound(_))));
1201 }
1202
1203 #[test]
1208 fn test_count_usecase_returns_error_when_no_active_session() {
1209 let repo = Arc::new(MockSessionRepository::new());
1210 let usecase = CountUseCaseImpl::new(repo);
1211
1212 let input = CountInput {
1213 session_id: None,
1214 role: Some("button".to_string()),
1215 name: None,
1216 text: None,
1217 };
1218
1219 let result = usecase.execute(input);
1220 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1221 }
1222
1223 #[test]
1224 fn test_count_usecase_returns_error_when_session_not_found() {
1225 let repo = Arc::new(
1226 MockSessionRepository::builder()
1227 .with_resolve_error(MockError::NotFound("unknown".to_string()))
1228 .build(),
1229 );
1230 let usecase = CountUseCaseImpl::new(repo);
1231
1232 let input = CountInput {
1233 session_id: Some(SessionId::new("unknown")),
1234 role: None,
1235 name: None,
1236 text: None,
1237 };
1238
1239 let result = usecase.execute(input);
1240 assert!(matches!(result, Err(SessionError::NotFound(_))));
1241 }
1242
1243 #[test]
1248 fn test_double_click_usecase_returns_error_when_no_active_session() {
1249 let repo = Arc::new(MockSessionRepository::new());
1250 let usecase = DoubleClickUseCaseImpl::new(repo);
1251
1252 let input = DoubleClickInput {
1253 session_id: None,
1254 element_ref: "@e1".to_string(),
1255 };
1256
1257 let result = usecase.execute(input);
1258 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1259 }
1260
1261 #[test]
1262 fn test_double_click_usecase_returns_error_when_session_not_found() {
1263 let repo = Arc::new(
1264 MockSessionRepository::builder()
1265 .with_resolve_error(MockError::NotFound("missing".to_string()))
1266 .build(),
1267 );
1268 let usecase = DoubleClickUseCaseImpl::new(repo);
1269
1270 let input = DoubleClickInput {
1271 session_id: Some(SessionId::new("missing")),
1272 element_ref: "@e1".to_string(),
1273 };
1274
1275 let result = usecase.execute(input);
1276 assert!(matches!(result, Err(SessionError::NotFound(_))));
1277 }
1278
1279 #[test]
1284 fn test_focus_usecase_returns_error_when_no_active_session() {
1285 let repo = Arc::new(MockSessionRepository::new());
1286 let usecase = FocusUseCaseImpl::new(repo);
1287
1288 let input = FocusInput {
1289 session_id: None,
1290 element_ref: "@inp1".to_string(),
1291 };
1292
1293 let result = usecase.execute(input);
1294 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1295 }
1296
1297 #[test]
1298 fn test_focus_usecase_returns_error_when_session_not_found() {
1299 let repo = Arc::new(
1300 MockSessionRepository::builder()
1301 .with_resolve_error(MockError::NotFound("missing".to_string()))
1302 .build(),
1303 );
1304 let usecase = FocusUseCaseImpl::new(repo);
1305
1306 let input = FocusInput {
1307 session_id: Some(SessionId::new("missing")),
1308 element_ref: "@inp1".to_string(),
1309 };
1310
1311 let result = usecase.execute(input);
1312 assert!(matches!(result, Err(SessionError::NotFound(_))));
1313 }
1314
1315 #[test]
1320 fn test_clear_usecase_returns_error_when_no_active_session() {
1321 let repo = Arc::new(MockSessionRepository::new());
1322 let usecase = ClearUseCaseImpl::new(repo);
1323
1324 let input = ClearInput {
1325 session_id: None,
1326 element_ref: "@inp1".to_string(),
1327 };
1328
1329 let result = usecase.execute(input);
1330 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1331 }
1332
1333 #[test]
1334 fn test_clear_usecase_returns_error_when_session_not_found() {
1335 let repo = Arc::new(
1336 MockSessionRepository::builder()
1337 .with_resolve_error(MockError::NotFound("missing".to_string()))
1338 .build(),
1339 );
1340 let usecase = ClearUseCaseImpl::new(repo);
1341
1342 let input = ClearInput {
1343 session_id: Some(SessionId::new("missing")),
1344 element_ref: "@inp1".to_string(),
1345 };
1346
1347 let result = usecase.execute(input);
1348 assert!(matches!(result, Err(SessionError::NotFound(_))));
1349 }
1350
1351 #[test]
1356 fn test_select_all_usecase_returns_error_when_no_active_session() {
1357 let repo = Arc::new(MockSessionRepository::new());
1358 let usecase = SelectAllUseCaseImpl::new(repo);
1359
1360 let input = SelectAllInput {
1361 session_id: None,
1362 element_ref: "@inp1".to_string(),
1363 };
1364
1365 let result = usecase.execute(input);
1366 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1367 }
1368
1369 #[test]
1370 fn test_select_all_usecase_returns_error_when_session_not_found() {
1371 let repo = Arc::new(
1372 MockSessionRepository::builder()
1373 .with_resolve_error(MockError::NotFound("missing".to_string()))
1374 .build(),
1375 );
1376 let usecase = SelectAllUseCaseImpl::new(repo);
1377
1378 let input = SelectAllInput {
1379 session_id: Some(SessionId::new("missing")),
1380 element_ref: "@inp1".to_string(),
1381 };
1382
1383 let result = usecase.execute(input);
1384 assert!(matches!(result, Err(SessionError::NotFound(_))));
1385 }
1386
1387 #[test]
1392 fn test_get_text_usecase_returns_error_when_no_active_session() {
1393 let repo = Arc::new(MockSessionRepository::new());
1394 let usecase = GetTextUseCaseImpl::new(repo);
1395
1396 let input = ElementStateInput {
1397 session_id: None,
1398 element_ref: "@e1".to_string(),
1399 };
1400
1401 let result = usecase.execute(input);
1402 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1403 }
1404
1405 #[test]
1410 fn test_get_value_usecase_returns_error_when_no_active_session() {
1411 let repo = Arc::new(MockSessionRepository::new());
1412 let usecase = GetValueUseCaseImpl::new(repo);
1413
1414 let input = ElementStateInput {
1415 session_id: None,
1416 element_ref: "@inp1".to_string(),
1417 };
1418
1419 let result = usecase.execute(input);
1420 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1421 }
1422
1423 #[test]
1428 fn test_is_visible_usecase_returns_error_when_no_active_session() {
1429 let repo = Arc::new(MockSessionRepository::new());
1430 let usecase = IsVisibleUseCaseImpl::new(repo);
1431
1432 let input = ElementStateInput {
1433 session_id: None,
1434 element_ref: "@e1".to_string(),
1435 };
1436
1437 let result = usecase.execute(input);
1438 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1439 }
1440
1441 #[test]
1446 fn test_is_focused_usecase_returns_error_when_no_active_session() {
1447 let repo = Arc::new(MockSessionRepository::new());
1448 let usecase = IsFocusedUseCaseImpl::new(repo);
1449
1450 let input = ElementStateInput {
1451 session_id: None,
1452 element_ref: "@e1".to_string(),
1453 };
1454
1455 let result = usecase.execute(input);
1456 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1457 }
1458
1459 #[test]
1464 fn test_is_enabled_usecase_returns_error_when_no_active_session() {
1465 let repo = Arc::new(MockSessionRepository::new());
1466 let usecase = IsEnabledUseCaseImpl::new(repo);
1467
1468 let input = ElementStateInput {
1469 session_id: None,
1470 element_ref: "@e1".to_string(),
1471 };
1472
1473 let result = usecase.execute(input);
1474 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1475 }
1476
1477 #[test]
1482 fn test_is_checked_usecase_returns_error_when_no_active_session() {
1483 let repo = Arc::new(MockSessionRepository::new());
1484 let usecase = IsCheckedUseCaseImpl::new(repo);
1485
1486 let input = ElementStateInput {
1487 session_id: None,
1488 element_ref: "@cb1".to_string(),
1489 };
1490
1491 let result = usecase.execute(input);
1492 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1493 }
1494
1495 #[test]
1500 fn test_get_focused_usecase_returns_error_when_no_active_session() {
1501 let repo = Arc::new(MockSessionRepository::new());
1502 let usecase = GetFocusedUseCaseImpl::new(repo);
1503
1504 let input = SessionInput { session_id: None };
1505 let result = usecase.execute(input);
1506 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1507 }
1508
1509 #[test]
1514 fn test_get_title_usecase_returns_error_when_no_active_session() {
1515 let repo = Arc::new(MockSessionRepository::new());
1516 let usecase = GetTitleUseCaseImpl::new(repo);
1517
1518 let input = SessionInput { session_id: None };
1519 let result = usecase.execute(input);
1520 assert!(matches!(result, Err(SessionError::NoActiveSession)));
1521 }
1522}