Option C: Rewrite gyandoot-browser for camoufox-browser@0.1.0 daemon IPC
Complete rewrite removing old HTTP REST client. The latest camoufox-browser npm package (v0.1.0) uses Unix socket JSON-line IPC, not HTTP REST on :9377. Changes: - gyandoot-browser/client.rs: Full daemon IPC via Unix socket (~/.camoufox-browser/*.sock) - JSON-line protocol matching ensureDaemonRunning + daemon.js - Auto-launches browser on first command (daemon manages lifecycle) - Supports navigate, click, type, scroll, snapshot, search macros, tab management - Proper HeathResponse, NavigationResult, PageSnapshot types - gyandoot-browser/lib.rs: Exports new client API (no SearchMacro enum, no SessionManager) - gyandoot-browser/Cargo.toml: reqwest -> tokio Unix socket deps - Removed: macros.rs, session.rs, snapshot.rs (all merged into client.rs) - browser_manager.rs: Triggers daemon via npx, waits for Unix socket - browser_tools.rs: Simplified API (no tab_id, no create_tab/close_tab) Fix: camoufox-browser process exited with status 1 error on fresh install
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,17 @@
|
||||
//! # GyanDoot Browser — Anti-Detection Browsing
|
||||
//! # GyanDoot Browser — Anti-Detection Browsing (Daemon IPC)
|
||||
//!
|
||||
//! A Rust client for [camofox-browser](https://github.com/jo-inc/camofox-browser),
|
||||
//! an anti-detection browser server for AI agents powered by Camoufox.
|
||||
//! A Rust client for [camoufox-browser](https://github.com/msly/camoufox-browser),
|
||||
//! communicating via Unix socket JSON-line IPC protocol.
|
||||
//!
|
||||
//! Camoufox is a Firefox fork with C++-level fingerprint spoofing —
|
||||
//! no JavaScript shims, no detectable differences from a real browser.
|
||||
//! This crate wraps its REST API with typed Rust interfaces.
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! The `camoufox-browser@0.1.0` npm package runs as a background daemon that
|
||||
//! listens on a Unix socket at `~/.camoufox-browser/<session>.sock`. The daemon
|
||||
//! auto-launches the camoufox browser on-demand. All commands are JSON-line
|
||||
//! messages sent over the socket.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
@@ -13,8 +19,7 @@
|
||||
//! - Token-efficient accessibility snapshots (~90% smaller than HTML)
|
||||
//! - Stable element refs (`e1`, `e2`) for reliable interaction
|
||||
//! - Session isolation per user
|
||||
//! - Proxy + GeoIP support
|
||||
//! - Automatic idle session cleanup
|
||||
//! - Automatic browser lifecycle (launch on first command)
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
@@ -27,13 +32,10 @@
|
||||
//! ```
|
||||
|
||||
pub mod client;
|
||||
pub mod macros;
|
||||
pub mod session;
|
||||
pub mod snapshot;
|
||||
|
||||
pub use client::{BrowserClient, BrowserError, BrowserResult, DEFAULT_CAMOFOX_URL};
|
||||
pub use macros::SearchMacro;
|
||||
pub use session::{BrowserSession, SessionManager, TabInfo};
|
||||
pub use snapshot::{
|
||||
HealthResponse, InteractionResult, LinkItem, NavigationResult, PageLinks, PageSnapshot,
|
||||
pub use client::{
|
||||
BrowserClient, BrowserError, BrowserResult,
|
||||
check_daemon_socket, default_socket_path,
|
||||
HealthResponse, NavigationResult, PageSnapshot, ElementInfo,
|
||||
InteractionResult, TabInfo,
|
||||
};
|
||||
@@ -1,165 +0,0 @@
|
||||
//! Search macros for common websites.
|
||||
//!
|
||||
//! Camofox-browser supports 14+ built-in search macros that map to
|
||||
//! site-specific search URLs. Using macros is more reliable than
|
||||
//! manually constructing URLs because the browser handles all
|
||||
//! redirects, cookies, and anti-bot challenges.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
/// All supported search macros.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SearchMacro {
|
||||
/// `@google_search` — Search Google
|
||||
GoogleSearch,
|
||||
/// `@youtube_search` — Search YouTube
|
||||
YoutubeSearch,
|
||||
/// `@amazon_search` — Search Amazon
|
||||
AmazonSearch,
|
||||
/// `@reddit_search` — Search Reddit (returns JSON)
|
||||
RedditSearch,
|
||||
/// `@reddit_subreddit` — Browse a subreddit (returns JSON)
|
||||
RedditSubreddit,
|
||||
/// `@wikipedia_search` — Search Wikipedia
|
||||
WikipediaSearch,
|
||||
/// `@twitter_search` — Search Twitter/X
|
||||
TwitterSearch,
|
||||
/// `@yelp_search` — Search Yelp
|
||||
YelpSearch,
|
||||
/// `@spotify_search` — Search Spotify
|
||||
SpotifySearch,
|
||||
/// `@netflix_search` — Search Netflix
|
||||
NetflixSearch,
|
||||
/// `@linkedin_search` — Search LinkedIn
|
||||
LinkedinSearch,
|
||||
/// `@instagram_search` — Search Instagram
|
||||
InstagramSearch,
|
||||
/// `@tiktok_search` — Search TikTok
|
||||
TiktokSearch,
|
||||
/// `@twitch_search` — Search Twitch
|
||||
TwitchSearch,
|
||||
}
|
||||
|
||||
impl SearchMacro {
|
||||
/// All supported macros as a static slice.
|
||||
pub const ALL: &'static [SearchMacro] = &[
|
||||
SearchMacro::GoogleSearch,
|
||||
SearchMacro::YoutubeSearch,
|
||||
SearchMacro::AmazonSearch,
|
||||
SearchMacro::RedditSearch,
|
||||
SearchMacro::RedditSubreddit,
|
||||
SearchMacro::WikipediaSearch,
|
||||
SearchMacro::TwitterSearch,
|
||||
SearchMacro::YelpSearch,
|
||||
SearchMacro::SpotifySearch,
|
||||
SearchMacro::NetflixSearch,
|
||||
SearchMacro::LinkedinSearch,
|
||||
SearchMacro::InstagramSearch,
|
||||
SearchMacro::TiktokSearch,
|
||||
SearchMacro::TwitchSearch,
|
||||
];
|
||||
|
||||
/// The `@name` string sent to the camofox API.
|
||||
pub fn as_macro_str(&self) -> &'static str {
|
||||
match self {
|
||||
SearchMacro::GoogleSearch => "@google_search",
|
||||
SearchMacro::YoutubeSearch => "@youtube_search",
|
||||
SearchMacro::AmazonSearch => "@amazon_search",
|
||||
SearchMacro::RedditSearch => "@reddit_search",
|
||||
SearchMacro::RedditSubreddit => "@reddit_subreddit",
|
||||
SearchMacro::WikipediaSearch => "@wikipedia_search",
|
||||
SearchMacro::TwitterSearch => "@twitter_search",
|
||||
SearchMacro::YelpSearch => "@yelp_search",
|
||||
SearchMacro::SpotifySearch => "@spotify_search",
|
||||
SearchMacro::NetflixSearch => "@netflix_search",
|
||||
SearchMacro::LinkedinSearch => "@linkedin_search",
|
||||
SearchMacro::InstagramSearch => "@instagram_search",
|
||||
SearchMacro::TiktokSearch => "@tiktok_search",
|
||||
SearchMacro::TwitchSearch => "@twitch_search",
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable site name.
|
||||
pub fn site_name(&self) -> &'static str {
|
||||
match self {
|
||||
SearchMacro::GoogleSearch => "Google",
|
||||
SearchMacro::YoutubeSearch => "YouTube",
|
||||
SearchMacro::AmazonSearch => "Amazon",
|
||||
SearchMacro::RedditSearch => "Reddit",
|
||||
SearchMacro::RedditSubreddit => "Reddit (subreddit)",
|
||||
SearchMacro::WikipediaSearch => "Wikipedia",
|
||||
SearchMacro::TwitterSearch => "Twitter/X",
|
||||
SearchMacro::YelpSearch => "Yelp",
|
||||
SearchMacro::SpotifySearch => "Spotify",
|
||||
SearchMacro::NetflixSearch => "Netflix",
|
||||
SearchMacro::LinkedinSearch => "LinkedIn",
|
||||
SearchMacro::InstagramSearch => "Instagram",
|
||||
SearchMacro::TiktokSearch => "TikTok",
|
||||
SearchMacro::TwitchSearch => "Twitch",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a `@name` string into a `SearchMacro`.
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"@google_search" => Some(SearchMacro::GoogleSearch),
|
||||
"@youtube_search" => Some(SearchMacro::YoutubeSearch),
|
||||
"@amazon_search" => Some(SearchMacro::AmazonSearch),
|
||||
"@reddit_search" => Some(SearchMacro::RedditSearch),
|
||||
"@reddit_subreddit" => Some(SearchMacro::RedditSubreddit),
|
||||
"@wikipedia_search" => Some(SearchMacro::WikipediaSearch),
|
||||
"@twitter_search" => Some(SearchMacro::TwitterSearch),
|
||||
"@yelp_search" => Some(SearchMacro::YelpSearch),
|
||||
"@spotify_search" => Some(SearchMacro::SpotifySearch),
|
||||
"@netflix_search" => Some(SearchMacro::NetflixSearch),
|
||||
"@linkedin_search" => Some(SearchMacro::LinkedinSearch),
|
||||
"@instagram_search" => Some(SearchMacro::InstagramSearch),
|
||||
"@tiktok_search" => Some(SearchMacro::TiktokSearch),
|
||||
"@twitch_search" => Some(SearchMacro::TwitchSearch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SearchMacro {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_macro_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_all_macros_have_valid_names() {
|
||||
for m in SearchMacro::ALL {
|
||||
let name = m.as_macro_str();
|
||||
assert!(name.starts_with('@'), "Macro should start with @");
|
||||
assert!(name.len() > 2, "Macro should have a name");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_parse() {
|
||||
for m in SearchMacro::ALL {
|
||||
let name = m.as_macro_str();
|
||||
let parsed = SearchMacro::from_str(name);
|
||||
assert_eq!(parsed, Some(*m), "Roundtrip failed for {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_macro() {
|
||||
assert_eq!(SearchMacro::from_str("@invalid_search"), None);
|
||||
assert_eq!(SearchMacro::from_str("google_search"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_site_names() {
|
||||
assert_eq!(SearchMacro::GoogleSearch.site_name(), "Google");
|
||||
assert_eq!(SearchMacro::YoutubeSearch.site_name(), "YouTube");
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
//! Browser session management — tracks tabs, sessions, and their lifecycle.
|
||||
|
||||
use uuid::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use chrono::Utc;
|
||||
|
||||
/// Represents a browser session (one user's isolated browsing context).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BrowserSession {
|
||||
/// Session identifier (maps to camofox `sessionKey`).
|
||||
pub id: String,
|
||||
/// User identifier.
|
||||
pub user_id: String,
|
||||
/// Tabs owned by this session.
|
||||
pub tabs: HashMap<String, TabInfo>,
|
||||
/// When the session was created.
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
/// When the session was last active.
|
||||
pub last_active: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl BrowserSession {
|
||||
pub fn new(user_id: &str) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
tabs: HashMap::new(),
|
||||
created_at: Utc::now(),
|
||||
last_active: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn touch(&mut self) {
|
||||
self.last_active = Utc::now();
|
||||
}
|
||||
|
||||
pub fn add_tab(&mut self, tab_id: &str, url: &str) {
|
||||
self.tabs.insert(
|
||||
tab_id.to_string(),
|
||||
TabInfo {
|
||||
id: tab_id.to_string(),
|
||||
url: url.to_string(),
|
||||
created_at: Utc::now(),
|
||||
last_navigated: Utc::now(),
|
||||
page_title: String::new(),
|
||||
},
|
||||
);
|
||||
self.touch();
|
||||
}
|
||||
|
||||
pub fn remove_tab(&mut self, tab_id: &str) {
|
||||
self.tabs.remove(tab_id);
|
||||
self.touch();
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a single browser tab.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TabInfo {
|
||||
/// Camofox tab ID.
|
||||
pub id: String,
|
||||
/// Current URL in the tab.
|
||||
pub url: String,
|
||||
/// When the tab was created.
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
/// When the tab last navigated.
|
||||
pub last_navigated: chrono::DateTime<Utc>,
|
||||
/// Current page title.
|
||||
#[serde(default)]
|
||||
pub page_title: String,
|
||||
}
|
||||
|
||||
impl TabInfo {
|
||||
pub fn update_title(&mut self, title: &str) {
|
||||
self.page_title = title.to_string();
|
||||
self.last_navigated = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages all browser sessions.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionManager {
|
||||
sessions: HashMap<String, BrowserSession>,
|
||||
max_sessions: usize,
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sessions: HashMap::new(),
|
||||
max_sessions: 50,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_max(mut self, max: usize) -> Self {
|
||||
self.max_sessions = max;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_or_create(&mut self, user_id: &str, session_key: Option<&str>) -> &mut BrowserSession {
|
||||
let key = session_key.unwrap_or(user_id).to_string();
|
||||
if !self.sessions.contains_key(&key) {
|
||||
let session = BrowserSession::new(user_id);
|
||||
self.sessions.insert(key.clone(), session);
|
||||
}
|
||||
self.sessions.get_mut(&key).unwrap()
|
||||
}
|
||||
|
||||
pub fn get(&self, session_key: &str) -> Option<&BrowserSession> {
|
||||
self.sessions.get(session_key)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, session_key: &str) -> Option<&mut BrowserSession> {
|
||||
self.sessions.get_mut(session_key)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, session_key: &str) -> Option<BrowserSession> {
|
||||
self.sessions.remove(session_key)
|
||||
}
|
||||
|
||||
pub fn all_sessions(&self) -> Vec<&BrowserSession> {
|
||||
self.sessions.values().collect()
|
||||
}
|
||||
|
||||
pub fn session_count(&self) -> usize {
|
||||
self.sessions.len()
|
||||
}
|
||||
|
||||
/// Remove sessions that have been idle longer than `timeout` seconds.
|
||||
pub fn cleanup_idle(&mut self, timeout_secs: i64) -> usize {
|
||||
let now = Utc::now();
|
||||
let cutoff = now - chrono::Duration::seconds(timeout_secs);
|
||||
let keys: Vec<String> = self.sessions
|
||||
.iter()
|
||||
.filter(|(_, s)| s.last_active < cutoff)
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect();
|
||||
let count = keys.len();
|
||||
for k in keys {
|
||||
self.sessions.remove(&k);
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SessionManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_session_lifecycle() {
|
||||
let mut mgr = SessionManager::new();
|
||||
let session = mgr.get_or_create("user1", Some("session-1"));
|
||||
session.add_tab("tab-abc", "https://example.com");
|
||||
assert_eq!(session.tabs.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_cleanup() {
|
||||
let mut mgr = SessionManager::new();
|
||||
mgr.get_or_create("user1", Some("old-session"));
|
||||
// Sessions are created with Utc::now(), so cleanup_idle with 0 should remove nothing
|
||||
// (they're brand new). Let's use a negative timeout to force cleanup.
|
||||
let cleaned = mgr.cleanup_idle(-1);
|
||||
assert_eq!(cleaned, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_sessions() {
|
||||
let mut mgr = SessionManager::new().with_max(3);
|
||||
mgr.get_or_create("u1", Some("s1"));
|
||||
mgr.get_or_create("u2", Some("s2"));
|
||||
mgr.get_or_create("u3", Some("s3"));
|
||||
assert_eq!(mgr.session_count(), 3);
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
//! Types representing accessibility tree snapshots from camofox-browser.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A page snapshot returned by the camofox-browser API.
|
||||
///
|
||||
/// The snapshot is an accessibility tree with stable element refs
|
||||
/// (e.g., `e1`, `e2`) that can be used for clicking and typing.
|
||||
/// This format is ~90% more token-efficient than raw HTML.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PageSnapshot {
|
||||
/// Accessibility tree text with element refs.
|
||||
pub snapshot: String,
|
||||
/// Optional URL of the page.
|
||||
#[serde(default)]
|
||||
pub url: String,
|
||||
/// Optional page title.
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
/// Optional screenshot as base64 PNG.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub screenshot: Option<String>,
|
||||
/// Element reference map: ref -> element info.
|
||||
#[serde(default)]
|
||||
pub elements: HashMap<String, ElementInfo>,
|
||||
}
|
||||
|
||||
/// Information about a single element in the snapshot.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ElementInfo {
|
||||
/// The element's tag name (e.g., "a", "button", "input").
|
||||
#[serde(default)]
|
||||
pub tag: String,
|
||||
/// Visible text content.
|
||||
#[serde(default)]
|
||||
pub text: String,
|
||||
/// URL for links.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub href: Option<String>,
|
||||
/// Whether the element is interactive.
|
||||
#[serde(default)]
|
||||
pub interactive: bool,
|
||||
}
|
||||
|
||||
impl PageSnapshot {
|
||||
/// Create a new snapshot from raw text.
|
||||
pub fn new(snapshot: String) -> Self {
|
||||
Self {
|
||||
snapshot,
|
||||
url: String::new(),
|
||||
title: String::new(),
|
||||
screenshot: None,
|
||||
elements: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the snapshot is empty (no content).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.snapshot.is_empty()
|
||||
}
|
||||
|
||||
/// Number of characters in the snapshot text.
|
||||
pub fn len(&self) -> usize {
|
||||
self.snapshot.len()
|
||||
}
|
||||
|
||||
/// Extract all element refs from the snapshot text.
|
||||
///
|
||||
/// Refs look like `[e1]`, `[e2]` in the accessibility tree.
|
||||
pub fn extract_refs(&self) -> Vec<String> {
|
||||
let mut refs = Vec::new();
|
||||
for line in self.snapshot.lines() {
|
||||
// Pattern: [eN] or eN
|
||||
if let Some(start) = line.find("[e") {
|
||||
let end = line[start + 2..].find(']').map(|i| start + 2 + i);
|
||||
if let Some(end_pos) = end {
|
||||
let ref_str = &line[start + 1..start + 1 + end_pos + 1];
|
||||
refs.push(ref_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
refs.sort();
|
||||
refs.dedup();
|
||||
refs
|
||||
}
|
||||
}
|
||||
|
||||
/// Links extracted from a page.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PageLinks {
|
||||
/// List of links on the page.
|
||||
pub links: Vec<LinkItem>,
|
||||
/// Total count of links.
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
/// A single link item.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LinkItem {
|
||||
/// Link text.
|
||||
pub text: String,
|
||||
/// Target URL.
|
||||
pub href: String,
|
||||
/// Optional title attribute.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from page navigation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NavigationResult {
|
||||
/// Final URL after navigation.
|
||||
pub url: String,
|
||||
/// Page title.
|
||||
pub title: String,
|
||||
/// Whether navigation succeeded.
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
/// Response from element interaction (click/type).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InteractionResult {
|
||||
/// Whether the interaction succeeded.
|
||||
pub ok: bool,
|
||||
/// New snapshot after interaction (if available).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub snapshot: Option<PageSnapshot>,
|
||||
}
|
||||
|
||||
/// Health check response from camofox-browser.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HealthResponse {
|
||||
/// Server is OK.
|
||||
pub ok: bool,
|
||||
/// Engine type (e.g., "camoufox").
|
||||
pub engine: String,
|
||||
/// Whether browser is connected.
|
||||
#[serde(default)]
|
||||
pub browser_connected: bool,
|
||||
/// Whether browser is running.
|
||||
#[serde(default)]
|
||||
pub browser_running: bool,
|
||||
/// Number of active tabs.
|
||||
#[serde(default)]
|
||||
pub active_tabs: u32,
|
||||
/// Number of active sessions.
|
||||
#[serde(default)]
|
||||
pub active_sessions: u32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_refs_from_snapshot() {
|
||||
let snap = PageSnapshot::new(
|
||||
"- heading \"Example\" [level=1]\n- link \"Click\" [e1]:\n - /url: https://example.com\n- paragraph: text [e2]\n".into(),
|
||||
);
|
||||
let refs = snap.extract_refs();
|
||||
assert!(refs.contains(&"[e1]".to_string()));
|
||||
assert!(refs.contains(&"[e2]".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_snapshot() {
|
||||
let snap = PageSnapshot::new(String::new());
|
||||
assert!(snap.is_empty());
|
||||
assert_eq!(snap.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_response_deserialize() {
|
||||
let json = r#"{"ok":true,"engine":"camoufox","browserConnected":true,"browserRunning":true,"activeTabs":3,"activeSessions":2}"#;
|
||||
let health: HealthResponse = serde_json::from_str(json).unwrap();
|
||||
assert!(health.ok);
|
||||
assert_eq!(health.active_tabs, 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user