Один и тот же экран iOS исправно работает локально, но в удалённой сборке сообщение «готово» от веб-страницы иногда не приходит. Не стоит сразу винить сеть облачного Mac: если страница обменивается сообщениями с нативным кодом через мост WKWebView, причина может быть в порядке загрузки, полях сообщения или жизненном цикле обработчика. Локальный HTML-файл, хранящийся в репозитории, позволяет проверять контракт обмена в обоих направлениях без зависимости от внешней страницы.
Сначала зафиксируйте контракт моста
Задайте минимальный протокол: страница отправляет нативному коду {kind: "ready", version: 1}, а нативный код отправляет странице {kind: "sync", value: "ok"}. Первое сообщение передаётся через window.webkit.messageHandlers.bridge.postMessage, второе — через evaluateJavaScript. Тест должен отдельно подтверждать доставку сообщений и правильность типов полей. Одной проверки отображения страницы недостаточно.
Сохраните следующий код в файле BridgeFixture.html, доступном тестовому target, и убедитесь, что файл добавлен в его ресурсы:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<body>
<script>
window.bridgeReceive = function (message) {
if (message.kind !== "sync" || typeof message.value !== "string") {
throw new Error("Invalid native message");
}
document.body.dataset.received = message.value;
};
window.webkit.messageHandlers.bridge.postMessage({
kind: "ready",
version: 1
});
</script>
</body>
</html>
dataset.received — точка наблюдения для теста, а не часть рабочего протокола. Рабочий код и тестовая страница должны использовать общие определения полей. При изменении полей обновляйте обе стороны и добивайтесь того, чтобы несовместимые изменения приводили к явному падению теста. Не пытайтесь угадать момент завершения загрузки страницы, выжидая фиксированное число секунд.
Проверьте оба направления в симуляторе
Тест создаёт WKUserContentController, регистрирует WKScriptMessageHandler под именем bridge и передаёт контроллер в WKWebViewConfiguration. Затем он читает HTML-файл из пакета тестов и вызывает loadHTMLString(_:baseURL:). При получении ready сначала проверьте, что message.body — словарь, а затем — что kind является строкой, version является числом и его значение равно 1. Незнакомое сообщение должно приводить к ошибке проверки.
Для отправки сообщения из нативного кода дождитесь, пока WKNavigationDelegate получит вызов webView(_:didFinish:). После этого закодируйте отправляемый словарь в JSON-текст с помощью JSONSerialization, подставьте его в window.bridgeReceive(...) и вызовите evaluateJavaScript. Не вставляйте пользовательский ввод непосредственно в строку скрипта: кавычки и переводы строк могут изменить смысл кода. Когда вызов завершится, прочитайте document.body.dataset.received и проверьте, что значение равно ok.
Возврат из
loadHTMLStringозначает лишь начало загрузки. Даже если страница уже отправилаready, этот момент не заменяет событие завершения навигации. Заведите отдельное ожидание XCTest для каждого направления: так будет видно, на каком участке не пришёл ответ.
Предусмотрите также провал теста при ошибке загрузки или выполнения скрипта. Задайте для ожиданий явные тайм-ауты, а в сообщениях об ошибках различайте случаи «страница не завершила загрузку», «нативный код не получил сообщение» и «страница не приняла сообщение от нативного кода». Иначе CI оставит лишь общее сообщение о тайм-ауте.
Подключите тест к облачной сборке
Сначала проверьте в консоли облачного Mac ArmMesh доступные конфигурации. Выбрав узел, зафиксируйте версию Xcode и среду выполнения симулятора. Найдите доступные устройства командой xcrun simctl list devices available и внесите UUID выбранного симулятора в конфигурацию задания. Не полагайтесь на неточное совпадение имени устройства. В следующей команде замените проект, scheme и UUID фактическими значениями из репозитория:
xcodebuild test \
-project Client.xcodeproj \
-scheme Client \
-destination 'platform=iOS Simulator,id=<SIMULATOR_UUID>' \
-only-testing:ClientTests/BridgeContractTests \
-resultBundlePath "$PWD/BridgeContract.xcresult"
Перед запуском убедитесь, что scheme является общедоступной, HTML-файл включён в ресурсы тестового target, а путь для результатов ещё не существует. xcodebuild не перезапишет существующий пакет результатов как пустой каталог. При сбое сохраните .xcresult и журнал сборки, а также запишите версию Xcode и UUID симулятора, чтобы повторить запуск в той же среде.
Этому тесту не нужна удалённая веб-страница, поэтому сетевые сбои не должны влиять на его результат. Отдельный интеграционный тест с реальной страницей позволит отличить нарушение контракта моста от ошибки развёртывания страницы или запроса.
Учтите порядок событий и очистку
Если страница вызовет postMessage до регистрации обработчика, уже первое сообщение может потеряться. Поэтому сначала завершите настройку и регистрацию, а затем создавайте и загружайте WebView. Не используйте в тесте повторно контроллер со старым обработчиком. По окончании тестового случая вызовите removeScriptMessageHandler(forName: "bridge") и освободите ссылку WebView на делегат навигации.
Проверьте и отклонение некорректных сообщений: пусть тестовая страница отправит сообщение без version, а затем сообщение, в котором version задан строкой. Убедитесь, что нативный код не принимает их за корректное ready. Если рабочий протокол содержит идентификатор запроса, ответ нужно сопоставлять по этому идентификатору: запоздалое сообщение от предыдущего запроса не должно завершать ожидание текущего. Если сообщения содержат пользовательские данные, записывайте в журнал теста только структуру полей и обезличенные сведения об ошибках.
В завершение проведите приёмочную проверку: запустите тест отдельно, а затем в составе полного набора тестов. При последовательных запусках не должно быть повторных вызовов обработчика; другой симулятор той же версии должен давать те же результаты проверок. В итоге локальная тестовая страница отвечает за стабильную проверку контракта интерфейса, а тест с реальной страницей — за интеграционное поведение после развёртывания. Причину сбоя в каждом случае можно определить отдельно.
Часто задаваемые вопросы
Почему не использовать рабочий сайт как тестовую страницу?
Изменения сайта и состояние сети делают входные данные непостоянными. Зафиксируйте контракт локально, а интеграцию проверяйте отдельно.
Когда нативный код может вызвать функцию страницы?
После didFinish у WKNavigationDelegate. Возврат из loadHTMLString ещё не означает, что функция JavaScript готова.
Подключите облачный Mac с фиксированной конфигурацией к своему рабочему процессу
Арендуйте выделенные физические узлы на день, неделю, месяц или квартал для сборок, автоматизации и задач инференса MLX на фиксированных аппаратных ресурсах.