Привет! Меня зовут Дима Баранов, я занимаюсь мобильной автоматизацией в Битрикс24. Мы разрабатываем и поддерживаем приложения для огромной пользовательской базы, и без мобильных автотестов тут не обойтись. Сегодня я расскажу, как мы решили одну из самых неприятных (из интересных) проблем в автоматизации iOS‑приложений — работу с системными запросами разрешений. Сами события возникли еще год назад, но хочется об этом рассказать. Если вы сталкивались с тем, что тесты падают из‑за неожиданно появляющихся системных диалогов — эта статья может вам помочь.
Почему Appium
В нашей команде мы рассматривали несколько инструментов для мобильной автоматизации. Часть из них — специализирована для конкретной платформы (например, XCUITest/iOS, Espresso/Android). Часть — требует внесения изменений в само приложение или рассчитана на определенный тип фреймворка на котором пишется приложение. Appium же выглядел достаточно выгодно на этом фоне, позволяя:
-
Писать тесты на том же языке (C#), на котором написана остальная автоматизация
-
Тестировать как iOS, так и Android без переписывания тестов
-
Избежать внедрения тестовых хуков в код приложения
-
Интегрироваться с существующей инфраструктурой тестирования
Но не всё так гладко
Со временем стало понятно, что у Appium есть узкие места. И одно из них — работа с диалогами запросов разрешений iOS. Если на Android можно быстро получить/задать состояние разрешений для тестируемого приложения, то на iOS это можно сделать только на симуляторе, но не на реальном устройстве. Но сама проблема о которой мы поговорим тут, возникла после очередного обновления драйвера XCUITest.
Проблема: а что не так с запросом разрешений
Большинство iOS-приложений запрашивают разрешения на доступ к камере, уведомлениям, контактам и другим функциям устройства. В тестировании важно корректно обрабатывать эти запросы:


Если приложение не получило доступ к нужным ресурсам, а отказ в доступе не обработан, то оно может начать вылетать или вести себя непредсказуемо.
Когда мы начали писать автоматизацию, всё обрабатывалось просто. Мы создали класс, который определял тип разрешения по тексту (проверяем по определенному локатору с нужным текстом наличие элемента) и соответствующим образом реагировал на него:
-
Ждем элемент запроса с определенным текстом
-
Нажимаем нужную кнопку (список вариантов мог быть универсальным, либо специфичным, указываемым отдельно для разрешения)
Мы точно знали, появился запрос или нет, управляя дальнейшей логи.
Изменения, которые всё усложнили
После очередного обновления Appium/XCUITest запросы разрешений перестали быть видны если активное приложение — не системное.
В частности, речь про версию 6.0.0 (2024-02-01).
В ченджлоге видим следующий пункт:
Bumped WDA to ^6.0.0, which includes fixes for the active application detection
This change might require switching the active application to com.apple.springboard in order to interact with system dialogs, such as permission dialogs, while the current active application is not the springboard. Using mobile:alert or acceptAlertButtonSelector/dismissAlertButtonSelector also should help. See also appium#19716.
После обновления запросы перестали быть видны для мобильного драйвера в прежнем формате и нужно было менять механизм работы с ними.
Может, просто включить автоподтверждение?
Многие (кто сталкивался с этим стеком) спросят: «А почему бы не использовать autoAcceptAlerts
?». Да, штука удобная и включается быстро и просто:
appiumOptions.AddAdditionalCapability("autoAcceptAlerts", true);
Но, после попытки внедрения, мы поняли, что у этого подхода есть существенные недостатки:
-
Потеря контроля: Мы не знаем, был запрос или нет. Это критично для нас, потому что нужно проверять разные сценарии.
-
Сломанные тесты: Если запрос появляется во время выполнения какого-либо действия, Appium обработает запрос, но само действие может не выполнится/выполнится не полностью. Кроме того, срабатывает автоматическая обработка не идеально быстро, усугубляя проблему потерянных действий.
-
Нереалистичные сценарии: В реальной жизни пользователи могут отклонять разрешения. Нам нужно тестировать и такие случаи. А делать для отдельных тестов какое‑то свойство с признаком необходимости включения этой настройки — выглядело не очень надежно и воспринималось как некое ненужное усложнение.
-
Запросы с нестандартными вариантами ответа: В запросе на доступ к файлам, например, автоматическое подтверждение выбирает уже из 3 вариантов, и это не вариант со всеми файлами. т. е. Если варианты не стандартные — вероятность успешного выбора нужного варианта сильно снижается.
Первая попытка: переключение контекста
Первый подход, который я попробовал — решить так проблему, как советовали в ченджлоге. т. е. переключаться на главный экран com.apple.springboard, получать текст, обрабатывать его и возвращаться на прошлое приложение:
-
Запоминаем активное приложение (это не всегда то, которое мы тестируем, может быть браузер или что‑то еще)
-
Переключаемся на домашний экран
-
Проверяем уведомления по тексту
-
Обрабатываем его
-
Возвращаемся в исходное приложение
Решение сработало, но создало новые проблемы. После возврата в приложение часть UI сбрасывалась: модальные окна закрывались, кнопки меняли состояние, контент перезагружался. Тесты стали падать еще чаще.
Нам нужно было найти способ определять тип разрешения без переключения контекста.
У нас же есть экран…
В процессе экспериментов, я заметил, что хотя мы не можем получить текст диалога, мы можем определить факт его появления и получить список кнопок через API:
var alertButtons = driver.ExecuteScript("mobile: alert", new Dictionary<string, object> { { "action", "getButtons" } });
Одновременно с этим мы внедряли снятие скриншотов в момент ошибок тестов. Запись видео — это здорово, но видеть сразу то, что было на экране в момент ошибки — это удобно. Возникла идея — а что если совместить эти два подхода?
-
Определяем наличие диалога через API (как и советовали в ченджлоге, mobile:alert)
-
Делаем скриншот
-
Распознаем текст со скриншота
-
Определяем тип разрешения по ключевым словам
Выглядело сомнительно, но захотелось попробовать. На тот момент я не знал, на каком этапе развития находятся инструменты OCR сейчас, потому, постарался найти возможные варианты. Из того, что было в открытом доступе и чаще всего использовалось, я выбрал библиотеку Tesseract.
Рождение решения: OCR на страже стабильности
После недели экспериментов и настройки параметров распознавания мы создали работающее решение. Вот как оно устроено:
-
Оболочка для драйвера Appium
public class AppiumDriverWrapper { private readonly IOSDriver _driver; // ... другие методы ... public bool HasSystemPermissionButtons() { try { var alertButtons = ((IOSDriver)_driver).ExecuteScript("mobile: alert", new Dictionary<string, object> { { "action", "getButtons" } }); return alertButtons != null && ((IList<object>)alertButtons).Count > 0; } catch { return false; } } }
Метод проверяет, отображается ли запрос разрешений, пытаясь получить варианты ответа. Если кнопок нет — значит, нет и диалога.
-
Распознавание текста с помощью Tesseract
Первые тесты OCR были неутешительными — качество распознавания оставляло желать лучшего. Приходилось настраивать предварительную обработку изображения:
public class TesseractTextRecognizer { // ... конструктор и другие методы ... public async Task<string> RecognizeTextFromBytesAsync(byte[] imageBytes) { // Преобразование байтов изображения и распознавание текста // ... return recognizedText; } }
Самым сложным оказалось справиться с разными темами интерфейса (светлой и темной) и разными языками интерфейса iOS. После многих проб и ошибок мы нашли оптимальные настройки и языковые пакеты. Отдельно хочется упомянуть, что острым вопросом оказалась и кроссплатформенность. Спустя некоторое время безуспешных экспериментов, удалось собрать библиотеки под Windows (они через nuget ставятся, тут вопросов нет), MacOS (пришлось ставить через brew и искать нужные файлы с переименованием и подсовыванием в проект), а также Linux (Ubuntu, в частности). На MacOS, кстати, могут быть запросы на использование библиотеки, их можно обработать в системных настройках.
-
Модель запросов разрешений
Для надежного определения типа разрешения мы создали модель, описывающую различные варианты текстов запроса, кнопок подтверждения и отклонения:
public class PermissionRequest { public string Type { get; set; } public string[] KeywordsToIdentify { get; set; } public string[] AllowButtonKeywords { get; set; } public string[] DenyButtonKeywords { get; set; } // ... другие свойства и методы ... }
Вместо точного совпадения текста мы ищем ключевые слова, что делает решение устойчивым к разным формулировкам и языкам интерфейса. Для каждого типа разрешения мы собрали набор характерных фраз:
// Пример для запроса доступа к камере new PermissionRequest { Type = "Camera", KeywordsToIdentify = new[] { "camera", "камера", "фото", "доступ к камере", "would like to access the camera", "wants to use your camera" }, // ... кнопки разрешения и отклонения ... }
-
Обработчик разрешений
Финальный компонент — обработчик, который связывает все части воедино:
public class PermissionHandler { // ... свойства и конструктор ... public PermissionHandlerResult HandlePermissionIfPresent(PermissionAction action = PermissionAction.Allow) { if (!_driver.HasSystemPermissionButtons()) return PermissionHandlerResult.NotFound(); // Получаем скриншот как массив байтов var screenshotBytes = _driver.TakeScreenshotAsBytes(); // Распознаем текст var recognizedText = _textRecognizer.RecognizeTextFromBytes(screenshotBytes); // Определяем тип разрешения по тексту var permission = KnownPermissions.IdentifyFromText(recognizedText); if (permission == null) return PermissionHandlerResult.UnknownPermission(recognizedText); // Нажимаем нужную кнопку и возвращаем результат // ... } }
На первых порах нам очень помогало логирование распознанного текста, когда запрос оставался неопознанным. Это позволило быстро дополнить базу ключевых слов.
Что получилось в итоге
Решение работает уже более года, и за это время:
-
Контекст сохраняется: приложение не «дергается» из‑за переключений
-
Тестирование стало реалистичнее: мы можем имитировать разное поведение пользователей, включая отказ от разрешений
-
Отладка упростилась: сохраненные скриншоты помогают понять, что происходило
Заключение
Проблема с запросами разрешений в iOS — классический пример того, как автоматизаторам приходится изобретать обходные пути там, где прямых решений нет. Но именно такие задачи и делают нашу работу интересной!
Весь код примера доступен в репозитории на GitHub. Для начала работы добавьте свои типы разрешений в KnownPermissions.cs
.
Мобильная автоматизация — область, где постоянно приходится придумывать что-то новое и преодолевать ограничения инструментов. Если вам интересны другие аспекты мобильного тестирования, рекомендую статью моего коллеги Кирилла о визуальном тестировании мобильных приложений.
Буду рад услышать, как вы решаете подобные проблемы в своих проектах!
ссылка на оригинал статьи https://habr.com/ru/articles/900578/
Добавить комментарий