cranpose-app-shell 0.1.143

Application orchestration shell for Cranpose
Documentation
use cranpose_ui::{Rect, SemanticsRole};

use super::*;

fn node(label: Option<&str>, rect: Rect) -> PlacedSemanticsNode {
    PlacedSemanticsNode {
        node_id: 0,
        role: SemanticsRole::Unknown,
        widget_role: None,
        label: label.map(str::to_string),
        state_description: None,
        clickable: false,
        interactive: false,
        toggled: None,
        selected: None,
        enabled: true,
        editable_text: false,
        focusable: false,
        hidden: false,
        pane_title: None,
        traversal_index: 0.0,
        list_position: None,
        layout_bounds: rect,
        touch_bounds: None,
        children: Vec::new(),
    }
}

fn button(label: Option<&str>, rect: Rect) -> PlacedSemanticsNode {
    PlacedSemanticsNode {
        widget_role: Some(SemanticsWidgetRole::Button),
        clickable: true,
        focusable: true,
        ..node(label, rect)
    }
}

fn rect(x: f32, y: f32, width: f32, height: f32) -> Rect {
    Rect {
        x,
        y,
        width,
        height,
    }
}

fn screen(children: Vec<PlacedSemanticsNode>) -> PlacedSemanticsNode {
    PlacedSemanticsNode {
        pane_title: Some("Library".to_string()),
        children,
        ..node(None, rect(0.0, 0.0, 400.0, 800.0))
    }
}

fn kinds(root: &PlacedSemanticsNode) -> Vec<AccessibilityIssueKind> {
    audit_accessibility(root)
        .into_iter()
        .map(|issue| issue.kind)
        .collect()
}

#[test]
fn a_named_screen_with_named_large_controls_in_order_passes() {
    let root = screen(vec![
        button(Some("Send"), rect(0.0, 0.0, 120.0, 48.0)),
        button(Some("Cancel"), rect(0.0, 60.0, 120.0, 48.0)),
    ]);
    assert!(audit_accessibility(&root).is_empty());
    assert_accessible(&root);
}

#[test]
fn a_control_with_nothing_to_say_is_named_as_such() {
    let root = screen(vec![button(None, rect(0.0, 0.0, 48.0, 48.0))]);
    let issues = audit_accessibility(&root);
    assert_eq!(issues.len(), 1);
    assert_eq!(
        issues[0].to_string(),
        "NoName: button \"\" is 48x48 points at (0, 0)"
    );
}

#[test]
fn the_words_inside_a_control_are_its_name() {
    let mut send = button(None, rect(0.0, 0.0, 120.0, 48.0));
    send.children.push(PlacedSemanticsNode {
        role: SemanticsRole::Text {
            value: "Send".to_string(),
        },
        ..node(Some("Send"), rect(8.0, 8.0, 40.0, 20.0))
    });
    assert!(audit_accessibility(&screen(vec![send])).is_empty());
}

#[test]
fn an_independent_child_cannot_name_its_parent() {
    let mut parent = button(None, rect(0.0, 0.0, 120.0, 48.0));
    parent
        .children
        .push(button(Some("Delete"), rect(0.0, 0.0, 48.0, 48.0)));
    assert_eq!(spoken_name(&parent), "");
    assert!(kinds(&screen(vec![parent])).contains(&AccessibilityIssueKind::NoName));
}

#[test]
fn an_explicit_empty_name_is_not_replaced_with_descendant_words() {
    let mut parent = button(Some(""), rect(0.0, 0.0, 120.0, 48.0));
    parent
        .children
        .push(node(Some("Independent action"), rect(0.0, 0.0, 48.0, 48.0)));
    assert_eq!(spoken_name(&parent), "");
}

#[test]
fn two_controls_with_one_name_and_one_role_are_reported_once() {
    let root = screen(vec![
        button(Some("Delete"), rect(0.0, 0.0, 120.0, 48.0)),
        button(Some("Delete"), rect(0.0, 60.0, 120.0, 48.0)),
    ]);
    let issues = audit_accessibility(&root);
    assert_eq!(issues.len(), 1);
    assert_eq!(
        issues[0].to_string(),
        "SameName: button \"Delete\" appears 2 times"
    );
}

#[test]
fn a_small_target_says_its_size() {
    let root = screen(vec![button(Some("Close"), rect(0.0, 0.0, 16.0, 16.0))]);
    let issues = audit_accessibility(&root);
    assert_eq!(
        issues[0].to_string(),
        "SmallTarget: button \"Close\" is 16x16 points"
    );
}

#[test]
fn a_touch_target_counts_over_the_drawn_size() {
    let mut close = button(Some("Close"), rect(0.0, 0.0, 16.0, 16.0));
    close.touch_bounds = Some(rect(-16.0, -16.0, 48.0, 48.0));
    assert!(audit_accessibility(&screen(vec![close])).is_empty());
}

#[test]
fn a_control_placed_above_the_one_before_it_is_out_of_order() {
    let root = screen(vec![
        button(Some("Second"), rect(0.0, 100.0, 120.0, 48.0)),
        button(Some("First"), rect(0.0, 0.0, 120.0, 48.0)),
    ]);
    let issues = audit_accessibility(&root);
    assert_eq!(kinds(&root), vec![AccessibilityIssueKind::OutOfOrder]);
    assert_eq!(
        issues[0].to_string(),
        "OutOfOrder: button \"First\" sits above \"Second\""
    );
    let mut on_purpose = root.clone();
    on_purpose.children[1].traversal_index = -1.0;
    assert!(audit_accessibility(&on_purpose).is_empty());
}

#[test]
fn a_screen_without_a_title_and_a_picture_without_words_are_reported() {
    let mut root = node(None, rect(0.0, 0.0, 400.0, 800.0));
    root.children.push(PlacedSemanticsNode {
        widget_role: Some(SemanticsWidgetRole::Image),
        ..node(None, rect(0.0, 0.0, 64.0, 64.0))
    });
    assert_eq!(
        kinds(&root),
        vec![
            AccessibilityIssueKind::UnnamedImage,
            AccessibilityIssueKind::NoPaneTitle
        ]
    );
}

#[test]
fn a_hidden_control_is_left_alone() {
    let root = screen(vec![PlacedSemanticsNode {
        hidden: true,
        ..button(None, rect(0.0, 0.0, 10.0, 10.0))
    }]);
    assert!(audit_accessibility(&root).is_empty());
}

#[test]
#[should_panic(expected = "NoName: button \"\"")]
fn the_assertion_names_every_issue_and_the_fix() {
    assert_accessible(&screen(vec![button(None, rect(0.0, 0.0, 48.0, 48.0))]));
}

#[test]
fn a_new_issue_and_a_listed_issue_that_went_away_both_fail_the_comparison() {
    let known: &[KnownIssue] = &[
        (
            "library",
            "SmallTarget: control \"Pill\"",
            "sits over a tab",
        ),
        (
            "*",
            "SameName: tab \"Apps\"",
            "two bars show one set of tabs",
        ),
    ];
    let issues = vec![
        "SmallTarget: control \"Pill\" is 79x17 points".to_string(),
        "SameName: tab \"Apps\" appears 2 times".to_string(),
    ];
    assert_eq!(audit_changes("library", &issues, known), Ok(()));
    assert_eq!(
        audit_changes("settings", &issues[1..], known),
        Ok(()),
        "a * entry matches on every screen"
    );
    let with_new = [
        issues.clone(),
        vec!["NoName: control \"\" is 40x40 points at (1, 2)".to_string()],
    ]
    .concat();
    let report = audit_changes("library", &with_new, known).unwrap_err();
    assert!(report.contains("new accessibility issues on library"));
    assert!(report.contains("NoName"));
    let report = audit_changes("library", &issues[1..], known).unwrap_err();
    assert!(report.contains("went away"));
    assert!(report.contains("SmallTarget"));
    assert_eq!(
        audit_changes("settings", &[], known),
        Ok(()),
        "a * entry is never reported as gone"
    );
}

#[test]
fn rows_of_a_list_with_one_name_are_apart_when_each_has_its_place() {
    let mut list = screen(vec![
        button(Some("Scan"), rect(0.0, 0.0, 200.0, 40.0)),
        button(Some("Scan"), rect(0.0, 50.0, 200.0, 40.0)),
    ]);
    let same_names = |root: &PlacedSemanticsNode| {
        audit_accessibility(root)
            .iter()
            .any(|issue| issue.kind == AccessibilityIssueKind::SameName)
    };
    assert!(same_names(&list), "two buttons with one name and no place");
    for (row, position) in list.children.iter_mut().zip(1..) {
        row.list_position = Some(position);
    }
    assert!(!same_names(&list), "the same two as rows 1 and 2 of a list");
}

#[test]
fn a_row_wrapped_by_its_list_still_gets_its_place() {
    let wrapped = |y: f32| PlacedSemanticsNode {
        children: vec![button(Some("Scan"), rect(0.0, y, 200.0, 40.0))],
        ..node(None, rect(0.0, y, 200.0, 40.0))
    };
    let mut list = screen(vec![wrapped(0.0), wrapped(50.0)]);
    for (row, position) in list.children.iter_mut().zip(1..) {
        crate::placed_semantics::place_row(row, position);
    }
    let issues = audit_accessibility(&list);
    assert!(
        !issues
            .iter()
            .any(|issue| issue.kind == AccessibilityIssueKind::SameName),
        "the buttons under the wrappers carry places 1 and 2: {issues:?}"
    );
}

#[test]
fn a_control_inside_a_row_carries_the_place_of_the_row() {
    let row = |y: f32| PlacedSemanticsNode {
        children: vec![
            button(Some("page thumbnail"), rect(0.0, y, 40.0, 40.0)),
            button(Some("More"), rect(160.0, y, 40.0, 40.0)),
        ],
        ..node(None, rect(0.0, y, 200.0, 40.0))
    };
    let mut list = screen(vec![row(0.0), row(50.0)]);
    for (row, position) in list.children.iter_mut().zip(1..) {
        crate::placed_semantics::place_row(row, position);
    }
    let issues = audit_accessibility(&list);
    assert!(
        !issues
            .iter()
            .any(|issue| issue.kind == AccessibilityIssueKind::SameName),
        "the thumbnails and the More buttons sit in rows 1 and 2: {issues:?}"
    );
}