agent_tui/daemon/usecases/
elements.rs

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// ============================================================================
25// Shared Element Filtering
26// ============================================================================
27
28/// Criteria for filtering elements. Used by Find and Count use cases.
29#[derive(Debug, Clone, Default)]
30pub struct ElementFilterCriteria {
31    /// Filter by element role/type (case-insensitive contains match)
32    pub role: Option<String>,
33    /// Filter by element name/label
34    pub name: Option<String>,
35    /// Filter by element text content
36    pub text: Option<String>,
37    /// Filter by focused state
38    pub focused: Option<bool>,
39    /// If true, name must match exactly; otherwise case-insensitive contains
40    pub exact: bool,
41}
42
43/// Filter elements based on the provided criteria.
44///
45/// Returns elements that match ALL specified criteria (AND logic).
46pub 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            // Filter by role/element_type if specified
54            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            // Filter by name/label if specified
62            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            // Filter by text if specified
74            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            // Filter by focused if specified
82            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        // Click on the element first to focus it
145        session_guard.click(&input.element_ref)?;
146
147        // Clear existing content and type new value
148        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        // Handle nth selection
191        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        // Verify element exists
339        if session_guard.find_element(&input.element_ref).is_none() {
340            return Err(SessionError::ElementNotFound(input.element_ref));
341        }
342
343        // Send Tab to focus (standard terminal focus navigation)
344        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        // Verify element exists
373        if session_guard.find_element(&input.element_ref).is_none() {
374            return Err(SessionError::ElementNotFound(input.element_ref));
375        }
376
377        // Send Ctrl+U to clear line
378        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        // Verify element exists
407        if session_guard.find_element(&input.element_ref).is_none() {
408            return Err(SessionError::ElementNotFound(input.element_ref));
409        }
410
411        // Send Ctrl+A to select all
412        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        // Verify element exists
540        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" ")?; // Toggle selection
549            session_guard.pty_write(&[0x15])?; // Ctrl+U to clear
550            selected.push(option.clone());
551        }
552
553        session_guard.pty_write(b"\r")?; // Confirm selection
554
555        Ok(MultiselectOutput {
556            success: true,
557            selected_options: selected,
558            message: None,
559        })
560    }
561}
562
563// ============================================================================
564// Element Query Use Cases
565// ============================================================================
566
567pub 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    // ========================================================================
909    // ClickUseCase Tests (Error paths)
910    // ========================================================================
911
912    #[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    // ========================================================================
945    // FillUseCase Tests (Error paths)
946    // ========================================================================
947
948    #[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    // ========================================================================
983    // FindUseCase Tests (Error paths)
984    // ========================================================================
985
986    #[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    // ========================================================================
1016    // ToggleUseCase Tests (Error paths)
1017    // ========================================================================
1018
1019    #[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    // ========================================================================
1054    // SelectUseCase Tests (Error paths)
1055    // ========================================================================
1056
1057    #[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    // ========================================================================
1092    // MultiselectUseCase Tests (Error paths)
1093    // ========================================================================
1094
1095    #[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    // ========================================================================
1130    // ScrollIntoViewUseCase Tests (Error paths)
1131    // ========================================================================
1132
1133    #[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    // ========================================================================
1166    // ScrollUseCase Tests (Error paths)
1167    // ========================================================================
1168
1169    #[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    // ========================================================================
1204    // CountUseCase Tests (Error paths)
1205    // ========================================================================
1206
1207    #[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    // ========================================================================
1244    // DoubleClickUseCase Tests (Error paths)
1245    // ========================================================================
1246
1247    #[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    // ========================================================================
1280    // FocusUseCase Tests (Error paths)
1281    // ========================================================================
1282
1283    #[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    // ========================================================================
1316    // ClearUseCase Tests (Error paths)
1317    // ========================================================================
1318
1319    #[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    // ========================================================================
1352    // SelectAllUseCase Tests (Error paths)
1353    // ========================================================================
1354
1355    #[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    // ========================================================================
1388    // GetTextUseCase Tests (Error paths)
1389    // ========================================================================
1390
1391    #[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    // ========================================================================
1406    // GetValueUseCase Tests (Error paths)
1407    // ========================================================================
1408
1409    #[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    // ========================================================================
1424    // IsVisibleUseCase Tests (Error paths)
1425    // ========================================================================
1426
1427    #[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    // ========================================================================
1442    // IsFocusedUseCase Tests (Error paths)
1443    // ========================================================================
1444
1445    #[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    // ========================================================================
1460    // IsEnabledUseCase Tests (Error paths)
1461    // ========================================================================
1462
1463    #[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    // ========================================================================
1478    // IsCheckedUseCase Tests (Error paths)
1479    // ========================================================================
1480
1481    #[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    // ========================================================================
1496    // GetFocusedUseCase Tests (Error paths)
1497    // ========================================================================
1498
1499    #[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    // ========================================================================
1510    // GetTitleUseCase Tests (Error paths)
1511    // ========================================================================
1512
1513    #[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}