同一套 iOS 页面在本地运行正常,放进远程构建任务后,偶尔收不到网页发来的“已就绪”消息。先别把这类失败归因于云端 Mac 的网络:如果页面与原生代码通过 WKWebView 桥接,加载时序、消息字段和处理器生命周期都可能是原因。可以用一份随仓库提交的本地 HTML,把双向通信变成不依赖外部页面的契约测试。
先固定桥接契约
设定一个最小协议:网页向原生发送 {kind: "ready", version: 1};原生向网页发送 {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,以 bridge 为名注册 WKScriptMessageHandler,再将控制器放进 WKWebViewConfiguration。从测试包读取 HTML 后调用 loadHTMLString(_:baseURL:)。收到 ready 时,先确认 message.body 是字典,再检查 kind 为字符串、version 为数字且值为 1;不认识的消息应使断言失败。
原生发起的方向要等 WKNavigationDelegate 收到 webView(_:didFinish:)。随后把待发送的字典用 JSONSerialization 编码为 JSON 文本,拼入 window.bridgeReceive(...) 并调用 evaluateJavaScript。不要直接把用户输入插进脚本字符串:引号和换行会改变脚本含义。待调用完成,再读取 document.body.dataset.received,断言结果为 ok。
loadHTMLString返回只表示开始加载;即使网页已发出ready,也不应以这个返回时刻代替导航完成事件。两个方向各设一个 XCTest expectation,能分别指出是哪一段没有响应。
对加载失败与脚本执行错误也要设置失败路径。为 expectation 设置明确超时,并在失败输出中区分“页面未完成加载”“原生未收到消息”和“网页未接受原生消息”,否则 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,便于在同一环境重跑。
这项测试不需要远端网页,因此网络故障不应改变其结论。另设一项使用实际页面的集成测试,才能区分“桥接契约坏了”和“页面部署或请求失败”。
处理时序与清理陷阱
如果页面在处理器注册前执行 postMessage,首条消息就可能丢失。因此先完成配置与注册,再创建并加载 WebView。不要在测试中复用带旧处理器的控制器;用例结束时调用 removeScriptMessageHandler(forName: "bridge"),并释放 WebView 的导航代理引用。
还要测试拒绝路径:让夹具发送缺少 version 的消息,以及把 version 写成字符串的消息,确认原生代码不会将其当作有效 ready。若生产协议含有请求编号,回执必须按编号匹配,不能让上一轮请求的迟到消息完成本轮 expectation。涉及用户内容时,仅把字段结构和脱敏错误写进测试日志。
最后做一次验收:单独运行该测试,再把它放进完整测试套件;连续运行时不应出现重复回调,换一台同版本模拟器也应得到相同断言结果。至此,本地夹具负责稳定的接口契约,实际页面测试负责部署后的集成行为,两类失败各有明确归属。
常见问题
桥接测试为什么不用线上页面?
线上页面的版本与网络状态会改变测试输入。本地 HTML 夹具能固定消息内容,使失败首先指向桥接实现。
什么时候可以向页面执行 JavaScript?
等待 WKNavigationDelegate 收到 didFinish,再调用 evaluateJavaScript;不能把 loadHTMLString 返回当作页面已就绪。
把固定配置的云端 Mac 接入你的工作流
按日、周、月或季租用独享物理节点,使用固定硬件资源运行构建、自动化和 MLX 推理任务。