Новенькое с WWDC23: Отчёты о тестировании и аудит доступности

Вот и закончилась очередная конференция Apple WWDC23, на которой ребята из Apple рассказали об изменениях Xcode и Swift.  

Я — Никита Коробейников, iOS Team Lead в Surf. Уже поставил бета-версию Xcode 15 и проверил на автотестах материал с сессий Fix failures faster with Xcode test reports и Perform accessibility audits for your app. Рассказываю, почему разработчикам гораздо удобнее теперь делать приложение доступным.

Небольшое отступление

В нашей студийной библиотеке RDDM есть Example-проект: в нем демонстрируются различные кейсы для наполнения и расширения функциональности UITableView и UICollectionView. Проект немаленький: в нем около сорока экранов и есть автотесты.

Reactive Data Display Manager

Reactive Data Display Manager

Мы активно используем RDDM в продакшене, поэтому ещё давно написали автотесты, контролирующие работу библиотеки. А в roadmap библиотеки была задача о поддержке доступности (accessibility) в Example-проекте. 

Мы не знали тем выступлений WWDC заранее, но изменения в отчётах о тестировании и авто-аудит доступности (accessibility) были как будто спроектированы специально для наших задач. 

Далее в статье будем использовать тест-кейс, который:  

  • Запускает приложение.

  • Выбирает один из трёх табов: Collection, Table, Stack. 

  • Перебирает каждый элемент списка и:  

    • кликает по элементу,

    • проводит аудит,

    • кликает на кнопку Back.

import XCTest

class ReactiveDataDisplayManagerExampleUITests: XCTestCase {

	override func setUpWithError() throws {
    	    try super.setUpWithError()
    	    continueAfterFailure = true
    	    app = XCUIApplication()
    	    app.launchArguments.append("-disableAnimations")
    	    app.launchArguments.append(contentsOf: additionalCommands)
    	    app.launch()
	}

	func testCollectionScreen() throws {
    	    app.tabBars.buttons["Collection"].tap()
    	    try assertAllScreensOpeningWithoutCrashes()
	}

	func testTableScreen() throws {
    	    app.tabBars.buttons["Table"].tap()
    	    try assertAllScreensOpeningWithoutCrashes()
	}

	func testStackScreen() throws {
    	    app.tabBars.buttons["Stack"].tap()
    	    try assertAllScreensOpeningWithoutCrashes()
	}

}

// MARK: - Private methods

private extension ReactiveDataDisplayManagerExampleUITests {

	func assertAllScreensOpeningWithoutCrashes() throws {
    	    let tablesQuery = app.tables
    	    for i in 0...tablesQuery.cells.count - 1 {
        	tablesQuery.cells.element(boundBy: i).tap()
        	try app.performAccessibilityAudit()
        	app.navigationBars.firstMatch.buttons["Back"].tap()
    	    }
	}
}

Таким образом мы автоматически проверим, насколько Example-проект доступен для людей с ограниченными возможностями, и попробуем разрешить некоторые проблемы.

Обзор отчётов о тестировании

Запустим тест. Флаг continueAfterFailure не будет прерывать прогон тестов при провале аудита или других ошибках, поэтому подождём немного, скрестив пальцы.

Не фортануло

Не фортануло

Тесты упали, зато мы можем посмотреть новый отчёт о тестировании.

UI главного отчёта понятный — как код опытного сеньора-помидора. Самое интересное — в инсайтах (Insights). Провалимся в разбор теста testTableScreen.

Инсайт (insight) теста. Слева — хронология действий, справа — текущий кадр, снизу — интерактивный таймлайн

Инсайт (insight) теста. Слева — хронология действий, справа — текущий кадр, снизу — интерактивный таймлайн

Apple добавил в Xcode 15 полную запись прогона теста.

Слева — хронологический список действий:

  • поиск объекта,

  • событие нажатия,

  • проверки присутствия элементов,

  • ошибки.

Снизу — интерактивный таймлайн. Справа — текущий кадр

На таймлайне можно быстро перемещаться прямо к месту падения теста. Очень удобно, когда тест упал много раз, как в нашем случае.

Таймлайн со всеми ошибками теста

Таймлайн со всеми ошибками теста

События, связанные с нажатием, подсвечиваются на текущем кадре. 

Подсветка события нажатия

Подсветка события нажатия

Не хватает только возможности поделиться записанным видео, чтобы его прикрепить к багу в Jira или другом issue-tracker. Во вкладке Gallery можно скачать артефакт, но он будет содержать лишь последний кадр (баг бета-версии Xcode? Может быть).

Аудит доступности

Apple большое внимание уделяет доступности своих устройств для людей с ограничениями зрения, слуха и так далее. Инженеры проектируют альтернативные способы управления устройствами Apple или специальные возможности:

  • увеличение текста,

  • VoiceOver — чтение содержимого экрана,

  • оптимизация световых вспышек на видео,

  • остановка воспроизведения анимированных GIF-изображений.

Тестирование доступности — задача не из легких. Существуют даже компании со специально обученными QA, которые занимаются таким тестированием.

Чтобы автоматизировать процесс тестирования доступности, Apple добавил новый метод.

XCUIApplication.performAccessibilityAudit(
    for auditTypes: XCUIAccessibilityAuditType = .all, 
    _ issueHandler: ((XCUIAccessibilityAuditIssue) throws -> Bool)? = nil)

auditTypes позволяет выбрать набор сценариев аудита

issueHandler позволяет игнорировать ошибки конкретных элементов, данные которых хранятся в XCUIAccessibilityAuditIssue

Чтобы понять, как работает метод performAccessibilityAudit, посмотрим отрывок записи прогона теста внимательно.

280f851514dfc158a955bde5b4cbb0ad.gif

Вы заметили эти странные увеличения элементов navigationBar? Именно так работает аудит доступности. То есть во время прогона теста Xcode:

  • динамически изменяет размер шрифта на несколько категорий,

  • отслеживает каждый шаг,

  • анализирует полученные данные.

Так выглядит визуальная часть работы метода, но на самом деле аудит включает в себя проверку по следующим сценариям:

// Types of audits supported on all platforms

// Присутствуют ли на экране элементы нечитаемые из-за плохой контрастности с фоном?
public static var contrast: XCUIAccessibilityAuditType { get }

//Возможно ли обнаружить на экране элементы?
public static var elementDetection: XCUIAccessibilityAuditType { get }

//Соответствует ли область нажатия кнопки минимально-требуемому размеру 44x44?
public static var hitRegion: XCUIAccessibilityAuditType { get }

//Есть ли у элемента ёмкое описание, позволяющее пользователю понять назначение элемента?
public static var sufficientElementDescription: XCUIAccessibilityAuditType { get }

    
// Types of audits supported on iOS, watchOS, and tvOS

//Поддерживает ли элемент, содержащий текст, динамическое изменение шрифта?
public static var dynamicType: XCUIAccessibilityAuditType { get }

//Не будет ли текст обрезан (теоретически) при динамическом изменении шрифта или изменении верстки?
public static var textClipped: XCUIAccessibilityAuditType { get }

//Есть ли у элемента характеристика, позволяющая пользователю понять способ взаимодействия с элементом или его текущее состояние?
public static var trait: XCUIAccessibilityAuditType { get }

Далее рассмотрим, какие же замечания нашлись в нашем Example-проекте и попробуем их исправить. Пойдём от легких к сложным.

Отсутствующее описание (sufficientElementDescription)

Для начала отфильтруем сценарий аудита в коде теста, чтобы в отчёте не было лишних замечаний.

try app.performAccessibilityAudit(for: [.sufficientElementDescription])

Это пример аудита по одному сценарию. Получим отчёт, из которого будет ясно, что элемент без описания найден на трёх экранах проекта.

Отчёт по сценарию sufficientElementDescription

Отчёт по сценарию sufficientElementDescription

Провалимся в тест и обнаружим проблемный элемент — UISearchBar.

Просмотр скриншота проблемного элемента на экране с инсайтом

Просмотр скриншота проблемного элемента на экране с инсайтом

Исправить ошибку можно, добавив accessibilityLabel этому элементу. Например, так.

searchBar.searchTextField.accessibilityLabel = "Search field"

Интересно, что UISearchBar, будучи комплексным, но всё-таки системным элементом, недоступен по умолчанию. То есть его не видит VoiceOver и его нельзя однозначно идентифицировать через автотесты.

Учтите, что searchTextField доступен только с iOS 13. На более ранних версиях придется искать альтернативное решение.

Сработало!

Сработало!

Обрезанный текст (textClipped)

Рассмотрим несколько случаев срабатывания сценария textClipped. Первый — с длинным заголовком в navigationBar.

Слишком длинный заголовок в navigationBar, который теоретически может быть обрезан

Слишком длинный заголовок в navigationBar, который теоретически может быть обрезан

Вы скажете, что текст на этом элементе совсем не обрезан, и будете правы. Но если размер шрифта изменится, то…

Вот видите, обрезан

Вот видите, обрезан

или же в navigationBar может быть добавлен дополнительный элемент справа.

И опять обрезан

И опять обрезан

Нехорошо. Скрывая часть текста, мы скрываем важный контекст. Следует подумать над другой формулировкой заголовка. Например,  

title = "Table with alphabetize sections"
 title = "alphabetize sections"

Это — лёгкий путь, если позволяют требования.

Второй случай — кривая вёрстка заголовка секции.

Заголовок SectionFirst обрезан снизу

Заголовок SectionFirst обрезан снизу

Стоит отметить что для отладки аудита можно также использовать accessibility inspector — утилиту, встроенную в Xcode. В Xcode 15 она снова работает. 

Аудит в accessibility inspector

Аудит в accessibility inspector

Такой способ отладки позволит получить описание предупреждения и советы по исправлению замечания. Например, Use flexible and stackable layouts, word wrap and hyphenate, or increase row heights. Дельный совет, который исправит ошибку в этом случае.

Текст статичного размера (dynamicType)

Пожалуй, самый зрелищный сценарий и самый трудоёмкий для исправления. 

На предыдущих записях прогонов теста мы уже видели, что performAccessibilityAudit меняет размер шрифта, но этот эффект был заметен только на UINavigationBar. Почему? Спросим у нашего верного инспектора.

32bb2b3ff54463a7d9f5e22d152073f1.png

Иными словами, для поддержки dynamicType требуется, чтобы шрифт соответствовал категории из таблицы dynamic type sizes. 

Задать категорию можно:  

  • через xib,

  • методом UIFont.preferredFont(forTextStyle: TextStyle)

  • Используя UIFontMetrics(forTextStyle: TextStyle).scaledFont(for: UIFont,..

Во всех случаях надо исходить из заданного дизайнером размера шрифта и подбирать максимально близкую категорию из таблицы. От себя добавлю, что не лишним будет добавить estimatedSize для ячеек и убрать все статические значения высоты. В этом случае следовать лишь совету инспектора недостаточно.

Настоящий resize

Настоящий resize

Вот так выглядит экран с поддержкой динамического изменения размера шрифта. Осталось исправить ещё 200 замечаний по сценарию dynamicType, и Example-проект будет полностью доступен.

Так уж повелось, что Apple в первую очередь думает о пользователях, а не о разработчиках. Однако на нынешней WWDC23 Apple удивила нас улучшениями в отчётах о тестировании и суперполезным методом для автоматического аудита доступности. Эти улучшения действительно упрощают отладку и написание тестов и повышают качество вашего продукта.

Надеюсь, наши советы по разрешению замечаний аудита доступности тоже будут вам полезны. Как говорилось на одной из сессий WWDC23, доступный продукт — это премиальный продукт.

© Habrahabr.ru