bao-servo-webdriver-server 0.5.2

A component of the servo web-engine.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use std::collections::HashMap;

use rustc_hash::FxHashSet;
use serde_json::{Map, Value, json};
use servo_base::id::{BrowsingContextId, WebViewId};
use uuid::Uuid;
use webdriver::error::WebDriverResult;

use crate::Handler;
use crate::actions::{ActionItem, InputSourceState};
use crate::capabilities::ServoCapabilities;
use crate::timeout::{
    TimeoutsConfiguration, deserialize_as_timeouts_configuration, serialize_timeouts_configuration,
};
use crate::user_prompt::{
    UserPromptHandler, default_unhandled_prompt_behavior, deserialize_unhandled_prompt_behaviour,
};

#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub(crate) enum PageLoadStrategy {
    None,
    Eager,
    Normal,
}

// Need a different implementation for ToString than Display
#[expect(clippy::to_string_trait_impl)]
impl ToString for PageLoadStrategy {
    fn to_string(&self) -> String {
        match self {
            PageLoadStrategy::None => String::from("none"),
            PageLoadStrategy::Eager => String::from("eager"),
            PageLoadStrategy::Normal => String::from("normal"),
        }
    }
}

/// Represents the current WebDriver session and holds relevant session state.
/// Currently, only 1 webview is supported per session.
/// So only there is only 1 InputState.
pub(crate) struct WebDriverSession {
    /// <https://www.w3.org/TR/webdriver2/#dfn-session-id>
    id: Uuid,

    /// <https://www.w3.org/TR/webdriver2/#dfn-current-top-level-browsing-context>
    /// The id of the current top-level browsing context
    webview_id: Option<WebViewId>,

    /// <https://www.w3.org/TR/webdriver2/#dfn-current-browsing-context>
    /// The id of the current browsing context
    browsing_context_id: Option<BrowsingContextId>,

    timeouts: TimeoutsConfiguration,

    page_loading_strategy: PageLoadStrategy,

    strict_file_interactability: bool,

    /// <https://w3c.github.io/webdriver/#dfn-user-prompt-handler>
    user_prompt_handler: UserPromptHandler,

    /// <https://w3c.github.io/webdriver/#dfn-input-state-map>
    pub(crate) input_state_table: HashMap<String, InputSourceState>,

    /// <https://w3c.github.io/webdriver/#dfn-input-cancel-list>
    pub(crate) input_cancel_list: Vec<(String, ActionItem)>,
}

impl WebDriverSession {
    pub(crate) fn new() -> WebDriverSession {
        WebDriverSession {
            id: Uuid::new_v4(),
            webview_id: None,
            browsing_context_id: None,
            timeouts: TimeoutsConfiguration::default(),
            page_loading_strategy: PageLoadStrategy::Normal,
            strict_file_interactability: false,
            user_prompt_handler: UserPromptHandler::new(),
            input_state_table: Default::default(),
            input_cancel_list: Default::default(),
        }
    }

    pub(crate) fn set_webview_id(&mut self, webview_id: WebViewId) {
        self.webview_id = Some(webview_id);
    }

    pub(crate) fn set_browsing_context_id(&mut self, browsing_context_id: BrowsingContextId) {
        self.browsing_context_id = Some(browsing_context_id);
    }

    pub(crate) fn current_webview_id(&self) -> Option<WebViewId> {
        self.webview_id
    }

    pub(crate) fn current_browsing_context_id(&self) -> Option<BrowsingContextId> {
        self.browsing_context_id
    }

    pub(crate) fn session_timeouts(&self) -> &TimeoutsConfiguration {
        &self.timeouts
    }

    pub(crate) fn session_timeouts_mut(&mut self) -> &mut TimeoutsConfiguration {
        &mut self.timeouts
    }

    pub(crate) fn page_loading_strategy(&self) -> PageLoadStrategy {
        self.page_loading_strategy.clone()
    }

    pub(crate) fn strict_file_interactability(&self) -> bool {
        self.strict_file_interactability
    }

    pub(crate) fn user_prompt_handler(&self) -> &UserPromptHandler {
        &self.user_prompt_handler
    }

    pub(crate) fn pointer_ids(&self) -> FxHashSet<u32> {
        self.input_state_table
            .values()
            .filter_map(|source| match source {
                InputSourceState::Pointer(pointer_state) => Some(pointer_state.pointer_id),
                _ => None,
            })
            .collect()
    }
}

impl Handler {
    /// <https://w3c.github.io/webdriver/#dfn-create-a-session>
    pub(crate) fn create_session(
        &mut self,
        capabilities: &mut Map<String, Value>,
        servo_capabilities: &ServoCapabilities,
    ) -> WebDriverResult<Uuid> {
        // Step 2. Let session be a new session
        let mut session = WebDriverSession::new();

        // Step 3. Let proxy be the result of getting property "proxy" from capabilities
        match capabilities.get("proxy") {
            // Proxy is a proxy configuration object
            Some(_) => {
                // TODO:
                // Take implementation-defined steps to set the user agent proxy
                // using the extracted proxy configuration.
                // If the defined proxy cannot be configured return error with error code
                // session not created. Otherwise set the has proxy configuration flag to true.
            },
            // Otherwise, set a property of capabilities with name "proxy"
            // and a value that is a new JSON Object.
            None => {
                capabilities.insert(String::from("proxy"), json!({}));
            },
        }

        // Step 4. If capabilites has a property named "acceptInsecureCerts"
        match capabilities.get("acceptInsecureCerts") {
            Some(_accept_insecure_certs) => {
                // TODO: Set the endpoint node's accept insecure TLS flag
            },
            None => {
                capabilities.insert(String::from("acceptInsecureCerts"), json!(false));
            },
        }

        // Step 5. Let user prompt handler capability be the result of
        // getting property "unhandledPromptBehavior" from capabilities
        match capabilities.get("unhandledPromptBehavior") {
            // Step 6. If user prompt handler capability is not undefined
            Some(unhandled_prompt_behavior) => {
                session.user_prompt_handler =
                    deserialize_unhandled_prompt_behaviour(unhandled_prompt_behavior.clone())?;
            },
            // Step 7. Let serialized user prompt handler be serialize the user prompt handler.
            // Step 8. Set a property on capabilities with the name "unhandledPromptBehavior",
            // and the value serialized user prompt handler.
            // Ignore because the user prompt handler is already in the capabilities object
            None => {
                capabilities.insert(
                    String::from("unhandledPromptBehavior"),
                    json!(default_unhandled_prompt_behavior()),
                );
            },
        }

        // TODO: flag is http by default for now
        // Step 9. If flags contains "http"
        // Step 9.1. Let strategy be the result of getting property "pageLoadStrategy" from capabilities.
        match capabilities.get("pageLoadStrategy") {
            // If strategy is a string, set the session's page loading strategy to strategy.
            Some(strategy) => match strategy.to_string().as_str() {
                "none" => session.page_loading_strategy = PageLoadStrategy::None,
                "eager" => session.page_loading_strategy = PageLoadStrategy::Eager,
                _ => session.page_loading_strategy = PageLoadStrategy::Normal,
            },
            // Otherwise, set the page loading strategy to normal and set a property of capabilities
            // with name "pageLoadStrategy" and value "normal".
            None => {
                capabilities.insert(
                    String::from("pageLoadStrategy"),
                    json!(session.page_loading_strategy.to_string()),
                );
                session.page_loading_strategy = PageLoadStrategy::Normal;
            },
        }

        // Step 9.2. Let strictFileInteractability be the result of getting property
        // "strictFileInteractability" from capabilities
        if let Some(Value::Bool(strict_file_interactability)) =
            capabilities.get("strictFileInteractability")
        {
            session.strict_file_interactability = *strict_file_interactability;
        } else {
            capabilities.insert(String::from("strictFileInteractability"), json!(false));
        }

        // Step 9.3. Let timeouts be the result of getting a property "timeouts" from capabilities.
        // If timeouts is not undefined, set session's session timeouts to timeouts.
        if let Some(timeouts) = capabilities.get("timeouts") {
            session.timeouts = deserialize_as_timeouts_configuration(timeouts)?;
        }

        // Step 9.4 Set a property on capabilities with name "timeouts"
        // and value serialize the timeouts configuration with session's session timeouts.
        capabilities.insert(
            "timeouts".to_string(),
            serialize_timeouts_configuration(&session.timeouts),
        );

        // Step 10. Process any extension capabilities in capabilities in an implementation-defined manner
        // Nothing to processed

        // Step 11. Run any WebDriver new session algorithm defined in external specifications
        capabilities.insert(
            "browserName".to_string(),
            json!(servo_capabilities.browser_name),
        );
        capabilities.insert(
            "browserVersion".to_string(),
            json!(servo_capabilities.browser_version),
        );
        capabilities.insert(
            "platformName".to_string(),
            json!(
                servo_capabilities
                    .platform_name
                    .clone()
                    .unwrap_or("unknown".to_string())
            ),
        );
        capabilities.insert(
            "setWindowRect".to_string(),
            json!(servo_capabilities.set_window_rect),
        );
        capabilities.insert(
            "userAgent".to_string(),
            servo_config::pref!(user_agent).into(),
        );

        // Step 12. Append session to active sessions
        let id = session.id;
        self.session = Some(session);

        Ok(id)
    }
}