use std::cell::Cell;
use std::rc::Rc;
use accesskit::{Live, Role};
use teksilo_canvas::SizeProposal;
use crate::accessibility::AccessNodeBuilder;
use crate::signal::Signal;
use crate::test_widgets::{FillWidget, StackWidget};
use crate::widget::{LayoutContext, LayoutResponse, Widget};
use crate::widget_builder::WidgetBuilder;
use crate::widget_tree::WidgetTree;
fn settled(build: impl FnOnce(&mut WidgetTree)) -> (WidgetTree, u64) {
let mut tree = WidgetTree::new();
build(&mut tree);
tree.layout(SizeProposal::exact(400.0, 300.0));
let _ = tree.sync_accessibility();
let seen = tree.announcements_since(0).last().map_or(0, |a| a.seq);
(tree, seen)
}
fn heard(tree: &mut WidgetTree, seen: &mut u64) -> Vec<(String, bool)> {
tree.layout(SizeProposal::exact(400.0, 300.0));
let _ = tree.sync_accessibility();
let got = tree.announcements_since(*seen);
if let Some(last) = got.last() {
*seen = last.seq;
}
got.into_iter().map(|a| (a.text, a.assertive)).collect()
}
fn polite(text: &str) -> Vec<(String, bool)> {
vec![(text.to_string(), false)]
}
#[test]
fn a_named_status_is_heard_when_its_name_changes() {
let text = Signal::new("Prêt".to_string());
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_label(text.clone())
.access_live(Live::Polite),
);
});
text.set("Évènement enregistré".to_string());
assert_eq!(heard(&mut tree, &mut seen), polite("Évènement enregistré"));
}
#[test]
fn a_status_that_carries_its_text_only_as_a_value_is_not_heard() {
let text = Signal::new(String::new());
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_value(text.clone())
.access_live(Live::Polite),
);
});
text.set("Évènement enregistré".to_string());
assert_eq!(
heard(&mut tree, &mut seen),
Vec::new(),
"no adapter names a Status from its value, so none announces it"
);
}
#[test]
fn an_alert_that_carries_its_text_only_as_a_value_is_not_heard() {
let text = Signal::new(String::new());
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Alert)
.access_value(text.clone())
.access_live(Live::Assertive),
);
});
text.set("Impossible d'enregistrer".to_string());
assert_eq!(heard(&mut tree, &mut seen), Vec::new());
}
#[test]
fn a_live_label_is_heard_through_its_value() {
let text = Signal::new("3 évènements".to_string());
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Label)
.access_value(text.clone())
.access_live(Live::Polite),
);
});
text.set("4 évènements".to_string());
assert_eq!(heard(&mut tree, &mut seen), polite("4 évènements"));
}
#[test]
fn a_hidden_live_node_is_not_heard() {
let text = Signal::new("Prêt".to_string());
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_label(text.clone())
.access_live(Live::Polite)
.access_hidden(true),
);
});
text.set("Évènement enregistré".to_string());
assert_eq!(heard(&mut tree, &mut seen), Vec::new());
}
#[test]
fn a_live_node_inside_a_hidden_subtree_is_not_heard() {
let text = Signal::new("Prêt".to_string());
let (mut tree, mut seen) = settled(|tree| {
let panel = tree.add(
StackWidget::new()
.access_role(Role::Group)
.access_label("Panneau")
.access_hidden(true),
);
tree.add_child(
panel,
FillWidget::new()
.access_role(Role::Status)
.access_label(text.clone())
.access_live(Live::Polite),
);
});
text.set("Évènement enregistré".to_string());
assert_eq!(heard(&mut tree, &mut seen), Vec::new());
}
#[test]
fn a_live_node_is_heard_again_when_it_is_shown_again() {
let hidden = Signal::new(false);
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_label("Hors ligne")
.access_live(Live::Polite)
.access_hidden(hidden.clone()),
);
});
hidden.set(true);
assert_eq!(
heard(&mut tree, &mut seen),
Vec::new(),
"leaving says nothing"
);
hidden.set(false);
assert_eq!(heard(&mut tree, &mut seen), polite("Hors ligne"));
}
#[test]
fn a_live_region_named_through_labelled_by_is_heard_by_that_name() {
let hidden = Signal::new(true);
let (mut tree, mut seen) = settled(|tree| {
let title = tree.add(FillWidget::new().label("Erreur de saisie"));
tree.add(
FillWidget::new()
.access_role(Role::Alert)
.access_live(Live::Assertive)
.access_labelled_by(title)
.access_hidden(hidden.clone()),
);
});
hidden.set(false);
assert_eq!(
heard(&mut tree, &mut seen),
vec![("Erreur de saisie".to_string(), true)]
);
}
#[test]
fn a_node_inside_a_live_region_is_heard_at_the_regions_level() {
let text = Signal::new("Aucun résultat".to_string());
let (mut tree, mut seen) = settled(|tree| {
let region = tree.add(
StackWidget::new()
.access_role(Role::Group)
.access_label("Recherche")
.access_live(Live::Assertive),
);
tree.add_child(
region,
FillWidget::new()
.access_role(Role::Label)
.access_value(text.clone()),
);
});
text.set("2 résultats".to_string());
assert_eq!(
heard(&mut tree, &mut seen),
vec![("2 résultats".to_string(), true)]
);
}
#[test]
fn a_cleared_name_is_not_heard() {
let text = Signal::new("Prêt".to_string());
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_label(text.clone())
.access_live(Live::Polite),
);
});
text.set(String::new());
assert_eq!(heard(&mut tree, &mut seen), Vec::new());
}
#[test]
fn an_unchanged_name_is_not_heard_twice() {
let text = Signal::new("Prêt".to_string());
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_label(text.clone())
.access_live(Live::Polite),
);
});
text.set("Enregistré".to_string());
assert_eq!(heard(&mut tree, &mut seen), polite("Enregistré"));
tree.request_accessibility_update();
assert_eq!(heard(&mut tree, &mut seen), Vec::new());
}
#[test]
fn a_live_node_in_the_first_update_is_heard() {
let mut tree = WidgetTree::new();
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_label("Prêt")
.access_live(Live::Polite),
);
tree.layout(SizeProposal::exact(400.0, 300.0));
let mut seen = 0;
assert_eq!(heard(&mut tree, &mut seen), polite("Prêt"));
}
#[derive(Debug)]
struct Escalating {
live: Rc<Cell<Live>>,
}
impl Widget for Escalating {
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
proposal.resolve(120.0, 20.0).into()
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(Role::Status);
builder.set_name("Connexion perdue");
builder.set_live(self.live.get());
}
}
#[test]
fn a_change_of_politeness_alone_is_not_heard() {
let live = Rc::new(Cell::new(Live::Polite));
let (mut tree, mut seen) = settled(|tree| {
tree.add(Escalating { live: live.clone() });
});
live.set(Live::Assertive);
tree.request_accessibility_update();
assert_eq!(heard(&mut tree, &mut seen), Vec::new());
}
#[test]
fn several_announcements_in_one_update_keep_the_trees_order() {
let lines: Vec<Signal<String>> = (0..6).map(|_| Signal::new(String::new())).collect();
let (mut tree, mut seen) = settled(|tree| {
for line in &lines {
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_label(line.clone())
.access_live(Live::Polite),
);
}
});
for (index, line) in lines.iter().enumerate() {
line.set(format!("ligne {index}"));
}
let texts: Vec<String> = heard(&mut tree, &mut seen)
.into_iter()
.map(|(text, _)| text)
.collect();
let expected: Vec<String> = (0..6).map(|index| format!("ligne {index}")).collect();
assert_eq!(texts, expected);
}
#[test]
fn the_frameworks_announcement_comes_after_the_applications_own() {
let text = Signal::new("Prêt".to_string());
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_label(text.clone())
.access_live(Live::Polite),
);
});
text.set("Enregistré".to_string());
tree.announce("Évènement enregistré");
assert_eq!(
heard(&mut tree, &mut seen),
vec![
("Enregistré".to_string(), false),
("Évènement enregistré".to_string(), false),
]
);
}
#[test]
fn a_snapshot_records_nothing() {
let text = Signal::new("Prêt".to_string());
let (mut tree, mut seen) = settled(|tree| {
tree.add(
FillWidget::new()
.access_role(Role::Status)
.access_label(text.clone())
.access_live(Live::Polite),
);
});
text.set("Enregistré".to_string());
tree.layout(SizeProposal::exact(400.0, 300.0));
let _ = tree.accessibility_tree_snapshot();
assert!(tree.announcements_since(seen).is_empty());
assert_eq!(heard(&mut tree, &mut seen), polite("Enregistré"));
}
#[derive(Debug)]
struct Viewport {
offset: Signal<f32>,
child: Option<crate::widget_id::WidgetId>,
}
impl Widget for Viewport {
fn build(
&mut self,
ctx: &mut crate::build_context::BuildContext,
) -> Vec<crate::widget_id::WidgetId> {
self.offset.bind_to(
ctx.self_id(),
ctx.binding_registry(),
crate::binding::BindingLevel::Relayout,
);
let child = ctx.add(
FillWidget::new()
.access_role(Role::Status)
.access_label("Nouveau message")
.access_live(Live::Polite),
);
self.child = Some(child);
vec![child]
}
fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
teksilo_canvas::Size::new(100.0, 50.0).into()
}
fn children(&self) -> Vec<crate::widget_id::WidgetId> {
self.child.into_iter().collect()
}
fn place_children(
&self,
bounds: teksilo_canvas::Rect,
_proposal: SizeProposal,
children: &mut [crate::widget::WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = teksilo_canvas::Point::new(bounds.x, bounds.y + self.offset.get());
child.size = teksilo_canvas::Size::new(80.0, 20.0);
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(Role::ScrollView);
builder.set_name("Messages");
builder.inner_mut().set_clips_children();
}
}
#[test]
fn a_live_node_scrolled_into_view_is_heard() {
let offset = Signal::new(400.0_f32);
let (mut tree, mut seen) = settled(|tree| {
tree.add(Viewport {
offset: offset.clone(),
child: None,
});
});
assert_eq!(
heard(&mut tree, &mut seen),
Vec::new(),
"out of view, the status is filtered out"
);
offset.set(10.0);
assert_eq!(heard(&mut tree, &mut seen), polite("Nouveau message"));
}
fn status(text: &Signal<String>) -> impl Widget {
FillWidget::new()
.access_role(Role::Status)
.access_label(text.clone())
.access_live(Live::Polite)
}
#[test]
fn a_tree_that_does_not_record_replays_nothing() {
let text = Signal::new("Prêt".to_string());
let mut tree = WidgetTree::new();
assert!(tree.records_announcements(), "a new tree records");
tree.set_records_announcements(false);
tree.add(status(&text));
let mut seen = 0;
assert_eq!(heard(&mut tree, &mut seen), Vec::new());
text.set("Enregistré".to_string());
assert_eq!(heard(&mut tree, &mut seen), Vec::new());
assert!(!tree.announcement_ring.holds_a_replay());
}
#[test]
fn recording_switched_on_hears_what_changes_after() {
let text = Signal::new("Prêt".to_string());
let mut tree = WidgetTree::new();
tree.set_records_announcements(false);
tree.add(status(&text));
let mut seen = 0;
assert_eq!(heard(&mut tree, &mut seen), Vec::new());
tree.set_records_announcements(true);
tree.request_accessibility_update();
assert_eq!(
heard(&mut tree, &mut seen),
Vec::new(),
"no arrival replayed"
);
text.set("Enregistré".to_string());
assert_eq!(heard(&mut tree, &mut seen), polite("Enregistré"));
}
#[test]
fn recording_switched_off_and_on_between_updates_misses_nothing() {
let text = Signal::new("Prêt".to_string());
let (mut tree, mut seen) = settled(|tree| {
tree.add(status(&text));
});
tree.set_records_announcements(false);
tree.set_records_announcements(true);
text.set("Enregistré".to_string());
assert_eq!(heard(&mut tree, &mut seen), polite("Enregistré"));
}