| name | accessibility-testing |
| description | Expert guidance on automated and manual accessibility testing for mobile apps — Espresso AccessibilityChecks, XCTest accessibility audits, Flutter a11y guidelines, RN a11y assertions. Use when adding a11y coverage or when asked how to catch accessibility regressions in CI. |
Accessibility Testing
Instructions
Accessibility bugs are defects, not polish items. A tap target that is 24 dp square, an icon button with no content description, or a form field unreachable by VoiceOver is a shipped regression. Cover them with automated checks in CI plus a small set of manual heuristics for each critical flow.
1. What Automation Can Catch
Automated scanners reliably catch:
- Missing content descriptions / accessibility labels on interactive elements.
- Contrast ratios below WCAG AA for known text colors.
- Touch targets below the platform minimum (48 dp Android, 44 pt iOS).
- Duplicate or confusing focus order (platform-dependent).
- Form fields without labels.
They do not catch: whether the label makes sense, whether the reading order is logical, whether animations trigger vestibular issues. That is the manual pass.
2. Android — Espresso / Compose
Enable AccessibilityChecks for the whole suite:
@BeforeClass @JvmStatic fun enableA11yChecks() {
AccessibilityChecks.enable().setRunChecksFromRootView(true)
}
For Compose, use SemanticsMatcher and assertContentDescriptionEquals:
composeRule.onNodeWithTag("pay")
.assertContentDescriptionEquals("Pay 19.99 USD")
.assert(hasClickAction())
For Views, AccessibilityNodeInfo assertions are surfaced automatically by AccessibilityChecks — a failing check fails the test.
3. iOS — XCTest Accessibility Audit
iOS 17+ ships a native accessibility audit:
func test_checkout_isAccessible() throws {
let app = XCUIApplication(); app.launch()
app.buttons["openCheckout"].tap()
try app.performAccessibilityAudit()
}
Scope audits per screen, not per entire app launch — failures are easier to read. For SwiftUI unit-level checks, assert .accessibilityLabel, .accessibilityHint, and .accessibilityTraits on the rendered view hierarchy.
4. Flutter — flutter_test Guidelines
testWidgets('Checkout meets tap-target guidelines', (tester) async {
await tester.pumpWidget(const AppShell(child: CheckoutScreen()));
final handle = tester.ensureSemantics();
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
await expectLater(tester, meetsGuideline(textContrastGuideline));
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
handle.dispose();
});
Use Semantics(label: ...) wrappers in production code for icon-only widgets.
5. React Native
import { render } from '@testing-library/react-native';
test('Pay button has accessible label', () => {
const { getByRole } = render(<CheckoutScreen />);
const btn = getByRole('button', { name: /pay/i });
expect(btn).toBeTruthy();
expect(btn.props.accessibilityLabel).toBeDefined();
});
For contrast and tap size, rely on design-system tokens that enforce minimums at build time, plus a periodic manual sweep — RN does not have a first-class runtime a11y auditor.
6. Manual Heuristics
For every critical flow, before release:
- Screen reader sweep. Enable TalkBack / VoiceOver; complete the flow without looking at the screen. Every focusable element must announce a meaningful label.
- Dynamic type. Set the OS text size to the largest supported; verify no clipped labels, truncated buttons, or overlapping views.
- Contrast. Spot-check with a contrast checker on non-token surfaces (marketing screens, onboarding).
- Keyboard / switch control (iPadOS, Android with keyboard). The app must be usable without touch on large-screen surfaces.
- Reduce motion. Turn on "Reduce motion" / "Remove animations"; make sure the flow still works and is not disorienting.
7. Linting at Build Time
- Android Lint rules:
ContentDescription, LabelFor, ClickableViewAccessibility.
- SwiftLint / custom rule to forbid
Image(systemName:) inside a Button without .accessibilityLabel.
- Dart:
flutter analyze + custom lints (missing_contentdescription-style rules via dart_code_metrics).
- RN:
eslint-plugin-react-native-a11y.
Enable these in CI so regressions fail before tests even run.
8. Reporting Violations
Upload the a11y audit JSON (Android) / audit output (iOS) as a CI artifact. When a test fails, the report must name: element, rule, and how to fix (content description / larger target / higher contrast).
9. Checklist