mobile-a11y-test-author
Authors native mobile accessibility tests covering iOS (Accessibility Inspector, XCUITest `performAccessibilityAudit()` introduced in iOS 17, VoiceOver label/trait/hint verification) and Android (Espresso `AccessibilityChecks.enable()`, Accessibility Scanner, TalkBack traversal, `contentDescription` labelling) with WCAG-aligned checks for element labels, 44pt/48dp touch targets, contrast ratios, and focus order. Use when an iOS or Android app needs automated and manual accessibility test coverage beyond what `xcuitest-suite` or `espresso-suite` provide.
mobile-a11y-test-author
Overview
Native mobile accessibility testing spans two complementary layers:
This skill covers both layers on iOS and Android.
Nearest neighbors and differentiation:
When to use
iOS - automated audit with performAccessibilityAudit
Per the WWDC 2023 session "Perform accessibility audits for your app" ([developer.apple.com/videos/play/wwdc2023/10035][wwdc23]):
"Enables automated accessibility audits in UI tests... performs the same checks as Xcode's Accessibility Inspector tool."
Available from iOS 17. The test fails automatically if any audit issue is found (no explicit assertions needed).
Step 1 - Basic audit
import XCTest
final class HomeAccessibilityTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = true // report ALL issues, not just the first
XCUIApplication().launch()
}
func testHomeScreenAudit() throws {
try XCUIApplication().performAccessibilityAudit()
}
}continueAfterFailure = true is intentional here ([wwdc23][wwdc23]): the audit may find multiple issues per screen; stopping at the first hides the rest.
Step 2 - Scope the audit to specific categories
try app.performAccessibilityAudit(for: [.dynamicType, .contrast])Pass an XCUIAccessibilityAuditType option set. Documented audit types include .dynamicType and .contrast ([wwdc23][wwdc23]); passing no argument runs all available checks.
Step 3 - Suppress known false positives
try app.performAccessibilityAudit(for: [.contrast]) { issue in
// Ignore contrast issue on the watermark label (decorative, no contrast fix planned)
if let element = issue.element,
element.label == "WatermarkLabel",
issue.auditType == .contrast {
return true // suppress this issue
}
return false
}The closure receives an XCUIAccessibilityAuditIssue. Return true to suppress. Narrow suppressions by both auditType and element.label to avoid masking real regressions ([wwdc23][wwdc23]).
Step 4 - Cover each screen
Each performAccessibilityAudit() call inspects only the currently visible elements. Navigate to each distinct screen and run the audit:
func testCheckoutFlowAudit() throws {
let app = XCUIApplication()
app.launch()
// Screen 1
try app.performAccessibilityAudit()
app.buttons["place-order-button"].tap()
// Screen 2
try app.performAccessibilityAudit()
}iOS - VoiceOver label, trait, and hint checks
Per [UIAccessibility][uia] (Apple Developer Documentation):
Production code sets them; XCUITest verifies via XCUIElement.label:
// Production (UIKit)
let submitButton = UIButton()
submitButton.accessibilityLabel = "Submit order"
submitButton.accessibilityHint = "Places your order and charges the saved card"
submitButton.accessibilityTraits = [.button]// Test
func testSubmitButtonLabel() {
let btn = XCUIApplication().buttons["Submit order"]
XCTAssertTrue(btn.exists, "VoiceOver cannot find the Submit button")
XCTAssertEqual(btn.label, "Submit order")
}Touch target size (iOS 44pt minimum)
Per [UIAccessibility][uia]:
"Minimum recommended size: 44pt x 44pt."
Assert via XCUIElement.frame:
func testSubmitButtonTouchTarget() {
let frame = XCUIApplication().buttons["Submit order"].frame
XCTAssertGreaterThanOrEqual(frame.width, 44, "Touch target width below 44pt")
XCTAssertGreaterThanOrEqual(frame.height, 44, "Touch target height below 44pt")
}Accessibility Inspector (manual complement)
Run Accessibility Inspector (Xcode: Xcode menu > Open Developer Tool > Accessibility Inspector). It performs the same checks as performAccessibilityAudit() interactively, and supports inspection of real devices and simulators. Use it to diagnose issues found by the automated audit before writing a suppression ([wwdc23][wwdc23]).
Android - automated audit with AccessibilityChecks
Per [developer.android.com/training/testing/espresso/accessibility-checking][atf]:
"Checks run automatically when performing any view action defined in ViewActions. Each check includes the view on which the action is performed plus all descendant views."
Step 1 - Add dependency
// app/build.gradle
dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.6.1'
}Step 2 - Enable checks
import androidx.test.espresso.accessibility.AccessibilityChecks
@RunWith(AndroidJUnit4::class)
class CheckoutAccessibilityTest {
init {
AccessibilityChecks.enable().setRunChecksFromRootView(true)
}
@Test
fun applyPromoCode() {
onView(withId(R.id.promo_field)).perform(typeText("WELCOME10"), closeSoftKeyboard())
onView(withId(R.id.apply_button)).perform(click())
// AccessibilityChecks fires automatically on every perform() call
}
}setRunChecksFromRootView(true) evaluates the whole hierarchy, not just the interacted view ([atf][atf]).
Step 3 - Suppress known issues
AccessibilityChecks.enable().apply {
setSuppressingResultMatcher(
allOf(
matchesCheck(TextContrastCheck::class.java),
matchesViews(withId(R.id.decorative_watermark))
)
)
}The matcher must satisfy both the check type and the specific view ([atf][atf]).
Touch target size (Android 48dp minimum)
Per [developer.android.com/guide/topics/ui/accessibility/apps#large-controls][atgt]:
"For touch interfaces, we recommend that each interactive UI element have a focusable area, or touch target size, of at least 48dp x 48dp."
AccessibilityChecks validates this automatically on every perform() call.
Contrast thresholds
Per [developer.android.com/guide/topics/ui/accessibility/apps#text-visibility][contrast]:
AccessibilityChecks (via the Accessibility Test Framework) checks these thresholds on every perform() call.
Android - contentDescription labelling
Per [developer.android.com/guide/topics/ui/accessibility/apps#describe-ui-element][cd]:
"Convey purpose, not visual details."
// Icon-only button: set contentDescription
Icon(
imageVector = Icons.Filled.Share,
contentDescription = stringResource(R.string.label_share)
)
// Decorative image: suppress from accessibility
Icon(
imageVector = Icons.Filled.Decoration,
contentDescription = null // TalkBack skips this element
)Key rules ([cd][cd]):
Verify with Espresso:
onView(withId(R.id.share_button))
.check(matches(withContentDescription(R.string.label_share)))Android - TalkBack manual workflow
Per [developer.android.com/guide/topics/ui/accessibility/testing][at]:
Manual checklist:
CI integration
iOS (GitHub Actions)
jobs:
a11y-audit:
runs-on: macos-15
steps:
- uses: actions/checkout@v5
- run: |
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
-only-testing MyAppUITests/HomeAccessibilityTests \
-resultBundlePath A11yResults.xcresult
- uses: actions/upload-artifact@v4
if: always()
with:
name: a11y-xcresult
path: A11yResults.xcresultAndroid (GitHub Actions)
jobs:
a11y-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
script: ./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.CheckoutAccessibilityTest
- uses: actions/upload-artifact@v4
if: always()
with:
name: a11y-test-results
path: app/build/outputs/androidTest-resultsAnti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
continueAfterFailure = false in audit tests | Stops after the first issue; misses the rest on the same screen | Set continueAfterFailure = true for audit tests ([wwdc23][wwdc23]) |
| No suppression closure; globally ignoring an audit type | Hides all failures of that type, not just the known one | Suppress by both auditType and element.label (Step 3, iOS) |
Setting accessibilityLabel to the element type | "Submit button" - VoiceOver already announces "button" from traits | Set label to purpose only: "Submit order" ([uia][uia]) |
contentDescription on every Text composable | Redundant; TalkBack reads text content automatically | Omit for plain text; set only for icon-only or image elements ([cd][cd]) |
Running AccessibilityChecks without setRunChecksFromRootView(true) | Checks only the interacted view; off-screen violations pass | Enable root-view mode (Step 2, Android) ([atf][atf]) |
| Manual TalkBack only, no automated checks | Inconsistent; regressions slip in on refactors | Pair TalkBack manual review with AccessibilityChecks in CI |