#![cfg(not(target_arch = "wasm32"))]
use accesskit::{Action, ActionData, ActionRequest, NodeId, TreeId};
use accesskit_consumer::Tree as ConsumerTree;
use argui_accessibility::{
AccessKitTree, LiveRegion, Orientation, Role, SemanticAction, SemanticNode, SemanticNodeId,
SemanticRequest, SemanticState, SemanticTree, SemanticValue, Semantics,
};
use argui_core::{Point, Rect, Size};
#[test]
fn protected_text_uses_the_native_password_role_without_a_value() {
let root = SemanticNodeId::new(1);
let tree = SemanticTree {
root,
focus: root,
nodes: vec![SemanticNode {
id: root,
bounds: Rect::default(),
children: Vec::new(),
semantics: Semantics::new(Role::TextInput)
.label("Password")
.state(SemanticState {
protected: true,
..Default::default()
}),
}],
};
let update = AccessKitTree::full(&tree);
assert_eq!(update.nodes[0].1.role(), accesskit::Role::PasswordInput);
assert!(update.nodes[0].1.value().is_none());
}
#[test]
fn complete_snapshots_are_valid_accesskit_trees() {
let root = SemanticNodeId::new(1);
let button = SemanticNodeId::new(2);
let tree = SemanticTree {
root,
focus: button,
nodes: vec![
SemanticNode {
id: root,
bounds: Rect::new(Point::default(), Size::new(800.0, 600.0)),
semantics: Semantics::new(Role::Window).label("Demo"),
children: vec![button],
},
SemanticNode {
id: button,
bounds: Rect::new(Point::new(10.0, 20.0), Size::new(100.0, 40.0)),
semantics: Semantics::new(Role::Button)
.label("Save")
.action(SemanticAction::Click),
children: Vec::new(),
},
],
};
let consumer = ConsumerTree::new(AccessKitTree::full(&tree), true);
assert_eq!(consumer.state().root().label().as_deref(), Some("Demo"));
assert_eq!(
consumer.state().focus().and_then(|node| node.label()),
Some("Save".into())
);
}
#[test]
fn incremental_updates_only_replace_changed_nodes_and_root_metadata() {
let root = SemanticNodeId::new(1);
let child = SemanticNodeId::new(2);
let original = SemanticTree {
root,
focus: root,
nodes: vec![SemanticNode {
id: root,
bounds: Rect::default(),
semantics: Semantics::new(Role::Window).label("Before"),
children: Vec::new(),
}],
};
let changed = SemanticTree {
root,
focus: root,
nodes: vec![SemanticNode {
id: root,
bounds: Rect::default(),
semantics: Semantics::new(Role::Window).label("After"),
children: Vec::new(),
}],
};
let update = AccessKitTree::patch(&original.diff(&changed), &changed);
assert!(update.tree.is_none());
assert_eq!(update.nodes.len(), 1);
let replaced = SemanticTree {
root: child,
focus: child,
nodes: vec![SemanticNode {
id: child,
bounds: Rect::default(),
semantics: Semantics::new(Role::Window).label("Replacement"),
children: Vec::new(),
}],
};
let update = AccessKitTree::patch(&changed.diff(&replaced), &replaced);
assert_eq!(update.tree.unwrap().root, NodeId(child.get()));
assert_eq!(update.focus, NodeId(child.get()));
}
#[test]
fn rich_nodes_lower_every_value_state_and_relation() {
let root = SemanticNodeId::new(1);
let slider = SemanticNodeId::new(2);
let text = SemanticNodeId::new(3);
let unbounded = SemanticNodeId::new(4);
let mut semantics = Semantics::new(Role::Slider)
.label("Volume")
.description("Output volume")
.value(SemanticValue::Number {
value: 42.0,
minimum: Some(0.0),
maximum: Some(100.0),
step: Some(2.0),
})
.state(SemanticState {
pressed: Some(true),
protected: false,
disabled: true,
selected: true,
multiselectable: true,
checked: Some(argui_accessibility::CheckedState::Checked),
expanded: Some(false),
required: true,
read_only: true,
invalid: true,
modal: true,
busy: true,
})
.live(LiveRegion::Assertive)
.orientation(Orientation::Horizontal)
.level(3)
.position_in_set(2, 5);
for action in [
SemanticAction::Click,
SemanticAction::Focus,
SemanticAction::Blur,
SemanticAction::Increment,
SemanticAction::Decrement,
SemanticAction::Expand,
SemanticAction::Collapse,
SemanticAction::SetValue,
SemanticAction::ScrollIntoView,
] {
semantics = semantics.action(action);
}
let tree = SemanticTree {
root,
focus: slider,
nodes: vec![
SemanticNode {
id: root,
bounds: Rect::default(),
semantics: Semantics::new(Role::Window),
children: vec![slider, text, unbounded],
},
SemanticNode {
id: slider,
bounds: Rect::default(),
semantics,
children: Vec::new(),
},
SemanticNode {
id: text,
bounds: Rect::default(),
semantics: Semantics::new(Role::TextInput)
.value(SemanticValue::Text("value".into()))
.state(SemanticState {
checked: Some(argui_accessibility::CheckedState::Unchecked),
expanded: Some(true),
..SemanticState::default()
})
.live(LiveRegion::Polite)
.orientation(Orientation::Vertical),
children: Vec::new(),
},
SemanticNode {
id: unbounded,
bounds: Rect::default(),
semantics: Semantics::new(Role::Progress).value(SemanticValue::Number {
value: 0.5,
minimum: None,
maximum: None,
step: None,
}),
children: Vec::new(),
},
],
};
let consumer = ConsumerTree::new(AccessKitTree::full(&tree), true);
let node = consumer
.state()
.node_by_tree_local_id(NodeId(2), TreeId::ROOT)
.unwrap();
assert_eq!(node.numeric_value(), Some(42.0));
assert_eq!(node.min_numeric_value(), Some(0.0));
assert_eq!(node.max_numeric_value(), Some(100.0));
assert_eq!(node.numeric_value_step(), Some(2.0));
assert!(node.is_disabled());
assert!(node.is_modal());
assert_eq!(node.is_selected(), Some(true));
assert_eq!(node.level(), Some(3));
assert_eq!(node.position_in_set(), Some(2));
assert_eq!(node.size_of_set(), Some(5));
assert_eq!(node.orientation(), Some(accesskit::Orientation::Horizontal));
assert_eq!(node.live(), accesskit::Live::Assertive);
let unbounded = consumer
.state()
.node_by_tree_local_id(NodeId(4), TreeId::ROOT)
.unwrap();
assert_eq!(unbounded.numeric_value(), Some(0.5));
assert_eq!(unbounded.min_numeric_value(), None);
assert_eq!(unbounded.max_numeric_value(), None);
assert_eq!(unbounded.numeric_value_step(), None);
}
#[test]
fn accesskit_actions_lower_supported_data_and_reject_unknown_actions() {
let actions = [
(Action::Click, SemanticAction::Click),
(Action::Focus, SemanticAction::Focus),
(Action::Blur, SemanticAction::Blur),
(Action::Increment, SemanticAction::Increment),
(Action::Decrement, SemanticAction::Decrement),
(Action::Expand, SemanticAction::Expand),
(Action::Collapse, SemanticAction::Collapse),
(Action::SetValue, SemanticAction::SetValue),
(Action::ScrollIntoView, SemanticAction::ScrollIntoView),
];
for (action, expected) in actions {
let request = SemanticRequest::try_from(ActionRequest {
action,
target_tree: TreeId::ROOT,
target_node: NodeId(7),
data: None,
})
.unwrap();
assert_eq!(request.target, SemanticNodeId::new(7));
assert_eq!(request.action, expected);
}
let text = SemanticRequest::try_from(ActionRequest {
action: Action::SetValue,
target_tree: TreeId::ROOT,
target_node: NodeId(7),
data: Some(ActionData::Value("hello".into())),
})
.unwrap();
assert_eq!(text.value, Some(SemanticValue::Text("hello".into())));
let number = SemanticRequest::try_from(ActionRequest {
action: Action::SetValue,
target_tree: TreeId::ROOT,
target_node: NodeId(7),
data: Some(ActionData::NumericValue(4.5)),
})
.unwrap();
assert!(matches!(
number.value,
Some(SemanticValue::Number { value: 4.5, .. })
));
assert!(
SemanticRequest::try_from(ActionRequest {
action: Action::ShowContextMenu,
target_tree: TreeId::ROOT,
target_node: NodeId(7),
data: None,
})
.is_err()
);
}
#[test]
fn table_roles_and_multiple_selection_reach_accesskit() {
for (role, expected) in [
(Role::Table, accesskit::Role::Table),
(Role::Navigation, accesskit::Role::Navigation),
(Role::Grid, accesskit::Role::Table),
(Role::Row, accesskit::Role::Row),
(Role::ColumnHeader, accesskit::Role::ColumnHeader),
(Role::Cell, accesskit::Role::Cell),
] {
let id = SemanticNodeId::new(1);
let tree = SemanticTree {
root: id,
focus: id,
nodes: vec![SemanticNode {
id,
bounds: Rect::default(),
children: vec![],
semantics: Semantics::new(role).state(SemanticState {
multiselectable: true,
..Default::default()
}),
}],
};
let update = AccessKitTree::full(&tree);
assert_eq!(update.nodes[0].1.role(), expected);
assert!(update.nodes[0].1.is_multiselectable());
}
}
#[test]
fn toggle_buttons_export_both_persistent_pressed_states() {
for (pressed, expected) in [
(None, None),
(Some(false), Some(accesskit::Toggled::False)),
(Some(true), Some(accesskit::Toggled::True)),
] {
let id = SemanticNodeId::new(1);
let tree = SemanticTree {
root: id,
focus: id,
nodes: vec![SemanticNode {
id,
bounds: Rect::default(),
children: vec![],
semantics: Semantics::new(Role::Button).state(SemanticState {
pressed,
..Default::default()
}),
}],
};
assert_eq!(AccessKitTree::full(&tree).nodes[0].1.toggled(), expected);
}
}
#[test]
fn relations_mixed_state_and_grid_metadata_reach_the_native_tree() {
use argui_accessibility::{
CheckedState, GridPosition, PopupKind, SemanticRelations, SortDirection,
};
let root = SemanticNodeId::new(1);
let child = SemanticNodeId::new(2);
let mut semantics = Semantics::new(Role::ComboBox);
semantics.state.checked = Some(CheckedState::Mixed);
semantics.relations = SemanticRelations {
labelled_by: vec![child],
described_by: vec![child],
controls: vec![child],
active_descendant: Some(child),
};
semantics.grid = GridPosition {
row_count: Some(4),
column_count: Some(3),
row_index: Some(2),
column_index: Some(1),
};
semantics.sort = Some(SortDirection::Descending);
semantics.popup = Some(PopupKind::Grid);
let tree = SemanticTree {
root,
focus: root,
nodes: vec![
SemanticNode {
id: root,
bounds: Rect::default(),
semantics,
children: vec![child],
},
SemanticNode {
id: child,
bounds: Rect::default(),
semantics: Semantics::new(Role::Text).label("Label"),
children: Vec::new(),
},
],
};
let update = AccessKitTree::full(&tree);
let node = &update.nodes[0].1;
assert_eq!(node.toggled(), Some(accesskit::Toggled::Mixed));
assert_eq!(node.labelled_by(), &[NodeId(2)]);
assert_eq!(node.described_by(), &[NodeId(2)]);
assert_eq!(node.controls(), &[NodeId(2)]);
assert_eq!(node.active_descendant(), Some(NodeId(2)));
assert_eq!(node.row_count(), Some(4));
assert_eq!(node.column_count(), Some(3));
assert_eq!(node.row_index(), Some(1));
assert_eq!(node.column_index(), Some(0));
assert_eq!(
node.sort_direction(),
Some(accesskit::SortDirection::Descending)
);
assert_eq!(node.has_popup(), Some(accesskit::HasPopup::Grid));
}