genUiSemanticsAudit function

List<GenUiAuditFinding> genUiSemanticsAudit(
  1. Map<String, List<GenUiSemanticNode>> recorded, {
  2. Set<String> allowEmpty = const <String>{},
})

Reads a recording made by genUiSemantics and reports what a person using a screen reader could not work with.

Written to run over the same recording the golden test keeps, so a catalog gets its accessibility checked by the file it already has rather than by a second pass nobody remembers to run.

final findings = genUiSemanticsAudit(recorded);
expect(findings, isEmpty, reason: findings.join('\n'));

allowEmpty names the components that are decoration and are meant to expose nothing, so that the rule stays sharp for every other component.

Implementation

List<GenUiAuditFinding> genUiSemanticsAudit(
  Map<String, List<GenUiSemanticNode>> recorded, {
  Set<String> allowEmpty = const <String>{},
}) {
  final findings = <GenUiAuditFinding>[];

  for (final entry in recorded.entries) {
    final String component = entry.key;
    final List<GenUiSemanticNode> nodes = entry.value;

    if (nodes.isEmpty) {
      if (!allowEmpty.contains(component)) {
        findings.add(
          GenUiAuditFinding(
            GenUiAuditRule.exposesNothing,
            component: component,
          ),
        );
      }
      continue;
    }

    final controlNames = <String>[];
    for (final node in nodes) {
      final bool operable = node.actions.any(_operableActions.contains);
      // A tooltip counts as a name here. It is weaker than one — Android
      // announces it, other platforms are less reliable — and the recording
      // shows which of the two a control has, so the distinction stays
      // visible without failing a build over it.
      if (node.name.isEmpty && node.tooltip.isEmpty) {
        if (operable) {
          findings.add(
            GenUiAuditFinding(
              GenUiAuditRule.unnamedControl,
              component: component,
              detail: node.role,
            ),
          );
        } else if (node.role != 'text' && node.role != 'group') {
          findings.add(
            GenUiAuditFinding(
              GenUiAuditRule.unnamedNode,
              component: component,
              detail: node.role,
            ),
          );
        }
      } else if (operable) {
        controlNames.add(node.name.isEmpty ? node.tooltip : node.name);
      }
    }

    final seen = <String>{};
    for (final name in controlNames) {
      if (!seen.add(name)) {
        findings.add(
          GenUiAuditFinding(
            GenUiAuditRule.ambiguousControls,
            component: component,
            detail: name,
          ),
        );
      }
    }
  }

  return findings;
}