basic impl + more comprehensive group tests

This commit is contained in:
Ben Kunkle 2025-08-13 10:12:21 -05:00
parent 0c161d6890
commit e594541e5e
3 changed files with 292 additions and 187 deletions

1
Cargo.lock generated
View file

@ -7432,6 +7432,7 @@ dependencies = [
"parking_lot",
"pathfinder_geometry",
"postage",
"pretty_assertions",
"profiling",
"rand 0.8.5",
"raw-window-handle",

View file

@ -229,9 +229,10 @@ collections = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
http_client = { workspace = true, features = ["test-support"] }
lyon = { version = "1.0", features = ["extra"] }
pretty_assertions.workspace = true
rand.workspace = true
unicode-segmentation.workspace = true
reqwest_client = { workspace = true, features = ["test-support"] }
unicode-segmentation.workspace = true
util = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "windows")'.build-dependencies]

View file

@ -6,6 +6,13 @@ use crate::{FocusHandle, FocusId};
#[derive(Default)]
pub(crate) struct TabHandles {
pub(crate) handles: Vec<FocusHandle>,
groups: Vec<GroupDef>,
group_depth: usize,
}
struct GroupDef {
index: isize,
offset: usize,
}
impl TabHandles {
@ -14,7 +21,10 @@ impl TabHandles {
return;
}
let focus_handle = focus_handle.clone();
let mut focus_handle = focus_handle.clone();
for group in self.groups.iter().rev().take(self.group_depth) {
focus_handle.tab_index += group.index;
}
// Insert handle with same tab_index last
if let Some(ix) = self
@ -67,6 +77,18 @@ impl TabHandles {
None
}
}
fn begin_group(&mut self, tab_index: isize) {
self.groups.push(GroupDef {
index: tab_index,
offset: 0,
});
self.group_depth += 1;
}
fn end_group(&mut self) {
self.group_depth -= 1;
}
}
#[cfg(test)]
@ -107,7 +129,7 @@ mod tests {
#[derive(Debug, Clone)]
enum NodeType {
TabStop {
tab_index: Option<isize>,
tab_index: isize,
actual: usize, // Required for tab stops
},
NonTabStop {
@ -115,7 +137,7 @@ mod tests {
// No actual field - these aren't in the tab order
},
Group {
tab_index: Option<isize>,
tab_index: isize,
children: Vec<TreeNode>,
},
FocusTrap {
@ -129,7 +151,7 @@ mod tests {
xml_tag: "root".to_string(),
handle: None,
node_type: NodeType::Group {
tab_index: None,
tab_index: isize::MIN,
children: Vec::new(),
},
};
@ -220,6 +242,8 @@ mod tests {
if actual.is_some() {
panic!("tab-group elements should not have 'actual' attribute");
}
let tab_index = tab_index
.expect("tab-group elements should have 'tab-index' attribute");
NodeType::Group {
tab_index,
children: Vec::new(),
@ -236,18 +260,16 @@ mod tests {
_ => {
// Determine if it's a tab stop based on tab-stop attribute
let is_tab_stop = tab_stop.unwrap_or(true);
if is_tab_stop {
// Tab stops must have an actual value
match actual {
Some(actual_value) => NodeType::TabStop {
tab_index,
actual: actual_value,
},
None => panic!(
"Tab stop with tab-index={} must have an 'actual' attribute",
tab_index.map_or("None".to_string(), |v| v.to_string())
),
}
let tab_index =
tab_index.expect("Tab stop must have a 'tab-index' attribute");
let actual = actual.expect(&format!(
"Tab stop with tab-index={} must have an 'actual' attribute",
tab_index
));
NodeType::TabStop { tab_index, actual }
} else {
// Non-tab stops should not have an actual value
if actual.is_some() {
@ -307,8 +329,8 @@ mod tests {
NodeType::TabStop { tab_index, actual } => {
let mut handle = FocusHandle::new(focus_map);
if let Some(idx) = tab_index {
handle = handle.tab_index(*idx);
if *tab_index != isize::MIN {
handle = handle.tab_index(*tab_index);
}
handle = handle.tab_stop(true);
@ -332,11 +354,16 @@ mod tests {
node.handle = Some(handle);
}
NodeType::Group { children, .. } => {
NodeType::Group {
children,
tab_index,
} => {
// For now, just process children without special group handling
tab_handles.begin_group(*tab_index);
for child in children {
construct_recursive(child, focus_map, tab_handles, actual_to_handle);
}
tab_handles.end_group();
}
NodeType::FocusTrap { children, .. } => {
// TODO: Implement focus trap behavior
@ -454,30 +481,25 @@ mod tests {
handle_contexts.sort_by_key(|c| c.actual);
// Helper function to format tree structure as XML for error messages
fn format_tree_structure(
node: &TreeNode,
label: &str,
actual_map: &HashMap<FocusId, usize>,
) -> String {
let mut result = format!("{}:\n", label);
fn format_tree_structure(node: &TreeNode, tab_handles: &TabHandles) -> String {
let mut result = String::new();
fn format_node(
node: &TreeNode,
actual_map: &HashMap<FocusId, usize>,
indent: usize,
) -> String {
fn format_node(node: &TreeNode, tab_handles: &TabHandles, indent: usize) -> String {
let mut result = String::new();
let indent_str = " ".repeat(indent);
match &node.node_type {
NodeType::TabStop { tab_index, actual } => {
let actual = node
.handle
.as_ref()
.and_then(|handle| tab_handles.current_index(Some(&handle.id)))
.unwrap_or(*actual);
let actual_str = format!(" actual={}", actual);
result.push_str(&format!(
"{}<tab-index={}{}>\n",
indent_str,
tab_index.map_or("None".to_string(), |v| v.to_string()),
actual_str
indent_str, tab_index, actual_str
));
}
NodeType::NonTabStop { tab_index } => {
@ -493,18 +515,17 @@ mod tests {
} => {
result.push_str(&format!(
"{}<tab-group tab-index={}>\n",
indent_str,
tab_index.map_or("None".to_string(), |v| v.to_string())
indent_str, tab_index
));
for child in children {
result.push_str(&format_node(child, actual_map, indent + 1));
result.push_str(&format_node(child, tab_handles, indent + 1));
}
result.push_str(&format!("{}</tab-group>\n", indent_str));
}
NodeType::FocusTrap { children } => {
result.push_str(&format!("{}<focus-trap>\n", indent_str));
for child in children {
result.push_str(&format_node(child, actual_map, indent + 1));
result.push_str(&format_node(child, tab_handles, indent + 1));
}
result.push_str(&format!("{}</focus-trap>\n", indent_str));
}
@ -516,7 +537,7 @@ mod tests {
// Skip the root node and format its children
if let NodeType::Group { children, .. } = &node.node_type {
for child in children {
result.push_str(&format_node(child, actual_map, 0));
result.push_str(&format_node(child, tab_handles, 0));
}
}
@ -530,12 +551,17 @@ mod tests {
current_id: FocusId,
actual_id: Option<FocusId>,
expected_id: FocusId,
tab_handles: &TabHandles,
) {
if actual_id != Some(expected_id) {
panic!(
"Tab navigation error!\n\n{}\n\n{}",
"Tab navigation error!\n\n{}\n\n{}\n\n{}",
error_label,
format_tree_with_navigation(tree, current_id, actual_id, expected_id)
format_tree_with_navigation(tree, current_id, actual_id, expected_id),
pretty_assertions::StrComparison::new(
&format_tree_structure(tree, tab_handles),
&format_tree_structure(tree, &TabHandles::default()),
),
);
}
}
@ -584,10 +610,7 @@ mod tests {
result.push_str(&format!(
"{}<tab-index={}{}>{}\n",
indent_str,
tab_index.map_or("None".to_string(), |v| v.to_string()),
actual_str,
nav_comment
indent_str, tab_index, actual_str, nav_comment
));
}
NodeType::NonTabStop { tab_index } => {
@ -607,8 +630,7 @@ mod tests {
} => {
result.push_str(&format!(
"{}<tab-group tab-index={}>\n",
indent_str,
tab_index.map_or("None".to_string(), |v| v.to_string())
indent_str, tab_index
));
for child in children {
result.push_str(&format_node_with_nav(
@ -682,11 +704,13 @@ mod tests {
}
panic!(
"Number of tab stops doesn't match! Expected {} but found {}\n\n{}\n{}",
"Number of tab stops doesn't match! Expected {} but found {}\n\n{}",
actual_to_handle.len(),
handle_contexts.len(),
format_tree_structure(tree, "Got", &actual_map),
format_tree_structure(tree, "Expected", &expected_map)
pretty_assertions::StrComparison::new(
&format_tree_structure(tree, tab_handles),
&format_tree_structure(tree, &TabHandles::default()),
),
);
}
@ -742,10 +766,11 @@ mod tests {
check_navigation(
tree,
&format!("Tab order mismatch at position {}:\n\n", position),
&format!("Tab order mismatch at position {}", position),
started_id,
went_to_id,
expected_handle.id,
tab_handles,
);
}
}
@ -764,6 +789,7 @@ mod tests {
last_id,
actual_next_id,
first_id,
tab_handles,
);
// Test prev wraps from first to last
@ -775,6 +801,7 @@ mod tests {
first_id,
actual_prev_id,
last_id,
tab_handles,
);
}
@ -836,6 +863,7 @@ mod tests {
current_id,
actual_next_id,
expected_next,
tab_handles,
);
// Test prev navigation
@ -847,6 +875,7 @@ mod tests {
current_id,
actual_prev_id,
expected_prev,
tab_handles,
);
}
}
@ -865,168 +894,242 @@ mod tests {
eval(&tree, &tab_handles, &actual_to_handle);
}
#[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);
macro_rules! xml_test {
($test_name:ident, $xml:expr) => {
#[test]
fn $test_name() {
let xml = $xml;
check(xml);
}
};
}
#[test]
fn test_check_helper_with_nested_structures() {
// Test parsing and structure with nested groups and focus traps
let xml = r#"
<tab-index=0 actual=0>
<tab-group tab-index=1>
<tab-index=0 actual=1>
<focus-trap>
<tab-index=0 actual=2>
<tab-index=1 actual=3>
</focus-trap>
<tab-index=1 actual=4>
</tab-group>
<tab-index=2 actual=5>
"#;
mod test_helper {
use super::*;
// This should parse successfully even though navigation won't work correctly yet
// The test verifies that our tree structure correctly represents nested elements
check(xml);
}
xml_test!(
test_simple_ordering,
r#"
<tab-index=0 actual=0>
<tab-index=1 actual=1>
<tab-index=2 actual=2>
"#
);
#[test]
fn test_tab_group_functionality() {
// This test defines the expected behavior for tab-group
// Tab-group should create a nested tab context where inner elements
// have tab indices relative to the group
let xml = r#"
<tab-index=0 actual=0>
<tab-index=1 actual=1>
<tab-group tab-index=2>
<tab-index=0 actual=2>
<tab-index=1 actual=3>
</tab-group>
<tab-index=3 actual=4>
"#;
check(xml);
}
#[test]
fn test_focus_trap_functionality() {
// This test defines the expected behavior for focus-trap
// Focus-trap should trap navigation within its boundaries
let xml = r#"
<tab-index=0 actual=0>
<focus-trap tab-index=1>
xml_test!(
test_duplicate_indices_maintain_insertion_order,
r#"
<tab-index=0 actual=0>
<tab-index=0 actual=1>
<tab-index=1 actual=2>
</focus-trap>
<tab-index=2 actual=3>
"#;
check(xml);
}
#[test]
fn test_nested_groups_and_traps() {
// This test defines the expected behavior for nested structures
let xml = r#"
<tab-index=0 actual=0>
<tab-group tab-index=1>
<tab-index=0 actual=1>
<focus-trap tab-index=1>
<tab-index=0 actual=2>
<tab-index=1 actual=3>
</focus-trap>
<tab-index=1 actual=3>
<tab-index=2 actual=4>
</tab-group>
<tab-index=2 actual=5>
"#;
check(xml);
"#
);
xml_test!(
test_positive_and_negative_indices,
r#"
<tab-index=1 actual=2>
<tab-index=-1 actual=0>
<tab-index=0 actual=1>
<tab-index=2 actual=3>
"#
);
#[test]
#[should_panic(
expected = "Non-tab stop (tab-stop=false) should not have an 'actual' attribute"
)]
fn test_non_tab_stop_with_actual_panics() {
let xml = r#"
<tab-index=0 actual=0>
<tab-index=1 tab-stop=false actual=1>
<tab-index=2 actual=2>
"#;
check(xml);
}
#[test]
#[should_panic(expected = "Tab stop with tab-index=1 must have an 'actual' attribute")]
fn test_tab_stop_without_actual_panics() {
// Tab stops must have an actual value
let xml = r#"
<tab-index=0 actual=0>
<tab-index=1>
<tab-index=2 actual=2>
"#;
check(xml);
}
#[test]
#[should_panic(expected = "Tab order mismatch at position")]
fn test_incorrect_tab_order_shows_xml_format() {
// This test intentionally has wrong expected order to demonstrate error reporting
// The actual tab order will be: tab-index=-1, 0, 1, 2 (positions 0, 1, 2, 3)
// But we're expecting them at wrong positions
let xml = r#"
<tab-index=0 actual=0>
<tab-index=-1 actual=1>
<tab-index=2 actual=2>
<tab-index=1 actual=3>
"#;
check(xml);
}
}
#[test]
fn test_with_disabled_tab_stops() {
// Test with mixed tab-stop values
let xml = r#"
mod basic {
use super::*;
xml_test!(
test_with_disabled_tab_stop,
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#"
xml_test!(
test_with_disabled_tab_stops,
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]
#[should_panic(expected = "Tab stop with tab-index=1 must have an 'actual' attribute")]
fn test_tab_stop_without_actual_panics() {
// Tab stops must have an actual value
let xml = r#"
<tab-index=0 actual=0>
<tab-index=1>
<tab-index=2 actual=2>
"#;
check(xml);
mod tab_group {
use super::*;
// This test defines the expected behavior for tab-group
// Tab-group should create a nested tab context where inner elements
// have tab indices relative to the group
xml_test!(
test_tab_group_functionality,
r#"
<tab-index=0 actual=0>
<tab-index=1 actual=1>
<tab-group tab-index=2>
<tab-index=0 actual=2>
<tab-index=1 actual=3>
</tab-group>
<tab-index=3 actual=4>
<tab-index=4 actual=5>
"#
);
xml_test!(
test_sibling_groups,
r#"
<tab-index=0 actual=0>
<tab-index=1 actual=1>
<tab-group tab-index=2>
<tab-index=0 actual=2>
<tab-index=1 actual=3>
</tab-group>
<tab-index=3 actual=4>
<tab-index=4 actual=5>
<tab-group tab-index=6>
<tab-index=0 actual=6>
<tab-index=1 actual=7>
</tab-group>
<tab-index=7 actual=8>
<tab-index=8 actual=9>
"#
);
xml_test!(
test_nested_group,
r#"
<tab-index=0 actual=0>
<tab-index=1 actual=1>
<tab-group tab-index=2>
<tab-group tab-index=0>
<tab-index=0 actual=2>
<tab-index=1 actual=3>
</tab-group>
</tab-group>
<tab-index=3 actual=4>
<tab-index=4 actual=5>
"#
);
xml_test!(
test_sibling_nested_groups,
r#"
<tab-index=0 actual=0>
<tab-index=1 actual=1>
<tab-group tab-index=2>
<tab-index=0 actual=2>
<tab-group tab-index=1>
<tab-index=0 actual=3>
<tab-index=1 actual=4>
</tab-group>
<tab-index=2 actual=5>
<tab-group tab-index=3>
<tab-index=0 actual=6>
<tab-index=1 actual=7>
</tab-group>
</tab-group>
<tab-index=3 actual=8>
<tab-index=4 actual=9>
"#
);
}
#[test]
#[should_panic(
expected = "Non-tab stop (tab-stop=false) should not have an 'actual' attribute"
)]
fn test_non_tab_stop_with_actual_panics() {
// Non-tab stops should not have an actual value
let xml = r#"
<tab-index=0 actual=0>
<tab-index=1 tab-stop=false actual=1>
<tab-index=2 actual=2>
"#;
check(xml);
}
mod focus_trap {
use super::*;
#[test]
#[should_panic(expected = "Tab order mismatch at position")]
fn test_incorrect_tab_order_shows_xml_format() {
// This test intentionally has wrong expected order to demonstrate error reporting
// The actual tab order will be: tab-index=-1, 0, 1, 2 (positions 0, 1, 2, 3)
// But we're expecting them at wrong positions
let xml = r#"
<tab-index=0 actual=0>
<tab-index=-1 actual=1>
<tab-index=2 actual=2>
<tab-index=1 actual=3>
"#;
check(xml);
xml_test!(
test_focus_trap_in_group,
r#"
<tab-index=0 actual=0>
<tab-group tab-index=1>
<tab-index=0 actual=1>
<focus-trap>
<tab-index=0 actual=2>
<tab-index=1 actual=3>
</focus-trap>
<tab-index=1 actual=4>
</tab-group>
<tab-index=2 actual=5>
"#
);
// This test defines the expected behavior for focus-trap
// Focus-trap should trap navigation within its boundaries
xml_test!(
test_focus_trap_functionality,
r#"
<tab-index=0 actual=0>
<focus-trap tab-index=1>
<tab-index=0 actual=1>
<tab-index=1 actual=2>
</focus-trap>
<tab-index=2 actual=3>
"#
);
xml_test!(
test_nested_groups_and_traps,
r#"
<tab-index=0 actual=0>
<tab-group tab-index=1>
<tab-index=0 actual=1>
<focus-trap tab-index=1>
<tab-index=0 actual=2>
<tab-index=1 actual=3>
</focus-trap>
<tab-index=2 actual=4>
</tab-group>
<tab-index=2 actual=5>
"#
);
}
#[test]