firefox_webdriver/driver/
assets.rs

1//! Static assets and HTML templates for driver initialization.
2//!
3//! This module generates the HTML page that Firefox loads on startup to
4//! establish the WebSocket connection between the browser extension and
5//! the Rust driver.
6//!
7//! # Connection Flow
8//!
9//! 1. Firefox opens data URI as initial page
10//! 2. Page posts `WEBDRIVER_INIT` message to window
11//! 3. Content script receives message, validates localhost URL
12//! 4. Content script forwards to background script
13//! 5. Background script connects to WebSocket server
14
15// ============================================================================
16// Imports
17// ============================================================================
18
19use serde_json::json;
20
21use crate::identifiers::SessionId;
22
23// ============================================================================
24// Public Functions
25// ============================================================================
26
27/// Generates the initialization HTML page as a data URI.
28///
29/// This page is loaded as the first tab when Firefox starts. It contains
30/// JavaScript that posts a message to the content script, triggering the
31/// WebSocket handshake.
32///
33/// # Arguments
34///
35/// * `ws_url` - WebSocket server URL (e.g., "ws://127.0.0.1:12345")
36/// * `session_id` - Session identifier for this window
37///
38/// # Returns
39///
40/// A `data:text/html,...` URI that can be passed to Firefox.
41#[must_use]
42pub fn build_init_data_uri(ws_url: &str, session_id: &SessionId) -> String {
43    let config_json = build_config_json(ws_url, session_id);
44    let html = build_init_html(ws_url, session_id, &config_json);
45
46    format!("data:text/html,{}", urlencoding::encode(&html))
47}
48
49// ============================================================================
50// Internal Functions
51// ============================================================================
52
53/// Builds the JSON configuration object for the extension.
54fn build_config_json(ws_url: &str, session_id: &SessionId) -> String {
55    let config = json!({
56        "type": "WEBDRIVER_INIT",
57        "wsUrl": ws_url,
58        "sessionId": session_id.as_u32(),
59    });
60
61    config.to_string()
62}
63
64/// Builds the initialization HTML page content.
65fn build_init_html(ws_url: &str, session_id: &SessionId, config_json: &str) -> String {
66    let session_id_str = session_id.as_u32().to_string();
67
68    INIT_HTML_TEMPLATE
69        .replace("$WS_URL", ws_url)
70        .replace("$SESSION_ID", &session_id_str)
71        .replace("$CONFIG_JSON", config_json)
72}
73
74// ============================================================================
75// Constants
76// ============================================================================
77
78/// HTML template for the initialization page.
79///
80/// This page displays connection info and posts `WEBDRIVER_INIT` message
81/// for the content script to forward to the background script.
82const INIT_HTML_TEMPLATE: &str = r##"<!DOCTYPE html>
83<html>
84<head>
85    <meta charset="UTF-8">
86    <title>WebDriver Init</title>
87    <style>
88        body {
89            background: #1a1a2e;
90            color: #ccc;
91            font-family: monospace;
92            padding: 40px;
93            line-height: 1.6;
94        }
95        h1 { color: #e94560; margin-bottom: 20px; }
96        .key { color: #4ade80; font-weight: bold; }
97        .val { color: #fff; word-break: break-all; }
98        hr { border: 0; border-top: 1px dashed #333; margin: 20px 0; }
99    </style>
100</head>
101<body>
102    <h1>Firefox WebDriver</h1>
103    <div><span class="key">WS_URL:</span> <span class="val">$WS_URL</span></div>
104    <div><span class="key">SESSION:</span> <span class="val">$SESSION_ID</span></div>
105    <hr>
106    <div style="color: #4ade80;">> Initializing connection...</div>
107    <script>window.postMessage($CONFIG_JSON, '*');</script>
108</body>
109</html>"##;
110
111// ============================================================================
112// Tests
113// ============================================================================
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use serde_json::Value;
119
120    #[test]
121    fn test_build_init_data_uri_format() {
122        let session_id = SessionId::next();
123        let uri = build_init_data_uri("ws://127.0.0.1:12345", &session_id);
124
125        assert!(uri.starts_with("data:text/html,"));
126        assert!(uri.len() > 100); // Should have substantial content
127    }
128
129    #[test]
130    fn test_build_config_json_structure() {
131        let session_id = SessionId::next();
132        let json_str = build_config_json("ws://127.0.0.1:12345", &session_id);
133
134        let parsed: Value = serde_json::from_str(&json_str).expect("valid json");
135        assert_eq!(parsed["type"], "WEBDRIVER_INIT");
136        assert_eq!(parsed["wsUrl"], "ws://127.0.0.1:12345");
137        assert!(parsed["sessionId"].is_number());
138    }
139
140    #[test]
141    fn test_build_init_html_contains_required_elements() {
142        let session_id = SessionId::next();
143        let config_json = build_config_json("ws://127.0.0.1:12345", &session_id);
144        let html = build_init_html("ws://127.0.0.1:12345", &session_id, &config_json);
145
146        assert!(html.contains("<!DOCTYPE html>"));
147        assert!(html.contains("<title>WebDriver Init</title>"));
148        assert!(html.contains("window.postMessage"));
149        assert!(html.contains("WEBDRIVER_INIT"));
150        assert!(html.contains("ws://127.0.0.1:12345"));
151    }
152
153    #[test]
154    fn test_data_uri_is_url_encoded() {
155        let session_id = SessionId::next();
156        let uri = build_init_data_uri("ws://127.0.0.1:12345", &session_id);
157
158        // URL encoding should escape special characters
159        assert!(!uri.contains('<'));
160        assert!(!uri.contains('>'));
161    }
162}