tauri-plugin-wdio-webdriver 1.0.0

Embedded WebDriver server for WDIO Tauri testing
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde::Serialize;
use serde_json::{json, Value};

/// W3C `WebDriver` success response
#[derive(Debug, Serialize)]
pub struct WebDriverResponse {
    pub value: Value,
}

impl WebDriverResponse {
    pub fn success<T: Serialize>(value: T) -> Self {
        Self {
            value: serde_json::to_value(value).unwrap_or(Value::Null),
        }
    }

    pub fn null() -> Self {
        Self { value: Value::Null }
    }
}

impl IntoResponse for WebDriverResponse {
    fn into_response(self) -> Response {
        (
            StatusCode::OK,
            [("Content-Type", "application/json; charset=utf-8")],
            Json(self),
        )
            .into_response()
    }
}

/// W3C `WebDriver` error response
#[derive(Debug)]
pub struct WebDriverErrorResponse {
    pub status: StatusCode,
    pub error: String,
    pub message: String,
    pub stacktrace: Option<String>,
}

impl WebDriverErrorResponse {
    pub fn new(status: StatusCode, error: &str, message: &str, stacktrace: Option<String>) -> Self {
        Self {
            status,
            error: error.to_string(),
            message: message.to_string(),
            stacktrace,
        }
    }

    pub fn invalid_session_id(session_id: &str) -> Self {
        Self::new(
            StatusCode::NOT_FOUND,
            "invalid session id",
            &format!("Session {session_id} not found"),
            None,
        )
    }

    pub fn no_such_element() -> Self {
        Self::new(
            StatusCode::NOT_FOUND,
            "no such element",
            "Unable to locate element",
            None,
        )
    }

    pub fn no_such_window() -> Self {
        Self::new(
            StatusCode::NOT_FOUND,
            "no such window",
            "No window could be found",
            None,
        )
    }

    pub fn no_such_alert() -> Self {
        Self::new(
            StatusCode::NOT_FOUND,
            "no such alert",
            "No alert is currently open",
            None,
        )
    }

    pub fn javascript_error(message: &str, stacktrace: Option<String>) -> Self {
        // Map known JS error messages to their correct W3C error codes
        if message.contains("stale element reference") {
            return Self::stale_element_reference();
        }

        Self::new(
            StatusCode::INTERNAL_SERVER_ERROR,
            "javascript error",
            message,
            stacktrace,
        )
    }

    pub fn stale_element_reference() -> Self {
        Self::new(
            StatusCode::NOT_FOUND,
            "stale element reference",
            "Element is no longer attached to the DOM",
            None,
        )
    }

    pub fn unknown_error(message: &str) -> Self {
        Self::new(
            StatusCode::INTERNAL_SERVER_ERROR,
            "unknown error",
            message,
            None,
        )
    }

    pub fn invalid_argument(message: &str) -> Self {
        Self::new(StatusCode::BAD_REQUEST, "invalid argument", message, None)
    }

    pub fn unsupported_operation(message: &str) -> Self {
        Self::new(
            StatusCode::INTERNAL_SERVER_ERROR,
            "unsupported operation",
            message,
            None,
        )
    }

    pub fn no_such_shadow_root() -> Self {
        Self::new(
            StatusCode::NOT_FOUND,
            "no such shadow root",
            "Element does not have a shadow root",
            None,
        )
    }

    pub fn script_timeout() -> Self {
        Self::new(
            StatusCode::INTERNAL_SERVER_ERROR,
            "script timeout",
            "Script execution timed out",
            None,
        )
    }

    pub fn no_such_cookie(name: &str) -> Self {
        Self::new(
            StatusCode::NOT_FOUND,
            "no such cookie",
            &format!("Cookie '{name}' not found"),
            None,
        )
    }

    pub fn no_such_frame() -> Self {
        Self::new(
            StatusCode::NOT_FOUND,
            "no such frame",
            "Unable to locate frame",
            None,
        )
    }

    pub fn element_not_interactable(message: &str) -> Self {
        Self::new(
            StatusCode::BAD_REQUEST,
            "element not interactable",
            message,
            None,
        )
    }
}

impl IntoResponse for WebDriverErrorResponse {
    fn into_response(self) -> Response {
        let body = json!({
            "value": {
                "error": self.error,
                "message": self.message,
                "stacktrace": self.stacktrace.unwrap_or_default()
            }
        });

        (
            self.status,
            [("Content-Type", "application/json; charset=utf-8")],
            Json(body),
        )
            .into_response()
    }
}

/// Result type for `WebDriver` handlers
pub type WebDriverResult = Result<WebDriverResponse, WebDriverErrorResponse>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn javascript_error_maps_stale_element_reference() {
        let err = WebDriverErrorResponse::javascript_error("stale element reference", None);
        assert_eq!(err.status, StatusCode::NOT_FOUND);
        assert_eq!(err.error, "stale element reference");
    }

    #[test]
    fn javascript_error_preserves_generic_errors() {
        let err = WebDriverErrorResponse::javascript_error("TypeError: x is not a function", None);
        assert_eq!(err.status, StatusCode::INTERNAL_SERVER_ERROR);
        assert_eq!(err.error, "javascript error");
        assert_eq!(err.message, "TypeError: x is not a function");
    }

    #[test]
    fn javascript_error_preserves_stacktrace_for_generic_errors() {
        let err = WebDriverErrorResponse::javascript_error(
            "ReferenceError: foo is not defined",
            Some("at eval:1:1".to_string()),
        );
        assert_eq!(err.error, "javascript error");
        assert_eq!(err.stacktrace, Some("at eval:1:1".to_string()));
    }

    #[test]
    fn stale_element_reference_returns_not_found() {
        let err = WebDriverErrorResponse::stale_element_reference();
        assert_eq!(err.status, StatusCode::NOT_FOUND);
        assert_eq!(err.error, "stale element reference");
    }
}