Skip to main content

thirtyfour_testing_library_ext/options/
role.rs

1use crate::options::common::{TestingLibraryOptions, TextMatch};
2use serde::{Serialize, Serializer};
3
4#[cfg(test)]
5use crate::options::common::process_raw_javascript_markers;
6
7#[cfg(test)]
8use serde_json::Value;
9
10/// Options for value-based queries on range widgets
11#[derive(Debug, Clone, Default, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct ValueOptions {
14    /// Minimum value (aria-valuemin)
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub min: Option<i32>,
17    /// Maximum value (aria-valuemax)
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub max: Option<i32>,
20    /// Current value (aria-valuenow)
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub now: Option<i32>,
23    /// Text representation of value (aria-valuetext)
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub text: Option<TextMatch>,
26}
27
28/// Comprehensive options for role-based queries
29#[derive(Debug, Clone, Default, Serialize)]
30#[serde(rename_all = "camelCase")]
31pub struct ByRoleOptions {
32    /// Include elements normally excluded from accessibility tree
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub hidden: Option<bool>,
35    /// Filter by accessible name
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub name: Option<TextMatch>,
38    /// Filter by accessible description
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub description: Option<TextMatch>,
41    /// Filter by selected state (aria-selected)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub selected: Option<bool>,
44    /// Filter by busy state (aria-busy)
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub busy: Option<bool>,
47    /// Filter by checked state (aria-checked)
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub checked: Option<bool>,
50    /// Filter by pressed state (aria-pressed)
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub pressed: Option<bool>,
53    /// Enable/disable query suggestions
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub suggest: Option<bool>,
56    /// Filter by current state (aria-current)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub current: Option<CurrentState>,
59    /// Filter by expanded state (aria-expanded)
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub expanded: Option<bool>,
62    /// Enable querying fallback roles
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub query_fallbacks: Option<bool>,
65    /// Filter by heading level (only for heading role)
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub level: Option<u8>,
68    /// Filter by value properties (only for range widgets)
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub value: Option<ValueOptions>,
71}
72
73/// Represents the current state for aria-current attribute
74#[derive(Debug, Clone)]
75pub enum CurrentState {
76    /// aria-current="false" or no aria-current attribute
77    False,
78    /// aria-current="true"
79    True,
80    /// aria-current="page"
81    Page,
82    /// aria-current="step"
83    Step,
84    /// aria-current="location"
85    Location,
86    /// aria-current="date"
87    Date,
88    /// aria-current="time"
89    Time,
90    /// Custom aria-current value
91    Custom(String),
92}
93
94impl Serialize for CurrentState {
95    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96    where
97        S: Serializer,
98    {
99        match self {
100            CurrentState::False => false.serialize(serializer),
101            CurrentState::True => true.serialize(serializer),
102            CurrentState::Page => "page".serialize(serializer),
103            CurrentState::Step => "step".serialize(serializer),
104            CurrentState::Location => "location".serialize(serializer),
105            CurrentState::Date => "date".serialize(serializer),
106            CurrentState::Time => "time".serialize(serializer),
107            CurrentState::Custom(s) => s.serialize(serializer),
108        }
109    }
110}
111
112impl ByRoleOptions {
113    /// Create a new empty ByRoleOptions
114    pub fn new() -> Self {
115        Self::default()
116    }
117
118    /// Set the hidden option
119    pub fn hidden(mut self, hidden: bool) -> Self {
120        self.hidden = Some(hidden);
121        self
122    }
123
124    /// Set the name option
125    /// Accepts strings and automatically detects regex patterns (strings starting and ending with '/')
126    pub fn name(mut self, name: impl Into<String>) -> Self {
127        let name_str = name.into();
128        self.name = Some(TextMatch::from(name_str));
129        self
130    }
131
132    /// Set the description option
133    /// Accepts strings and automatically detects regex patterns (strings starting and ending with '/')
134    pub fn description(mut self, description: impl Into<String>) -> Self {
135        let desc_str = description.into();
136        self.description = Some(TextMatch::from(desc_str));
137        self
138    }
139
140    /// Set the selected option
141    pub fn selected(mut self, selected: bool) -> Self {
142        self.selected = Some(selected);
143        self
144    }
145
146    /// Set the busy option
147    pub fn busy(mut self, busy: bool) -> Self {
148        self.busy = Some(busy);
149        self
150    }
151
152    /// Set the checked option
153    pub fn checked(mut self, checked: bool) -> Self {
154        self.checked = Some(checked);
155        self
156    }
157
158    /// Set the pressed option
159    pub fn pressed(mut self, pressed: bool) -> Self {
160        self.pressed = Some(pressed);
161        self
162    }
163
164    /// Set the suggest option
165    pub fn suggest(mut self, suggest: bool) -> Self {
166        self.suggest = Some(suggest);
167        self
168    }
169
170    /// Set the current option
171    pub fn current(mut self, current: CurrentState) -> Self {
172        self.current = Some(current);
173        self
174    }
175
176    /// Set the expanded option
177    pub fn expanded(mut self, expanded: bool) -> Self {
178        self.expanded = Some(expanded);
179        self
180    }
181
182    /// Set the query_fallbacks option
183    pub fn query_fallbacks(mut self, query_fallbacks: bool) -> Self {
184        self.query_fallbacks = Some(query_fallbacks);
185        self
186    }
187
188    /// Set the level option (only for heading role)
189    pub fn level(mut self, level: u8) -> Self {
190        self.level = Some(level);
191        self
192    }
193
194    /// Set the value option (only for range widgets)
195    pub fn value(mut self, value: ValueOptions) -> Self {
196        self.value = Some(value);
197        self
198    }
199}
200
201impl TestingLibraryOptions for ByRoleOptions {}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_empty_options_serialization() {
209        let options = ByRoleOptions::new();
210        let json = options.to_json_string().unwrap();
211        assert_eq!(json, "{}");
212    }
213
214    #[test]
215    fn test_basic_options_serialization() {
216        let options = ByRoleOptions::new().hidden(true).selected(false);
217
218        let json_value = options.to_json_value().unwrap();
219        assert_eq!(json_value["hidden"], true);
220        assert_eq!(json_value["selected"], false);
221        assert!(json_value["name"].is_null());
222    }
223
224    #[test]
225    fn test_text_match_exact_serialization() {
226        let options = ByRoleOptions::new().name("Submit");
227
228        let json_value = options.to_json_value().unwrap();
229        assert_eq!(json_value["name"], "Submit");
230    }
231
232    #[test]
233    fn test_text_match_regex_serialization() {
234        let options = ByRoleOptions::new().name("/^submit.*/");
235
236        // Test the string serialization (which processes markers)
237        let json_string = options.to_json_string().unwrap();
238        assert!(json_string.contains("/^submit.*/"));
239
240        // Note: to_json_value returns the raw marker, which is expected
241        // since it's used internally before marker processing
242        let json_value = options.to_json_value().unwrap();
243        assert_eq!(json_value["name"], "__RAW_JS__/^submit.*/");
244    }
245
246    #[test]
247    fn test_current_state_serialization() {
248        let options_false = ByRoleOptions::new().current(CurrentState::False);
249        let json_false = options_false.to_json_value().unwrap();
250        assert_eq!(json_false["current"], false);
251
252        let options_page = ByRoleOptions::new().current(CurrentState::Page);
253        let json_page = options_page.to_json_value().unwrap();
254        assert_eq!(json_page["current"], "page");
255
256        let options_custom =
257            ByRoleOptions::new().current(CurrentState::Custom("custom-value".to_string()));
258        let json_custom = options_custom.to_json_value().unwrap();
259        assert_eq!(json_custom["current"], "custom-value");
260    }
261
262    #[test]
263    fn test_value_options_serialization() {
264        let value_opts = ValueOptions {
265            min: Some(0),
266            max: Some(100),
267            now: Some(50),
268            text: Some(TextMatch::String("medium".to_string())),
269        };
270
271        let options = ByRoleOptions::new().value(value_opts);
272
273        let json_value = options.to_json_value().unwrap();
274        assert_eq!(json_value["value"]["min"], 0);
275        assert_eq!(json_value["value"]["max"], 100);
276        assert_eq!(json_value["value"]["now"], 50);
277        assert_eq!(json_value["value"]["text"], "medium");
278    }
279
280    #[test]
281    fn test_query_fallbacks_rename() {
282        let options = ByRoleOptions::new().query_fallbacks(true);
283
284        let json_value = options.to_json_value().unwrap();
285        assert_eq!(json_value["queryFallbacks"], true);
286        assert!(json_value["query_fallbacks"].is_null());
287    }
288
289    #[test]
290    fn test_complex_options_serialization() {
291        let options = ByRoleOptions::new()
292            .name("button")
293            .hidden(false)
294            .pressed(true)
295            .level(2)
296            .current(CurrentState::Page);
297
298        let json_string = options.to_json_string().unwrap();
299
300        // Parse back to verify structure
301        let parsed: Value = serde_json::from_str(&json_string).unwrap();
302        assert_eq!(parsed["name"], "button");
303        assert_eq!(parsed["hidden"], false);
304        assert_eq!(parsed["pressed"], true);
305        assert_eq!(parsed["level"], 2);
306        assert_eq!(parsed["current"], "page");
307    }
308
309    #[test]
310    fn test_serialization_example() {
311        // Example usage: Creating complex options for a button query
312        let options = ByRoleOptions::new()
313            .name("/submit|send/")
314            .pressed(false)
315            .hidden(false)
316            .suggest(true);
317
318        let json_string = options.to_json_string().unwrap();
319        println!("Serialized options: {json_string}");
320
321        // This would be used in JavaScript like:
322        // getByRole('button', {name: /submit|send/, pressed: false, hidden: false, suggest: true})
323
324        // Note: The processed JSON is not valid JSON because regex is unquoted
325        // This is intentional for JavaScript consumption
326        assert!(json_string.contains("/submit|send/"));
327        assert!(json_string.contains("\"pressed\":false"));
328        assert!(json_string.contains("\"hidden\":false"));
329        assert!(json_string.contains("\"suggest\":true"));
330    }
331
332    #[test]
333    fn test_regex_validation() {
334        // Valid regex patterns
335        let valid_regex = TextMatch::Regex("/test.*/".to_string());
336        assert!(valid_regex.validate_regex().is_ok());
337
338        let valid_regex_with_flags = TextMatch::Regex("/test.*/i".to_string());
339        assert!(valid_regex_with_flags.validate_regex().is_ok());
340
341        // Invalid regex patterns
342        let invalid_no_slashes = TextMatch::Regex("test.*".to_string());
343        assert!(invalid_no_slashes.validate_regex().is_err());
344
345        let invalid_pattern = TextMatch::Regex("/[/".to_string());
346        assert!(invalid_pattern.validate_regex().is_err());
347
348        // String variants should always be valid
349        let string_match = TextMatch::String("test".to_string());
350        assert!(string_match.validate_regex().is_ok());
351    }
352
353    #[test]
354    fn test_raw_javascript_marker_processing() {
355        // Test the marker processing function
356        let json_with_markers = r#"{"name":"__RAW_JS__/Save.*/","pressed":false}"#;
357        let processed = process_raw_javascript_markers(json_with_markers);
358        assert_eq!(processed, r#"{"name":/Save.*/,"pressed":false}"#);
359
360        // Test with flags
361        let json_with_flags = r#"{"name":"__RAW_JS__/save/i","hidden":true}"#;
362        let processed_flags = process_raw_javascript_markers(json_with_flags);
363        assert_eq!(processed_flags, r#"{"name":/save/i,"hidden":true}"#);
364
365        // Test with multiple markers
366        let json_multiple = r#"{"name":"__RAW_JS__/button/","description":"__RAW_JS__/click.*/i"}"#;
367        let processed_multiple = process_raw_javascript_markers(json_multiple);
368        assert_eq!(
369            processed_multiple,
370            r#"{"name":/button/,"description":/click.*/i}"#
371        );
372
373        // Test with no markers (should remain unchanged)
374        let json_no_markers = r#"{"name":"button","pressed":true}"#;
375        let processed_no_markers = process_raw_javascript_markers(json_no_markers);
376        assert_eq!(processed_no_markers, r#"{"name":"button","pressed":true}"#);
377    }
378}