This commit is contained in:
Ben Kunkle 2025-08-06 14:03:33 -05:00
parent 33f198fef1
commit 9f429987e7

View file

@ -74,6 +74,295 @@ mod tests {
use crate::{FocusHandle, FocusMap, TabHandles};
use std::sync::Arc;
/// Helper function to parse XML-like structure and test tab navigation
///
/// The XML structure should define elements with tab-index and actual (expected order) values.
/// Elements like tab-group and focus-trap are parsed as regular elements with their own tab-index.
/// All elements are treated as flat - there is no nesting concept in TabHandles.
///
/// Example:
/// ```
/// <tab-index=0 actual=0>
/// <tab-index=1 actual=1>
/// <tab-group tab-index=2 actual=2>
/// <tab-index=0 actual=3>
/// <focus-trap tab-index=3 actual=4>
/// <tab-index=0 actual=5>
/// ```
fn check(xml: &str) {
let focus_map = Arc::new(FocusMap::default());
let mut tab_handles = TabHandles::default();
// Parse the XML-like structure
let elements = parse_xml_structure(xml);
// Create focus handles based on parsed elements
let mut all_handles = Vec::new();
let mut actual_to_handle = std::collections::HashMap::<usize, FocusHandle>::new();
for element in elements {
let mut handle = FocusHandle::new(&focus_map);
// Set tab_index if specified
if let Some(tab_index) = element.tab_index {
handle = handle.tab_index(tab_index);
}
// Enable tab_stop by default unless it's explicitly disabled
handle = handle.tab_stop(element.tab_stop.unwrap_or(true));
// Store the handle
all_handles.push(handle.clone());
tab_handles.insert(&handle);
// Track handles by their actual position
if let Some(actual) = element.actual {
if actual_to_handle.insert(actual, handle).is_some() {
panic!("Duplicate actual value: {}", actual);
}
}
}
// Get the actual tab order from TabHandles
let mut tab_order: Vec<FocusHandle> = Vec::new();
let mut current = None;
// Build the actual navigation order
for _ in 0..tab_handles.handles.len() {
if let Some(next_handle) =
tab_handles.next(current.as_ref().map(|h: &FocusHandle| &h.id))
{
// Check if we've cycled back to the beginning
if !tab_order.is_empty() && tab_order[0].id == next_handle.id {
break;
}
current = Some(next_handle.clone());
tab_order.push(next_handle);
} else {
break;
}
}
// Check that we have the expected number of tab stops
assert_eq!(
tab_order.len(),
actual_to_handle.len(),
"Number of tab stops ({}) doesn't match expected ({})",
tab_order.len(),
actual_to_handle.len()
);
// Check each position matches the expected handle
for (position, handle) in tab_order.iter().enumerate() {
let expected_handle = actual_to_handle.get(&position).unwrap_or_else(|| {
panic!(
"No element specified with actual={}, but tab order has {} elements",
position,
tab_order.len()
)
});
assert_eq!(
handle.id, expected_handle.id,
"Tab order at position {} doesn't match expected. Got {:?}, expected {:?}",
position, handle.id, expected_handle.id
);
}
// Test that navigation wraps correctly
if !tab_order.is_empty() {
// Test next wraps from last to first
let last_id = tab_order.last().unwrap().id;
let first_id = tab_order.first().unwrap().id;
assert_eq!(
tab_handles.next(Some(&last_id)).map(|h| h.id),
Some(first_id),
"next should wrap from last to first"
);
// Test prev wraps from first to last
assert_eq!(
tab_handles.prev(Some(&first_id)).map(|h| h.id),
Some(last_id),
"prev should wrap from first to last"
);
}
#[derive(Debug)]
struct ParsedElement {
element_type: String,
tab_index: Option<isize>,
actual: Option<usize>,
tab_stop: Option<bool>,
}
fn parse_xml_structure(xml: &str) -> Vec<ParsedElement> {
let mut elements = Vec::new();
for line in xml.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
// Parse opening tags like <tab-index=0 actual=1> or <tab-group tab-index=2 actual=3>
if line.starts_with('<') && !line.starts_with("</") {
let mut element = ParsedElement {
element_type: String::new(),
tab_index: None,
actual: None,
tab_stop: None,
};
// Remove < and > brackets
let content = line.trim_start_matches('<').trim_end_matches('>');
let parts: Vec<&str> = content.split_whitespace().collect();
if !parts.is_empty() {
// First part might be element type or tab-index
let first_part = parts[0];
if first_part.starts_with("tab-index=") {
element.element_type = "element".to_string();
} else if let Some(idx) = first_part.find(' ') {
element.element_type = first_part[..idx].to_string();
} else if !first_part.contains('=') {
element.element_type = first_part.to_string();
} else {
element.element_type = "element".to_string();
}
}
// Parse attributes
for part in parts {
if let Some(eq_pos) = part.find('=') {
let key = &part[..eq_pos];
let value = &part[eq_pos + 1..];
match key {
"tab-index" => {
element.tab_index = value.parse::<isize>().ok();
}
"actual" => {
element.actual = value.parse::<usize>().ok();
}
"tab-stop" => {
element.tab_stop = value.parse::<bool>().ok();
}
_ => {}
}
}
}
// Special handling for focus-trap and tab-group
if element.element_type == "focus-trap" {
// Focus traps might have special behavior
// For now, treat them as regular elements
}
elements.push(element);
}
}
elements
}
}
#[test]
fn test_check_helper() {
// Test simple ordering
let xml = r#"
<tab-index=0 actual=0>
<tab-index=1 actual=1>
<tab-index=2 actual=2>
"#;
check(xml);
// Test with duplicate tab indices (should maintain insertion order within same index)
let xml2 = r#"
<tab-index=0 actual=0>
<tab-index=0 actual=1>
<tab-index=1 actual=2>
<tab-index=1 actual=3>
<tab-index=2 actual=4>
"#;
check(xml2);
// Test with negative and positive indices
let xml3 = r#"
<tab-index=1 actual=2>
<tab-index=-1 actual=0>
<tab-index=0 actual=1>
<tab-index=2 actual=3>
"#;
check(xml3);
}
#[test]
fn test_with_nested_structures() {
// Note: tab-group and focus-trap are parsed as regular elements
// since TabHandles treats all elements as flat (no nesting concept)
// Elements with same tab_index are kept in insertion order
// Test with elements that look like tab groups (but are just regular elements)
// Order: tab_index=0 (first two), tab_index=1 (next two), tab_index=2, tab_index=3
let xml = r#"
<tab-index=0 actual=0>
<tab-index=1 actual=2>
<tab-group tab-index=2>
<tab-index=0 actual=1>
<tab-index=1 actual=3>
</tab-group>
<tab-index=3 actual=5>
"#;
check(xml);
// Test with elements that look like focus traps (but are just regular elements)
// Order: tab_index=0 (first two), tab_index=1 (next two), tab_index=2
let xml2 = r#"
<tab-index=0 actual=0>
<focus-trap tab-index=1 actual=2>
<tab-index=0 actual=1>
<tab-index=1 actual=3>
<tab-index=2 actual=4>
"#;
check(xml2);
// Test mixed element types (all treated as flat)
// Order: tab_index=0 (all three), tab_index=1 (next two), tab_index=2 (last two)
let xml3 = r#"
<tab-index=0 actual=0>
<tab-group tab-index=1 actual=3>
<tab-index=0 actual=1>
<focus-trap tab-index=1 actual=4>
<tab-index=0 actual=2>
<tab-index=2 actual=5>
<tab-index=2 actual=6>
"#;
check(xml3);
}
#[test]
fn test_with_disabled_tab_stops() {
// Test with mixed tab-stop values
let xml = r#"
<tab-index=0 actual=0>
<tab-index=1 tab-stop=false>
<tab-index=2 actual=1>
<tab-index=3 actual=2>
"#;
check(xml);
// Test with all disabled except specific ones
let xml2 = r#"
<tab-index=0 tab-stop=false>
<tab-index=1 actual=0>
<tab-index=2 tab-stop=false>
<tab-index=3 actual=1>
<tab-index=4 tab-stop=false>
"#;
check(xml2);
}
#[test]
fn test_tab_handles() {
let focus_map = Arc::new(FocusMap::default());