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 /// v1.0.3 — attach a `Session-Id` header to every subsequent
73 /// request. Set to `Some(id)` after `POST /session/open`; set to
74 /// `None` to revert to the legacy per-request rebind path (which
75 /// is rate-limited to at most one `.activate()` per 5 s per
76 /// bundle-id as of v1.0.2). Default no-op — only impls backed by
77 /// an HTTP runner override.
78 fn set_session_id(&mut self, _id: Option<String>) {}
79
80 // === Sense (9) ============================================================
81
82 /// Fetch full a11y tree (`GET /tree`).
83 async fn tree(&self, include: Option<IncludeScope>) -> Result<A11yNode, ExpectationFailure>;
84
85 /// Boolean existence quick-probe.
86 async fn find(
87 &self,
88 selector: &Selector,
89 include: Option<IncludeScope>,
90 ) -> Result<bool, ExpectationFailure>;
91
92 /// Resolve selector → single matching node.
93 async fn find_one(
94 &self,
95 selector: &Selector,
96 include: Option<IncludeScope>,
97 ) -> Result<Option<A11yNode>, ExpectationFailure>;
98
99 /// Resolve selector → all matching nodes.
100 async fn find_all(
101 &self,
102 selector: &Selector,
103 include: Option<IncludeScope>,
104 ) -> Result<Vec<A11yNode>, ExpectationFailure>;
105
106 /// Resolve selector → centroid as viewport-normalized `(nx, ny)`.
107 async fn find_norm_coord(
108 &self,
109 selector: &Selector,
110 ) -> Result<Option<(f64, f64)>, ExpectationFailure>;
111
112 /// Apple Vision / ML Kit OCR find text in current screenshot.
113 /// `recognition_level` is `"accurate"` or `"fast"`.
114 async fn find_text_by_ocr(
115 &self,
116 text: &str,
117 locales: &[String],
118 recognition_level: &str,
119 ) -> Result<Option<OcrFrame>, ExpectationFailure>;
120
121 /// List visible system-level popups (alerts, permission dialogs).
122 async fn system_popups(
123 &self,
124 include: Option<IncludeScope>,
125 ) -> Result<Vec<SystemPopup>, ExpectationFailure>;
126
127 /// Tap a button on a system popup by id.
128 async fn system_popup_action(
129 &self,
130 popup_id: &str,
131 button_id: &str,
132 ) -> Result<bool, ExpectationFailure>;
133
134 /// Wait until selector resolves (returns matched node when ready).
135 async fn wait_for(
136 &self,
137 selector: &Selector,
138 timeout: Duration,
139 include: Option<IncludeScope>,
140 ) -> Result<A11yNode, ExpectationFailure>;
141
142 // === Act (17) ============================================================
143
144 /// Tap an element (default mode = element-anchored coord tap).
145 async fn tap(
146 &self,
147 selector: &Selector,
148 include: Option<IncludeScope>,
149 ) -> Result<(), ExpectationFailure>;
150
151 /// Tap with explicit mode (Path A vs Path B).
152 async fn tap_with_mode(
153 &self,
154 selector: &Selector,
155 mode: TapMode,
156 include: Option<IncludeScope>,
157 ) -> Result<(), ExpectationFailure>;
158
159 /// Tap at viewport-normalized coordinate (§9 #3 escape hatch).
160 async fn tap_at_norm_coord(&self, nx: f64, ny: f64) -> Result<(), ExpectationFailure>;
161
162 /// Tap by accessibility identifier via the swift `/tap-by-id`
163 /// route (IOHID synthesize at element-frame center).
164 async fn tap_by_id(&self, id: &str) -> Result<(), ExpectationFailure>;
165
166 /// Double-tap a selector.
167 async fn double_tap(
168 &self,
169 selector: &Selector,
170 include: Option<IncludeScope>,
171 ) -> Result<(), ExpectationFailure>;
172
173 /// Long-press a selector for `duration`.
174 async fn long_press(
175 &self,
176 selector: &Selector,
177 duration: Duration,
178 include: Option<IncludeScope>,
179 ) -> Result<(), ExpectationFailure>;
180
181 /// Fill text into a focused / matched input.
182 async fn fill(
183 &self,
184 selector: &Selector,
185 text: &str,
186 include: Option<IncludeScope>,
187 ) -> Result<(), ExpectationFailure>;
188
189 /// Clear text from a focused / matched input.
190 async fn clear(
191 &self,
192 selector: &Selector,
193 include: Option<IncludeScope>,
194 ) -> Result<(), ExpectationFailure>;
195
196 /// Press a named key (Return / Tab / Backspace / etc).
197 async fn press_key(&self, key: KeyName) -> Result<(), ExpectationFailure>;
198
199 /// Scroll until a selector is visible.
200 async fn scroll(
201 &self,
202 selector: &Selector,
203 direction: SwipeDirection,
204 ) -> Result<(), ExpectationFailure>;
205
206 /// One-shot swipe in a direction (no probe loop).
207 async fn swipe_once(&self, direction: SwipeDirection) -> Result<(), ExpectationFailure>;
208
209 /// Swipe between two viewport-normalized coords.
210 async fn swipe_at_norm_coord(
211 &self,
212 from: (f64, f64),
213 to: (f64, f64),
214 ) -> Result<(), ExpectationFailure>;
215
216 /// Dismiss the keyboard.
217 async fn hide_keyboard(&self) -> Result<(), ExpectationFailure>;
218
219 /// Trigger "back" gesture (iOS edge-swipe / Android KEYCODE_BACK).
220 async fn back(&self) -> Result<(), ExpectationFailure>;
221
222 /// Rotate the device.
223 async fn set_orientation(&self, orientation: Orientation) -> Result<(), ExpectationFailure>;
224
225 /// Bring app to foreground.
226 async fn foreground(&self, bundle_id: &str) -> Result<(), ExpectationFailure>;
227
228 /// Evaluate JavaScript in a WebView (iOS: fixture bridge :28080;
229 /// Android: Chrome DevTools Protocol via adb forward).
230 async fn webview_eval(&self, js: &str) -> Result<serde_json::Value, ExpectationFailure>;
231}