Мобильная доступность
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 при максимальном масштабе шрифта.»