use std::collections::HashSet;
use std::time::Duration;
use accesskit::NodeId;
use accesskit_consumer::{FilterResult, NodeRef, Tree, TreeChangeHandler, common_filter};
use teksilo_canvas::SizeProposal;
use crate::announcer::Politeness;
use crate::test_widgets::FillWidget;
use crate::widget_tree::WidgetTree;
fn settled() -> WidgetTree {
let mut tree = WidgetTree::new();
tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(400.0, 300.0));
tree
}
struct Adapter {
replay: Tree,
defunct: HashSet<NodeId>,
}
#[derive(Default)]
struct Changes {
announced: Vec<(NodeId, String)>,
removed: Vec<NodeId>,
focus_moved: bool,
}
fn included(node: &NodeRef) -> bool {
common_filter(node) == FilterResult::Include
}
impl Changes {
fn entered(&mut self, node: &NodeRef) {
if node.live() != accesskit::Live::Off
&& let Some(name) = node.label()
{
self.announced.push((node.locate().0, name));
}
}
}
impl TreeChangeHandler for Changes {
fn node_added(&mut self, node: &NodeRef) {
if included(node) {
self.entered(node);
}
}
fn node_updated(&mut self, old: &NodeRef, new: &NodeRef) {
match (included(old), included(new)) {
(false, true) => self.entered(new),
(true, false) => self.removed.push(old.locate().0),
_ => {}
}
}
fn focus_moved(&mut self, _old: Option<&NodeRef>, _new: Option<&NodeRef>) {
self.focus_moved = true;
}
fn node_removed(&mut self, node: &NodeRef) {
if included(node) {
self.removed.push(node.locate().0);
}
}
}
impl Adapter {
fn attach(tree: &mut WidgetTree) -> Self {
Self {
replay: Tree::new(tree.sync_accessibility(), true),
defunct: HashSet::new(),
}
}
fn sync(&mut self, tree: &mut WidgetTree) -> Changes {
tree.layout(SizeProposal::exact(400.0, 300.0));
let mut changes = Changes::default();
self.replay
.update_and_process_changes(tree.sync_accessibility(), &mut changes);
changes
}
fn announcer_nodes(&self) -> usize {
self.replay
.state()
.root()
.children()
.filter(|child| {
crate::announcer::is_announcer_node(child.locate().0) && included(child)
})
.count()
}
fn holds(&self, text: &str) -> bool {
fn walk(node: NodeRef<'_>, text: &str) -> bool {
(node.live() != accesskit::Live::Off && node.label().as_deref() == Some(text))
|| node
.filtered_children(&common_filter)
.any(|child| walk(child, text))
}
walk(self.replay.state().root(), text)
}
}
#[test]
fn every_message_comes_from_a_node_the_platform_has_never_had() {
let mut tree = settled();
let mut adapter = Adapter::attach(&mut tree);
for message in ["Saved", "Saved", "Deleted", "Saved"] {
let removed_before = adapter.defunct.clone();
tree.announce(message);
let mut spoken = Vec::new();
for _ in 0..4 {
let changes = adapter.sync(&mut tree);
spoken.extend(changes.announced);
adapter.defunct.extend(changes.removed);
}
tree.advance_time(Duration::from_secs(10));
for _ in 0..2 {
let changes = adapter.sync(&mut tree);
spoken.extend(changes.announced);
adapter.defunct.extend(changes.removed);
}
let from: Vec<NodeId> = spoken
.iter()
.filter(|(_, text)| text == message)
.map(|(id, _)| *id)
.collect();
assert_eq!(
from.len(),
1,
"{message:?} must be announced once: {spoken:?}"
);
assert!(
!removed_before.contains(&from[0]),
"{message:?} was spoken from {:?}, a node the adapter had already removed \
and announced as defunct; Orca drops what it says",
from[0]
);
assert!(
adapter.defunct.contains(&from[0]),
"{message:?}'s node must have left the tree once its time was over"
);
}
}
#[test]
fn a_message_stays_in_the_tree_until_a_reader_can_ask_for_it() {
let mut tree = settled();
let mut adapter = Adapter::attach(&mut tree);
tree.advance_time(Duration::ZERO);
tree.announce("Event added");
let first = adapter.sync(&mut tree);
assert!(
first
.announced
.iter()
.any(|(_, text)| text == "Event added"),
"the message must be announced on the first update after it is asked for"
);
for frame in 1..=3 {
tree.advance_time(Duration::from_millis(16));
let _ = adapter.sync(&mut tree);
assert!(
adapter.holds("Event added"),
"frame {frame}: the message's node left the tree before a reader could \
ask for it; Orca answers \"Unknown object\" and drops the announcement"
);
}
tree.advance_time(Duration::from_secs(1));
let _ = adapter.sync(&mut tree);
assert!(
adapter.holds("Event added"),
"a second on, the node must still be there"
);
tree.advance_time(Duration::from_secs(10));
let _ = adapter.sync(&mut tree);
assert!(
!adapter.holds("Event added"),
"ten seconds on, the message must have left the tree"
);
}
#[test]
fn a_second_message_does_not_take_the_first_away() {
let mut tree = settled();
let mut adapter = Adapter::attach(&mut tree);
tree.advance_time(Duration::ZERO);
tree.announce("Moved to position 3");
tree.announce("Moved to position 4");
let mut order = Vec::new();
for _ in 0..4 {
let changes = adapter.sync(&mut tree);
order.extend(changes.announced.into_iter().map(|(_, text)| text));
if order.len() == 2 {
break;
}
tree.advance_time(Duration::from_millis(16));
}
assert_eq!(order, ["Moved to position 3", "Moved to position 4"]);
assert!(
adapter.holds("Moved to position 3"),
"the first message left the tree as the second arrived, before a reader \
busy with the second could ask the first for its role"
);
}
#[test]
fn a_spoken_message_wakes_the_loop_once_to_leave() {
let mut tree = settled();
let mut adapter = Adapter::attach(&mut tree);
tree.announce_with("Upload failed", Politeness::Assertive);
let _ = adapter.sync(&mut tree);
let _ = adapter.sync(&mut tree);
let now = std::time::Instant::now();
let deadline = tree
.next_timer_deadline()
.expect("a message in the tree must ask the loop to wake when it leaves");
assert!(
deadline > now + Duration::from_secs(1),
"the loop is woken when the message leaves, not every frame until then \
(deadline {:?} from now)",
deadline.saturating_duration_since(now)
);
}
#[test]
fn a_burst_of_messages_keeps_the_tree_small() {
let mut tree = settled();
let mut adapter = Adapter::attach(&mut tree);
tree.advance_time(Duration::ZERO);
for i in 0..30 {
tree.announce(format!("Row {i}"));
}
let mut heard = Vec::new();
for _ in 0..80 {
let changes = adapter.sync(&mut tree);
heard.extend(changes.announced.into_iter().map(|(_, text)| text));
let live = adapter.announcer_nodes();
assert!(live <= 8, "{live} announcer nodes in the tree at once");
tree.advance_time(Duration::from_millis(16));
}
let expected: Vec<String> = (0..30).map(|i| format!("Row {i}")).collect();
assert_eq!(heard, expected, "every message is heard, in order");
}
fn fate(adapter: &mut Adapter, tree: &mut WidgetTree, message: &str) -> Vec<&'static str> {
let mut fate = Vec::new();
for _ in 0..4 {
let changes = adapter.sync(tree);
for (_, text) in &changes.announced {
if text == message {
fate.push(if changes.focus_moved { "cut" } else { "heard" });
}
}
tree.advance_time(Duration::from_millis(16));
}
fate
}
#[test]
fn a_message_raised_as_focus_moves_is_heard_after_the_move() {
let mut tree = WidgetTree::new();
let first = tree.add(FillWidget::new().focusable());
let second = tree.add(FillWidget::new().focusable());
tree.layout(SizeProposal::exact(400.0, 300.0));
tree.focus(first);
let mut adapter = Adapter::attach(&mut tree);
tree.advance_time(Duration::ZERO);
tree.focus(second);
tree.announce("Doc 1 moved to 5 of 6");
assert_eq!(
fate(&mut adapter, &mut tree, "Doc 1 moved to 5 of 6"),
["heard"],
"the message reached the adapter in the update that moved focus, ahead \
of the focus event, and Orca stops speaking to read the new focus"
);
}
#[derive(Debug)]
struct Roving {
rows: Vec<crate::widget_id::WidgetId>,
current: std::rc::Rc<std::cell::Cell<usize>>,
}
impl crate::widget::Widget for Roving {
fn build(
&mut self,
ctx: &mut crate::build_context::BuildContext,
) -> Vec<crate::widget_id::WidgetId> {
ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
self.rows = (1..=3)
.map(|i| ctx.add(FillWidget::new().label(format!("Item {i}"))))
.collect();
self.rows.clone()
}
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &crate::widget::LayoutContext,
) -> crate::widget::LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
fn children(&self) -> Vec<crate::widget_id::WidgetId> {
self.rows.clone()
}
fn accessibility(&self, builder: &mut crate::accessibility::AccessNodeBuilder) {
builder.set_role(accesskit::Role::ListBox);
if let Some(&row) = self.rows.get(self.current.get()) {
builder.set_active_descendant(crate::accessibility::widget_id_to_node_id(row));
}
}
}
#[test]
fn a_message_raised_as_the_current_row_changes_is_heard_after_the_change() {
let current = std::rc::Rc::new(std::cell::Cell::new(0));
let mut tree = WidgetTree::new();
let list = tree.add(Roving {
rows: Vec::new(),
current: current.clone(),
});
tree.layout(SizeProposal::exact(400.0, 300.0));
tree.focus(list);
let mut adapter = Adapter::attach(&mut tree);
tree.advance_time(Duration::ZERO);
current.set(1);
tree.request_accessibility_update();
tree.announce("Moved to 2 of 3");
assert_eq!(
fate(&mut adapter, &mut tree, "Moved to 2 of 3"),
["heard"],
"the message reached the adapter in the update that moved the current \
row, ahead of the focus event, and Orca stops speaking to read the row"
);
}
#[test]
fn a_message_raised_while_focus_stays_is_not_held() {
let mut tree = WidgetTree::new();
let only = tree.add(FillWidget::new().focusable());
tree.layout(SizeProposal::exact(400.0, 300.0));
tree.focus(only);
let mut adapter = Adapter::attach(&mut tree);
tree.advance_time(Duration::ZERO);
tree.announce("Saved");
let changes = adapter.sync(&mut tree);
assert_eq!(
changes
.announced
.iter()
.map(|(_, text)| text.as_str())
.collect::<Vec<_>>(),
["Saved"],
"nothing moved focus, so the first update speaks the message"
);
}
#[test]
fn a_message_waits_for_focus_to_stop_moving() {
let mut tree = WidgetTree::new();
let first = tree.add(FillWidget::new().focusable());
let second = tree.add(FillWidget::new().focusable());
let third = tree.add(FillWidget::new().focusable());
tree.layout(SizeProposal::exact(400.0, 300.0));
tree.focus(first);
let mut adapter = Adapter::attach(&mut tree);
tree.advance_time(Duration::ZERO);
tree.focus(second);
tree.announce("Moved to 3 of 10");
let changes = adapter.sync(&mut tree);
assert!(changes.focus_moved, "the first frame moves focus");
assert!(
changes.announced.is_empty(),
"the message went out with the first move, ahead of its focus event: {:?}",
changes.announced
);
tree.advance_time(Duration::from_millis(16));
tree.focus(third);
assert_eq!(
fate(&mut adapter, &mut tree, "Moved to 3 of 10"),
["heard"],
"the message went out with the second move, ahead of its focus event"
);
}