同じiOSページがローカルでは正常に動くのに、リモートビルドのジョブでは、Webページからの「準備完了」メッセージを受信できないことがあります。こうした失敗をすぐにクラウドMacのネットワークのせいにしないでください。ページとネイティブコードがWKWebViewのブリッジで通信しているなら、読み込み順序、メッセージのフィールド、ハンドラのライフサイクルが原因かもしれません。リポジトリに含めたローカルHTMLを使えば、外部ページに依存しない双方向通信の契約テストを作れます。
ブリッジの契約を先に定める
最小限のプロトコルを決めます。Webページからネイティブコードへは {kind: "ready", version: 1} を、ネイティブコードからWebページへは {kind: "sync", value: "ok"} を送ります。前者は window.webkit.messageHandlers.bridge.postMessage、後者は evaluateJavaScript を使います。テストでは、ページが表示されることだけでなく、メッセージが届くこととフィールドの型が正しいことをそれぞれ検証します。
次の内容を、テストtargetから読み取れる 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:) を受け取るまで待ちます。その後、送信する辞書を JSONSerialization でJSONテキストにエンコードし、window.bridgeReceive(...) に組み込んで evaluateJavaScript を呼び出します。ユーザー入力をスクリプト文字列に直接埋め込まないでください。引用符や改行によってスクリプトの意味が変わります。呼び出しの完了後に document.body.dataset.received を読み取り、結果が ok であることをアサートします。
loadHTMLStringが戻っても、それは読み込みが始まったことを示すだけです。Webページがすでにreadyを送っていても、その時点をナビゲーション完了イベントの代わりにしてはいけません。方向ごとにXCTestのexpectationを一つずつ設けると、どちらで応答が途絶えたかを区別できます。
読み込み失敗とスクリプト実行エラーについても、失敗を検出する経路を用意します。expectationには明確なタイムアウトを設定し、失敗時の出力では「ページの読み込みが完了しなかった」「ネイティブコードがメッセージを受信しなかった」「Webページがネイティブコードからのメッセージを受け付けなかった」を区別してください。そうしないと、CIには漠然としたタイムアウト結果しか残りません。
クラウドビルドにテストを組み込む
まずArmMeshのクラウドMacのコンソールで現在選択できる構成を確認します。ノードを選んだら、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が共有されていること、テストtargetにHTMLリソースが含まれていること、結果の出力先パスがまだ存在しないことを確認します。xcodebuild は既存の結果バンドルを空のディレクトリとして上書きしません。失敗した場合は .xcresult とビルドログを保存し、XcodeのバージョンとシミュレータのUUIDを記録して、同じ環境で再実行できるようにします。
このテストはリモートのWebページを必要としないため、ネットワーク障害で結論が変わってはいけません。実際のページを使う統合テストを別に設けて初めて、「ブリッジの契約違反」と「ページのデプロイまたはリクエストの失敗」を区別できます。
読み込み順序と後片付けの落とし穴に対処する
ハンドラを登録する前にページが postMessage を実行すると、最初のメッセージが失われる可能性があります。そのため、設定と登録を済ませてからWebViewを作成し、読み込ませます。テストでは、以前のハンドラが残ったコントローラを再利用しないでください。テストケースの終了時には removeScriptMessageHandler(forName: "bridge") を呼び出し、WebViewのナビゲーションデリゲートへの参照を解放します。
拒否する経路もテストします。フィクスチャから version のないメッセージと、version を文字列にしたメッセージを送り、ネイティブコードが有効な ready として扱わないことを確認します。本番のプロトコルにリクエスト番号が含まれる場合、応答は番号で照合しなければなりません。前回のリクエストに対する遅延メッセージで、今回のexpectationが満たされないようにします。ユーザーのコンテンツを扱う場合、テストログにはフィールド構造と機密情報を除いたエラーだけを記録します。
最後に受け入れ確認を行います。このテストを単独で実行し、次にテストスイート全体に含めて実行します。連続実行してもコールバックが重複せず、同じバージョンの別のシミュレータでも同じアサーション結果になることを確認してください。これで、ローカルのフィクスチャは安定したインターフェース契約を、実際のページを使うテストはデプロイ後の統合動作を担当し、失敗の原因もそれぞれ明確になります。
よくある質問
公開中のページを契約テストに使わない理由は?
ページの更新や通信状態で入力が変わるためです。ローカルHTMLで契約を固定し、公開ページとの結合確認は別に行います。
ネイティブからJavaScriptを呼ぶタイミングは?
WKNavigationDelegateのdidFinishを待ってからevaluateJavaScriptを呼びます。loadHTMLStringの直後では早すぎます。
固定構成のクラウドMacをワークフローに導入
専有物理ノードを日単位・週単位・月単位・四半期単位でレンタル。固定のハードウェアリソースでビルド、自動化、MLX推論を実行できます。