Puppeteer BluetoothEmulation 接口详解:page.bluetooth 模拟蓝牙适配器与外设的完整指南
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
本文基于 Puppeteer 的BluetoothEmulation接口文档展开,系统讲解如何通过page.bluetooth模拟蓝牙适配器状态、注入预连接外设,并结合 CDP 与 WebDriver BiDi 两套底层实现、以及仓库内的端到端测试,说明其调用链路与使用限制。读完本文,你将能够理解emulateAdapter、simulatePreconnectedPeripheral、disableEmulation三个方法的确切语义,掌握配合waitForDevicePrompt完成 Web Bluetooth 设备请求自动化的完整流程,并明确该功能在 Chromium 中的隔离边界与实验性约束。
一、BluetoothEmulation 接口概述
BluetoothEmulation是 Puppeteer 暴露的蓝牙模拟能力接口,官方文档描述其为 "Exposes the bluetooth emulation abilities"(暴露蓝牙模拟能力),通过page.bluetooth属性访问。接口签名如下:
export interface BluetoothEmulation该接口定义了三个方法,全部标记为Experimental(实验性):
| 方法 | 说明 |
|---|---|
| disableEmulation() | 禁用已模拟的蓝牙适配器。对应 Web Bluetooth 规范中的bluetooth.disableSimulation命令 |
| emulateAdapter(state, leSupported) | 模拟蓝牙适配器,是所有蓝牙模拟操作的前提。对应规范中的bluetooth.simulateAdapter命令 |
| simulatePreconnectedPeripheral(preconnectedPeripheral) | 模拟一个预连接的蓝牙外设。对应规范中的bluetooth.simulatePreconnectedPeripheral命令 |
由于三个方法均为实验性 API,接口在源码中同样以@experimental标注(见 接口定义文件)。这意味着 API 签名可能随版本演进调整,生产环境使用时应关注版本变更。
作用域限制:浏览器上下文级而非页面级
接口文档中最重要的 Remarks 指出:
Web Bluetooth specification requires the emulated adapters should be isolated per top-level navigable. However, at the moment Chromium's bluetooth emulation implementation is tight to the browser context, not the page. This means the bluetooth emulation exposed from different pages of the same browser context would interfere their states.
即规范要求模拟器按顶层可导航对象(页面)隔离,但当前 Chromium 的实现将模拟绑定在浏览器上下文(browser context)层面。同一个浏览器上下文中的不同页面会互相干扰蓝牙模拟状态。
这一限制在 CDP 实现中有直接体现。从 CdpPage 构造函数 可以看到:
// Use browser context's connection, as current Bluetooth emulation in Chromium is // implemented on the browser context level, and not tight to the specific tab. this.#cdpBluetoothEmulation = new CdpBluetoothEmulation( this.#primaryTargetClient.connection(), );源码注释明确说明:CDP 蓝牙模拟命令通过浏览器上下文级别的连接(而非标签页会话)发送。因此编写测试时,若要避免状态串扰,应使用独立浏览器上下文(browser.createBrowserContext())隔离各用例。
二、核心类型定义
接口涉及三个公共类型,全部定义在 packages/puppeteer-core/src/api/BluetoothEmulation.ts:
AdapterState:适配器状态
export type AdapterState = 'absent' | 'powered-off' | 'powered-on';模拟的蓝牙适配器支持三种状态(详见 AdapterState 文档):
| 取值 | 含义 |
|---|---|
absent | 设备不存在蓝牙适配器 |
powered-off | 适配器存在但已关闭 |
powered-on | 适配器存在且已开启(进行蓝牙模拟的前提) |
BluetoothManufacturerData:厂商数据
export interface BluetoothManufacturerData { /** * The company identifier, as defined by the Bluetooth SIG. */ key: number; /** * The manufacturer-specific data as a base64-encoded string. */ data: string; }key:蓝牙 SIG 定义的公司标识符(company identifier);data:厂商特定数据,必须以base64 编码字符串传入。
类型定义见 BluetoothManufacturerData 文档。
PreconnectedPeripheral:预连接外设
export interface PreconnectedPeripheral { address: string; name: string; manufacturerData: BluetoothManufacturerData[]; knownServiceUuids: string[]; }四个字段均有实际类型约束(见 PreconnectedPeripheral 文档):
| 字段 | 说明 |
|---|---|
address | 外设蓝牙地址,如'09:09:09:09:09:09' |
name | 外设名称,如'SOME_NAME' |
manufacturerData | 厂商数据数组,每项含key/data |
knownServiceUuids | 已知服务 UUID 列表,如['12345678-1234-5678-9abc-def123456789'] |
三、三个方法的签名与参数
emulateAdapter(state, leSupported)
interface BluetoothEmulation { emulateAdapter(state: AdapterState, leSupported?: boolean): Promise<void>; }state(AdapterState):期望的适配器状态;leSupported(boolean,可选):标记该适配器是否支持低功耗蓝牙(LE)。从源码签名emulateAdapter(state: AdapterState, leSupported = true)可见,默认值为true;- 返回
Promise<void>。
该方法是所有蓝牙模拟操作的前置条件:必须先让"适配器"处于开启状态,页面中的navigator.bluetoothAPI 才能发现设备。
simulatePreconnectedPeripheral(preconnectedPeripheral)
interface BluetoothEmulation { simulatePreconnectedPeripheral( preconnectedPeripheral: PreconnectedPeripheral, ): Promise<void>; }preconnectedPeripheral(PreconnectedPeripheral):要模拟的外设对象;- 返回
Promise<void>。
调用后,该外设会以"已发现"状态出现在页面的设备选择提示中,可供 DeviceRequestPrompt.select() 选中。
disableEmulation()
interface BluetoothEmulation { disableEmulation(): Promise<void>; }无参数,返回Promise<void>,用于结束模拟、恢复浏览器真实蓝牙状态,避免影响后续用例。
四、完整使用示例
文档给出的标准用法(与源码 JSDoc 中@example一致):
await page.bluetooth.emulateAdapter('powered-on'); await page.bluetooth.simulatePreconnectedPeripheral({ address: '09:09:09:09:09:09', name: 'SOME_NAME', manufacturerData: [ { key: 17, data: 'AP8BAX8=', }, ], knownServiceUuids: ['12345678-1234-5678-9abc-def123456789'], }); await page.bluetooth.disableEmulation();调用顺序即完整生命周期:开启适配器 → 注入预连接外设 → 在页面中触发navigator.bluetooth.requestDevice()完成断言 → 关闭模拟。示例中key: 17与data: 'AP8BAX8='分别演示了公司标识符数字和 base64 编码数据两种取值形态。
五、底层实现:CDP 与 WebDriver BiDi 双通道
Puppeteer 对该接口提供了两套实现,均位于packages/puppeteer-core/src/下:
CDP 实现:CdpBluetoothEmulation
CdpBluetoothEmulation 通过Connection直接发送 CDP 命令:
async emulateAdapter(state: AdapterState, leSupported = true): Promise<void> { // Bluetooth spec requires overriding the existing adapter (step 6). From the CDP // perspective, it means disabling the emulation first. await this.#connection.send('BluetoothEmulation.disable'); await this.#connection.send('BluetoothEmulation.enable', { state, leSupported, }); } async disableEmulation(): Promise<void> { await this.#connection.send('BluetoothEmulation.disable'); } async simulatePreconnectedPeripheral( preconnectedPeripheral: PreconnectedPeripheral, ): Promise<void> { await this.#connection.send( 'BluetoothEmulation.simulatePreconnectedPeripheral', preconnectedPeripheral, ); }两个值得注意的实现细节:
emulateAdapter会先发送BluetoothEmulation.disable再发送enable。源码注释解释这是规范要求的行为——Web Bluetooth 规范的simulateAdapter命令第 6 步要求覆盖(override)已存在的适配器,因此在 CDP 层面需要先禁用再启用。这也意味着连续调用emulateAdapter是幂等安全的。- 构造函数接收的是浏览器上下文级连接(见上文
CdpPage中的构造位置),这解释了文档 Remarks 中"上下文级隔离"的限制来源。
BiDi 实现:BidiBluetoothEmulation
BidiBluetoothEmulation 面向 WebDriver BiDi 协议,所有命令都显式携带context(上下文 ID),将模拟作用域限定在指定浏览器上下文:
async emulateAdapter(state: AdapterState, leSupported = true): Promise<void> { await this.#session.send('bluetooth.simulateAdapter', { context: this.#contextId, state, leSupported, }); }BiDi 版simulatePreconnectedPeripheral会显式解构外设对象,将address、name、manufacturerData、knownServiceUuids逐字段展平后发送bluetooth.simulatePreconnectedPeripheral命令。对比两套实现可以看到:CDP 版整体透传preconnectedPeripheral,BiDi 版按协议 schema 逐字段映射——接口抽象层(Page.bluetooth 抽象 getter)保证了用户代码在两种协议间无需改动。
六、端到端实战:配合 waitForDevicePrompt 完成设备选择
仓库测试 test/src/bluetooth-emulation.test.ts 展示了模拟蓝牙后与页面交互的完整闭环,其中包含几个文档示例未覆盖的关键前提:
1. 浏览器启动参数。测试通过setupSeparateTestBrowserHooks指定:
args: [ '--enable-features=WebBluetoothNewPermissionsBackend', '--enable-features=WebBluetooth', ], acceptInsecureCerts: true,即需要WebBluetoothNewPermissionsBackend与WebBluetooth两个 feature flag,并且页面必须运行在安全上下文(测试使用httpsServer.EMPTY_PAGE)。
2. 标准交互流程。以"选择设备"用例为例:
await page.goto(httpsServer.EMPTY_PAGE); await page.bluetooth.emulateAdapter('powered-on'); await page.bluetooth.simulatePreconnectedPeripheral(SIMULATED_PERIPHERAL); const devicePromptPromise = page.waitForDevicePrompt(); const navigatorRequestDevicePromise = page.evaluate( triggerBluetoothDevicePrompt, // 内部调用 navigator.bluetooth.requestDevice ); // 等待设备提示出现,然后选中模拟设备 const devicePrompt = await devicePromptPromise; await devicePrompt.select(devicePrompt.devices[0]!); // 断言:requestDevice 解析为模拟外设名称 expect(await navigatorRequestDevicePromise).toEqual(DEVICE_NAME);其中DeviceRequestPrompt由 page.waitForDevicePrompt() 返回(定义见 DeviceRequestPrompt 文档),核心成员有:
devices(readonly):当前可选设备列表;select(device):选中提示列表中的某个设备;cancel():取消提示(测试用例验证了取消后requestDevice会 reject);waitForDevice(filter, options):等待并解析第一个匹配过滤条件的设备。
典型写法是将waitForDevicePrompt()与触发请求的动作用Promise.all配对,例如点击页面上的"连接蓝牙"按钮:
const [devicePrompt] = Promise.all([ page.waitForDevicePrompt(), page.click('#connect-bluetooth'), ]); await devicePrompt.select( await devicePrompt.waitForDevice(({name}) => name.includes('My Device')), );Page类对waitForDevicePrompt的注释特别提醒:该方法必须在设备请求发起之前调用,否则无法返回提示对象。
3. 模拟数据的复用。测试中的SIMULATED_PERIPHERAL常量与文档示例逐字段一致:地址09:09:09:09:09:09、名称SOME_NAME、厂商数据{key: 17, data: 'AP8BAX8='}、服务 UUID12345678-1234-5678-9abc-def123456789,可直接作为复制模板。
七、小结
BluetoothEmulation接口以三个实验性方法提供了完整的 Web Bluetooth 模拟生命周期:用emulateAdapter设定absent/powered-off/powered-on三态适配器(LE 支持默认开启),用simulatePreconnectedPeripheral注入含地址、名称、base64 厂商数据与服务 UUID 的虚拟外设,再用disableEmulation收尾。理解它需要抓住三层事实:接口层定义于 packages/puppeteer-core/src/api/BluetoothEmulation.ts;实现层由 CDP 通道(先 disable 后 enable 覆盖旧适配器)与 BiDi 通道(按 context 限定作用域)分别落地;使用层则需配合waitForDevicePrompt与浏览器 feature flag 完成端到端断言。同时务必记住文档强调的约束——Chromium 当前的模拟绑定在浏览器上下文而非页面,同上下文多页面间的模拟状态会互相干扰,测试编排时宜用独立上下文隔离。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考