Skip to main content

allwright/
client_mobile.rs

1use serde::{Deserialize, Serialize};
2
3use super::bootstrap::{ensure_plugins_installed, invoke_plugin};
4use super::types::{ClickResult, CommandOptions, Error, FillResult, Result};
5
6#[derive(Debug, Clone, Default)]
7pub struct MobileAndroidConnectOptions {
8    pub device: Option<String>,
9    pub adb_endpoint: Option<String>,
10    pub preserve_app_state: bool,
11    pub timeout_ms: Option<u32>,
12}
13
14#[derive(Debug, Clone, Default)]
15pub struct MobileAndroidLaunchOptions {
16    pub apk_path: Option<String>,
17    pub app_id: Option<String>,
18    pub launch_activity: Option<String>,
19    pub stop_before_launch: bool,
20    pub timeout_ms: Option<u32>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24struct MobilePluginEnvelope<T> {
25    ok: bool,
26    result: Option<T>,
27    error: Option<String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31struct MobileBrowserSessionHandle {
32    platform: String,
33    automation: MobileAutomationSessionInfo,
34    device: MobileDeviceTarget,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38struct MobileAutomationSessionInfo {
39    session_id: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43struct MobileDeviceTarget {
44    device_id: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48struct MobilePageSessionHandle {
49    page_id: String,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53struct MobilePageInfo {
54    page_session: MobilePageSessionHandle,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58struct MobileConnectInfo {
59    browser_session: MobileBrowserSessionHandle,
60    initial_page: MobilePageInfo,
61}
62
63#[derive(Debug, Clone)]
64pub struct AndroidPage {
65    browser_session: MobileBrowserSessionHandle,
66    page_session: MobilePageSessionHandle,
67}
68
69#[derive(Debug, Clone)]
70pub struct AndroidDevice {
71    connect_info: MobileConnectInfo,
72    page: AndroidPage,
73}
74
75pub mod android {
76    use super::*;
77
78    pub fn connect(options: MobileAndroidConnectOptions) -> Result<AndroidDevice> {
79        ensure_plugins_installed(&["mobile-android"])?;
80        let request = serde_json::json!({
81            "command": "connect",
82            "platform": "android",
83            "device": options.device,
84            "adb_endpoint": options.adb_endpoint,
85            "preserve_app_state": options.preserve_app_state,
86            "timeout_ms": options.timeout_ms,
87        });
88        let connect_info: MobileConnectInfo = invoke_android("connect", request)?;
89        Ok(AndroidDevice::new(connect_info))
90    }
91}
92
93impl AndroidDevice {
94    fn new(connect_info: MobileConnectInfo) -> Self {
95        let page = AndroidPage {
96            browser_session: connect_info.browser_session.clone(),
97            page_session: connect_info.initial_page.page_session.clone(),
98        };
99        Self { connect_info, page }
100    }
101
102    pub fn session_id(&self) -> &str {
103        &self.connect_info.browser_session.automation.session_id
104    }
105
106    pub fn page(&self) -> AndroidPage {
107        self.page.clone()
108    }
109
110    pub fn initial_page(&self) -> AndroidPage {
111        self.page()
112    }
113
114    pub fn launch(&mut self, options: MobileAndroidLaunchOptions) -> Result<AndroidPage> {
115        let request = serde_json::json!({
116            "command": "launch_app",
117            "browser_session": self.connect_info.browser_session,
118            "options": {
119                "apk_path": options.apk_path,
120                "app_id": options.app_id,
121                "launch_activity": options.launch_activity,
122                "stop_before_launch": options.stop_before_launch,
123                "timeout_ms": options.timeout_ms,
124            },
125        });
126        let page_info: MobilePageInfo = invoke_android("launch", request)?;
127        self.page = AndroidPage {
128            browser_session: self.connect_info.browser_session.clone(),
129            page_session: page_info.page_session,
130        };
131        Ok(self.page.clone())
132    }
133}
134
135impl AndroidPage {
136    pub fn session_id(&self) -> &str {
137        &self.page_session.page_id
138    }
139
140    pub fn click(&self, selector: &str, options: CommandOptions) -> Result<ClickResult> {
141        #[derive(Deserialize)]
142        struct ClickInfo {
143            selector: String,
144            note: String,
145            session_id: String,
146        }
147        let result: ClickInfo = invoke_android(
148            "click",
149            serde_json::json!({
150                "command": "click_element",
151                "browser_session": self.browser_session,
152                "page_session": self.page_session,
153                "selector": selector,
154                "timeout_ms": options.timeout_ms,
155            }),
156        )?;
157        Ok(ClickResult {
158            selector: result.selector,
159            note: result.note,
160            bidi_session_id: result.session_id,
161        })
162    }
163
164    pub fn fill(&self, selector: &str, value: &str, options: CommandOptions) -> Result<FillResult> {
165        #[derive(Deserialize)]
166        struct FillInfo {
167            selector: String,
168            value: String,
169            note: String,
170        }
171        let result: FillInfo = invoke_android(
172            "fill",
173            serde_json::json!({
174                "command": "fill_element",
175                "browser_session": self.browser_session,
176                "page_session": self.page_session,
177                "selector": selector,
178                "value": value,
179                "timeout_ms": options.timeout_ms,
180            }),
181        )?;
182        Ok(FillResult {
183            selector: result.selector,
184            value: result.value,
185            note: result.note,
186        })
187    }
188}
189
190fn invoke_android<T>(command_name: &str, request: serde_json::Value) -> Result<T>
191where
192    T: for<'de> Deserialize<'de>,
193{
194    let payload = invoke_plugin("mobile-android", &request.to_string())?;
195    let envelope: MobilePluginEnvelope<T> =
196        serde_json::from_str(payload.trim()).map_err(|error| {
197            Error::new(format!(
198                "failed to decode mobile-android plugin response for {command_name}: {error}"
199            ))
200        })?;
201    if !envelope.ok {
202        return Err(Error::new(envelope.error.unwrap_or_else(|| {
203            format!("mobile-android plugin {command_name} failed")
204        })));
205    }
206    envelope.result.ok_or_else(|| {
207        Error::new(format!(
208            "mobile-android plugin {command_name} returned success without a result payload"
209        ))
210    })
211}