browsing 0.1.7

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation
//! Service worker management via CDP ServiceWorker domain

use crate::browser::views::{ServiceWorkerRegistration, ServiceWorkerVersion};
use crate::error::Result;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;

/// Manages service workers (list, start, stop, unregister)
#[derive(Debug, Clone)]
pub struct ServiceWorkerManager {
    client: Arc<crate::browser::cdp::CdpClient>,
    registrations: Arc<Mutex<HashMap<String, ServiceWorkerRegistration>>>,
    versions: Arc<Mutex<HashMap<String, ServiceWorkerVersion>>>,
}

impl ServiceWorkerManager {
    /// Create a new service worker manager
    pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
        Self {
            client,
            registrations: Arc::new(Mutex::new(HashMap::new())),
            versions: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    /// Enable service worker event tracking.
    /// Subscribes to `ServiceWorker.workerRegistrationUpdated` and `ServiceWorker.workerVersionUpdated`.
    pub async fn enable(&self) -> Result<()> {
        self.client
            .send_command("ServiceWorker.enable", serde_json::json!({}))
            .await?;

        let registrations = Arc::clone(&self.registrations);
        let versions = Arc::clone(&self.versions);
        let mut rx = self.client.subscribe_events().await?;

        tokio::spawn(async move {
            while let Ok(event) = rx.recv().await {
                match event.method.as_str() {
                    "ServiceWorker.workerRegistrationUpdated" => {
                        if let Some(regs) = event.params.get("registrations").and_then(|v| v.as_array()) {
                            for reg in regs {
                                let reg_id = reg.get("registrationId").and_then(|v| v.as_str()).unwrap_or("").to_string();
                                let scope_url = reg.get("scopeURL").and_then(|v| v.as_str()).unwrap_or("").to_string();
                                let is_deleted = reg.get("isDeleted").and_then(|v| v.as_bool()).unwrap_or(false);
                                let info = ServiceWorkerRegistration {
                                    registration_id: reg_id.clone(),
                                    scope_url,
                                    is_deleted,
                                };
                                registrations.lock().await.insert(reg_id, info);
                            }
                        }
                    }
                    "ServiceWorker.workerVersionUpdated" => {
                        if let Some(vers) = event.params.get("versions").and_then(|v| v.as_array()) {
                            for ver in vers {
                                let version_id = ver.get("versionId").and_then(|v| v.as_str()).unwrap_or("").to_string();
                                let registration_id = ver.get("registrationId").and_then(|v| v.as_str()).unwrap_or("").to_string();
                                let script_url = ver.get("scriptURL").and_then(|v| v.as_str()).unwrap_or("").to_string();
                                let status = ver.get("status").and_then(|v| v.as_str()).unwrap_or("").to_string();
                                let running_status = ver.get("runningStatus").and_then(|v| v.as_str()).map(|s| s.to_string());
                                let info = ServiceWorkerVersion {
                                    version_id: version_id.clone(),
                                    registration_id,
                                    script_url,
                                    status,
                                    running_status,
                                };
                                versions.lock().await.insert(version_id, info);
                            }
                        }
                    }
                    _ => {}
                }
            }
        });

        Ok(())
    }

    /// Disable service worker event tracking
    pub async fn disable(&self) -> Result<()> {
        self.client
            .send_command("ServiceWorker.disable", serde_json::json!({}))
            .await?;
        self.registrations.lock().await.clear();
        self.versions.lock().await.clear();
        Ok(())
    }

    /// Stop all running service workers
    pub async fn stop_all(&self) -> Result<()> {
        self.client
            .send_command("ServiceWorker.stopAllWorkers", serde_json::json!({}))
            .await?;
        Ok(())
    }

    /// Start a service worker by registration scope URL
    pub async fn start(&self, scope_url: &str) -> Result<()> {
        self.client
            .send_command(
                "ServiceWorker.startWorker",
                serde_json::json!({ "scopeURL": scope_url }),
            )
            .await?;
        Ok(())
    }

    /// Unregister a service worker by scope URL
    pub async fn unregister(&self, scope_url: &str) -> Result<()> {
        self.client
            .send_command(
                "ServiceWorker.unregister",
                serde_json::json!({ "scopeURL": scope_url }),
            )
            .await?;
        Ok(())
    }

    /// Update a service worker registration by scope URL
    pub async fn update(&self, scope_url: &str) -> Result<()> {
        self.client
            .send_command(
                "ServiceWorker.updateRegistration",
                serde_json::json!({ "scopeURL": scope_url }),
            )
            .await?;
        Ok(())
    }

    /// Get all tracked registrations
    pub async fn get_registrations(&self) -> Vec<ServiceWorkerRegistration> {
        self.registrations.lock().await.values().cloned().collect()
    }

    /// Get all tracked versions
    pub async fn get_versions(&self) -> Vec<ServiceWorkerVersion> {
        self.versions.lock().await.values().cloned().collect()
    }

    /// Get registration count
    pub async fn registration_count(&self) -> usize {
        self.registrations.lock().await.len()
    }

    /// Get version count
    pub async fn version_count(&self) -> usize {
        self.versions.lock().await.len()
    }

    /// Clear tracked data without disabling
    pub async fn clear(&self) {
        self.registrations.lock().await.clear();
        self.versions.lock().await.clear();
    }
}

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

    #[test]
    fn test_service_worker_registration() {
        let reg = ServiceWorkerRegistration {
            registration_id: "reg-1".to_string(),
            scope_url: "https://example.com/app/".to_string(),
            is_deleted: false,
        };
        assert_eq!(reg.registration_id, "reg-1");
        assert_eq!(reg.scope_url, "https://example.com/app/");
    }

    #[test]
    fn test_service_worker_version() {
        let ver = ServiceWorkerVersion {
            version_id: "v-1".to_string(),
            registration_id: "reg-1".to_string(),
            script_url: "https://example.com/sw.js".to_string(),
            status: "activated".to_string(),
            running_status: Some("running".to_string()),
        };
        assert_eq!(ver.status, "activated");
    }

    #[test]
    fn test_service_worker_serialization() {
        let reg = ServiceWorkerRegistration {
            registration_id: "r1".to_string(),
            scope_url: "/".to_string(),
            is_deleted: false,
        };
        let json = serde_json::to_string(&reg).unwrap();
        assert!(json.contains("r1"));
    }
}