工程实践

在云端 Mac 上测试 WKWebView 的双向桥接契约

在云端 Mac 上测试 WKWebView 的双向桥接契约

同一套 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 返回当作页面已就绪。

独享 Apple Silicon

把固定配置的云端 Mac 接入你的工作流

按日、周、月或季租用独享物理节点,使用固定硬件资源运行构建、自动化和 MLX 推理任务。

查看可租用配置