use super::*;
const RECORD_FIELDS: usize = 41;
fn encode_elements(elements: &[AccessibilityElement], changed: &[bool], density: f32) -> String {
let mut snapshot = AccessibilitySnapshot::default();
AccessibilityWire::default()
.publish(&mut snapshot, elements.to_vec(), changed, density)
.expect("unique identities")
.records
}
fn published(elements: &[AccessibilityElement]) -> (AccessibilityWire, AccessibilitySnapshot) {
let mut wire = AccessibilityWire::default();
let mut snapshot = AccessibilitySnapshot::default();
wire.publish(&mut snapshot, elements.to_vec(), &[], 2.0)
.expect("unique identities");
(wire, snapshot)
}
fn record_ids(update: &AccessibilityUpdate) -> Vec<i32> {
update
.records
.split('\n')
.filter(|record| !record.is_empty())
.map(|record| {
record
.split('\t')
.next()
.and_then(|id| id.parse().ok())
.expect("a record starts with its id")
})
.collect()
}
fn labelled(node_id: cranpose_core::NodeId, label: &str, y: f32) -> AccessibilityElement {
AccessibilityElement {
node_id,
label: label.into(),
bounds: AccessibilityRect::new(0.0, y, 100.0, 20.0),
role: AccessibilityRole::Button,
clickable: true,
..AccessibilityElement::default()
}
}
#[test]
fn the_first_update_sends_every_control_in_order() {
let elements = [labelled(1, "One", 0.0), labelled(2, "Two", 30.0)];
let mut snapshot = AccessibilitySnapshot::default();
let update = AccessibilityWire::default()
.publish(&mut snapshot, elements.to_vec(), &[], 2.0)
.expect("unique identities");
assert_eq!(update.order, snapshot.ids);
assert_eq!(record_ids(&update), snapshot.ids);
assert!(update.moves.is_empty());
assert!(!update.is_empty());
}
#[test]
fn an_unchanged_snapshot_leaves_the_host_alone() {
let elements = [labelled(1, "One", 0.0), labelled(2, "Two", 30.0)];
let (mut wire, mut snapshot) = published(&elements);
let update = wire
.publish(&mut snapshot, elements.to_vec(), &[], 2.0)
.expect("unique identities");
assert!(update.is_empty(), "{update:?}");
}
#[test]
fn a_control_that_only_moved_is_sent_as_its_new_bounds() {
let (mut wire, mut snapshot) = published(&[labelled(1, "One", 0.0), labelled(2, "Two", 30.0)]);
let ids = snapshot.ids.clone();
let update = wire
.publish(
&mut snapshot,
vec![labelled(1, "One", 0.0), labelled(2, "Two", 40.0)],
&[],
2.0,
)
.expect("unique identities");
assert!(update.records.is_empty(), "a move resends no record");
assert_eq!(update.moves, vec![ids[1], 0, 80, 200, 120]);
assert_eq!(update.order, ids);
assert!(!update.is_empty());
}
#[test]
fn a_move_smaller_than_a_pixel_leaves_the_host_alone() {
let (mut wire, mut snapshot) = published(&[labelled(1, "One", 0.0)]);
let update = wire
.publish(&mut snapshot, vec![labelled(1, "One", 0.1)], &[], 2.0)
.expect("unique identities");
assert!(update.is_empty(), "{update:?}");
}
#[test]
fn a_control_that_says_something_else_is_resent_in_full() {
let (mut wire, mut snapshot) = published(&[labelled(1, "One", 0.0), labelled(2, "Two", 30.0)]);
let ids = snapshot.ids.clone();
let update = wire
.publish(
&mut snapshot,
vec![labelled(1, "One", 0.0), labelled(2, "Three", 40.0)],
&[],
2.0,
)
.expect("unique identities");
assert_eq!(record_ids(&update), vec![ids[1]]);
assert!(update.records.contains("Three"));
assert!(
update.moves.is_empty(),
"the record carries the new bounds itself"
);
}
#[test]
fn a_spoken_change_is_resent_with_its_flag() {
let elements = [labelled(1, "One", 0.0), labelled(2, "Two", 30.0)];
let (mut wire, mut snapshot) = published(&elements);
let ids = snapshot.ids.clone();
let update = wire
.publish(&mut snapshot, elements.to_vec(), &[false, true], 2.0)
.expect("unique identities");
assert_eq!(record_ids(&update), vec![ids[1]]);
assert_eq!(update.records.split('\t').nth(29), Some("1"));
}
#[test]
fn a_new_control_is_sent_and_the_order_names_it() {
let (mut wire, mut snapshot) = published(&[labelled(1, "One", 0.0)]);
let update = wire
.publish(
&mut snapshot,
vec![labelled(3, "New", 0.0), labelled(1, "One", 30.0)],
&[],
2.0,
)
.expect("unique identities");
assert_eq!(update.order, snapshot.ids);
assert_eq!(record_ids(&update), vec![snapshot.ids[0]]);
assert_eq!(update.moves, vec![snapshot.ids[1], 0, 60, 200, 100]);
}
#[test]
fn a_removed_control_leaves_the_order() {
let (mut wire, mut snapshot) = published(&[labelled(1, "One", 0.0), labelled(2, "Two", 30.0)]);
let kept = snapshot.ids[0];
let update = wire
.publish(&mut snapshot, vec![labelled(1, "One", 0.0)], &[], 2.0)
.expect("unique identities");
assert_eq!(update.order, vec![kept]);
assert!(update.records.is_empty());
assert!(!update.is_empty(), "the host must drop the removed control");
}
#[test]
fn a_row_whose_list_was_replaced_is_resent() {
let list = |generation| AccessibilityElement {
node_id: 6,
node_generation: generation,
bounds: AccessibilityRect::new(0.0, 0.0, 400.0, 600.0),
vertical_scroll: Some(cranpose_ui::ScrollAxisRange::new(0.0, 900.0, false)),
..AccessibilityElement::default()
};
let row = AccessibilityElement {
scroll_parent: Some(6),
..labelled(9, "Milk", 10.0)
};
let (mut wire, mut snapshot) = published(&[list(0), row.clone()]);
let update = wire
.publish(&mut snapshot, vec![list(1), row], &[], 2.0)
.expect("unique identities");
assert_eq!(
record_ids(&update),
snapshot.ids,
"the row names its new list's id, so both are resent"
);
}
#[test]
fn a_new_density_resends_every_control() {
let elements = [labelled(1, "One", 0.0), labelled(2, "Two", 30.0)];
let (mut wire, mut snapshot) = published(&elements);
let update = wire
.publish(&mut snapshot, elements.to_vec(), &[], 3.0)
.expect("unique identities");
assert_eq!(record_ids(&update), snapshot.ids);
}
#[test]
fn a_forgotten_host_is_sent_every_control() {
let elements = [labelled(1, "One", 0.0), labelled(2, "Two", 30.0)];
let (mut wire, mut snapshot) = published(&elements);
wire.forget();
let update = wire
.publish(&mut snapshot, elements.to_vec(), &[], 2.0)
.expect("unique identities");
assert_eq!(record_ids(&update), snapshot.ids);
assert_eq!(update.order, snapshot.ids);
}
use crate::accessibility::{AccessibilityRect, AccessibilityRole, element_with};
#[test]
fn android_accessibility_wire_values_escape_record_delimiters() {
let mut out = String::from("kept|");
push_escaped_wire_field(
&mut out,
&format!("A%\tB\nC\r{ACTION_SEPARATOR}é"),
ACTION_SEPARATOR,
);
assert_eq!(out, "kept|A%25%09B%0AC%0D%1Fé");
}
#[test]
fn records_keep_the_layout_java_parses_byte_for_byte() {
let elements = vec![
AccessibilityElement {
node_id: 4,
label: "Tab\tand%".into(),
value: Some("line\nnext\r".into()),
state_description: Some("On".into()),
click_label: Some("Toggle".into()),
bounds: AccessibilityRect::new(1.25, 2.5, 30.0, 40.75),
role: AccessibilityRole::Switch,
clickable: true,
toggled: Some(true),
custom_actions: vec!["Pause".into(), format!("Re{ACTION_SEPARATOR}sume")],
pane_title: Some("Pane".into()),
error: Some("Bad%".into()),
long_click_label: Some("Hold".into()),
expanded: Some(false),
..AccessibilityElement::default()
},
element_with(5, Some(1)),
];
assert_eq!(
encode_elements(&elements, &[true, false], 1.5),
"1\t5\t2\t4\t47\t65\t16.25\t22.875\t1\tTab%09and%25\tline%0Anext%0D\tOn\tToggle\t-1\t1\t1\t\
Pause\u{1f}Re%1Fsume\t0\t0\t0\t0\t0\t0\t0\t0\t0\t-1\t0\t0\t1\t-1\t-1\tPane\tBad%25\t0\t0\t\
Hold\t0\t0\t-1\t-1\n\
2\t2\t0\t0\t15\t15\t5\t5\t0\tRow\t\t\t\t-1\t-1\t1\t\t0\t0\t0\t0\t0\t0\t0\t0\t0\t-1\t0\t0\t0\t\
-1\t-1\t\t\t0\t-1\t\t0\t0\t-1\t-1"
);
}
#[test]
fn tristate_distinguishes_unset_from_off() {
assert_eq!(tristate(None), -1);
assert_eq!(tristate(Some(false)), 0);
assert_eq!(tristate(Some(true)), 1);
}
#[test]
fn every_encoded_record_carries_the_fields_java_parses() {
let elements = vec![
AccessibilityElement {
node_id: 4,
label: "Haptics".into(),
state_description: Some("On".into()),
click_label: Some("Toggle".into()),
bounds: AccessibilityRect::new(1.0, 2.0, 30.0, 40.0),
role: AccessibilityRole::Switch,
clickable: true,
toggled: Some(true),
custom_actions: vec!["Pause".into(), "Resume".into()],
..AccessibilityElement::default()
},
element_with(5, Some(1)),
];
let payload = encode_elements(&elements, &[], 2.0);
let records: Vec<_> = payload.split('\n').collect();
assert_eq!(records.len(), 2);
for record in &records {
assert_eq!(
record.split('\t').count(),
RECORD_FIELDS,
"record: {record}"
);
}
let fields: Vec<_> = records[0].split('\t').collect();
assert_eq!(fields[1], "5", "Switch should encode as role 5");
assert_eq!(&fields[2..6], ["2", "4", "62", "84"]);
assert_eq!(fields[9], "Haptics");
assert_eq!(fields[11], "On");
assert_eq!(fields[12], "Toggle");
assert_eq!(fields[13], "-1", "selected was never set");
assert_eq!(fields[14], "1", "toggled on");
assert_eq!(fields[15], "1", "enabled by default");
assert_eq!(fields[16], format!("Pause{ACTION_SEPARATOR}Resume"));
assert_eq!(fields[17], "0", "this element registered no focus target");
assert_eq!(fields[18], "0", "and focus does not sit on it");
}
#[test]
fn the_record_carries_a_field_selection_in_utf16_units_and_nothing_for_the_rest() {
let elements = vec![
AccessibilityElement {
node_id: 4,
label: "Note".into(),
value: Some("añb😀c".into()),
text_selection: Some((8, 3)),
bounds: AccessibilityRect::new(1.0, 2.0, 30.0, 40.0),
role: AccessibilityRole::TextField,
..AccessibilityElement::default()
},
element_with(5, None),
];
let payload = encode_elements(&elements, &[], 1.0);
let records: Vec<Vec<_>> = payload
.split('\n')
.map(|record| record.split('\t').collect())
.collect();
assert_eq!(&records[0][39..41], ["5", "2"]);
assert_eq!(&records[1][39..41], ["-1", "-1"]);
}
#[test]
fn the_record_names_what_a_long_press_does() {
let elements = vec![
AccessibilityElement {
node_id: 4,
label: "Milk".into(),
long_click_label: Some("Remove receipt".into()),
bounds: AccessibilityRect::new(0.0, 0.0, 30.0, 40.0),
role: AccessibilityRole::Button,
clickable: true,
..AccessibilityElement::default()
},
save_button(5),
];
let payload = encode_elements(&elements, &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
let with_long_press: Vec<_> = records[0].split('\t').collect();
let plain: Vec<_> = records[1].split('\t').collect();
assert_eq!(with_long_press[36], "Remove receipt");
assert_eq!(plain[36], "", "a control with no long press says nothing");
}
#[test]
fn the_record_carries_whether_focus_can_land_on_a_control_and_whether_it_has() {
let elements = vec![
AccessibilityElement {
node_id: 4,
label: "Name".into(),
bounds: AccessibilityRect::new(0.0, 0.0, 30.0, 40.0),
role: AccessibilityRole::TextField,
focusable: true,
focused: true,
..AccessibilityElement::default()
},
AccessibilityElement {
node_id: 5,
label: "Save".into(),
bounds: AccessibilityRect::new(0.0, 50.0, 30.0, 40.0),
role: AccessibilityRole::Button,
clickable: true,
focusable: true,
..AccessibilityElement::default()
},
];
let payload = encode_elements(&elements, &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
let focused: Vec<_> = records[0].split('\t').collect();
let other: Vec<_> = records[1].split('\t').collect();
assert_eq!(focused[17], "1");
assert_eq!(focused[18], "1", "TalkBack follows the app onto this one");
assert_eq!(other[17], "1", "the button takes focus too");
assert_eq!(other[18], "0", "but focus does not sit on it");
}
fn save_button(node_id: cranpose_core::NodeId) -> AccessibilityElement {
AccessibilityElement {
node_id,
label: "Save".into(),
bounds: AccessibilityRect::new(0.0, 50.0, 30.0, 40.0),
role: AccessibilityRole::Button,
clickable: true,
..AccessibilityElement::default()
}
}
#[test]
fn the_record_names_the_list_above_a_row() {
let elements = vec![
AccessibilityElement {
node_id: 6,
bounds: AccessibilityRect::new(0.0, 0.0, 400.0, 600.0),
vertical_scroll: Some(cranpose_ui::ScrollAxisRange::new(0.0, 900.0, false)),
..AccessibilityElement::default()
},
AccessibilityElement {
node_id: 9,
label: "Milk".into(),
bounds: AccessibilityRect::new(0.0, 10.0, 400.0, 40.0),
scroll_parent: Some(6),
..AccessibilityElement::default()
},
save_button(8),
];
let payload = encode_elements(&elements, &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
let list: Vec<_> = records[0].split('\t').collect();
let row: Vec<_> = records[1].split('\t').collect();
let button: Vec<_> = records[2].split('\t').collect();
assert_eq!(list[26], "-1", "the list sits under the host");
assert_eq!(row[26], list[0], "the row names its list by virtual id");
assert_eq!(
button[26], "-1",
"a button outside any list sits under the host"
);
}
#[test]
fn the_record_says_which_way_a_list_can_still_page() {
let elements = vec![
AccessibilityElement {
node_id: 6,
bounds: AccessibilityRect::new(0.0, 0.0, 400.0, 600.0),
vertical_scroll: Some(cranpose_ui::ScrollAxisRange::new(0.0, 900.0, false)),
..AccessibilityElement::default()
},
AccessibilityElement {
node_id: 7,
bounds: AccessibilityRect::new(0.0, 0.0, 400.0, 600.0),
vertical_scroll: Some(cranpose_ui::ScrollAxisRange::new(900.0, 900.0, false)),
..AccessibilityElement::default()
},
save_button(8),
];
let payload = encode_elements(&elements, &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
let top: Vec<_> = records[0].split('\t').collect();
let bottom: Vec<_> = records[1].split('\t').collect();
let button: Vec<_> = records[2].split('\t').collect();
assert_eq!(top[23], "1", "a list is scrollable");
assert_eq!(top[24], "1", "at the top it pages forward");
assert_eq!(top[25], "0", "and not back");
assert_eq!(bottom[24], "0", "at the end it pages back only");
assert_eq!(bottom[25], "1");
assert_eq!(button[23], "0", "a button does not scroll");
}
#[test]
fn the_record_carries_the_range_of_an_adjustable_control() {
let elements = vec![
AccessibilityElement {
node_id: 4,
label: "Volume".into(),
bounds: AccessibilityRect::new(0.0, 0.0, 200.0, 40.0),
progress: Some(cranpose_ui::ProgressBarRangeInfo::new(0.25, 0.0, 1.0, 0)),
adjustable: true,
..AccessibilityElement::default()
},
save_button(5),
];
let payload = encode_elements(&elements, &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
let slider: Vec<_> = records[0].split('\t').collect();
let button: Vec<_> = records[1].split('\t').collect();
assert_eq!(slider[19], "1", "TalkBack may move this one");
assert_eq!(slider[20], "0.25");
assert_eq!(slider[21], "0");
assert_eq!(slider[22], "1");
assert_eq!(button[19], "0", "a button holds no range");
}
#[test]
fn the_record_says_how_many_rows_a_list_holds() {
let mut list = AccessibilityElement {
node_id: 6,
bounds: AccessibilityRect::new(0.0, 0.0, 400.0, 600.0),
vertical_scroll: Some(cranpose_ui::ScrollAxisRange::new(0.0, 900.0, false)),
..AccessibilityElement::default()
};
list.collection = Some(cranpose_ui::CollectionInfo {
rows: 12,
columns: 1,
});
let payload = encode_elements(&[list, save_button(8)], &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
let list: Vec<_> = records[0].split('\t').collect();
let button: Vec<_> = records[1].split('\t').collect();
assert_eq!(
(list[27], list[28]),
("12", "1"),
"the list says its rows and columns"
);
assert_eq!((button[27], button[28]), ("0", "0"), "a button is no list");
}
#[test]
fn the_record_flags_a_control_that_says_something_new() {
let flagged = encode_elements(&[save_button(8)], &[true], 1.0);
let quiet = encode_elements(&[save_button(8)], &[], 1.0);
assert_eq!(
flagged.split('\t').nth(29),
Some("1"),
"the button says something new"
);
assert_eq!(
quiet.split('\t').nth(29),
Some("0"),
"the button reads as before"
);
}
#[test]
fn the_record_places_a_tab_in_its_group() {
let mut tab = save_button(8);
tab.selected = Some(true);
tab.collection_item = Some(CollectionItem {
position: 2,
count: 5,
horizontal: true,
});
let payload = encode_elements(&[tab, save_button(9)], &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
let placed: Vec<_> = records[0].split('\t').collect();
let loose: Vec<_> = records[1].split('\t').collect();
assert_eq!(
(placed[30], placed[31]),
("0", "1"),
"the second tab of a row is column one"
);
assert_eq!(
(loose[30], loose[31]),
("-1", "-1"),
"a button outside a group has no place"
);
}
#[test]
fn the_record_carries_the_title_of_a_pane() {
let mut screen = AccessibilityElement {
node_id: 1,
bounds: AccessibilityRect::new(0.0, 0.0, 300.0, 600.0),
..AccessibilityElement::default()
};
screen.pane_title = Some("Library".into());
let payload = encode_elements(&[screen, save_button(8)], &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
assert_eq!(records[0].split('\t').nth(32), Some("Library"));
assert_eq!(
records[1].split('\t').nth(32),
Some(""),
"a button names no pane"
);
}
#[test]
fn the_record_says_why_a_field_is_wrong() {
let mut field = save_button(8);
field.error = Some("needs a number".into());
let payload = encode_elements(&[field, save_button(9)], &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
assert_eq!(records[0].split('\t').nth(33), Some("needs a number"));
assert_eq!(
records[1].split('\t').nth(33),
Some(""),
"a sound control names no error"
);
}
#[test]
fn the_record_marks_a_field_that_holds_a_secret() {
let mut field = save_button(8);
field.password = true;
let payload = encode_elements(&[field, save_button(9)], &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
assert_eq!(records[0].split('\t').nth(34), Some("1"));
assert_eq!(records[1].split('\t').nth(34), Some("0"));
}
#[test]
fn the_record_says_whether_a_control_is_open() {
let mut open = save_button(8);
open.expanded = Some(true);
let mut closed = save_button(9);
closed.expanded = Some(false);
let payload = encode_elements(&[open, closed, save_button(10)], &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
assert_eq!(records[0].split('\t').nth(35), Some("1"));
assert_eq!(records[1].split('\t').nth(35), Some("0"));
assert_eq!(
records[2].split('\t').nth(35),
Some("-1"),
"a plain button opens nothing"
);
}
#[test]
fn the_record_says_whether_a_control_has_a_way_out() {
let mut row = save_button(8);
row.dismissable = true;
let payload = encode_elements(&[row, save_button(9)], &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
assert_eq!(records[0].split('\t').nth(37), Some("1"));
assert_eq!(
records[1].split('\t').nth(37),
Some("0"),
"a plain button says nothing about a way out"
);
}
#[test]
fn the_record_says_whether_a_list_takes_a_row_number() {
let mut list = save_button(8);
list.scroll_to_index = true;
let payload = encode_elements(&[list, save_button(9)], &[], 1.0);
let records: Vec<_> = payload.split('\n').collect();
assert_eq!(records[0].split('\t').nth(38), Some("1"));
assert_eq!(
records[1].split('\t').nth(38),
Some("0"),
"a plain button takes no row number"
);
}
#[test]
fn numbers_are_written_as_display_writes_them() {
for value in [0, 7, -7, 10, 1_234_567, i32::MAX, i32::MIN] {
let mut out = String::new();
push_decimal(&mut out, value);
assert_eq!(out, value.to_string());
}
for value in [
0.0f32,
-0.0,
3.0,
-12.0,
12.5,
0.1,
16_777_216.0,
1e30,
f32::NAN,
f32::INFINITY,
] {
let mut out = String::new();
RecordWriter(&mut out).float(value);
assert_eq!(out, format!("{value}\t"));
}
assert_eq!(count(usize::MAX), i32::MAX);
assert_eq!(count(12), 12);
}