QA Engineer Skills 2026QA-2026Mobile Accessibility Testing

Mobile Accessibility Testing

Mobile Accessibility Is Not Web Accessibility

Mobile accessibility is not a subset of web accessibility. Touch interfaces, screen readers, and device-specific features introduce entirely different requirements. A web page that passes WCAG AA with flying colors may be completely inaccessible when loaded in a mobile WebView that lacks proper ARIA bridge support.


VoiceOver (iOS) and TalkBack (Android)

Screen readers on mobile work differently from desktop screen readers. Users navigate by swiping (linear navigation) or by exploring (touching the screen to hear what is under their finger). Both patterns must work.

Testing Screen Reader Navigation Order

# Testing screen reader accessibility
def test_voiceover_navigation_order(driver):
    """Verify that VoiceOver reads elements in logical order."""

    # Enable accessibility inspection
    elements = driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeAny")

    # Filter to accessible elements
    accessible = [
        {
            "label": el.get_attribute("label"),
            "trait": el.get_attribute("trait"),
            "value": el.get_attribute("value"),
            "frame": el.rect,
        }
        for el in elements
        if el.get_attribute("accessible") == "true"
    ]

    # Verify reading order follows visual layout (top-to-bottom, left-to-right)
    for i in range(len(accessible) - 1):
        current = accessible[i]
        next_el = accessible[i + 1]
        # Next element should be below or to the right
        assert (
            next_el["frame"]["y"] > current["frame"]["y"] or
            (next_el["frame"]["y"] == current["frame"]["y"] and
             next_el["frame"]["x"] >= current["frame"]["x"])
        ), f"Reading order violation: '{current['label']}' before '{next_el['label']}'"

Testing Accessibility Labels

def test_all_interactive_elements_have_labels(driver):
    """Every interactive element must have an accessibility label."""
    buttons = driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeButton")
    text_fields = driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeTextField")
    switches = driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeSwitch")

    for element in buttons + text_fields + switches:
        label = element.get_attribute("label") or element.get_attribute("name")
        assert label and len(label) > 0, \
            f"Interactive element at ({element.rect}) has no accessibility label"
        # Labels should not be technical identifiers
        assert not label.startswith("btn_"), \
            f"Label '{label}' looks like a technical ID, not a human description"
        assert not label.startswith("ic_"), \
            f"Label '{label}' looks like an icon identifier, not a description"

def test_images_have_content_descriptions(driver):
    """All meaningful images must have content descriptions (Android)."""
    images = driver.find_elements(AppiumBy.CLASS_NAME, "android.widget.ImageView")

    for img in images:
        content_desc = img.get_attribute("contentDescription")
        # Decorative images can have empty descriptions
        # but must explicitly set importantForAccessibility="no"
        important = img.get_attribute("importantForAccessibility")
        if important != "no":
            assert content_desc and len(content_desc) > 0, \
                f"Image at ({img.rect}) has no content description"

Touch Target Sizing

WCAG 2.2 requires a minimum touch target size of 24x24 CSS pixels. Apple recommends 44x44 points. Google recommends 48x48 dp. These are minimum sizes -- larger targets are always better.

def test_minimum_touch_target_size(driver):
    """All interactive elements must meet minimum touch target requirements."""
    # Platform-specific minimum sizes
    platform = driver.capabilities.get("platformName", "").lower()
    min_size = 44 if platform == "ios" else 48  # points (iOS) or dp (Android)

    interactive_elements = (
        driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeButton") +
        driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeLink") +
        driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeSwitch")
    )

    violations = []
    for el in interactive_elements:
        rect = el.rect
        if rect["width"] < min_size or rect["height"] < min_size:
            label = el.get_attribute("label") or "unknown"
            violations.append(
                f"'{label}' is {rect['width']}x{rect['height']}, "
                f"minimum is {min_size}x{min_size}"
            )

    assert len(violations) == 0, \
        f"Touch target violations:\n" + "\n".join(violations)

def test_touch_targets_have_adequate_spacing(driver):
    """Touch targets should have at least 8dp spacing between them."""
    min_spacing = 8
    buttons = driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeButton")
    rects = [(b, b.rect) for b in buttons if b.is_displayed()]

    violations = []
    for i, (el_a, rect_a) in enumerate(rects):
        for el_b, rect_b in rects[i+1:]:
            # Calculate gap between elements
            h_gap = max(0, max(rect_b["x"] - (rect_a["x"] + rect_a["width"]),
                               rect_a["x"] - (rect_b["x"] + rect_b["width"])))
            v_gap = max(0, max(rect_b["y"] - (rect_a["y"] + rect_a["height"]),
                               rect_a["y"] - (rect_b["y"] + rect_b["height"])))

            if h_gap < min_spacing and v_gap < min_spacing:
                label_a = el_a.get_attribute("label") or "?"
                label_b = el_b.get_attribute("label") or "?"
                violations.append(
                    f"'{label_a}' and '{label_b}' are only "
                    f"{min(h_gap, v_gap)}dp apart (minimum {min_spacing}dp)"
                )

    assert len(violations) == 0, \
        f"Spacing violations:\n" + "\n".join(violations[:10])

Dynamic Type (iOS) and Font Scaling (Android)

Users who increase system font size must not be punished with broken layouts. This is both an accessibility requirement and a legal obligation.

def test_dynamic_type_does_not_break_layout(driver):
    """App must remain usable at largest dynamic type setting."""

    # Set accessibility font scale to maximum
    if driver.capabilities["platformName"] == "iOS":
        # iOS Dynamic Type: AX_EXTRA_EXTRA_EXTRA_LARGE
        driver.execute_script("mobile: configureLocalization", {
            "accessibilityPreferences": {
                "preferredContentSizeCategory": "UICTContentSizeCategoryAccessibilityExtraExtraExtraLarge"
            }
        })
    else:
        # Android: maximum font scale is typically 2.0
        driver.execute_script("mobile: shell", {
            "command": "settings",
            "args": ["put", "system", "font_scale", "2.0"]
        })

    # Navigate through critical screens
    screens = ["home", "search", "product-detail", "cart", "checkout"]
    for screen_id in screens:
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, f"{screen_id}-tab").click()

        # Verify no text is clipped (truncated with ...)
        text_elements = driver.find_elements(AppiumBy.CLASS_NAME, "XCUIElementTypeStaticText")
        for text_el in text_elements:
            label = text_el.get_attribute("label") or ""
            # Check that critical text is not truncated
            assert not label.endswith("...") or len(label) > 20, \
                f"Text appears truncated at large font: '{label}' on {screen_id}"

        # Verify no overlapping elements
        all_rects = [el.rect for el in text_elements if el.is_displayed()]
        for i, r1 in enumerate(all_rects):
            for r2 in all_rects[i+1:]:
                overlap = (
                    r1["x"] < r2["x"] + r2["width"] and
                    r1["x"] + r1["width"] > r2["x"] and
                    r1["y"] < r2["y"] + r2["height"] and
                    r1["y"] + r1["height"] > r2["y"]
                )
                if overlap:
                    overlap_area = (
                        min(r1["x"]+r1["width"], r2["x"]+r2["width"]) - max(r1["x"], r2["x"])
                    ) * (
                        min(r1["y"]+r1["height"], r2["y"]+r2["height"]) - max(r1["y"], r2["y"])
                    )
                    assert overlap_area < 100, \
                        f"Significant element overlap detected on {screen_id} screen"

Mobile Accessibility Checklist

Check iOS Android Priority
All interactive elements labeled VoiceOver reads label TalkBack reads contentDescription Critical
Navigation order is logical VoiceOver swipe order TalkBack swipe order Critical
Touch targets >= platform minimum 44x44 points 48x48 dp Critical
Dynamic type / font scaling Test at XXXL Test at 2.0x scale High
Color is not sole indicator Check color-blind modes Check high-contrast mode High
Gestures have alternatives Shake-to-undo has button Swipe has tap alternative Medium
Focus management in modals VoiceOver trapped in modal TalkBack trapped in modal High
Custom actions for complex gestures Swipe-to-delete has button Long-press has menu Medium
Live regions for dynamic content UIAccessibility.post(.announcement) AccessibilityLiveRegion High
Reduced motion respected UIAccessibilityIsReduceMotionEnabled Settings.Secure.TRANSITION_ANIMATION_SCALE Medium

Key Takeaways

  • Device selection is a data-driven decision -- use your analytics to build a device matrix that covers 90% of users with the minimum number of devices.
  • Responsive testing is not just screenshots -- AI agents can reason about whether a layout is functionally correct, not just visually similar.
  • PWA testing requires offline-first thinking -- test service worker caching, push notifications, and install prompts as first-class features.
  • Appium is evolving toward AI-native patterns -- image-based element finding and self-healing locators reduce maintenance burden.
  • Cloud device farms are essential for scale -- no team can maintain the physical device diversity needed for confident releases.
  • Mobile accessibility is non-negotiable -- test touch targets, screen reader navigation order, and dynamic type at maximum settings.

Interview Talking Point

"My approach to mobile and cross-platform testing starts with data: I pull device and browser analytics to build a three-tier testing matrix. Tier 1 devices -- typically 3 to 5 covering 60% of users -- run full regression on every PR. Tier 2 runs critical paths weekly. Tier 3 gets monthly smoke tests. For responsive design, I go beyond screenshot comparison by having AI agents resize viewports and verify functional correctness -- like confirming the hamburger menu appears at mobile widths and all navigation items remain accessible. For native mobile, I use Appium on cloud device farms like BrowserStack, and I am tracking Appium's evolution toward AI-native testing where image-based element finding and self-healing locators reduce the maintenance burden that has historically made mobile automation brittle. I always include mobile accessibility in my test plans: VoiceOver traversal order, minimum touch targets of 44pt on iOS, and dynamic type testing at maximum font scale."