Skip to main content

smix_driver/
traits.rs

1//! `Driver` trait: cross-platform sense + act abstraction.
2//!
3//! Two-trait architecture:
4//!
5//! - `Driver` (this trait) — sense + act, backed by an HTTP runner
6//!   client (XCUITest server for iOS, ktor/UIAutomator2 for Android)
7//! - `DeviceControl` (in smix-sdk) — sim/host control, backed by
8//!   simctl (iOS) / adb (Android)
9//!
10//! Methods NOT on this trait (App-layer aggregation or platform-specific
11//! getter):
12//! - `App::describe()` — calls `Driver::tree()` + `collect_visible_summaries()`
13//! - `IosDriver::runner()` — typed HttpRunnerClient getter, iOS-internal
14//!   (Android uses a different runner client type)
15//! - `IosDriver::dispose()` — runner cleanup, internal
16//! - `App::screenshot()` — calls `DeviceControl::screenshot()` (simctl-backed)
17//! - `App::mark_fixture_action()` — pure ledger record, no platform dispatch
18
19use async_trait::async_trait;
20use std::time::Duration;
21
22use smix_error::ExpectationFailure;
23use smix_input::{KeyName, SwipeDirection};
24use smix_runner_client::{IncludeScope, OcrFrame, SystemPopup, TapMode};
25use smix_screen::A11yNode;
26use smix_selector::Selector;
27
28use crate::Orientation;
29
30/// Platform identifier for cross-platform dispatch.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum Platform {
33    Ios,
34    Android,
35}
36
37/// Sense + act capabilities for a single device. Two-trait architecture
38/// pair with `smix_sdk::DeviceControl` (sim/host control in smix-sdk).
39///
40/// Signatures intentionally match iOS impl (`IosDriver` inherent methods)
41/// 1:1 so the trait impl is a pure delegation layer.
42#[async_trait]
43pub trait Driver: Send + Sync {
44    /// Platform identifier — `Platform::Ios` or `Platform::Android`.
45    fn platform(&self) -> Platform;
46
47    /// iOS-only escape hatch: access `IosDriver`'s inherent
48    /// `runner()` for capsule recording (start_record / stop_record) +
49    /// `describe()` aggregation. Android impl returns `None`. Default
50    /// `None` so non-iOS impls don't need to implement.
51    fn as_ios_driver(&self) -> Option<&crate::IosDriver> {
52        None
53    }
54
55    /// Set the target bundle id sent to the runner. For iOS this
56    /// becomes the `App-Bundle-Id` header on every request so the
57    /// runner's `resolveApp()` rebinds
58    /// `XCUIApplication(bundleIdentifier:)` per request. Default no-op
59    /// — only iOS currently uses this; Android path stays unaffected.
60    fn set_target_bundle_id(&mut self, _bundle: &str) {}
61
62    /// Enable `App-Activate: true` header on every request so the iOS
63    /// runner calls `.activate()` on the resolved target before
64    /// operating. Default no-op.
65    fn set_auto_activate(&mut self, _activate: bool) {}
66
67    /// Force key-event dispatch for text input, bypassing a11y-focus
68    /// resolution. Wires as the `Input-Dispatch-Mode: key-events`
69    /// header. Default no-op.
70    fn set_force_key_events(&mut self, _force: bool) {}
71
72    // === Sense (9) ============================================================
73
74    /// Fetch full a11y tree (`GET /tree`).
75    async fn tree(&self, include: Option<IncludeScope>) -> Result<A11yNode, ExpectationFailure>;
76
77    /// Boolean existence quick-probe.
78    async fn find(
79        &self,
80        selector: &Selector,
81        include: Option<IncludeScope>,
82    ) -> Result<bool, ExpectationFailure>;
83
84    /// Resolve selector → single matching node.
85    async fn find_one(
86        &self,
87        selector: &Selector,
88        include: Option<IncludeScope>,
89    ) -> Result<Option<A11yNode>, ExpectationFailure>;
90
91    /// Resolve selector → all matching nodes.
92    async fn find_all(
93        &self,
94        selector: &Selector,
95        include: Option<IncludeScope>,
96    ) -> Result<Vec<A11yNode>, ExpectationFailure>;
97
98    /// Resolve selector → centroid as viewport-normalized `(nx, ny)`.
99    async fn find_norm_coord(
100        &self,
101        selector: &Selector,
102    ) -> Result<Option<(f64, f64)>, ExpectationFailure>;
103
104    /// Apple Vision / ML Kit OCR find text in current screenshot.
105    /// `recognition_level` is `"accurate"` or `"fast"`.
106    async fn find_text_by_ocr(
107        &self,
108        text: &str,
109        locales: &[String],
110        recognition_level: &str,
111    ) -> Result<Option<OcrFrame>, ExpectationFailure>;
112
113    /// List visible system-level popups (alerts, permission dialogs).
114    async fn system_popups(
115        &self,
116        include: Option<IncludeScope>,
117    ) -> Result<Vec<SystemPopup>, ExpectationFailure>;
118
119    /// Tap a button on a system popup by id.
120    async fn system_popup_action(
121        &self,
122        popup_id: &str,
123        button_id: &str,
124    ) -> Result<bool, ExpectationFailure>;
125
126    /// Wait until selector resolves (returns matched node when ready).
127    async fn wait_for(
128        &self,
129        selector: &Selector,
130        timeout: Duration,
131        include: Option<IncludeScope>,
132    ) -> Result<A11yNode, ExpectationFailure>;
133
134    // === Act (17) ============================================================
135
136    /// Tap an element (default mode = element-anchored coord tap).
137    async fn tap(
138        &self,
139        selector: &Selector,
140        include: Option<IncludeScope>,
141    ) -> Result<(), ExpectationFailure>;
142
143    /// Tap with explicit mode (Path A vs Path B).
144    async fn tap_with_mode(
145        &self,
146        selector: &Selector,
147        mode: TapMode,
148        include: Option<IncludeScope>,
149    ) -> Result<(), ExpectationFailure>;
150
151    /// Tap at viewport-normalized coordinate (§9 #3 escape hatch).
152    async fn tap_at_norm_coord(&self, nx: f64, ny: f64) -> Result<(), ExpectationFailure>;
153
154    /// Tap by accessibility identifier via the swift `/tap-by-id`
155    /// route (IOHID synthesize at element-frame center).
156    async fn tap_by_id(&self, id: &str) -> Result<(), ExpectationFailure>;
157
158    /// Double-tap a selector.
159    async fn double_tap(
160        &self,
161        selector: &Selector,
162        include: Option<IncludeScope>,
163    ) -> Result<(), ExpectationFailure>;
164
165    /// Long-press a selector for `duration`.
166    async fn long_press(
167        &self,
168        selector: &Selector,
169        duration: Duration,
170        include: Option<IncludeScope>,
171    ) -> Result<(), ExpectationFailure>;
172
173    /// Fill text into a focused / matched input.
174    async fn fill(
175        &self,
176        selector: &Selector,
177        text: &str,
178        include: Option<IncludeScope>,
179    ) -> Result<(), ExpectationFailure>;
180
181    /// Clear text from a focused / matched input.
182    async fn clear(
183        &self,
184        selector: &Selector,
185        include: Option<IncludeScope>,
186    ) -> Result<(), ExpectationFailure>;
187
188    /// Press a named key (Return / Tab / Backspace / etc).
189    async fn press_key(&self, key: KeyName) -> Result<(), ExpectationFailure>;
190
191    /// Scroll until a selector is visible.
192    async fn scroll(
193        &self,
194        selector: &Selector,
195        direction: SwipeDirection,
196    ) -> Result<(), ExpectationFailure>;
197
198    /// One-shot swipe in a direction (no probe loop).
199    async fn swipe_once(&self, direction: SwipeDirection) -> Result<(), ExpectationFailure>;
200
201    /// Swipe between two viewport-normalized coords.
202    async fn swipe_at_norm_coord(
203        &self,
204        from: (f64, f64),
205        to: (f64, f64),
206    ) -> Result<(), ExpectationFailure>;
207
208    /// Dismiss the keyboard.
209    async fn hide_keyboard(&self) -> Result<(), ExpectationFailure>;
210
211    /// Trigger "back" gesture (iOS edge-swipe / Android KEYCODE_BACK).
212    async fn back(&self) -> Result<(), ExpectationFailure>;
213
214    /// Rotate the device.
215    async fn set_orientation(&self, orientation: Orientation) -> Result<(), ExpectationFailure>;
216
217    /// Bring app to foreground.
218    async fn foreground(&self, bundle_id: &str) -> Result<(), ExpectationFailure>;
219
220    /// Evaluate JavaScript in a WebView (iOS: fixture bridge :28080;
221    /// Android: Chrome DevTools Protocol via adb forward).
222    async fn webview_eval(&self, js: &str) -> Result<serde_json::Value, ExpectationFailure>;
223}