firefox_webdriver/driver/profile/
preferences.rs

1//! Firefox preference serialization for `user.js`.
2//!
3//! Firefox preferences are written as JavaScript function calls:
4//!
5//! ```javascript
6//! user_pref("preference.name", value);
7//! ```
8//!
9//! # Example
10//!
11//! ```
12//! use firefox_webdriver::driver::profile::{FirefoxPreference, PreferenceValue};
13//!
14//! let pref = FirefoxPreference::new("browser.startup.page", PreferenceValue::Int(0))
15//!     .with_comment("Start on blank page");
16//!
17//! assert!(pref.to_user_pref_line().contains("user_pref"));
18//! ```
19
20// ============================================================================
21// PreferenceValue
22// ============================================================================
23
24/// A preference value in `user.js`.
25///
26/// Firefox preferences can be booleans, integers, or strings.
27///
28/// # Examples
29///
30/// ```
31/// use firefox_webdriver::driver::profile::PreferenceValue;
32///
33/// let bool_val = PreferenceValue::Bool(true);
34/// let int_val = PreferenceValue::Int(42);
35/// let str_val = PreferenceValue::String("value".to_string());
36/// ```
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub enum PreferenceValue {
39    /// Boolean value (true/false).
40    Bool(bool),
41
42    /// Integer value.
43    Int(i32),
44
45    /// String value.
46    String(String),
47}
48
49// ============================================================================
50// PreferenceValue - Methods
51// ============================================================================
52
53impl PreferenceValue {
54    /// Formats the value for `user.js`.
55    ///
56    /// - Booleans: `true` or `false`
57    /// - Integers: numeric literal
58    /// - Strings: quoted and escaped
59    #[must_use]
60    pub fn to_js_string(&self) -> String {
61        match self {
62            Self::Bool(b) => b.to_string(),
63            Self::Int(i) => i.to_string(),
64            Self::String(s) => format!("\"{}\"", escape_js_string(s)),
65        }
66    }
67}
68
69// ============================================================================
70// PreferenceValue - Trait Implementations
71// ============================================================================
72
73impl From<bool> for PreferenceValue {
74    #[inline]
75    fn from(value: bool) -> Self {
76        Self::Bool(value)
77    }
78}
79
80impl From<i32> for PreferenceValue {
81    #[inline]
82    fn from(value: i32) -> Self {
83        Self::Int(value)
84    }
85}
86
87impl From<String> for PreferenceValue {
88    #[inline]
89    fn from(value: String) -> Self {
90        Self::String(value)
91    }
92}
93
94impl From<&str> for PreferenceValue {
95    #[inline]
96    fn from(value: &str) -> Self {
97        Self::String(value.to_string())
98    }
99}
100
101// ============================================================================
102// FirefoxPreference
103// ============================================================================
104
105/// A Firefox preference with a name and value.
106///
107/// # Examples
108///
109/// ```
110/// use firefox_webdriver::driver::profile::{FirefoxPreference, PreferenceValue};
111///
112/// // Simple preference
113/// let pref = FirefoxPreference::new("browser.startup.page", PreferenceValue::Int(0));
114///
115/// // With comment
116/// let pref = FirefoxPreference::new("app.update.enabled", false)
117///     .with_comment("Disable auto-updates");
118/// ```
119#[derive(Debug, Clone)]
120pub struct FirefoxPreference {
121    /// Preference name (e.g., "browser.startup.page").
122    pub key: String,
123
124    /// Preference value.
125    pub value: PreferenceValue,
126
127    /// Optional comment explaining the preference.
128    pub comment: Option<String>,
129}
130
131// ============================================================================
132// FirefoxPreference - Implementation
133// ============================================================================
134
135impl FirefoxPreference {
136    /// Creates a new preference.
137    ///
138    /// # Arguments
139    ///
140    /// * `key` - Preference name (e.g., "browser.startup.page")
141    /// * `value` - Preference value
142    #[must_use]
143    pub fn new(key: impl Into<String>, value: impl Into<PreferenceValue>) -> Self {
144        Self {
145            key: key.into(),
146            value: value.into(),
147            comment: None,
148        }
149    }
150
151    /// Adds a comment to the preference.
152    ///
153    /// Comments appear as `// comment` above the preference line.
154    ///
155    /// # Arguments
156    ///
157    /// * `comment` - Comment text
158    #[must_use]
159    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
160        self.comment = Some(comment.into());
161        self
162    }
163
164    /// Generates the `user_pref("key", value);` line.
165    ///
166    /// If a comment is set, it appears on the line above.
167    ///
168    /// # Example Output
169    ///
170    /// ```text
171    /// // Disable auto-updates
172    /// user_pref("app.update.enabled", false);
173    /// ```
174    #[must_use]
175    pub fn to_user_pref_line(&self) -> String {
176        let mut output = String::new();
177
178        if let Some(comment) = &self.comment {
179            output.push_str("// ");
180            output.push_str(comment);
181            output.push('\n');
182        }
183
184        output.push_str(&format!(
185            "user_pref(\"{}\", {});",
186            self.key,
187            self.value.to_js_string()
188        ));
189
190        output
191    }
192}
193
194// ============================================================================
195// Private Helpers
196// ============================================================================
197
198/// Escapes special characters for JavaScript strings.
199fn escape_js_string(s: &str) -> String {
200    s.replace('\\', "\\\\")
201        .replace('"', "\\\"")
202        .replace('\n', "\\n")
203        .replace('\r', "\\r")
204        .replace('\t', "\\t")
205}
206
207// ============================================================================
208// Tests
209// ============================================================================
210
211#[cfg(test)]
212mod tests {
213    use super::{FirefoxPreference, PreferenceValue, escape_js_string};
214
215    // ------------------------------------------------------------------------
216    // PreferenceValue Tests
217    // ------------------------------------------------------------------------
218
219    #[test]
220    fn test_bool_to_js_string() {
221        assert_eq!(PreferenceValue::Bool(true).to_js_string(), "true");
222        assert_eq!(PreferenceValue::Bool(false).to_js_string(), "false");
223    }
224
225    #[test]
226    fn test_int_to_js_string() {
227        assert_eq!(PreferenceValue::Int(42).to_js_string(), "42");
228        assert_eq!(PreferenceValue::Int(-10).to_js_string(), "-10");
229        assert_eq!(PreferenceValue::Int(0).to_js_string(), "0");
230    }
231
232    #[test]
233    fn test_string_to_js_string() {
234        assert_eq!(
235            PreferenceValue::String("test".to_string()).to_js_string(),
236            "\"test\""
237        );
238        assert_eq!(
239            PreferenceValue::String(String::new()).to_js_string(),
240            "\"\""
241        );
242    }
243
244    #[test]
245    fn test_from_bool() {
246        let val: PreferenceValue = true.into();
247        assert_eq!(val, PreferenceValue::Bool(true));
248    }
249
250    #[test]
251    fn test_from_i32() {
252        let val: PreferenceValue = 42.into();
253        assert_eq!(val, PreferenceValue::Int(42));
254    }
255
256    #[test]
257    fn test_from_string() {
258        let val: PreferenceValue = String::from("test").into();
259        assert_eq!(val, PreferenceValue::String("test".to_string()));
260    }
261
262    #[test]
263    fn test_from_str() {
264        let val: PreferenceValue = "test".into();
265        assert_eq!(val, PreferenceValue::String("test".to_string()));
266    }
267
268    // ------------------------------------------------------------------------
269    // escape_js_string Tests
270    // ------------------------------------------------------------------------
271
272    #[test]
273    fn test_escape_backslash() {
274        assert_eq!(escape_js_string("path\\to\\file"), "path\\\\to\\\\file");
275    }
276
277    #[test]
278    fn test_escape_quotes() {
279        assert_eq!(escape_js_string("with\"quotes"), "with\\\"quotes");
280    }
281
282    #[test]
283    fn test_escape_newline() {
284        assert_eq!(escape_js_string("line1\nline2"), "line1\\nline2");
285    }
286
287    #[test]
288    fn test_escape_carriage_return() {
289        assert_eq!(escape_js_string("line1\rline2"), "line1\\rline2");
290    }
291
292    #[test]
293    fn test_escape_tab() {
294        assert_eq!(escape_js_string("col1\tcol2"), "col1\\tcol2");
295    }
296
297    #[test]
298    fn test_escape_combined() {
299        assert_eq!(
300            escape_js_string("path\\to\n\"file\""),
301            "path\\\\to\\n\\\"file\\\""
302        );
303    }
304
305    // ------------------------------------------------------------------------
306    // FirefoxPreference Tests
307    // ------------------------------------------------------------------------
308
309    #[test]
310    fn test_new_preference() {
311        let pref = FirefoxPreference::new("test.pref", PreferenceValue::Bool(true));
312        assert_eq!(pref.key, "test.pref");
313        assert_eq!(pref.value, PreferenceValue::Bool(true));
314        assert!(pref.comment.is_none());
315    }
316
317    #[test]
318    fn test_with_comment() {
319        let pref = FirefoxPreference::new("test.pref", PreferenceValue::Int(42))
320            .with_comment("Test comment");
321
322        assert_eq!(pref.comment, Some("Test comment".to_string()));
323    }
324
325    #[test]
326    fn test_to_user_pref_line_bool() {
327        let pref = FirefoxPreference::new("test.pref", PreferenceValue::Bool(true));
328        assert_eq!(pref.to_user_pref_line(), "user_pref(\"test.pref\", true);");
329    }
330
331    #[test]
332    fn test_to_user_pref_line_int() {
333        let pref = FirefoxPreference::new("test.pref", PreferenceValue::Int(42));
334        assert_eq!(pref.to_user_pref_line(), "user_pref(\"test.pref\", 42);");
335    }
336
337    #[test]
338    fn test_to_user_pref_line_string() {
339        let pref = FirefoxPreference::new("test.pref", PreferenceValue::String("value".into()));
340        assert_eq!(
341            pref.to_user_pref_line(),
342            "user_pref(\"test.pref\", \"value\");"
343        );
344    }
345
346    #[test]
347    fn test_to_user_pref_line_with_comment() {
348        let pref = FirefoxPreference::new("test.pref", PreferenceValue::Int(42))
349            .with_comment("Test preference");
350
351        let line = pref.to_user_pref_line();
352        assert!(line.starts_with("// Test preference\n"));
353        assert!(line.ends_with("user_pref(\"test.pref\", 42);"));
354    }
355
356    #[test]
357    fn test_new_with_into_value() {
358        // Test that Into<PreferenceValue> works
359        let pref = FirefoxPreference::new("test.bool", false);
360        assert_eq!(pref.value, PreferenceValue::Bool(false));
361
362        let pref = FirefoxPreference::new("test.int", 123);
363        assert_eq!(pref.value, PreferenceValue::Int(123));
364    }
365
366    #[test]
367    fn test_preference_clone() {
368        let pref = FirefoxPreference::new("test.pref", true).with_comment("comment");
369        let cloned = pref.clone();
370
371        assert_eq!(pref.key, cloned.key);
372        assert_eq!(pref.value, cloned.value);
373        assert_eq!(pref.comment, cloned.comment);
374    }
375}