allwright-core 0.0.46

Lightweight allwright engine core with shared client and transport APIs.
Documentation
use serde::{Deserialize, Serialize};

use super::bootstrap::{ensure_plugins_installed, invoke_plugin};
use super::types::{ClickResult, CommandOptions, Error, FillResult, Result};

#[derive(Debug, Clone, Default)]
pub struct MobileAndroidConnectOptions {
    pub device: Option<String>,
    pub adb_endpoint: Option<String>,
    pub preserve_app_state: bool,
    pub timeout_ms: Option<u32>,
}

#[derive(Debug, Clone, Default)]
pub struct MobileAndroidLaunchOptions {
    pub apk_path: Option<String>,
    pub app_id: Option<String>,
    pub launch_activity: Option<String>,
    pub stop_before_launch: bool,
    pub timeout_ms: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct MobilePluginEnvelope<T> {
    ok: bool,
    result: Option<T>,
    error: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct MobileBrowserSessionHandle {
    platform: String,
    automation: MobileAutomationSessionInfo,
    device: MobileDeviceTarget,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct MobileAutomationSessionInfo {
    session_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct MobileDeviceTarget {
    device_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct MobilePageSessionHandle {
    page_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct MobilePageInfo {
    page_session: MobilePageSessionHandle,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct MobileConnectInfo {
    browser_session: MobileBrowserSessionHandle,
    initial_page: MobilePageInfo,
}

#[derive(Debug, Clone)]
pub struct AndroidPage {
    browser_session: MobileBrowserSessionHandle,
    page_session: MobilePageSessionHandle,
}

#[derive(Debug, Clone)]
pub struct AndroidDevice {
    connect_info: MobileConnectInfo,
    page: AndroidPage,
}

pub mod android {
    use super::*;

    pub fn connect(options: MobileAndroidConnectOptions) -> Result<AndroidDevice> {
        ensure_plugins_installed(&["mobile-android"])?;
        let request = serde_json::json!({
            "command": "connect",
            "platform": "android",
            "device": options.device,
            "adb_endpoint": options.adb_endpoint,
            "preserve_app_state": options.preserve_app_state,
            "timeout_ms": options.timeout_ms,
        });
        let connect_info: MobileConnectInfo = invoke_android("connect", request)?;
        Ok(AndroidDevice::new(connect_info))
    }
}

impl AndroidDevice {
    fn new(connect_info: MobileConnectInfo) -> Self {
        let page = AndroidPage {
            browser_session: connect_info.browser_session.clone(),
            page_session: connect_info.initial_page.page_session.clone(),
        };
        Self { connect_info, page }
    }

    pub fn session_id(&self) -> &str {
        &self.connect_info.browser_session.automation.session_id
    }

    pub fn page(&self) -> AndroidPage {
        self.page.clone()
    }

    pub fn initial_page(&self) -> AndroidPage {
        self.page()
    }

    pub fn launch(&mut self, options: MobileAndroidLaunchOptions) -> Result<AndroidPage> {
        let request = serde_json::json!({
            "command": "launch_app",
            "browser_session": self.connect_info.browser_session,
            "options": {
                "apk_path": options.apk_path,
                "app_id": options.app_id,
                "launch_activity": options.launch_activity,
                "stop_before_launch": options.stop_before_launch,
                "timeout_ms": options.timeout_ms,
            },
        });
        let page_info: MobilePageInfo = invoke_android("launch", request)?;
        self.page = AndroidPage {
            browser_session: self.connect_info.browser_session.clone(),
            page_session: page_info.page_session,
        };
        Ok(self.page.clone())
    }
}

impl AndroidPage {
    pub fn session_id(&self) -> &str {
        &self.page_session.page_id
    }

    pub fn click(&self, selector: &str, options: CommandOptions) -> Result<ClickResult> {
        #[derive(Deserialize)]
        struct ClickInfo {
            selector: String,
            note: String,
            session_id: String,
        }
        let result: ClickInfo = invoke_android(
            "click",
            serde_json::json!({
                "command": "click_element",
                "browser_session": self.browser_session,
                "page_session": self.page_session,
                "selector": selector,
                "timeout_ms": options.timeout_ms,
            }),
        )?;
        Ok(ClickResult {
            selector: result.selector,
            note: result.note,
            bidi_session_id: result.session_id,
        })
    }

    pub fn fill(&self, selector: &str, value: &str, options: CommandOptions) -> Result<FillResult> {
        #[derive(Deserialize)]
        struct FillInfo {
            selector: String,
            value: String,
            note: String,
        }
        let result: FillInfo = invoke_android(
            "fill",
            serde_json::json!({
                "command": "fill_element",
                "browser_session": self.browser_session,
                "page_session": self.page_session,
                "selector": selector,
                "value": value,
                "timeout_ms": options.timeout_ms,
            }),
        )?;
        Ok(FillResult {
            selector: result.selector,
            value: result.value,
            note: result.note,
        })
    }
}

fn invoke_android<T>(command_name: &str, request: serde_json::Value) -> Result<T>
where
    T: for<'de> Deserialize<'de>,
{
    let payload = invoke_plugin("mobile-android", &request.to_string())?;
    let envelope: MobilePluginEnvelope<T> =
        serde_json::from_str(payload.trim()).map_err(|error| {
            Error::new(format!(
                "failed to decode mobile-android plugin response for {command_name}: {error}"
            ))
        })?;
    if !envelope.ok {
        return Err(Error::new(envelope.error.unwrap_or_else(|| {
            format!("mobile-android plugin {command_name} failed")
        })));
    }
    envelope.result.ok_or_else(|| {
        Error::new(format!(
            "mobile-android plugin {command_name} returned success without a result payload"
        ))
    })
}