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