From 1337725d4e04064d503ba3f153f7ab6be2b60fb3 Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Wed, 18 Sep 2024 11:11:46 -0700 Subject: [PATCH 01/10] Initial mapping for simulateAdapter. The CDDL generated portions are hardcoded and will be removed once generated fro spec. --- src/bidiMapper/CommandProcessor.ts | 2 + .../modules/bluetooth/BluetoothProcessor.ts | 13 +++++ src/protocol/chromium-bidi.ts | 3 ++ .../generated/webdriver-bidi-bluetooth.ts | 13 +++++ tests/bluetooth/test_emulation.py | 54 +++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 tests/bluetooth/test_emulation.py diff --git a/src/bidiMapper/CommandProcessor.ts b/src/bidiMapper/CommandProcessor.ts index aba940876..cf64157a5 100644 --- a/src/bidiMapper/CommandProcessor.ts +++ b/src/bidiMapper/CommandProcessor.ts @@ -148,6 +148,8 @@ export class CommandProcessor extends EventEmitter { return await this.#bluetoothProcessor.handleRequestDevicePrompt( this.#parser.parseHandleRequestDevicePromptParams(command.params) ); + case 'bluetooth.simulateAdapter': + return await this.#bluetoothProcessor.simulateAdapter(command.params); // keep-sorted end // Browser domain diff --git a/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts b/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts index 07147ddae..b4f85f880 100644 --- a/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts +++ b/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts @@ -32,6 +32,19 @@ export class BluetoothProcessor { this.#browsingContextStorage = browsingContextStorage; } + async simulateAdapter( + params: Bluetooth.SimulateAdapterParameters + ): Promise { + const context = this.#browsingContextStorage.getContext(params.context); + await context.cdpTarget.browserCdpClient.sendCommand( + 'BluetoothEmulation.enable', + { + state: params.state, + } + ); + return {}; + } + onCdpTargetCreated(cdpTarget: CdpTarget) { cdpTarget.cdpClient.on('DeviceAccess.deviceRequestPrompted', (event) => { this.#eventManager.registerEvent( diff --git a/src/protocol/chromium-bidi.ts b/src/protocol/chromium-bidi.ts index 52d373e83..bbdaa8413 100644 --- a/src/protocol/chromium-bidi.ts +++ b/src/protocol/chromium-bidi.ts @@ -110,6 +110,9 @@ export type Command = ( // not re-define it. Therefore, it's not part of generated types. id: WebDriverBidi.JsUint; } & WebDriverBidiBluetooth.Bluetooth.HandleRequestDevicePrompt) + | ({ + id: WebDriverBidi.JsUint; + } & WebDriverBidiBluetooth.Bluetooth.SimulateAdapter) ) & { channel?: WebDriverBidi.Script.Channel; }; diff --git a/src/protocol/generated/webdriver-bidi-bluetooth.ts b/src/protocol/generated/webdriver-bidi-bluetooth.ts index 69f408565..eb6671026 100644 --- a/src/protocol/generated/webdriver-bidi-bluetooth.ts +++ b/src/protocol/generated/webdriver-bidi-bluetooth.ts @@ -32,6 +32,7 @@ export namespace Bluetooth { } export namespace Bluetooth { export type RequestDevicePrompt = string; + export type AdapterState = 'powered-on' | 'powered-off' | 'absent'; } export namespace Bluetooth { export type HandleRequestDevicePrompt = { @@ -39,6 +40,18 @@ export namespace Bluetooth { params: Bluetooth.HandleRequestDevicePromptParameters; }; } +export namespace Bluetooth { + export type SimulateAdapter = { + method: 'bluetooth.simulateAdapter'; + params: Bluetooth.SimulateAdapterParameters; + }; +} +export namespace Bluetooth { + export type SimulateAdapterParameters = { + context: string; + state: Bluetooth.AdapterState; + }; +} export namespace Bluetooth { export type HandleRequestDevicePromptParameters = { context: string; diff --git a/tests/bluetooth/test_emulation.py b/tests/bluetooth/test_emulation.py new file mode 100644 index 000000000..d5d4e28a8 --- /dev/null +++ b/tests/bluetooth/test_emulation.py @@ -0,0 +1,54 @@ +# Copyright 2024 Google LLC. +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from test_helpers import (AnyExtending, execute_command, goto_url, + send_JSON_command, subscribe, wait_for_event) + +HTML_SINGLE_PERIPHERAL = """ +
+ bluetooth + +
+""" + +@pytest.mark.asyncio +@pytest.mark.parametrize('capabilities', [{ + 'goog:chromeOptions': { + 'args': ['--enable-features=WebBluetooth'] + } +}], + indirect=True) +async def test_bluetooth_simulate_adapter(websocket, context_id, html): + await subscribe(websocket, ['bluetooth']) + + url = html(HTML_SINGLE_PERIPHERAL) + await goto_url(websocket, context_id, url) + + # Simulate a Bluetooth adapter. + await execute_command( + websocket, { + 'method': 'bluetooth.simulateAdapter', + 'params': { + 'context': context_id, + 'state': 'powered-on', + } + }) + From c3fb815e1e44330f67087739d4b6e7284beeeecb Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Wed, 18 Sep 2024 13:19:49 -0700 Subject: [PATCH 02/10] Map simulatePreconnectedPeripheral command --- src/bidiMapper/CommandProcessor.ts | 4 ++ .../modules/bluetooth/BluetoothProcessor.ts | 16 ++++++++ src/protocol/chromium-bidi.ts | 3 ++ .../generated/webdriver-bidi-bluetooth.ts | 27 +++++++++++++ tests/bluetooth/test_emulation.py | 40 +++++++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/src/bidiMapper/CommandProcessor.ts b/src/bidiMapper/CommandProcessor.ts index cf64157a5..6a97bcd2e 100644 --- a/src/bidiMapper/CommandProcessor.ts +++ b/src/bidiMapper/CommandProcessor.ts @@ -150,6 +150,10 @@ export class CommandProcessor extends EventEmitter { ); case 'bluetooth.simulateAdapter': return await this.#bluetoothProcessor.simulateAdapter(command.params); + case 'bluetooth.simulatePreconnectedPeripheral': + return await this.#bluetoothProcessor.simulatePreconnectedPeripheral( + command.params + ); // keep-sorted end // Browser domain diff --git a/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts b/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts index b4f85f880..c97b313e4 100644 --- a/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts +++ b/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts @@ -45,6 +45,22 @@ export class BluetoothProcessor { return {}; } + async simulatePreconnectedPeripheral( + params: Bluetooth.SimulatePreconnectedPeripheralParameters + ): Promise { + const context = this.#browsingContextStorage.getContext(params.context); + await context.cdpTarget.browserCdpClient.sendCommand( + 'BluetoothEmulation.simulatePreconnectedPeripheral', + { + address: params.address, + name: params.name, + knownServiceUuids: params.knownServiceUuids, + manufacturerData: params.manufacturerData, + } + ); + return {}; + } + onCdpTargetCreated(cdpTarget: CdpTarget) { cdpTarget.cdpClient.on('DeviceAccess.deviceRequestPrompted', (event) => { this.#eventManager.registerEvent( diff --git a/src/protocol/chromium-bidi.ts b/src/protocol/chromium-bidi.ts index bbdaa8413..b82e23eff 100644 --- a/src/protocol/chromium-bidi.ts +++ b/src/protocol/chromium-bidi.ts @@ -113,6 +113,9 @@ export type Command = ( | ({ id: WebDriverBidi.JsUint; } & WebDriverBidiBluetooth.Bluetooth.SimulateAdapter) + | ({ + id: WebDriverBidi.JsUint; + } & WebDriverBidiBluetooth.Bluetooth.SimulatePreconnectedPeripheral) ) & { channel?: WebDriverBidi.Script.Channel; }; diff --git a/src/protocol/generated/webdriver-bidi-bluetooth.ts b/src/protocol/generated/webdriver-bidi-bluetooth.ts index eb6671026..bd675bd6c 100644 --- a/src/protocol/generated/webdriver-bidi-bluetooth.ts +++ b/src/protocol/generated/webdriver-bidi-bluetooth.ts @@ -33,6 +33,18 @@ export namespace Bluetooth { export namespace Bluetooth { export type RequestDevicePrompt = string; export type AdapterState = 'powered-on' | 'powered-off' | 'absent'; + export type ManufacturerData = { + /** + * Company identifier + * https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml + * https://usb.org/developers + */ + key: number; + /** + * Manufacturer-specific data (Encoded as a base64 string when passed over JSON) + */ + data: string; +} } export namespace Bluetooth { export type HandleRequestDevicePrompt = { @@ -52,6 +64,21 @@ export namespace Bluetooth { state: Bluetooth.AdapterState; }; } +export namespace Bluetooth { + export type SimulatePreconnectedPeripheral = { + method: 'bluetooth.simulatePreconnectedPeripheral'; + params: Bluetooth.SimulatePreconnectedPeripheralParameters; + }; +} +export namespace Bluetooth { + export type SimulatePreconnectedPeripheralParameters = { + context: string; + address: string; + name: string; + manufacturerData: Bluetooth.ManufacturerData[]; + knownServiceUuids: string[]; + }; +} export namespace Bluetooth { export type HandleRequestDevicePromptParameters = { context: string; diff --git a/tests/bluetooth/test_emulation.py b/tests/bluetooth/test_emulation.py index d5d4e28a8..19343d7d9 100644 --- a/tests/bluetooth/test_emulation.py +++ b/tests/bluetooth/test_emulation.py @@ -52,3 +52,43 @@ async def test_bluetooth_simulate_adapter(websocket, context_id, html): } }) + # Create a fake BT device. + fake_device_address = '09:09:09:09:09:09' + await execute_command( + websocket, { + 'method': 'bluetooth.simulatePreconnectedPeripheral', + 'params': { + 'context': context_id, + 'address': fake_device_address, + 'name': 'SomeDevice', + 'manufacturerData': [], + 'knownServiceUuids': + ['12345678-1234-5678-9abc-def123456789', ], + } + }) + + # await send_JSON_command( + # websocket, { + # 'method': 'script.evaluate', + # 'params': { + # 'expression': 'document.querySelector("#bluetooth").click();', + # 'awaitPromise': True, + # 'target': { + # 'context': context_id, + # }, + # 'userActivation': True + # } + # }) + + # response = await wait_for_event(websocket, + # 'bluetooth.requestDevicePromptOpened') + # assert response == AnyExtending({ + # 'type': 'event', + # 'method': 'bluetooth.requestDevicePromptOpened', + # 'params': { + # 'context': context_id, + # 'devices': [{ + # 'id': fake_device_address + # }], + # } + # }) From c4076bec930f8b91298ff36c8c91d99883618611 Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Wed, 18 Sep 2024 13:33:19 -0700 Subject: [PATCH 03/10] Map simulateAdvertisement command. --- src/bidiMapper/CommandProcessor.ts | 4 ++ .../modules/bluetooth/BluetoothProcessor.ts | 13 +++++ src/protocol/chromium-bidi.ts | 3 ++ .../generated/webdriver-bidi-bluetooth.ts | 36 +++++++++++++- tests/bluetooth/test_emulation.py | 48 +++++++++---------- 5 files changed, 79 insertions(+), 25 deletions(-) diff --git a/src/bidiMapper/CommandProcessor.ts b/src/bidiMapper/CommandProcessor.ts index 6a97bcd2e..1b51f3532 100644 --- a/src/bidiMapper/CommandProcessor.ts +++ b/src/bidiMapper/CommandProcessor.ts @@ -150,6 +150,10 @@ export class CommandProcessor extends EventEmitter { ); case 'bluetooth.simulateAdapter': return await this.#bluetoothProcessor.simulateAdapter(command.params); + case 'bluetooth.simulateAdvertisement': + return await this.#bluetoothProcessor.simulateAdvertisement( + command.params + ); case 'bluetooth.simulatePreconnectedPeripheral': return await this.#bluetoothProcessor.simulatePreconnectedPeripheral( command.params diff --git a/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts b/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts index c97b313e4..378da415e 100644 --- a/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts +++ b/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts @@ -61,6 +61,19 @@ export class BluetoothProcessor { return {}; } + async simulateAdvertisement( + params: Bluetooth.SimulateAdvertisementParameters + ): Promise { + const context = this.#browsingContextStorage.getContext(params.context); + await context.cdpTarget.browserCdpClient.sendCommand( + 'BluetoothEmulation.simulateAdvertisement', + { + entry: params.entry, + } + ); + return {}; + } + onCdpTargetCreated(cdpTarget: CdpTarget) { cdpTarget.cdpClient.on('DeviceAccess.deviceRequestPrompted', (event) => { this.#eventManager.registerEvent( diff --git a/src/protocol/chromium-bidi.ts b/src/protocol/chromium-bidi.ts index b82e23eff..08c885d36 100644 --- a/src/protocol/chromium-bidi.ts +++ b/src/protocol/chromium-bidi.ts @@ -116,6 +116,9 @@ export type Command = ( | ({ id: WebDriverBidi.JsUint; } & WebDriverBidiBluetooth.Bluetooth.SimulatePreconnectedPeripheral) + | ({ + id: WebDriverBidi.JsUint; + } & WebDriverBidiBluetooth.Bluetooth.SimulateAdvertisement) ) & { channel?: WebDriverBidi.Script.Channel; }; diff --git a/src/protocol/generated/webdriver-bidi-bluetooth.ts b/src/protocol/generated/webdriver-bidi-bluetooth.ts index bd675bd6c..648fe3d83 100644 --- a/src/protocol/generated/webdriver-bidi-bluetooth.ts +++ b/src/protocol/generated/webdriver-bidi-bluetooth.ts @@ -44,7 +44,29 @@ export namespace Bluetooth { * Manufacturer-specific data (Encoded as a base64 string when passed over JSON) */ data: string; -} + } + export type ScanRecord = { + name?: string; + uuids?: string[]; + /** + * Stores the external appearance description of the device. + */ + appearance?: number; + /** + * Stores the transmission power of a broadcasting device. + */ + txPower?: number; + /** + * Key is the company identifier and the value is an array of bytes of + * manufacturer specific data. + */ + manufacturerData?: Bluetooth.ManufacturerData[]; + } + export type ScanEntry = { + deviceAddress: string; + rssi: number; + scanRecord: Bluetooth.ScanRecord; + } } export namespace Bluetooth { export type HandleRequestDevicePrompt = { @@ -64,6 +86,18 @@ export namespace Bluetooth { state: Bluetooth.AdapterState; }; } +export namespace Bluetooth { + export type SimulateAdvertisement = { + method: 'bluetooth.simulateAdvertisement'; + params: Bluetooth.SimulateAdvertisementParameters; + }; +} +export namespace Bluetooth { + export type SimulateAdvertisementParameters = { + context: string; + entry: Bluetooth.ScanEntry; + }; +} export namespace Bluetooth { export type SimulatePreconnectedPeripheral = { method: 'bluetooth.simulatePreconnectedPeripheral'; diff --git a/tests/bluetooth/test_emulation.py b/tests/bluetooth/test_emulation.py index 19343d7d9..4517f5560 100644 --- a/tests/bluetooth/test_emulation.py +++ b/tests/bluetooth/test_emulation.py @@ -67,28 +67,28 @@ async def test_bluetooth_simulate_adapter(websocket, context_id, html): } }) - # await send_JSON_command( - # websocket, { - # 'method': 'script.evaluate', - # 'params': { - # 'expression': 'document.querySelector("#bluetooth").click();', - # 'awaitPromise': True, - # 'target': { - # 'context': context_id, - # }, - # 'userActivation': True - # } - # }) + await send_JSON_command( + websocket, { + 'method': 'script.evaluate', + 'params': { + 'expression': 'document.querySelector("#bluetooth").click();', + 'awaitPromise': True, + 'target': { + 'context': context_id, + }, + 'userActivation': True + } + }) - # response = await wait_for_event(websocket, - # 'bluetooth.requestDevicePromptOpened') - # assert response == AnyExtending({ - # 'type': 'event', - # 'method': 'bluetooth.requestDevicePromptOpened', - # 'params': { - # 'context': context_id, - # 'devices': [{ - # 'id': fake_device_address - # }], - # } - # }) + response = await wait_for_event(websocket, + 'bluetooth.requestDevicePromptOpened') + assert response == AnyExtending({ + 'type': 'event', + 'method': 'bluetooth.requestDevicePromptOpened', + 'params': { + 'context': context_id, + 'devices': [{ + 'id': fake_device_address + }], + } + }) From b6c759f6c62bcfda8990185380ae1b3fc8adbbfa Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Wed, 18 Sep 2024 20:24:44 -0700 Subject: [PATCH 04/10] Use CDDLconv to generate type definitions from BT spec. --- .../modules/bluetooth/BluetoothProcessor.ts | 2 +- .../generated/webdriver-bidi-bluetooth.ts | 83 ++++++++++++++ src/protocol/chromium-bidi.ts | 6 + .../generated/webdriver-bidi-bluetooth.ts | 105 ++++++++---------- 4 files changed, 138 insertions(+), 58 deletions(-) diff --git a/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts b/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts index 378da415e..88efd351c 100644 --- a/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts +++ b/src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts @@ -68,7 +68,7 @@ export class BluetoothProcessor { await context.cdpTarget.browserCdpClient.sendCommand( 'BluetoothEmulation.simulateAdvertisement', { - entry: params.entry, + entry: params.scanEntry, } ); return {}; diff --git a/src/protocol-parser/generated/webdriver-bidi-bluetooth.ts b/src/protocol-parser/generated/webdriver-bidi-bluetooth.ts index 407ad6abf..88d5a7a72 100644 --- a/src/protocol-parser/generated/webdriver-bidi-bluetooth.ts +++ b/src/protocol-parser/generated/webdriver-bidi-bluetooth.ts @@ -26,6 +26,19 @@ import z from 'zod'; +export namespace Bluetooth { + export const BluetoothServiceUuidSchema = z.lazy(() => z.string()); +} +export namespace Bluetooth { + export const BluetoothManufacturerDataMapSchema = z.lazy(() => + z.array( + z.object({ + key: z.number().int().nonnegative(), + data: z.string(), + }) + ) + ); +} export namespace Bluetooth { export const RequestDeviceSchema = z.lazy(() => z.string()); } @@ -40,6 +53,16 @@ export namespace Bluetooth { export namespace Bluetooth { export const RequestDevicePromptSchema = z.lazy(() => z.string()); } +export namespace Bluetooth { + export const ScanRecordSchema = z.lazy(() => + z.object({ + name: z.string().optional(), + uuids: z.array(Bluetooth.BluetoothServiceUuidSchema).optional(), + appearance: z.number().optional(), + manufacturerData: Bluetooth.BluetoothManufacturerDataMapSchema.optional(), + }) + ); +} export namespace Bluetooth { export const HandleRequestDevicePromptSchema = z.lazy(() => z.object({ @@ -78,6 +101,66 @@ export namespace Bluetooth { }) ); } +export namespace Bluetooth { + export const SimulateAdapterSchema = z.lazy(() => + z.object({ + method: z.literal('bluetooth.simulateAdapter'), + params: Bluetooth.SimulateAdapterParametersSchema, + }) + ); +} +export namespace Bluetooth { + export const SimulateAdapterParametersSchema = z.lazy(() => + z.object({ + context: z.string(), + state: z.enum(['absent', 'powered-off', 'powered-on']), + }) + ); +} +export namespace Bluetooth { + export const SimulatePreconnectedPeripheralSchema = z.lazy(() => + z.object({ + method: z.literal('bluetooth.simulatePreconnectedPeripheral'), + params: Bluetooth.SimulatePreconnectedPeripheralParametersSchema, + }) + ); +} +export namespace Bluetooth { + export const SimulatePreconnectedPeripheralParametersSchema = z.lazy(() => + z.object({ + context: z.string(), + address: z.string(), + name: z.string(), + manufacturerData: Bluetooth.BluetoothManufacturerDataMapSchema, + knownServiceUuids: z.array(Bluetooth.BluetoothServiceUuidSchema), + }) + ); +} +export namespace Bluetooth { + export const SimulateAdvertisementSchema = z.lazy(() => + z.object({ + method: z.literal('bluetooth.simulateAdvertisement'), + params: Bluetooth.SimulateAdvertisementParametersSchema, + }) + ); +} +export namespace Bluetooth { + export const SimulateAdvertisementParametersSchema = z.lazy(() => + z.object({ + context: z.string(), + scanEntry: Bluetooth.SimulateAdvertisementScanEntryParametersSchema, + }) + ); +} +export namespace Bluetooth { + export const SimulateAdvertisementScanEntryParametersSchema = z.lazy(() => + z.object({ + deviceAddress: z.string(), + rssi: z.number(), + scanRecord: Bluetooth.ScanRecordSchema, + }) + ); +} export namespace Bluetooth { export const RequestDevicePromptUpdatedSchema = z.lazy(() => z.object({ diff --git a/src/protocol/chromium-bidi.ts b/src/protocol/chromium-bidi.ts index 08c885d36..882976e52 100644 --- a/src/protocol/chromium-bidi.ts +++ b/src/protocol/chromium-bidi.ts @@ -111,12 +111,18 @@ export type Command = ( id: WebDriverBidi.JsUint; } & WebDriverBidiBluetooth.Bluetooth.HandleRequestDevicePrompt) | ({ + // id is defined by the main WebDriver BiDi spec and extension specs do + // not re-define it. Therefore, it's not part of generated types. id: WebDriverBidi.JsUint; } & WebDriverBidiBluetooth.Bluetooth.SimulateAdapter) | ({ + // id is defined by the main WebDriver BiDi spec and extension specs do + // not re-define it. Therefore, it's not part of generated types. id: WebDriverBidi.JsUint; } & WebDriverBidiBluetooth.Bluetooth.SimulatePreconnectedPeripheral) | ({ + // id is defined by the main WebDriver BiDi spec and extension specs do + // not re-define it. Therefore, it's not part of generated types. id: WebDriverBidi.JsUint; } & WebDriverBidiBluetooth.Bluetooth.SimulateAdvertisement) ) & { diff --git a/src/protocol/generated/webdriver-bidi-bluetooth.ts b/src/protocol/generated/webdriver-bidi-bluetooth.ts index 648fe3d83..49281b96f 100644 --- a/src/protocol/generated/webdriver-bidi-bluetooth.ts +++ b/src/protocol/generated/webdriver-bidi-bluetooth.ts @@ -21,6 +21,17 @@ * @see https://github.com/w3c/webdriver-bidi/blob/master/index.bs */ +export namespace Bluetooth { + export type BluetoothServiceUuid = string; +} +export namespace Bluetooth { + export type BluetoothManufacturerDataMap = [ + ...{ + key: number; + data: string; + }[], + ]; +} export namespace Bluetooth { export type RequestDevice = string; } @@ -32,41 +43,14 @@ export namespace Bluetooth { } export namespace Bluetooth { export type RequestDevicePrompt = string; - export type AdapterState = 'powered-on' | 'powered-off' | 'absent'; - export type ManufacturerData = { - /** - * Company identifier - * https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml - * https://usb.org/developers - */ - key: number; - /** - * Manufacturer-specific data (Encoded as a base64 string when passed over JSON) - */ - data: string; - } +} +export namespace Bluetooth { export type ScanRecord = { name?: string; - uuids?: string[]; - /** - * Stores the external appearance description of the device. - */ + uuids?: [...Bluetooth.BluetoothServiceUuid[]]; appearance?: number; - /** - * Stores the transmission power of a broadcasting device. - */ - txPower?: number; - /** - * Key is the company identifier and the value is an array of bytes of - * manufacturer specific data. - */ - manufacturerData?: Bluetooth.ManufacturerData[]; - } - export type ScanEntry = { - deviceAddress: string; - rssi: number; - scanRecord: Bluetooth.ScanRecord; - } + manufacturerData?: Bluetooth.BluetoothManufacturerDataMap; + }; } export namespace Bluetooth { export type HandleRequestDevicePrompt = { @@ -75,27 +59,35 @@ export namespace Bluetooth { }; } export namespace Bluetooth { - export type SimulateAdapter = { - method: 'bluetooth.simulateAdapter'; - params: Bluetooth.SimulateAdapterParameters; + export type HandleRequestDevicePromptParameters = { + context: string; + prompt: Bluetooth.RequestDevicePrompt; + } & ( + | Bluetooth.HandleRequestDevicePromptAcceptParameters + | Bluetooth.HandleRequestDevicePromptCancelParameters + ); +} +export namespace Bluetooth { + export type HandleRequestDevicePromptAcceptParameters = { + accept: true; + device: Bluetooth.RequestDevice; }; } export namespace Bluetooth { - export type SimulateAdapterParameters = { - context: string; - state: Bluetooth.AdapterState; + export type HandleRequestDevicePromptCancelParameters = { + accept: false; }; } export namespace Bluetooth { - export type SimulateAdvertisement = { - method: 'bluetooth.simulateAdvertisement'; - params: Bluetooth.SimulateAdvertisementParameters; + export type SimulateAdapter = { + method: 'bluetooth.simulateAdapter'; + params: Bluetooth.SimulateAdapterParameters; }; } export namespace Bluetooth { - export type SimulateAdvertisementParameters = { + export type SimulateAdapterParameters = { context: string; - entry: Bluetooth.ScanEntry; + state: 'absent' | 'powered-off' | 'powered-on'; }; } export namespace Bluetooth { @@ -109,28 +101,27 @@ export namespace Bluetooth { context: string; address: string; name: string; - manufacturerData: Bluetooth.ManufacturerData[]; - knownServiceUuids: string[]; + manufacturerData: Bluetooth.BluetoothManufacturerDataMap; + knownServiceUuids: [...Bluetooth.BluetoothServiceUuid[]]; }; } export namespace Bluetooth { - export type HandleRequestDevicePromptParameters = { - context: string; - prompt: Bluetooth.RequestDevicePrompt; - } & ( - | Bluetooth.HandleRequestDevicePromptAcceptParameters - | Bluetooth.HandleRequestDevicePromptCancelParameters - ); + export type SimulateAdvertisement = { + method: 'bluetooth.simulateAdvertisement'; + params: Bluetooth.SimulateAdvertisementParameters; + }; } export namespace Bluetooth { - export type HandleRequestDevicePromptAcceptParameters = { - accept: true; - device: Bluetooth.RequestDevice; + export type SimulateAdvertisementParameters = { + context: string; + scanEntry: Bluetooth.SimulateAdvertisementScanEntryParameters; }; } export namespace Bluetooth { - export type HandleRequestDevicePromptCancelParameters = { - accept: false; + export type SimulateAdvertisementScanEntryParameters = { + deviceAddress: string; + rssi: number; + scanRecord: Bluetooth.ScanRecord; }; } export namespace Bluetooth { From 6527e2d70a27e19113ce0424e45db74ad56dcaca Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Wed, 18 Sep 2024 20:34:15 -0700 Subject: [PATCH 05/10] Update tests to use new BiDi apis --- tests/bluetooth/test_emulation.py | 95 +++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/tests/bluetooth/test_emulation.py b/tests/bluetooth/test_emulation.py index 4517f5560..40c2b4996 100644 --- a/tests/bluetooth/test_emulation.py +++ b/tests/bluetooth/test_emulation.py @@ -19,29 +19,21 @@ HTML_SINGLE_PERIPHERAL = """
- bluetooth +
""" -@pytest.mark.asyncio -@pytest.mark.parametrize('capabilities', [{ - 'goog:chromeOptions': { - 'args': ['--enable-features=WebBluetooth'] - } -}], - indirect=True) -async def test_bluetooth_simulate_adapter(websocket, context_id, html): - await subscribe(websocket, ['bluetooth']) +# Create a fake BT device. +fake_device_address = '09:09:09:09:09:09' - url = html(HTML_SINGLE_PERIPHERAL) - await goto_url(websocket, context_id, url) +async def setup_device(websocket, context_id): # Simulate a Bluetooth adapter. await execute_command( websocket, { @@ -67,6 +59,27 @@ async def test_bluetooth_simulate_adapter(websocket, context_id, html): } }) + + +@pytest.mark.asyncio +@pytest.mark.parametrize('capabilities', [{ + 'goog:chromeOptions': { + 'args': ['--enable-features=WebBluetooth'] + } +}], + indirect=True) +async def test_bluetooth_requestDevicePromptUpdated(websocket, context_id, + html, test_headless_mode): + if test_headless_mode == "old": + pytest.xfail("Old headless mode does not support Bluetooth") + + await subscribe(websocket, ['bluetooth']) + + url = html(HTML_SINGLE_PERIPHERAL) + await goto_url(websocket, context_id, url) + + await setup_device(websocket, context_id) + await send_JSON_command( websocket, { 'method': 'script.evaluate', @@ -81,10 +94,10 @@ async def test_bluetooth_simulate_adapter(websocket, context_id, html): }) response = await wait_for_event(websocket, - 'bluetooth.requestDevicePromptOpened') + 'bluetooth.requestDevicePromptUpdated') assert response == AnyExtending({ 'type': 'event', - 'method': 'bluetooth.requestDevicePromptOpened', + 'method': 'bluetooth.requestDevicePromptUpdated', 'params': { 'context': context_id, 'devices': [{ @@ -92,3 +105,51 @@ async def test_bluetooth_simulate_adapter(websocket, context_id, html): }], } }) + + +@pytest.mark.asyncio +@pytest.mark.parametrize('capabilities', [{ + 'goog:chromeOptions': { + 'args': ['--enable-features=WebBluetooth'] + } +}], + indirect=True) +@pytest.mark.parametrize('accept', [True, False]) +async def test_bluetooth_handleRequestDevicePrompt(websocket, context_id, html, + test_headless_mode, accept): + if test_headless_mode == "old": + pytest.xfail("Old headless mode does not support Bluetooth") + + await subscribe(websocket, ['bluetooth']) + + url = html(HTML_SINGLE_PERIPHERAL) + await goto_url(websocket, context_id, url) + + await setup_device(websocket, context_id) + + await send_JSON_command( + websocket, { + 'method': 'script.evaluate', + 'params': { + 'expression': 'document.querySelector("#bluetooth").click();', + 'awaitPromise': True, + 'target': { + 'context': context_id, + }, + 'userActivation': True + } + }) + + event = await wait_for_event(websocket, + 'bluetooth.requestDevicePromptUpdated') + + await execute_command( + websocket, { + 'method': 'bluetooth.handleRequestDevicePrompt', + 'params': { + 'context': context_id, + 'accept': accept, + 'prompt': event['params']['prompt'], + 'device': event['params']['devices'][0]['id'] + } + }) From 03ed42abddd15c3a8d5ba84a6b6f25577b64fef7 Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Wed, 18 Sep 2024 20:38:03 -0700 Subject: [PATCH 06/10] Fix precommit lint --- tests/bluetooth/test_emulation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/bluetooth/test_emulation.py b/tests/bluetooth/test_emulation.py index 40c2b4996..b55316d8f 100644 --- a/tests/bluetooth/test_emulation.py +++ b/tests/bluetooth/test_emulation.py @@ -60,7 +60,6 @@ async def setup_device(websocket, context_id): }) - @pytest.mark.asyncio @pytest.mark.parametrize('capabilities', [{ 'goog:chromeOptions': { From 7ee958b07503afa54e8662d78a10b15d6a681c48 Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Fri, 4 Oct 2024 14:49:47 -0700 Subject: [PATCH 07/10] Update CDDL to the latest and merged spec. --- .../generated/webdriver-bidi-bluetooth.ts | 18 +++++++++--------- .../generated/webdriver-bidi-bluetooth.ts | 14 ++++++-------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/protocol-parser/generated/webdriver-bidi-bluetooth.ts b/src/protocol-parser/generated/webdriver-bidi-bluetooth.ts index 88d5a7a72..f02ffdbd3 100644 --- a/src/protocol-parser/generated/webdriver-bidi-bluetooth.ts +++ b/src/protocol-parser/generated/webdriver-bidi-bluetooth.ts @@ -30,13 +30,11 @@ export namespace Bluetooth { export const BluetoothServiceUuidSchema = z.lazy(() => z.string()); } export namespace Bluetooth { - export const BluetoothManufacturerDataMapSchema = z.lazy(() => - z.array( - z.object({ - key: z.number().int().nonnegative(), - data: z.string(), - }) - ) + export const BluetoothManufacturerDataSchema = z.lazy(() => + z.object({ + key: z.number().int().nonnegative(), + data: z.string(), + }) ); } export namespace Bluetooth { @@ -59,7 +57,9 @@ export namespace Bluetooth { name: z.string().optional(), uuids: z.array(Bluetooth.BluetoothServiceUuidSchema).optional(), appearance: z.number().optional(), - manufacturerData: Bluetooth.BluetoothManufacturerDataMapSchema.optional(), + manufacturerData: z + .array(Bluetooth.BluetoothManufacturerDataSchema) + .optional(), }) ); } @@ -131,7 +131,7 @@ export namespace Bluetooth { context: z.string(), address: z.string(), name: z.string(), - manufacturerData: Bluetooth.BluetoothManufacturerDataMapSchema, + manufacturerData: z.array(Bluetooth.BluetoothManufacturerDataSchema), knownServiceUuids: z.array(Bluetooth.BluetoothServiceUuidSchema), }) ); diff --git a/src/protocol/generated/webdriver-bidi-bluetooth.ts b/src/protocol/generated/webdriver-bidi-bluetooth.ts index 49281b96f..6191d3d80 100644 --- a/src/protocol/generated/webdriver-bidi-bluetooth.ts +++ b/src/protocol/generated/webdriver-bidi-bluetooth.ts @@ -25,12 +25,10 @@ export namespace Bluetooth { export type BluetoothServiceUuid = string; } export namespace Bluetooth { - export type BluetoothManufacturerDataMap = [ - ...{ - key: number; - data: string; - }[], - ]; + export type BluetoothManufacturerData = { + key: number; + data: string; + }; } export namespace Bluetooth { export type RequestDevice = string; @@ -49,7 +47,7 @@ export namespace Bluetooth { name?: string; uuids?: [...Bluetooth.BluetoothServiceUuid[]]; appearance?: number; - manufacturerData?: Bluetooth.BluetoothManufacturerDataMap; + manufacturerData?: [...Bluetooth.BluetoothManufacturerData[]]; }; } export namespace Bluetooth { @@ -101,7 +99,7 @@ export namespace Bluetooth { context: string; address: string; name: string; - manufacturerData: Bluetooth.BluetoothManufacturerDataMap; + manufacturerData: [...Bluetooth.BluetoothManufacturerData[]]; knownServiceUuids: [...Bluetooth.BluetoothServiceUuid[]]; }; } From f1330aafa4b92b564a8609eb53ee2ffdbd1499f2 Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Mon, 7 Oct 2024 06:35:49 -0700 Subject: [PATCH 08/10] Update BluetoothManufacturerData data type that got missed. --- src/protocol/generated/webdriver-bidi-bluetooth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocol/generated/webdriver-bidi-bluetooth.ts b/src/protocol/generated/webdriver-bidi-bluetooth.ts index 6191d3d80..ad8d98e84 100644 --- a/src/protocol/generated/webdriver-bidi-bluetooth.ts +++ b/src/protocol/generated/webdriver-bidi-bluetooth.ts @@ -27,7 +27,7 @@ export namespace Bluetooth { export namespace Bluetooth { export type BluetoothManufacturerData = { key: number; - data: string; + data: Uint8Array; }; } export namespace Bluetooth { From 935b26979b7dc1d6f1ad7c54c199d539f201dd91 Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Thu, 10 Oct 2024 06:39:42 -0700 Subject: [PATCH 09/10] Revert the last change, in favor of spec modification --- src/protocol/generated/webdriver-bidi-bluetooth.ts | 2 +- tests/bluetooth/test_emulation.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/protocol/generated/webdriver-bidi-bluetooth.ts b/src/protocol/generated/webdriver-bidi-bluetooth.ts index ad8d98e84..6191d3d80 100644 --- a/src/protocol/generated/webdriver-bidi-bluetooth.ts +++ b/src/protocol/generated/webdriver-bidi-bluetooth.ts @@ -27,7 +27,7 @@ export namespace Bluetooth { export namespace Bluetooth { export type BluetoothManufacturerData = { key: number; - data: Uint8Array; + data: string; }; } export namespace Bluetooth { diff --git a/tests/bluetooth/test_emulation.py b/tests/bluetooth/test_emulation.py index b55316d8f..22c0a0f27 100644 --- a/tests/bluetooth/test_emulation.py +++ b/tests/bluetooth/test_emulation.py @@ -46,20 +46,22 @@ async def setup_device(websocket, context_id): # Create a fake BT device. fake_device_address = '09:09:09:09:09:09' - await execute_command( + response = await execute_command( websocket, { 'method': 'bluetooth.simulatePreconnectedPeripheral', 'params': { 'context': context_id, 'address': fake_device_address, 'name': 'SomeDevice', - 'manufacturerData': [], + 'manufacturerData': [{ + 'key': 17, + 'data': 'AP8BAX8=', + }], 'knownServiceUuids': ['12345678-1234-5678-9abc-def123456789', ], } }) - @pytest.mark.asyncio @pytest.mark.parametrize('capabilities', [{ 'goog:chromeOptions': { From 039e551263fb651b34155e7050dd9b1a0a57e30f Mon Sep 17 00:00:00 2001 From: "Alex N. Jose" Date: Thu, 10 Oct 2024 06:49:42 -0700 Subject: [PATCH 10/10] Fix flake8 errors --- tests/bluetooth/test_emulation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/bluetooth/test_emulation.py b/tests/bluetooth/test_emulation.py index 22c0a0f27..8dbee18b0 100644 --- a/tests/bluetooth/test_emulation.py +++ b/tests/bluetooth/test_emulation.py @@ -46,7 +46,7 @@ async def setup_device(websocket, context_id): # Create a fake BT device. fake_device_address = '09:09:09:09:09:09' - response = await execute_command( + await execute_command( websocket, { 'method': 'bluetooth.simulatePreconnectedPeripheral', 'params': { @@ -62,6 +62,7 @@ async def setup_device(websocket, context_id): } }) + @pytest.mark.asyncio @pytest.mark.parametrize('capabilities', [{ 'goog:chromeOptions': {