XCUITest падал без видимой причины в коде
Проблема
Сквозной UI-тест (SmokeFlowTests.testFullEightScreenFlow), проходящий весь пользовательский сценарий приложения от онбординга до paywall, падал на первом же запуске:
/LanguagiUITests/SmokeFlowTests.swift:18: error:
-[LanguagiUITests.SmokeFlowTests testFullEightScreenFlow] : XCTAssertTrue failed
Test Case '-[LanguagiUITests.SmokeFlowTests testFullEightScreenFlow]' failed (16.445 seconds).
Строка 18 проверяет, что кнопка «Продолжить» на экране онбординга неактивна до выбора цели. В коде приложения и теста на тот момент не было изменений, объясняющих падение: онбординг был готов заранее, 19 unit-тестов проходили стабильно.
Причина
Экспорт accessibility-дерева в момент падения (xcrun xcresulttool export attachments) показал, что тест стартовал не с онбординга, а сразу с уже пройденной тропы обучения — стрик 6, XP 45, узел «Заказ еды» помечен пройденным. Тест был запущен с launch-аргументом --uitest-reset, который должен был гарантировать чистый старт.
Причина оказалась не в тесте и не в логике приложения, а в способе, которым ранее вручную засеивались демонстрационные данные для скриншотов:
xcrun simctl spawn "iPhone 17 Pro" defaults write com.languagi.app \
languagi.progress.v1 -data <hex>
Эта команда пишет в device-уровневый домен defaults, физически отделённый от container-уровневого UserDefaults, который читает и очищает сам код приложения. Приложение не может увидеть или удалить то, что записано в обход его собственного контейнера — «сброс» в коде срабатывал, но старые данные оставались видны через другой слой хранилища.
Во втором прогоне после этого исправления тест снова падал — на шаге сборки фразы. Слово a дважды встречается в банке слов упражнения, и тест иногда выбирал уже использованный дубликат: плитки зоны ответа в accessibility-дереве идут раньше плиток банка слов, а тест брал первый найденный активный элемент.
Решение
Оба исправления — в коде и конфигурации, не через интерфейс симулятора:
xcrun simctl spawn "iPhone 17 Pro" defaults delete com.languagi.app
— удаляет весь device-уровневый домен явно, вместо частичной перезаписи. Отдельно, выбор слова в тесте переписан на детерминированный обход с конца списка совпадений:
for i in stride(from: chips.count - 1, through: 0, by: -1)
where chips.element(boundBy: i).isEnabled { ... }
Проверка
Полный прогон xcodebuild test, дважды подряд на чистом симуляторе (переустановка .app между прогонами):
Test Suite 'All tests' passed at 2026-07-23 12:22:59.578.
Test Case '-[LanguagiUITests.SmokeFlowTests testFullEightScreenFlow]' passed (31.751 seconds).
** TEST SUCCEEDED **
19 unit-тестов + 1 UI-тест — 20 из 20, воспроизводимо
Собранная HTML-страница не рендерилась в превью
Проблема
Локальная страница (self-hosted шрифты, изображения по относительным путям), открытая через file:// в браузерном превью, отображалась без единого стиля: системный шрифт, никаких цветов, вместо картинок — alt-текст.
Причина
A — разметка. document.fonts оказался пуст (ноль зарегистрированных @font-face), а getComputedStyle(hero).backgroundColor вернул rgba(0,0,0,0) — то есть не «шрифт не подгрузился», а вся таблица стилей не распарсилась. В исходнике был дублирующийся открывающий тег <style> и внутри CSS — случайно оставшийся закрывающий тег HTML-комментария -->. По правилам разбора HTML браузер трактует всё от первого <style> до первого </style> как один текстовый блок CSS; второй <style> не открывает новый элемент, а невалидный токен --> внутри ломает разбор всего листа.
B — среда просмотра. После исправления разметки картинки всё ещё не грузились. tabs_context показал, что происхождение вкладки — data:, а не file:: инструмент предпросмотра конвертирует локальный файл в data-URL снимок без разрешаемого базового пути, поэтому относительные ссылки не резолвятся.
Решение
Для A — убраны дублирующийся тег и невалидный -->. Для B — поднят настоящий локальный HTTP-сервер вместо файлового превью. Первая попытка падала при старте:
File ".../http/server.py", line 1274, in <module>
parser.add_argument('--directory', '-d', default=os.getcwd(),
PermissionError: [Errno 1] Operation not permitted
Ошибка — на вызове os.getcwd(): песочница процесса запрещает даже узнать текущую директорию. Собственный сервер, вообще не вызывающий os.getcwd(), падал так же — уже на попытке открыть свой .py-файл, лежащий в ~/Desktop/…. Перенос того же скрипта в каталог вне ~/Desktop устранил ошибку немедленно.
Проверка
document.fonts.forEach(f => ...) // все 7 @font-face: "loaded"
document.documentElement.scrollWidth
- window.innerWidth // 0 при 1440px и при 375px
и расчёт контраста по формуле WCAG ((L1+0.05)/(L2+0.05) из относительной яркости) для 7 пар текст/фон — все ≥ 5.5:1.
Что здесь не является инженерным кейсом
- Выбор цветовых токенов, типографики и структуры лендинг-страницы — дизайн-решения, не отладка кода.
- Замена шрифтов (после того как линтер
detect.mjsотметил исходную пару как «частый ИИ-дефолт») проверена повторным прогоном того же линтера до нулевого результата — это эвристика стиля, не функциональный тест. - Флаттенинг альфа-канала иконки сделан отдельным Python/CoreGraphics-скриптом (код, не руками в редакторе изображений), но проверка (
sips -g hasAlpha→no) — разовая проверка одного файла, не шаг сборки.
Что усилило бы кейс в следующий раз
- Флаттенинг иконки и проверку
hasAlpha— вынести в build-фазу Xcode с падением сборки при прозрачном альфа-канале. - Набор символов для self-hosted шрифтов подобран вручную «с запасом»; вместо этого — сканировать реальный текст страницы на этапе сборки и явно падать при символе вне подготовленного набора.
- Визуальную проверку (контраст, переполнение) — перевести из разовых JS-снипетов в CI-тест с baseline-скриншотом и пиксельным диффом.