109. Памятка по fastlane snapshot
Недавно захотел на скорую руку сгенерировать актуальные скриншоты для приложения с площадками, но пришлось потратить целый выходной, чтобы все завелось. Эта статья будет памяткой/шпаргалкой по аналогии со статьей № 102 про Swift Package.
Предусловия
- В проекте настроены и работают UI-тесты для последующей автоматизации, ссылка на статью № 77 про UI-тесты
- В проекте установлен
fastlane
, ссылка на документацию по установке - В проекте установлен
snapshot
, ссылка на документацию по установке - Вы прошли по предыдущим двум ссылкам и прочитали документацию (или уже знали как это работает)
Итак, в проекте установлен fastlane
, snapshot
и вы не поленились разобраться в настройке rbenv
/bundle
для работы с ruby
(при возникновении любых сложностей с этими вещами можно обращаться к какой-нибудь нейросети, чтобы во всем быстро разобраться).
Поехали
Проверяем тесты
Прежде чем запускать генерацию скриншотов, нужно убедиться, что тесты успешно проходят при ручном запуске из Xcode
для всех нужных симуляторов и локализаций.
Если приложение поддерживает несколько языков, и скриншоты нужно сгенерировать для каждого языка, то лучше убедиться хотя бы на двух языках в работоспособности тестов как минимум на двух симуляторах.
Причина в том, что на девайсе с маленьким экраном нужная кнопка может находиться за пределами экрана, и чтобы тест не упал, нужно добавить в сценарий скролл до этого самого элемента. Это может пригодиться:
extension XCTestCase { enum SwipeDirection { case up, down, left, right } @MainActor func swipeToFind( element: XCUIElement, in app: XCUIApplication, direction: SwipeDirection = .up ) { while !element.isVisibleOnScreen { switch direction { case .up: app.swipeUp(velocity: .slow) case .down: app.swipeDown(velocity: .slow) case .left: app.swipeLeft(velocity: .slow) case .right: app.swipeRight(velocity: .slow) } } waitAndTapOrFail(element: element) } @MainActor func waitAndTapOrFail(timeout: TimeInterval = 3, element: XCUIElement) { if !waitAndTap(timeout: timeout, element: element) { XCTFail("Не нашли элемент \(element)") } } @MainActor @discardableResult func waitAndTap(timeout: TimeInterval, element: XCUIElement) -> Bool { let isElementFound = element.waitForExistence(timeout: timeout) if isElementFound { element.tapElement() } return isElementFound } } extension XCUIElement { func tapElement() { if isHittable { tap() } else { coordinate(withNormalizedOffset: .init(dx: 0.0, dy: 0.0)).tap() } } var isVisibleOnScreen: Bool { exists && isHittable } }
Насчет локализации все просто: для каждого языка тексты на экране будут разными, и если забыть что-то локализовать, тесты будут падать. Чтобы такого не было, достаточно настроить Target Membership
для вашего файла с локализованными строками и добавить туда таргет для UI-тестов, а потом в самих тестах использовать эту штуку:
extension XCUIElementQuery { func element(for localizationKey: String) -> XCUIElement { let bundle = Bundle(for: WorkoutAppUITests.self) let localizedString = NSLocalizedString(localizationKey, bundle: bundle, comment: "") return self[localizedString] } }
Использовать эту штуку очень просто, вот пример:
var tabbar: XCUIElement { app.tabBars.firstMatch } var profileTabButton: XCUIElement { tabbar.buttons.element(for: "Профиль") }
Итак, тесты успешно проходят при ручном запуске на нужных симуляторах с нужной версией iOS и локализацией, едем дальше.
Даем приложению разрешения
Если ваше приложение запрашивает любые разрешения у пользователя, нужно обработать это в тестах. Т.е. для каждого из запрашиваемых разрешений нужно написать явную обработку.
Для начала создаем вашем тестовом классе ссылку на спрингборд (системный алерт):
private let springBoard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
Далее для каждого запроса разрешений пишем отдельный метод, например:
func handleLocationAlert() { let alert = springBoard.alerts.firstMatch let button = alert.buttons.element( matching: NSPredicate( format: "label IN {'Allow While Using App', 'При использовании приложения'}" ) ) waitAndTap(timeout: 5, element: button) } func handleNotificationAlert() { let alert = springBoard.alerts.firstMatch let button = alert.buttons.element( matching: NSPredicate( format: "label IN {'Allow', 'Разрешить'}" ) ) waitAndTap(timeout: 5, element: button) }
Как видно из моего примера - для каждого языка (ru/en) мы пытаемся найти нужную кнопку в системных алертах. Если забыть какой-то из языков, тесты будут падать.
Добавляем вызов этих методов в UI-тесты. В моем случае разрешения запрашивают сразу на старте приложения, поэтому эти обработчики вызываются первым делом в ходе тестов:
func testMakeScreenshots() { handleLocationAlert() // <- handleNotificationAlert() // <- checkParks() checkEvents() checkProfile() }
Ждем загрузку данных
Этот момент опциональный - если у вас в приложении нет работы с сетью, то можно его пропустить.
Если же приложение что-либо скачивает с интернета, то лучше сразу добавить несколько секунд ожидания перед каждым созданием скриншота, чтобы сервер точно ответил, и картинки точно загрузились. Ожидание можно добавить например так:
extension XCTestCase { /// Иногда сервер очень долго отвечает, или картинки долго грузятся, поэтому ждем func waitForServerResponse(_ timeout: UInt32 = 5) { sleep(timeout) } }
private func checkParks() { waitForServerResponse() waitAndTapOrFail(timeout: 10, element: parksListPickerButton) waitForServerResponse() // <- snapshot("1-parksList") waitAndTapOrFail(timeout: 10, element: firstParkCell) waitForServerResponse() // <- snapshot("2-parkDetails") waitAndTapOrFail(timeout: 5, element: closeButton) }
Если не дождаться загрузки данных, то скриншоты будут некрасивыми, хотя тесты не упадут.
Худшая ситуация
Самая плохая ситуация - получение такой ошибки при попытке запустить тест (вручную или через fastlane
, не важно):
Tests-Runner (94464) encountered an error (The test runner failed to initialize for UI testing. (Underlying Error: Timed out waiting for AX loaded notification))
Два года назад fastlane
генерировал мне скриншоты с четырех разных симуляторов, и тогда этот процесс работал стабильнее, вообще не помню такую ошибку в то время (может быть, мне и повезло тогда).
Эта ошибка хуже остальных, потому что фиг знает, что с ней делать.
Можно попробовать стандартные действия:
- почистить билд (
cmd + shift + k
) * - почистить
derived data
- стереть и установить заново все симуляторы - это можно сделать при помощи команды в терминале
fastlane snapshot reset_simulators
, но нужно учитывать, что будут стерты все симуляторы, иfastlane
попытается установить все симуляторы заново (включая айпады, часы и т.д.) - переустановить приложение на симуляторе *
- стереть настройки и контент для используемых в тестах симуляторов *
Звездочками отметил шаги, которые можно автоматизировать и выполнять перед каждым запуском тестов через fastlane snapshot
- об этом будет следующий раздел.
Настраиваем snapfile
Как и написано в документации, этот файл необходим для генерации скриншотов, в нем можно настроить много всего для тестов, например:
Полный перечень настроек можно посмотреть, запустив в терминале из папки с проектом команду: fastlane action snapshot
.
Именно с настройками в этом файле я экспериментировал дольше всего, прежде чем написать эту статью.
Пока писал эту статью, провел еще два эксперимента и нашел комбинацию настроек, с которой даже для трех симуляторов с двумя языками все работает корректно, вот эти настройки (содержимое файла snapfile
):
# Список устройств, с которых нужно сделать скриншоты devices(["iPhone 16 Pro Max", "iPhone 16 Pro", "iPhone SE (3rd generation)"]) # Список языков, которые нужно использовать. Подробнее здесь: https://docs.fastlane.tools/actions/snapshot/#available-language-codes languages(["en-US", "ru"]) # По умолчанию должна использоваться последняя версия iOS. Если нужно изменить, сделайте это здесь. ios_version("18.1") # Пропустить открытие HTML-файла с результатами skip_open_summary(true) # Переустановить приложение перед запуском reinstall_app(true) # Очистить папку сборки перед началом работы # clean(true) # Очистить данные симулятора перед началом # erase_simulator(true) # Включение этой опции установит системный язык симулятора localize_simulator(true) # Должен ли процесс прерваться сразу после полной неудачи тестов на одном устройстве? stop_after_first_error(true) # Количество повторных попыток в случае ошибок number_of_retries(0) # Предотвращает обновление пакетов до версий, отличных от тех, что записаны в файле Package.resolved disable_package_automatic_updates(true) # Название схемы, содержащей UI-тесты scheme("WorkoutAppUITests") # Удалить все ранее созданные скриншоты перед созданием новых clear_previous_screenshots(true) # Аргументы для запуска приложения. Подробнее здесь: https://docs.fastlane.tools/actions/snapshot/#launch-arguments. В моем проекте эта настройка отключает анимации для тестов. launch_arguments(["UITest"])
Решеткой (#
) отмечены комментарии и выключенные настройки. Очистку билда и симуляторов я оставил на потом, но выключил, т.к. без этих настроек сейчас все работает. Если эти настройки включить, то для генерации скриншотов потребуется намного больше времени, т.к. эти шаги знатно продлевают процесс. Одно лишь стирание симулятора занимает не меньше 20 секунд, а это будет происходить перед каждым запуском тестов для каждого языка и симулятора.
Подводные камни snapfile
Snapfile
хоть и дает возможность настроить версию iOS 18.3.1, например, но при этом актуальный fastlane
про такую версию ничего не знает и попытается запустить симуляторы с версией 18.3, что конечно приведет к падению тестов, а время будет потрачено.
Указывать нужно версию максимум с одной точкой.
Коды языков иногда меняются - актуальные можно посмотреть в документации. Если указать некорректный код, тесты будут падать.
number_of_retries
- очень полезная вещь, потому что позволяет сэкономить много времени при возникновении ошибок. В документации пишут, что иногда тесты могут падать по неведомым причинам. Пока я пишу этот абзац, у меня в фоне fastlane
генерирует скриншоты для трех девайсов - решил еще раз проверить, сработает ли, заодно и время засечем… Итак, три девайса прошли тесты за 6.5 минут.
Запуск snapshot
В зависимости от способа установки fastlane
на вашем девайсе запустить генерацию скриншотов можно несколькими способами в терминале (всегда из папки с проектом):
Если в проекте есть Gemfile
, после запуска первой команды в терминале вылезет такой лог:
[10:16:40]: fastlane detected a Gemfile in the current directory [10:16:40]: However, it seems like you didn't use `bundle exec` [10:16:40]: To launch fastlane faster, please use [10:16:40]: [10:16:40]: $ bundle exec fastlane snapshot
Что означает, что для ускорения запуска генерации скриншотов нужно использовать не первую (стандартную) команду, а вторую или третью, смотря что установлено. Это ощутимо влияет на скорость процесса.
После успешного завершения генерации скриншотов терминал покажет что-то вроде такого:
+------------------------------------------+ | snapshot results | +----------------------------+-------+-----+ | Device | en-US | ru | +----------------------------+-------+-----+ | iPhone 16 Pro Max | 💚 | 💚 | | iPhone 16 Pro | 💚 | 💚 | | iPhone SE (3rd generation) | 💚 | 💚 | +----------------------------+-------+-----+
Что еще может пригодиться
В настройках схемы для UI-тестов в вашем проекте может помочь отключение всей диагностики в разделе Test -> Diagnostics
, а в разделе Run -> Info
может быть полезным поставить галку Wait for the executable to be launched
.
У меня в UI-тестах для этого проекта есть единственный тестовый метод (начинается со слова test
) — мне так удобнее.
Хорошая новость
Если не требуется генерировать скриншоты для разных размеров экранов, достаточно остановиться на одном симуляторе - 6.9 или 6.7 дюймов, т.е. iPhone 16 Pro Max или iPhone 16 Plus, соответственно. Скриншотов с любого из этих девайсов достаточно - Apple их масштабирует для всех других диагоналей экранов.
Все диагонали на момент публикации статьи можно посмотреть тут.
Заключение
Для корректной генерации скриншотов необходимо убедиться в работоспособности тестов при ручном запуске, правильно настроить fastlane
и snapfile
, и надеяться на удачу, чтобы не получать ошибку таймаута 😁
Код для этой статьи можно посмотреть тут, а другие статьи - тут.