尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Playwright 模拟浏览器 API:用 addInitScript 测试实验性与只读浏览器特性

Playwright 模拟浏览器 API:用 addInitScript 测试实验性与只读浏览器特性 Playwright 模拟浏览器 API用 addInitScript 测试实验性与只读浏览器特性【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright当被测应用依赖浏览器原生特性如 Battery Status API时这些 API 往往因浏览器支持不一致或属于实验性接口而无法被 Playwright 直接控制。本文以 Playwright 官方文档mock-browser-js.md为核心完整讲解如何用Page.addInitScript在页面加载前注入 Mock 对象、如何处理只读属性、如何用exposeFunction校验 API 调用序列以及如何构造可动态更新的 Mock 类来验证 UI 对事件变化的响应。文末结合examples/mock-battery示例工程与playwright-core源码剖析 init script 的注入时机与各浏览器的底层实现。读完本文后你将能够为任意浏览器 API 编写可复用的 Mock 脚本、验证页面对 API 的调用行为golden 比对、模拟状态变更并断言 UI 实时刷新并理解这些机制在 Playwright 三层架构客户端、服务端、浏览器端中的实际调用链。为什么需要 Mock 浏览器 APIPlaywright 对绝大多数浏览器特性都提供了原生自动化能力如通过 context 选项设置时区、地理位置、设备描述符等。但存在两类特性 Playwright 不提供专用 API实验性 API尚未被所有浏览器完整支持的接口只读 / 受限 API属性在 JS 层面只读无法直接赋值。官方文档给出的思路是既然无法配置这些 API就在页面 JS 运行之前把 API 本身替换成 Mock。文档以 Battery Status API 为例——一个用于读取设备电量、充电状态和剩余时间的接口。典型应用会用navigator.getBattery()获取一个BatteryManager对象读取level0~1 的电量比例、charging是否插电、chargingTime/dischargingTime充/放电剩余秒数未知时为Infinity并监听chargingchange与levelchange事件刷新界面。创建 Mock在页面加载前注入页面可能在加载早期就调用目标 API因此所有 Mock 必须在页面任何脚本执行前就位。最简单的方式是调用Page.addInitScript——其回调会在该页面上之后加载的每个文档包括 iframe的脚本执行前运行一次await page.addInitScript(() { const mockBattery { level: 0.75, charging: true, chargingTime: 1800, dischargingTime: Infinity, addEventListener: () { } }; // 覆盖方法使其始终返回 mock 电池信息。 window.navigator.getBattery async () mockBattery; });注入完成后即可正常导航页面并断言 UI 状态// 在每个测试前配置 mock API。 test.beforeEach(async ({ page }) { await page.addInitScript(() { const mockBattery { level: 0.90, charging: true, chargingTime: 1800, // 秒 dischargingTime: Infinity, addEventListener: () { } }; // 覆盖方法使其始终返回 mock 电池信息。 window.navigator.getBattery async () mockBattery; }); }); test(show battery status, async ({ page }) { await page.goto(/); await expect(page.locator(.battery-percentage)).toHaveText(90%); await expect(page.locator(.battery-status)).toHaveText(Adapter); await expect(page.locator(.battery-fully)).toHaveText(00:30); });chargingTime: 180030 分钟正是断言00:30的来源示例应用的 demo-battery-api/src/index.js 中的readBattery会把chargingTime换算成时:分格式写入.battery-fully元素并根据charging决定.battery-status显示Adapter还是Battery。实战细节来自示例工程examples/mock-battery中的 show-battery-status.spec.js 在addInitScript里多了一步delete window.navigator.battery;。原因在于被测页面的 src/index.js 的探测顺序是先查同步属性navigator.battery旧版 API再回退到navigator.getBattery()if (navigator.battery) { readBattery(navigator.battery); } else if (navigator.getBattery) { navigator.getBattery().then(readBattery); } else { document.querySelector(.not-support).removeAttribute(hidden); }若某些浏览器环境中navigator.battery存在mock 的getBattery()将永远不会被调用。因此 Mock 时应把被测应用会探测的所有入口都覆盖或删除这是文档未展开、但可直接复用的重要经验。该示例工程的运行方式也值得参考playwright.config.js 配置了webServer以 9900 端口启动静态服务器托管demo-battery-apipackage.json 中start脚本为http-server -c-1 -p 9900 demo-battery-api测试通过page.goto(/)访问该服务。Mock 只读 APIObject.defineProperty部分 API 是只读的直接赋值无效。例如// 这一行不会产生任何效果。 navigator.cookieEnabled true;但只要属性是configurable可配置的就可以用Object.defineProperty在原型上覆盖await page.addInitScript(() { Object.defineProperty(Object.getPrototypeOf(navigator), cookieEnabled, { value: false }); });关键点有两个定位到原型cookieEnabled定义在Navigator.prototype上而非实例上必须对Object.getPrototypeOf(navigator)操作才能覆盖实例读取configurable 前提若目标属性被声明为configurable: falsedefineProperty会抛出TypeError。此时需要在属性被固化前介入或改用其他注入手段。验证 API 调用记录调用并与期望序列比对有时不仅要 Mock 返回值还要确认页面确实发起了预期的一组 API 调用不多不少、顺序正确。做法是在 Mock 中记录每次方法调用再通过Page.exposeFunction把消息回传到 Node.js 侧的测试代码中test(log battery calls, async ({ page }) { const log []; // 暴露一个函数用于从页面把消息推送到 Node.js 脚本。 await page.exposeFunction(logCall, msg log.push(msg)); await page.addInitScript(() { const mockBattery { level: 0.75, charging: true, chargingTime: 1800, dischargingTime: Infinity, // 记录 addEventListener 调用。 addEventListener: (name, cb) logCall(addEventListener:${name}) }; // 覆盖方法使其始终返回 mock 电池信息。 window.navigator.getBattery async () { logCall(getBattery); return mockBattery; }; }); await page.goto(/); await expect(page.locator(.battery-percentage)).toHaveText(75%); // 将实际调用与期望的 golden 序列比对。 expect(log).toEqual([ getBattery, addEventListener:chargingchange, addEventListener:levelchange ]); });示例工程中的 verify-calls.spec.js 在此基础上还做了一个有价值的补充断言expect(log).toEqual([ getBattery, addEventListener:chargingchange, addEventListener:levelchange ]); log []; // 重置日志 await page.evaluate(() window.mockBattery._setLevel(0.275)); expect(log).toEqual([]); // getBattery 不会被再次调用页面复用了缓存实例。这说明被测应用缓存了getBattery()返回的实例、只通过事件驱动刷新——Mock 不仅用于造假数据还能验证应用对 API 的使用契约例如只调用一次、依赖事件通知。更新 Mock模拟状态变化与事件通知要验证应用能否正确响应电池状态更新Mock 对象必须像真实浏览器实现一样向已注册的监听器派发事件。官方文档用一个带监听器队列的 Mock 类实现test(update battery status (no golden), async ({ page }) { await page.addInitScript(() { // Mock 类电池状态变化时通知对应的监听器。 class BatteryMock { level 0.10; charging false; chargingTime 1800; dischargingTime Infinity; _chargingListeners []; _levelListeners []; addEventListener(eventName, listener) { if (eventName chargingchange) this._chargingListeners.push(listener); if (eventName levelchange) this._levelListeners.push(listener); } // 以下方法由测试调用。 _setLevel(value) { this.level value; this._levelListeners.forEach(cb cb()); } _setCharging(value) { this.charging value; this._chargingListeners.forEach(cb cb()); } } const mockBattery new BatteryMock(); // 覆盖方法使其始终返回 mock 电池信息。 window.navigator.getBattery async () mockBattery; // 把 mock 对象挂到 window 上方便测试访问。 window.mockBattery mockBattery; }); await page.goto(/); await expect(page.locator(.battery-percentage)).toHaveText(10%); // 更新电量到 27.5% await page.evaluate(() window.mockBattery._setLevel(0.275)); await expect(page.locator(.battery-percentage)).toHaveText(27.5%); await expect(page.locator(.battery-status)).toHaveText(Battery); // 模拟接入电源适配器 await page.evaluate(() window.mockBattery._setCharging(true)); await expect(page.locator(.battery-status)).toHaveText(Adapter); await expect(page.locator(.battery-fully)).toHaveText(00:30); });这里的关键手法把 mock 对象挂到windowwindow.mockBattery mockBattery使测试代码可通过page.evaluate直接调用其内部方法_setLevel/_setCharging从而在页面生命周期内主动拨动状态监听器队列 手动派发addEventListener只收集回调_setLevel/_setCharging在修改字段后同步遍历调用——与浏览器真实事件时序先改值再派发一致软断言依赖自动等待expect(...).toHaveText()会轮询直到状态刷新天然适配异步事件派发。对应示例 update-battery-status.spec.js 的断言序列与文档一致先 10%改电量后 27.5% 且状态变Battery再插电后状态变Adapter、充满时间显示00:30。源码视角addInitScript 的注入链路从playwright-core源码可以确认 init script 的完整调用链理解为什么它一定先于页面脚本执行客户端入口client/page.ts 中Page.addInitScript先把函数/源码序列化为字符串evaluationScript再经 RPC 通道this._channel.addInitScript({ source })发给服务端服务端登记server/page.ts 与 server/browserContext.ts 将脚本存入上下文级列表——这意味着同一 context 下之后打开的每个页面、以及页面的每次导航/每个 iframe都会自动带上这些脚本浏览器端下发各浏览器驱动各自实现注入——Chromium 走 CDPcrPage.ts 中addInitScript默认作用于mainworldWebKit 走 wkPage.tsFirefox 走 ffPage.ts。从源码结构看这正是 Mock 能在页面任何业务脚本之前生效的底层保证脚本在文档创建时由浏览器引擎注入而非等页面加载后由 Playwright 执行。另外注意 client/page.ts 支持options.exposeFunctions参数——addInitScript可显式携带需要在脚本中可用的exposeFunction使注入 回传两类机制可以组合在同一脚本内。适用边界与最佳实践注入时机addInitScript必须在page.goto之前调用或放入test.beforeEach否则首屏脚本已执行Mock 对首次调用无效作用范围page.addInitScript只作用于当前页面跨页面共享 Mock 时从源码签名看client/browserContext.ts 同样实现了addInitScript可改用 context 级注入覆盖所有探测入口如前文navigator.battery所示务必检查被测代码的 API 回退链删除或覆盖所有旁路只读属性优先尝试Object.defineProperty于原型上确认属性为 configurable与网络 Mock 区分本文针对的是浏览器内置 API若需 Mock 的是 HTTP 请求应使用 Playwright 的路由拦截能力可参见 docs/src/mock.md。参考文件文件说明docs/src/mock-browser-js.md本文核心依据的官方文档examples/mock-battery/playwright.config.js示例工程配置webServer 9900 端口examples/mock-battery/tests/show-battery-status.spec.js静态 Mock UI 断言含delete navigator.battery技巧examples/mock-battery/tests/verify-calls.spec.js调用序列 golden 比对与缓存行为验证examples/mock-battery/tests/update-battery-status.spec.js可事件通知的 Mock 类与状态更新断言examples/mock-battery/demo-battery-api/src/index.js被测应用电池 API 探测与渲染逻辑packages/playwright-core/src/client/page.tsPage.addInitScript客户端实现packages/playwright-core/src/server/chromium/crPage.tsChromium 端注入实现【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表