Modern QA2026Мобильная доступность
Join

Course09 Mobile & Cross-Platform Testing

Cutting-edge · Chapter 09

Мобильная доступность

Updated Jul 2026

Мобильная доступность — это не веб-доступность

Мобильная доступность — не подмножество веб-доступности. Тач-интерфейсы, экранные чтецы и функции, специфичные для устройств, вводят совершенно другие требования. Веб-страница, которая блестяще проходит WCAG AA, может быть полностью недоступной при загрузке в мобильный WebView без надлежащей поддержки ARIA-моста.

VoiceOver (iOS) и TalkBack (Android)

Экранные чтецы на мобильных устройствах работают иначе, чем десктопные экранные чтецы. Пользователи навигируют свайпами (линейная навигация) или исследованием (касаясь экрана, чтобы услышать, что находится под пальцем). Оба паттерна должны работать.

Тестирование порядка навигации экранного чтеца

# 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']}'"

Тестирование меток доступности

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"

Размер области нажатия

WCAG 2.2 требует минимальный размер области нажатия 24x24 CSS-пикселя. Apple рекомендует 44x44 пойнта. Google рекомендует 48x48 dp. Это минимальные размеры — большие области всегда лучше.

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) и масштабирование шрифтов (Android)

Пользователи, увеличивающие системный размер шрифта, не должны наказываться сломанной вёрсткой. Это одновременно требование доступности и юридическое обязательство.

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"

Чеклист мобильной доступности

Проверка iOS Android Приоритет
Все интерактивные элементы подписаны VoiceOver читает label TalkBack читает contentDescription Критический
Порядок навигации логичен Порядок свайпа VoiceOver Порядок свайпа TalkBack Критический
Области нажатия >= минимума платформы 44x44 пойнта 48x48 dp Критический
Dynamic type / масштабирование шрифтов Тест при XXXL Тест при масштабе 2.0x Высокий
Цвет не единственный индикатор Проверка режимов для дальтоников Проверка режима высокой контрастности Высокий
Жесты имеют альтернативы Встряхивание для отмены имеет кнопку Свайп имеет альтернативу нажатием Средний
Управление фокусом в модальных окнах VoiceOver заблокирован в модальном окне TalkBack заблокирован в модальном окне Высокий
Пользовательские действия для сложных жестов Свайп для удаления имеет кнопку Долгое нажатие имеет меню Средний
Live regions для динамического контента UIAccessibility.post(.announcement) AccessibilityLiveRegion Высокий
Уважение уменьшенного движения UIAccessibilityIsReduceMotionEnabled Settings.Secure.TRANSITION_ANIMATION_SCALE Средний

Ключевые выводы

  • Выбор устройств — решение на основе данных -- используйте вашу аналитику для построения матрицы устройств, покрывающей 90% пользователей минимальным количеством устройств.
  • Тестирование адаптивности — это не только скриншоты -- AI-агенты могут рассуждать о функциональной корректности вёрстки, а не только о визуальном сходстве.
  • Тестирование PWA требует мышления «офлайн в первую очередь» -- тестируйте кэширование service worker, push-уведомления и промпты установки как первоклассные функции.
  • Appium эволюционирует в сторону AI-нативных паттернов -- поиск элементов по изображению и самовосстанавливающиеся локаторы снижают бремя обслуживания.
  • Облачные фермы устройств необходимы для масштаба -- ни одна команда не может поддерживать физическое разнообразие устройств, необходимое для уверенных релизов.
  • Мобильная доступность не подлежит обсуждению -- тестируйте области нажатия, порядок навигации экранного чтеца и dynamic type при максимальных настройках.

Тезис для собеседования

«Мой подход к мобильному и кроссплатформенному тестированию начинается с данных: я извлекаю аналитику по устройствам и браузерам для построения трёхуровневой матрицы тестирования. Устройства Уровня 1 — обычно 3-5, покрывающие 60% пользователей — проходят полную регрессию при каждом PR. Уровень 2 проходит критические пути еженедельно. Уровень 3 получает ежемесячные смоук-тесты. Для адаптивного дизайна я выхожу за рамки сравнения скриншотов, поручая AI-агентам изменять viewport и проверять функциональную корректность — например, подтверждать появление гамбургер-меню на мобильных ширинах и доступность всех элементов навигации. Для нативного мобильного тестирования я использую Appium на облачных фермах устройств, таких как BrowserStack, и слежу за эволюцией Appium в сторону AI-нативного тестирования, где поиск элементов по изображению и самовосстанавливающиеся локаторы снижают бремя обслуживания, которое исторически делало мобильную автоматизацию хрупкой. Я всегда включаю мобильную доступность в свои тест-планы: порядок обхода VoiceOver, минимальные области нажатия 44pt на iOS и тестирование dynamic type при максимальном масштабе шрифта.»