diff options
Diffstat (limited to 'testing/web-platform/tests/bluetooth/resources')
4 files changed, 1700 insertions, 0 deletions
diff --git a/testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js b/testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js new file mode 100644 index 0000000000..b718ab579a --- /dev/null +++ b/testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js @@ -0,0 +1,1203 @@ +'use strict'; + +/* Bluetooth Constants */ + +/** + * HCI Error Codes. + * Used for simulateGATT{Dis}ConnectionResponse. For a complete list of + * possible error codes see BT 4.2 Vol 2 Part D 1.3 List Of Error Codes. + */ +const HCI_SUCCESS = 0x0000; +const HCI_CONNECTION_TIMEOUT = 0x0008; + +/** + * GATT Error codes. + * Used for GATT operations responses. BT 4.2 Vol 3 Part F 3.4.1.1 Error + * Response + */ +const GATT_SUCCESS = 0x0000; +const GATT_INVALID_HANDLE = 0x0001; + +/* Bluetooth UUID Constants */ + +/* Service UUIDs */ +var blocklist_test_service_uuid = '611c954a-263b-4f4a-aab6-01ddb953f985'; +var request_disconnection_service_uuid = '01d7d889-7451-419f-aeb8-d65e7b9277af'; + +/* Characteristic UUIDs */ +var blocklist_exclude_reads_characteristic_uuid = + 'bad1c9a2-9a5b-4015-8b60-1579bbbf2135'; +var request_disconnection_characteristic_uuid = + '01d7d88a-7451-419f-aeb8-d65e7b9277af'; + +/* Descriptor UUIDs */ +var blocklist_test_descriptor_uuid = 'bad2ddcf-60db-45cd-bef9-fd72b153cf7c'; +var blocklist_exclude_reads_descriptor_uuid = + 'bad3ec61-3cc3-4954-9702-7977df514114'; + +/** + * Helper objects that associate Bluetooth names, aliases, and UUIDs. These are + * useful for tests that check that the same result is produces when using all + * three methods of referring to a Bluetooth UUID. + */ +var generic_access = { + alias: 0x1800, + name: 'generic_access', + uuid: '00001800-0000-1000-8000-00805f9b34fb' +}; +var device_name = { + alias: 0x2a00, + name: 'gap.device_name', + uuid: '00002a00-0000-1000-8000-00805f9b34fb' +}; +var reconnection_address = { + alias: 0x2a03, + name: 'gap.reconnection_address', + uuid: '00002a03-0000-1000-8000-00805f9b34fb' +}; +var heart_rate = { + alias: 0x180d, + name: 'heart_rate', + uuid: '0000180d-0000-1000-8000-00805f9b34fb' +}; +var health_thermometer = { + alias: 0x1809, + name: 'health_thermometer', + uuid: '00001809-0000-1000-8000-00805f9b34fb' +}; +var body_sensor_location = { + alias: 0x2a38, + name: 'body_sensor_location', + uuid: '00002a38-0000-1000-8000-00805f9b34fb' +}; +var glucose = { + alias: 0x1808, + name: 'glucose', + uuid: '00001808-0000-1000-8000-00805f9b34fb' +}; +var battery_service = { + alias: 0x180f, + name: 'battery_service', + uuid: '0000180f-0000-1000-8000-00805f9b34fb' +}; +var battery_level = { + alias: 0x2A19, + name: 'battery_level', + uuid: '00002a19-0000-1000-8000-00805f9b34fb' +}; +var user_description = { + alias: 0x2901, + name: 'gatt.characteristic_user_description', + uuid: '00002901-0000-1000-8000-00805f9b34fb' +}; +var client_characteristic_configuration = { + alias: 0x2902, + name: 'gatt.client_characteristic_configuration', + uuid: '00002902-0000-1000-8000-00805f9b34fb' +}; +var measurement_interval = { + alias: 0x2a21, + name: 'measurement_interval', + uuid: '00002a21-0000-1000-8000-00805f9b34fb' +}; + +/** + * An advertisement packet object that simulates a Health Thermometer device. + * @type {ScanResult} + */ +const health_thermometer_ad_packet = { + deviceAddress: '09:09:09:09:09:09', + rssi: -10, + scanRecord: { + name: 'Health Thermometer', + uuids: [health_thermometer.uuid], + }, +}; + +/** + * An advertisement packet object that simulates a Heart Rate device. + * @type {ScanResult} + */ +const heart_rate_ad_packet = { + deviceAddress: '08:08:08:08:08:08', + rssi: -10, + scanRecord: { + name: 'Heart Rate', + uuids: [heart_rate.uuid], + }, +}; + +const uuid1234 = BluetoothUUID.getService(0x1234); +const uuid5678 = BluetoothUUID.getService(0x5678); +const uuidABCD = BluetoothUUID.getService(0xABCD); +const manufacturer1Data = new Uint8Array([1, 2]); +const manufacturer2Data = new Uint8Array([3, 4]); +const uuid1234Data = new Uint8Array([5, 6]); +const uuid5678Data = new Uint8Array([7, 8]); +const uuidABCDData = new Uint8Array([9, 10]); + +// TODO(crbug.com/1163207): Add the blocklist link. +// Fake manufacturer data following iBeacon format listed in +// https://en.wikipedia.org/wiki/IBeacon, which will be blocked according to [TBD blocklist link]. +const blocklistedManufacturerId = 0x4c; +const blocklistedManufacturerData = new Uint8Array([ + 0x02, 0x15, 0xb3, 0xeb, 0x8d, 0xb1, 0x30, 0xa5, 0x44, 0x8d, 0xb4, 0xac, + 0xfb, 0x68, 0xc9, 0x23, 0xa3, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xbf +]); +// Fake manufacturer data that is not in [TBD blocklist link]. +const nonBlocklistedManufacturerId = 0x0001; +const nonBlocklistedManufacturerData = new Uint8Array([1, 2]); + +/** + * An advertisement packet object that simulates a device that advertises + * service and manufacturer data. + * @type {ScanResult} + */ +const service_and_manufacturer_data_ad_packet = { + deviceAddress: '07:07:07:07:07:07', + rssi: -10, + scanRecord: { + name: 'LE Device', + uuids: [uuid1234], + manufacturerData: {0x0001: manufacturer1Data, 0x0002: manufacturer2Data}, + serviceData: { + [uuid1234]: uuid1234Data, + [uuid5678]: uuid5678Data, + [uuidABCD]: uuidABCDData + } + } +}; + +/** Bluetooth Helpers */ + +/** + * Helper class to create a BluetoothCharacteristicProperties object using an + * array of strings corresponding to the property bit to set. + */ +class TestCharacteristicProperties { + /** @param {Array<string>} properties */ + constructor(properties) { + this.broadcast = false; + this.read = false; + this.writeWithoutResponse = false; + this.write = false; + this.notify = false; + this.indicate = false; + this.authenticatedSignedWrites = false; + this.reliableWrite = false; + this.writableAuxiliaries = false; + + properties.forEach(val => { + if (this.hasOwnProperty(val)) + this[val] = true; + else + throw `Invalid member '${val}'`; + }); + } +} + +/** + * Produces an array of BluetoothLEScanFilterInit objects containing the list of + * services in |services| and various permutations of the other + * BluetoothLEScanFilterInit properties. This method is used to test that the + * |services| are valid so the other properties do not matter. + * @param {BluetoothServiceUUID} services + * @returns {Array<RequestDeviceOptions>} A list of options containing + * |services| and various permutations of other options. + */ +function generateRequestDeviceArgsWithServices(services = ['heart_rate']) { + return [ + {filters: [{services: services}]}, + {filters: [{services: services, name: 'Name'}]}, + {filters: [{services: services, namePrefix: 'Pre'}]}, { + filters: [ + {services: services, manufacturerData: [{companyIdentifier: 0x0001}]} + ] + }, + { + filters: [{ + services: services, + name: 'Name', + namePrefix: 'Pre', + manufacturerData: [{companyIdentifier: 0x0001}] + }] + }, + {filters: [{services: services}], optionalServices: ['heart_rate']}, { + filters: [{services: services, name: 'Name'}], + optionalServices: ['heart_rate'] + }, + { + filters: [{services: services, namePrefix: 'Pre'}], + optionalServices: ['heart_rate'] + }, + { + filters: [ + {services: services, manufacturerData: [{companyIdentifier: 0x0001}]} + ], + optionalServices: ['heart_rate'] + }, + { + filters: [{ + services: services, + name: 'Name', + namePrefix: 'Pre', + manufacturerData: [{companyIdentifier: 0x0001}] + }], + optionalServices: ['heart_rate'] + } + ]; +} + +/** + * Causes |fake_peripheral| to disconnect and returns a promise that resolves + * once `gattserverdisconnected` has been fired on |device|. + * @param {BluetoothDevice} device The device to check if the + * `gattserverdisconnected` promise was fired. + * @param {FakePeripheral} fake_peripheral The device fake that represents + * |device|. + * @returns {Promise<Array<Object>>} A promise that resolves when the device has + * successfully disconnected. + */ +function simulateGATTDisconnectionAndWait(device, fake_peripheral) { + return Promise.all([ + eventPromise(device, 'gattserverdisconnected'), + fake_peripheral.simulateGATTDisconnection(), + ]); +} + +/** @type {FakeCentral} The fake adapter for the current test. */ +let fake_central = null; + +async function initializeFakeCentral({state = 'powered-on'}) { + if (!fake_central) { + fake_central = await navigator.bluetooth.test.simulateCentral({state}); + } +} + +/** + * A dictionary for specifying fake Bluetooth device setup options. + * @typedef {{address: !string, name: !string, + * manufacturerData: !Object<uint16,Array<uint8>>, + * knownServiceUUIDs: !Array<string>, connectable: !boolean, + * serviceDiscoveryComplete: !boolean}} + */ +let FakeDeviceOptions; + +/** + * @typedef {{fakeDeviceOptions: FakeDeviceOptions, + * requestDeviceOptions: RequestDeviceOptions}} + */ +let SetupOptions; + +/** + * Default options for setting up a Bluetooth device. + * @type {FakeDeviceOptions} + */ +const fakeDeviceOptionsDefault = { + address: '00:00:00:00:00:00', + name: 'LE Device', + manufacturerData: {}, + knownServiceUUIDs: [], + connectable: false, + serviceDiscoveryComplete: false, +}; + +/** + * A dictionary containing the fake Bluetooth device object. The dictionary can + * optionally contain its fake services and its BluetoothDevice counterpart. + * @typedef {{fake_peripheral: !FakePeripheral, + * fake_services: Object<string, FakeService>, + * device: BluetoothDevice}} + */ +let FakeDevice; + +/** + * Creates a SetupOptions object using |setupOptionsDefault| as the base options + * object with the options from |setupOptionsOverride| overriding these + * defaults. + * @param {SetupOptions} setupOptionsDefault The default options object to use + * as the base. + * @param {SetupOptions} setupOptionsOverride The options to override the + * defaults with. + * @returns {SetupOptions} The merged setup options containing the defaults with + * the overrides applied. + */ +function createSetupOptions(setupOptionsDefault, setupOptionsOverride) { + // Merge the properties of |setupOptionsDefault| and |setupOptionsOverride| + // without modifying |setupOptionsDefault|. + let fakeDeviceOptions = Object.assign( + {...setupOptionsDefault.fakeDeviceOptions}, + setupOptionsOverride.fakeDeviceOptions); + let requestDeviceOptions = Object.assign( + {...setupOptionsDefault.requestDeviceOptions}, + setupOptionsOverride.requestDeviceOptions); + + return {fakeDeviceOptions, requestDeviceOptions}; +} + +/** + * Adds a preconnected device with the given options. A preconnected device is a + * device that has been paired with the system previously. This can be done if, + * for example, the user pairs the device using the OS'es settings. + * + * By default, the preconnected device will be set up using the + * |fakeDeviceOptionsDefault| and will not use a RequestDeviceOption object. + * This means that the device will not be requested during the setup. + * + * If |setupOptionsOverride| is provided, these options will override the + * defaults. If |setupOptionsOverride| includes the requestDeviceOptions + * property, then the device will be requested using those options. + * @param {SetupOptions} setupOptionsOverride An object containing options for + * setting up a fake Bluetooth device and for requesting the device. + * @returns {Promise<FakeDevice>} The device fake initialized with the + * parameter values. + */ +async function setUpPreconnectedFakeDevice(setupOptionsOverride) { + await initializeFakeCentral({state: 'powered-on'}); + + let setupOptions = createSetupOptions( + {fakeDeviceOptions: fakeDeviceOptionsDefault}, setupOptionsOverride); + + // Simulate the fake peripheral. + let preconnectedDevice = {}; + preconnectedDevice.fake_peripheral = + await fake_central.simulatePreconnectedPeripheral({ + address: setupOptions.fakeDeviceOptions.address, + name: setupOptions.fakeDeviceOptions.name, + manufacturerData: setupOptions.fakeDeviceOptions.manufacturerData, + knownServiceUUIDs: setupOptions.fakeDeviceOptions.knownServiceUUIDs, + }); + + if (setupOptions.fakeDeviceOptions.connectable) { + await preconnectedDevice.fake_peripheral.setNextGATTConnectionResponse( + {code: HCI_SUCCESS}); + } + + // Add known services. + preconnectedDevice.fake_services = new Map(); + for (let service of setupOptions.fakeDeviceOptions.knownServiceUUIDs) { + let fake_service = await preconnectedDevice.fake_peripheral.addFakeService( + {uuid: service}); + preconnectedDevice.fake_services.set(service, fake_service); + } + + // Request the device if the request option isn't empty. + if (Object.keys(setupOptions.requestDeviceOptions).length !== 0) { + preconnectedDevice.device = + await requestDeviceWithTrustedClick(setupOptions.requestDeviceOptions); + } + + // Set up services discovered state. + if (setupOptions.fakeDeviceOptions.serviceDiscoveryComplete) { + await preconnectedDevice.fake_peripheral.setNextGATTDiscoveryResponse( + {code: HCI_SUCCESS}); + } + + return preconnectedDevice; +} + +/** Blocklisted GATT Device Helper Methods */ + +/** @type {FakeDeviceOptions} */ +const blocklistFakeDeviceOptionsDefault = { + address: '11:11:11:11:11:11', + name: 'Blocklist Device', + knownServiceUUIDs: ['generic_access', blocklist_test_service_uuid], + connectable: true, + serviceDiscoveryComplete: true +}; + +/** @type {RequestDeviceOptions} */ +const blocklistRequestDeviceOptionsDefault = { + filters: [{services: [blocklist_test_service_uuid]}] +}; + +/** @type {SetupOptions} */ +const blocklistSetupOptionsDefault = { + fakeDeviceOptions: blocklistFakeDeviceOptionsDefault, + requestDeviceOptions: blocklistRequestDeviceOptionsDefault +}; + +/** + * Returns an object containing a BluetoothDevice discovered using |options|, + * its corresponding FakePeripheral and FakeRemoteGATTServices. + * The simulated device is called 'Blocklist Device' and it has one known + * service UUID |blocklist_test_service_uuid|. The |blocklist_test_service_uuid| + * service contains two characteristics: + * - |blocklist_exclude_reads_characteristic_uuid| (read, write) + * - 'gap.peripheral_privacy_flag' (read, write) + * The 'gap.peripheral_privacy_flag' characteristic contains three descriptors: + * - |blocklist_test_descriptor_uuid| + * - |blocklist_exclude_reads_descriptor_uuid| + * - 'gatt.client_characteristic_configuration' + * These are special UUIDs that have been added to the blocklist found at + * https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt + * There are also test UUIDs that have been added to the test environment which + * other implementations should add as test UUIDs as well. + * The device has been connected to and its attributes are ready to be + * discovered. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor}>} An + * object containing the BluetoothDevice object and its corresponding + * GATT fake objects. + */ +async function getBlocklistDevice(setupOptionsOverride = {}) { + let setupOptions = + createSetupOptions(blocklistSetupOptionsDefault, setupOptionsOverride); + let fakeDevice = await setUpPreconnectedFakeDevice(setupOptions); + await fakeDevice.device.gatt.connect(); + + let fake_blocklist_test_service = + fakeDevice.fake_services.get(blocklist_test_service_uuid); + + let fake_blocklist_exclude_reads_characteristic = + await fake_blocklist_test_service.addFakeCharacteristic({ + uuid: blocklist_exclude_reads_characteristic_uuid, + properties: ['read', 'write'], + }); + let fake_blocklist_exclude_writes_characteristic = + await fake_blocklist_test_service.addFakeCharacteristic({ + uuid: 'gap.peripheral_privacy_flag', + properties: ['read', 'write'], + }); + + let fake_blocklist_descriptor = + await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor( + {uuid: blocklist_test_descriptor_uuid}); + let fake_blocklist_exclude_reads_descriptor = + await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor( + {uuid: blocklist_exclude_reads_descriptor_uuid}); + let fake_blocklist_exclude_writes_descriptor = + await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor( + {uuid: 'gatt.client_characteristic_configuration'}); + return { + device: fakeDevice.device, + fake_peripheral: fakeDevice.fake_peripheral, + fake_blocklist_test_service, + fake_blocklist_exclude_reads_characteristic, + fake_blocklist_exclude_writes_characteristic, + fake_blocklist_descriptor, + fake_blocklist_exclude_reads_descriptor, + fake_blocklist_exclude_writes_descriptor, + }; +} + +/** + * Returns an object containing a Blocklist Test BluetoothRemoteGattService and + * its corresponding FakeRemoteGATTService. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService}>} An object containing the + * BluetoothDevice object and its corresponding GATT fake objects. + */ +async function getBlocklistTestService() { + let result = await getBlocklistDevice(); + let service = + await result.device.gatt.getPrimaryService(blocklist_test_service_uuid); + return Object.assign(result, { + service, + fake_service: result.fake_blocklist_test_service, + }); +} + +/** + * Returns an object containing a blocklisted BluetoothRemoteGATTCharacteristic + * that excludes reads and its corresponding FakeRemoteGATTCharacteristic. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic}>} An object + * containing the BluetoothDevice object and its corresponding GATT fake + * objects. + */ +async function getBlocklistExcludeReadsCharacteristic() { + let result = await getBlocklistTestService(); + let characteristic = await result.service.getCharacteristic( + blocklist_exclude_reads_characteristic_uuid); + return Object.assign(result, { + characteristic, + fake_characteristic: result.fake_blocklist_exclude_reads_characteristic + }); +} + +/** + * Returns an object containing a blocklisted BluetoothRemoteGATTCharacteristic + * that excludes writes and its corresponding FakeRemoteGATTCharacteristic. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic}>} An object + * containing the BluetoothDevice object and its corresponding GATT fake + * objects. + */ +async function getBlocklistExcludeWritesCharacteristic() { + let result = await getBlocklistTestService(); + let characteristic = + await result.service.getCharacteristic('gap.peripheral_privacy_flag'); + return Object.assign(result, { + characteristic, + fake_characteristic: result.fake_blocklist_exclude_writes_characteristic + }); +} + +/** + * Returns an object containing a blocklisted BluetoothRemoteGATTDescriptor that + * excludes reads and its corresponding FakeRemoteGATTDescriptor. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic, + * descriptor: BluetoothRemoteGATTDescriptor, + * fake_descriptor: FakeBluetoothRemoteGATTDescriptor}>} An object + * containing the BluetoothDevice object and its corresponding GATT fake + * objects. + */ +async function getBlocklistExcludeReadsDescriptor() { + let result = await getBlocklistExcludeWritesCharacteristic(); + let descriptor = await result.characteristic.getDescriptor( + blocklist_exclude_reads_descriptor_uuid); + return Object.assign(result, { + descriptor, + fake_descriptor: result.fake_blocklist_exclude_reads_descriptor + }); +} + +/** + * Returns an object containing a blocklisted BluetoothRemoteGATTDescriptor that + * excludes writes and its corresponding FakeRemoteGATTDescriptor. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic, + * descriptor: BluetoothRemoteGATTDescriptor, + * fake_descriptor: FakeBluetoothRemoteGATTDescriptor}>} An object + * containing the BluetoothDevice object and its corresponding GATT fake + * objects. + */ +async function getBlocklistExcludeWritesDescriptor() { + let result = await getBlocklistExcludeWritesCharacteristic(); + let descriptor = await result.characteristic.getDescriptor( + 'gatt.client_characteristic_configuration'); + return Object.assign(result, { + descriptor: descriptor, + fake_descriptor: result.fake_blocklist_exclude_writes_descriptor, + }); +} + +/** Bluetooth HID Device Helper Methods */ + +/** @type {FakeDeviceOptions} */ +const connectedHIDFakeDeviceOptionsDefault = { + address: '10:10:10:10:10:10', + name: 'HID Device', + knownServiceUUIDs: [ + 'generic_access', + 'device_information', + 'human_interface_device', + ], + connectable: true, + serviceDiscoveryComplete: false +}; + +/** @type {RequestDeviceOptions} */ +const connectedHIDRequestDeviceOptionsDefault = { + filters: [{services: ['device_information']}], + optionalServices: ['human_interface_device'] +}; + +/** @type {SetupOptions} */ +const connectedHIDSetupOptionsDefault = { + fakeDeviceOptions: connectedHIDFakeDeviceOptionsDefault, + requestDeviceOptions: connectedHIDRequestDeviceOptionsDefault +}; + +/** + * Similar to getHealthThermometerDevice except the GATT discovery + * response has not been set yet so more attributes can still be added. + * TODO(crbug.com/719816): Add descriptors. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object + * containing a requested BluetoothDevice and its fake counter part. + */ +async function getConnectedHIDDevice( + requestDeviceOptionsOverride, fakeDeviceOptionsOverride) { + let setupOptions = createSetupOptions(connectedHIDSetupOptionsDefault, { + fakeDeviceOptions: fakeDeviceOptionsOverride, + requestDeviceOptions: requestDeviceOptionsOverride + }); + + let fakeDevice = await setUpPreconnectedFakeDevice(setupOptions); + await fakeDevice.device.gatt.connect(); + + // Blocklisted Characteristic: + // https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt + let dev_info = fakeDevice.fake_services.get('device_information'); + await dev_info.addFakeCharacteristic({ + uuid: 'serial_number_string', + properties: ['read'], + }); + return fakeDevice; +} + +/** + * Returns a BluetoothDevice discovered using |options| and its + * corresponding FakePeripheral. + * The simulated device is called 'HID Device' it has three known service + * UUIDs: 'generic_access', 'device_information', 'human_interface_device'. + * The primary service with 'device_information' UUID has a characteristics + * with UUID 'serial_number_string'. The device has been connected to and its + * attributes are ready to be discovered. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object + * containing a requested BluetoothDevice and its fake counter part. + */ +async function getHIDDevice(options) { + let result = + await getConnectedHIDDevice(options, {serviceDiscoveryComplete: true}); + return result; +} + +/** Health Thermometer Bluetooth Device Helper Methods */ + +/** @type {FakeDeviceOptions} */ +const healthTherometerFakeDeviceOptionsDefault = { + address: '09:09:09:09:09:09', + name: 'Health Thermometer', + manufacturerData: {0x0001: manufacturer1Data, 0x0002: manufacturer2Data}, + knownServiceUUIDs: ['generic_access', 'health_thermometer'], +}; + +/** + * Returns a FakeDevice that corresponds to a simulated pre-connected device + * called 'Health Thermometer'. The device has two known serviceUUIDs: + * 'generic_access' and 'health_thermometer' and some fake manufacturer data. + * @returns {Promise<FakeDevice>} The device fake initialized as a Health + * Thermometer device. + */ +async function setUpHealthThermometerDevice(setupOptionsOverride = {}) { + let setupOptions = createSetupOptions( + {fakeDeviceOptions: healthTherometerFakeDeviceOptionsDefault}, + setupOptionsOverride); + return await setUpPreconnectedFakeDevice(setupOptions); +} + +/** + * Returns the same fake device as setUpHealthThermometerDevice() except + * that connecting to the peripheral will succeed. + * @returns {Promise<FakeDevice>} The device fake initialized as a + * connectable Health Thermometer device. + */ +async function setUpConnectableHealthThermometerDevice() { + let fake_device = await setUpHealthThermometerDevice( + {fakeDeviceOptions: {connectable: true}}); + return fake_device; +} + +/** + * Populates a fake_device with various fakes appropriate for a health + * thermometer. This resolves to an associative array composed of the fakes, + * including the |fake_peripheral|. + * @param {FakeDevice} fake_device The Bluetooth fake to populate GATT + * services, characteristics, and descriptors on. + * @returns {Promise<{fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}>} The FakePeripheral + * passed into this method along with the fake GATT services, characteristics, + * and descriptors added to it. + */ +async function populateHealthThermometerFakes(fake_device) { + let fake_peripheral = fake_device.fake_peripheral; + let fake_generic_access = fake_device.fake_services.get('generic_access'); + let fake_health_thermometer = + fake_device.fake_services.get('health_thermometer'); + let fake_measurement_interval = + await fake_health_thermometer.addFakeCharacteristic({ + uuid: 'measurement_interval', + properties: ['read', 'write', 'indicate'], + }); + let fake_user_description = + await fake_measurement_interval.addFakeDescriptor({ + uuid: 'gatt.characteristic_user_description', + }); + let fake_cccd = await fake_measurement_interval.addFakeDescriptor({ + uuid: 'gatt.client_characteristic_configuration', + }); + let fake_temperature_measurement = + await fake_health_thermometer.addFakeCharacteristic({ + uuid: 'temperature_measurement', + properties: ['indicate'], + }); + let fake_temperature_type = + await fake_health_thermometer.addFakeCharacteristic({ + uuid: 'temperature_type', + properties: ['read'], + }); + return { + fake_peripheral, + fake_generic_access, + fake_health_thermometer, + fake_measurement_interval, + fake_cccd, + fake_user_description, + fake_temperature_measurement, + fake_temperature_type, + }; +} + +/** + * Returns the same device and fake peripheral as getHealthThermometerDevice() + * after another frame (an iframe we insert) discovered the device, + * connected to it and discovered its services. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getHealthThermometerDeviceWithServicesDiscovered(options) { + let iframe = document.createElement('iframe'); + let fake_device = await setUpConnectableHealthThermometerDevice(); + let fakes = populateHealthThermometerFakes(fake_device); + await fake_device.fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS, + }); + await new Promise(resolve => { + let src = '/bluetooth/resources/health-thermometer-iframe.html'; + // TODO(509038): Can be removed once LayoutTests/bluetooth/* that + // use health-thermometer-iframe.html have been moved to + // LayoutTests/external/wpt/bluetooth/* + if (window.location.pathname.includes('/LayoutTests/')) { + src = + '../../../external/wpt/bluetooth/resources/health-thermometer-iframe.html'; + } + iframe.src = src; + document.body.appendChild(iframe); + iframe.addEventListener('load', resolve); + }); + await new Promise((resolve, reject) => { + callWithTrustedClick(() => { + iframe.contentWindow.postMessage( + {type: 'DiscoverServices', options: options}, '*'); + }); + + function messageHandler(messageEvent) { + if (messageEvent.data == 'DiscoveryComplete') { + window.removeEventListener('message', messageHandler); + resolve(); + } else { + reject(new Error(`Unexpected message: ${messageEvent.data}`)); + } + } + window.addEventListener('message', messageHandler); + }); + let device = await requestDeviceWithTrustedClick(options); + await device.gatt.connect(); + return Object.assign({device}, fakes); +} + +/** + * Returns the device requested and connected in the given iframe context and + * fakes from populateHealthThermometerFakes(). + * @param {object} iframe The iframe element set up by the caller document. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getHealthThermometerDeviceFromIframe(iframe) { + const fake_device = await setUpConnectableHealthThermometerDevice(); + const fakes = await populateHealthThermometerFakes(fake_device); + await new Promise(resolve => { + let src = '/bluetooth/resources/health-thermometer-iframe.html'; + iframe.src = src; + document.body.appendChild(iframe); + iframe.addEventListener('load', resolve, {once: true}); + }); + await new Promise((resolve, reject) => { + callWithTrustedClick(() => { + iframe.contentWindow.postMessage( + { + type: 'RequestAndConnect', + options: {filters: [{services: [health_thermometer.name]}]} + }, + '*'); + }); + + function messageHandler(messageEvent) { + if (messageEvent.data == 'Connected') { + window.removeEventListener('message', messageHandler); + resolve(); + } else { + reject(new Error(`Unexpected message: ${messageEvent.data}`)); + } + } + window.addEventListener('message', messageHandler, {once: true}); + }); + const devices = await iframe.contentWindow.navigator.bluetooth.getDevices(); + assert_equals(devices.length, 1); + return Object.assign({device: devices[0]}, {fakes}); +} + +/** + * Similar to getHealthThermometerDevice() except the device + * is not connected and thus its services have not been + * discovered. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object + * containing a requested BluetoothDevice and its fake counter part. + */ +async function getDiscoveredHealthThermometerDevice(options = { + filters: [{services: ['health_thermometer']}] +}) { + return await setUpHealthThermometerDevice({requestDeviceOptions: options}); +} + +/** + * Similar to getHealthThermometerDevice() except the device has no services, + * characteristics, or descriptors. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object + * containing a requested BluetoothDevice and its fake counter part. + */ +async function getEmptyHealthThermometerDevice(options) { + let fake_device = await getDiscoveredHealthThermometerDevice(options); + let fake_generic_access = fake_device.fake_services.get('generic_access'); + let fake_health_thermometer = + fake_device.fake_services.get('health_thermometer'); + // Remove services that have been set up by previous steps. + await fake_generic_access.remove(); + await fake_health_thermometer.remove(); + await fake_device.fake_peripheral.setNextGATTConnectionResponse( + {code: HCI_SUCCESS}); + await fake_device.device.gatt.connect(); + await fake_device.fake_peripheral.setNextGATTDiscoveryResponse( + {code: HCI_SUCCESS}); + return fake_device; +} + +/** + * Similar to getHealthThermometerService() except the service has no + * characteristics or included services. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {service: BluetoothRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService} An object containing the + * health themometer service object and its corresponding fake. + */ +async function getEmptyHealthThermometerService(options) { + let result = await getDiscoveredHealthThermometerDevice(options); + await result.fake_peripheral.setNextGATTConnectionResponse( + {code: HCI_SUCCESS}); + await result.device.gatt.connect(); + let fake_health_thermometer = + await result.fake_peripheral.addFakeService({uuid: 'health_thermometer'}); + await result.fake_peripheral.setNextGATTDiscoveryResponse( + {code: HCI_SUCCESS}); + let service = + await result.device.gatt.getPrimaryService('health_thermometer'); + return { + service: service, + fake_health_thermometer: fake_health_thermometer, + }; +} + +/** + * Similar to getHealthThermometerDevice except the GATT discovery + * response has not been set yet so more attributes can still be added. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getConnectedHealthThermometerDevice(options) { + let fake_device = await getDiscoveredHealthThermometerDevice(options); + await fake_device.fake_peripheral.setNextGATTConnectionResponse({ + code: HCI_SUCCESS, + }); + let fakes = await populateHealthThermometerFakes(fake_device); + await fake_device.device.gatt.connect(); + return Object.assign({device: fake_device.device}, fakes); +} + +/** + * Returns an object containing a BluetoothDevice discovered using |options|, + * its corresponding FakePeripheral and FakeRemoteGATTServices. + * The simulated device is called 'Health Thermometer' it has two known service + * UUIDs: 'generic_access' and 'health_thermometer' which correspond to two + * services with the same UUIDs. The 'health thermometer' service contains three + * characteristics: + * - 'temperature_measurement' (indicate), + * - 'temperature_type' (read), + * - 'measurement_interval' (read, write, indicate) + * The 'measurement_interval' characteristic contains a + * 'gatt.client_characteristic_configuration' descriptor and a + * 'characteristic_user_description' descriptor. + * The device has been connected to and its attributes are ready to be + * discovered. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getHealthThermometerDevice(options) { + let result = await getConnectedHealthThermometerDevice(options); + await result.fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS, + }); + return result; +} + +/** + * Similar to getHealthThermometerDevice except that the peripheral has two + * 'health_thermometer' services. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, fake_health_thermometer1: + * FakeRemoteGATTService, fake_health_thermometer2: FakeRemoteGATTService}>} An + * object containing a requested Bluetooth device and two fake health + * thermometer GATT services. + */ +async function getTwoHealthThermometerServicesDevice(options) { + let result = await getConnectedHealthThermometerDevice(options); + let fake_health_thermometer2 = + await result.fake_peripheral.addFakeService({uuid: 'health_thermometer'}); + await result.fake_peripheral.setNextGATTDiscoveryResponse( + {code: HCI_SUCCESS}); + return { + device: result.device, + fake_peripheral: result.fake_peripheral, + fake_generic_access: result.fake_generic_access, + fake_health_thermometer1: result.fake_health_thermometer, + fake_health_thermometer2: fake_health_thermometer2 + }; +} + +/** + * Returns an object containing a Health Thermometer BluetoothRemoteGattService + * and its corresponding FakeRemoteGATTService. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic, + * service: BluetoothRemoteGATTService, + * fake_service: FakeRemoteGATTService}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getHealthThermometerService() { + let result = await getHealthThermometerDevice(); + let service = + await result.device.gatt.getPrimaryService('health_thermometer'); + return Object.assign(result, { + service, + fake_service: result.fake_health_thermometer, + }); +} + +/** + * Returns an object containing a Measurement Interval + * BluetoothRemoteGATTCharacteristic and its corresponding + * FakeRemoteGATTCharacteristic. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic, + * service: BluetoothRemoteGATTService, + * fake_service: FakeRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getMeasurementIntervalCharacteristic() { + let result = await getHealthThermometerService(); + let characteristic = + await result.service.getCharacteristic('measurement_interval'); + return Object.assign(result, { + characteristic, + fake_characteristic: result.fake_measurement_interval, + }); +} + +/** + * Returns an object containing a User Description + * BluetoothRemoteGATTDescriptor and its corresponding + * FakeRemoteGATTDescriptor. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic, + * service: BluetoothRemoteGATTService, + * fake_service: FakeRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeRemoteGATTCharacteristic + * descriptor: BluetoothRemoteGATTDescriptor, + * fake_descriptor: FakeRemoteGATTDescriptor}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getUserDescriptionDescriptor() { + let result = await getMeasurementIntervalCharacteristic(); + let descriptor = await result.characteristic.getDescriptor( + 'gatt.characteristic_user_description'); + return Object.assign(result, { + descriptor, + fake_descriptor: result.fake_user_description, + }); +} + +/** Heart Rate Bluetooth Device Helper Methods */ + +/** @type {FakeDeviceOptions} */ +const heartRateFakeDeviceOptionsDefault = { + address: '08:08:08:08:08:08', + name: 'Heart Rate', + knownServiceUUIDs: ['generic_access', 'heart_rate'], + connectable: false, + serviceDiscoveryComplete: false, +}; + +/** @type {RequestDeviceOptions} */ +const heartRateRequestDeviceOptionsDefault = { + filters: [{services: ['heart_rate']}] +}; + +async function getHeartRateDevice(setupOptionsOverride) { + let setupOptions = createSetupOptions( + {fakeDeviceOptions: heartRateFakeDeviceOptionsDefault}, + setupOptionsOverride); + return await setUpPreconnectedFakeDevice(setupOptions); +} + +/** + * Returns an array containing two FakePeripherals corresponding + * to the simulated devices. + * @returns {Promise<Array<FakePeripheral>>} The device fakes initialized as + * Health Thermometer and Heart Rate devices. + */ +async function setUpHealthThermometerAndHeartRateDevices() { + await initializeFakeCentral({state: 'powered-on'}); + return Promise.all([ + fake_central.simulatePreconnectedPeripheral({ + address: '09:09:09:09:09:09', + name: 'Health Thermometer', + manufacturerData: {}, + knownServiceUUIDs: ['generic_access', 'health_thermometer'], + }), + fake_central.simulatePreconnectedPeripheral({ + address: '08:08:08:08:08:08', + name: 'Heart Rate', + manufacturerData: {}, + knownServiceUUIDs: ['generic_access', 'heart_rate'], + }) + ]); +} diff --git a/testing/web-platform/tests/bluetooth/resources/bluetooth-scanning-helpers.js b/testing/web-platform/tests/bluetooth/resources/bluetooth-scanning-helpers.js new file mode 100644 index 0000000000..f474c9c306 --- /dev/null +++ b/testing/web-platform/tests/bluetooth/resources/bluetooth-scanning-helpers.js @@ -0,0 +1,42 @@ +'use strict'; + +const company_id = '224'; +const data = new TextEncoder().encode('foo'); +const manufacturerDataMap = {[company_id]: data}; +const health_uuid = health_thermometer.uuid; +const serviceDataMap = {[health_uuid]: data}; +const scanRecord = { + name: 'Health Thermometer', + uuids: ['generic_access', health_uuid], + txPower: 20, + appearance: 100, + manufacturerData: manufacturerDataMap, + serviceData: serviceDataMap, +}; +const scanResult = { + deviceAddress: '09:09:09:09:09:09', + rssi: 100, + scanRecord: scanRecord, +}; + +function verifyBluetoothAdvertisingEvent(e) { + assert_equals(e.constructor.name, 'BluetoothAdvertisingEvent') + assert_equals(e.device.name, scanRecord.name) + assert_equals(e.name, scanRecord.name) + assert_array_equals(e.uuids, + ["00001800-0000-1000-8000-00805f9b34fb", + "00001809-0000-1000-8000-00805f9b34fb"]) + assert_equals(e.txPower, 20) + assert_equals(e.rssi, 100) + + assert_equals(e.manufacturerData.constructor.name, + 'BluetoothManufacturerDataMap') + assert_equals(data[0], e.manufacturerData.get(224).getUint8(0)) + assert_equals(data[1], e.manufacturerData.get(224).getUint8(1)) + assert_equals(data[2], e.manufacturerData.get(224).getUint8(2)) + + assert_equals(e.serviceData.constructor.name, 'BluetoothServiceDataMap') + assert_equals(data[0], e.serviceData.get(health_uuid).getUint8(0)) + assert_equals(data[1], e.serviceData.get(health_uuid).getUint8(1)) + assert_equals(data[2], e.serviceData.get(health_uuid).getUint8(2)) +} diff --git a/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js b/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js new file mode 100644 index 0000000000..7ad1b937e1 --- /dev/null +++ b/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js @@ -0,0 +1,363 @@ +'use strict'; + +/** + * Test Setup Helpers + */ + +/** + * Loads a script by creating a <script> element pointing to |path|. + * @param {string} path The path of the script to load. + * @returns {Promise<void>} Resolves when the script has finished loading. + */ +function loadScript(path) { + let script = document.createElement('script'); + let promise = new Promise(resolve => script.onload = resolve); + script.src = path; + script.async = false; + document.head.appendChild(script); + return promise; +} + +/** + * Performs the Chromium specific setup necessary to run the tests in the + * Chromium browser. This test file is shared between Web Platform Tests and + * Blink Web Tests, so this method figures out the correct paths to use for + * loading scripts. + * + * TODO(https://crbug.com/569709): Update this description when all Web + * Bluetooth Blink Web Tests have been migrated into this repository. + * @returns {Promise<void>} Resolves when Chromium specific setup is complete. + */ +async function performChromiumSetup() { + // Determine path prefixes. + let resPrefix = '/resources'; + const chromiumResources = ['/resources/chromium/web-bluetooth-test.js']; + const pathname = window.location.pathname; + if (pathname.includes('/wpt_internal/')) { + chromiumResources.push( + '/wpt_internal/bluetooth/resources/bluetooth-fake-adapter.js'); + } + + await loadScript(`${resPrefix}/test-only-api.js`); + if (!isChromiumBased) { + return; + } + + for (const path of chromiumResources) { + await loadScript(path); + } + + await initializeChromiumResources(); + + // Call setBluetoothFakeAdapter() to clean up any fake adapters left over by + // legacy tests. Legacy tests that use setBluetoothFakeAdapter() sometimes + // fail to clean their fake adapter. This is not a problem for these tests + // because the next setBluetoothFakeAdapter() will clean it up anyway but it + // is a problem for the new tests that do not use setBluetoothFakeAdapter(). + // TODO(https://crbug.com/569709): Remove once setBluetoothFakeAdapter is no + // longer used. + if (typeof setBluetoothFakeAdapter !== 'undefined') { + setBluetoothFakeAdapter(''); + } +} + +/** + * These tests rely on the User Agent providing an implementation of the Web + * Bluetooth Testing API. + * https://docs.google.com/document/d/1Nhv_oVDCodd1pEH_jj9k8gF4rPGb_84VYaZ9IG8M_WY/edit?ts=59b6d823#heading=h.7nki9mck5t64 + * @param {function{*}: Promise<*>} test_function The Web Bluetooth test to run. + * @param {string} name The name or description of the test. + * @param {object} properties An object containing extra options for the test. + * @param {Boolean} validate_response_consumed Whether to validate all response + * consumed or not. + * @returns {Promise<void>} Resolves if Web Bluetooth test ran successfully, or + * rejects if the test failed. + */ +function bluetooth_test( + test_function, name, properties, validate_response_consumed = true) { + return promise_test(async (t) => { + assert_implements(navigator.bluetooth, 'missing navigator.bluetooth'); + // Trigger Chromium-specific setup. + await performChromiumSetup(); + assert_implements( + navigator.bluetooth.test, 'missing navigator.bluetooth.test'); + await test_function(t); + if (validate_response_consumed) { + let consumed = await navigator.bluetooth.test.allResponsesConsumed(); + assert_true(consumed); + } + }, name, properties); +} + +/** + * Test Helpers + */ + +/** + * Waits until the document has finished loading. + * @returns {Promise<void>} Resolves if the document is already completely + * loaded or when the 'onload' event is fired. + */ +function waitForDocumentReady() { + return new Promise(resolve => { + if (document.readyState === 'complete') { + resolve(); + } + + window.addEventListener('load', () => { + resolve(); + }, {once: true}); + }); +} + +/** + * Simulates a user activation prior to running |callback|. + * @param {Function} callback The function to run after the user activation. + * @returns {Promise<*>} Resolves when the user activation has been simulated + * with the result of |callback|. + */ +async function callWithTrustedClick(callback) { + await waitForDocumentReady(); + return new Promise(resolve => { + let button = document.createElement('button'); + button.textContent = 'click to continue test'; + button.style.display = 'block'; + button.style.fontSize = '20px'; + button.style.padding = '10px'; + button.onclick = () => { + document.body.removeChild(button); + resolve(callback()); + }; + document.body.appendChild(button); + test_driver.click(button); + }); +} + +/** + * Calls requestDevice() in a context that's 'allowed to show a popup'. + * @returns {Promise<BluetoothDevice>} Resolves with a Bluetooth device if + * successful or rejects with an error. + */ +function requestDeviceWithTrustedClick() { + let args = arguments; + return callWithTrustedClick( + () => navigator.bluetooth.requestDevice.apply(navigator.bluetooth, args)); +} + +/** + * Calls requestLEScan() in a context that's 'allowed to show a popup'. + * @returns {Promise<BluetoothLEScan>} Resolves with the properties of the scan + * if successful or rejects with an error. + */ +function requestLEScanWithTrustedClick() { + let args = arguments; + return callWithTrustedClick( + () => navigator.bluetooth.requestLEScan.apply(navigator.bluetooth, args)); +} + +/** + * Function to test that a promise rejects with the expected error type and + * message. + * @param {Promise} promise + * @param {object} expected + * @param {string} description + * @returns {Promise<void>} Resolves if |promise| rejected with |expected| + * error. + */ +function assert_promise_rejects_with_message(promise, expected, description) { + return promise.then( + () => { + assert_unreached('Promise should have rejected: ' + description); + }, + error => { + assert_equals(error.name, expected.name, 'Unexpected Error Name:'); + if (expected.message) { + assert_equals( + error.message, expected.message, 'Unexpected Error Message:'); + } + }); +} + +/** + * Helper class that can be created to check that an event has fired. + */ +class EventCatcher { + /** + * @param {EventTarget} object The object to listen for events on. + * @param {string} event The type of event to listen for. + */ + constructor(object, event) { + /** @type {boolean} */ + this.eventFired = false; + + /** @type {function()} */ + let event_listener = () => { + object.removeEventListener(event, event_listener); + this.eventFired = true; + }; + object.addEventListener(event, event_listener); + } +} + +/** + * Notifies when the event |type| has fired. + * @param {EventTarget} target The object to listen for the event. + * @param {string} type The type of event to listen for. + * @param {object} options Characteristics about the event listener. + * @returns {Promise<Event>} Resolves when an event of |type| has fired. + */ +function eventPromise(target, type, options) { + return new Promise(resolve => { + let wrapper = function(event) { + target.removeEventListener(type, wrapper); + resolve(event); + }; + target.addEventListener(type, wrapper, options); + }); +} + +/** + * The action that should occur first in assert_promise_event_order_(). + * @enum {string} + */ +const ShouldBeFirst = { + EVENT: 'event', + PROMISE_RESOLUTION: 'promiseresolved', +}; + +/** + * Helper function to assert that events are fired and a promise resolved + * in the correct order. + * 'event' should be passed as |should_be_first| to indicate that the events + * should be fired first, otherwise 'promiseresolved' should be passed. + * Attaches |num_listeners| |event| listeners to |object|. If all events have + * been fired and the promise resolved in the correct order, returns a promise + * that fulfills with the result of |object|.|func()| and |event.target.value| + * of each of event listeners. Otherwise throws an error. + * @param {ShouldBeFirst} should_be_first Indicates whether |func| should + * resolve before |event| is fired. + * @param {EventTarget} object The target object to add event listeners to. + * @param {function(*): Promise<*>} func The function to test the resolution + * order for. + * @param {string} event The event type to listen for. + * @param {number} num_listeners The number of events to listen for. + * @returns {Promise<*>} The return value of |func|. + */ +function assert_promise_event_order_( + should_be_first, object, func, event, num_listeners) { + let order = []; + let event_promises = []; + for (let i = 0; i < num_listeners; i++) { + event_promises.push(new Promise(resolve => { + let event_listener = (e) => { + object.removeEventListener(event, event_listener); + order.push(ShouldBeFirst.EVENT); + resolve(e.target.value); + }; + object.addEventListener(event, event_listener); + })); + } + + let func_promise = object[func]().then(result => { + order.push(ShouldBeFirst.PROMISE_RESOLUTION); + return result; + }); + + return Promise.all([func_promise, ...event_promises]).then((result) => { + if (should_be_first !== order[0]) { + throw should_be_first === ShouldBeFirst.PROMISE_RESOLUTION ? + `'${event}' was fired before promise resolved.` : + `Promise resolved before '${event}' was fired.`; + } + + if (order[0] !== ShouldBeFirst.PROMISE_RESOLUTION && + order[order.length - 1] !== ShouldBeFirst.PROMISE_RESOLUTION) { + throw 'Promise resolved in between event listeners.'; + } + + return result; + }); +} + +/** + * Asserts that the promise returned by |func| resolves before events of type + * |event| are fired |num_listeners| times on |object|. See + * assert_promise_event_order_ above for more details. + * @param {EventTarget} object The target object to add event listeners to. + * @param {function(*): Promise<*>} func The function whose promise should + * resolve first. + * @param {string} event The event type to listen for. + * @param {number} num_listeners The number of events to listen for. + * @returns {Promise<*>} The return value of |func|. + */ +function assert_promise_resolves_before_event( + object, func, event, num_listeners = 1) { + return assert_promise_event_order_( + ShouldBeFirst.PROMISE_RESOLUTION, object, func, event, num_listeners); +} + +/** + * Asserts that the promise returned by |func| resolves after events of type + * |event| are fired |num_listeners| times on |object|. See + * assert_promise_event_order_ above for more details. + * @param {EventTarget} object The target object to add event listeners to. + * @param {function(*): Promise<*>} func The function whose promise should + * resolve first. + * @param {string} event The event type to listen for. + * @param {number} num_listeners The number of events to listen for. + * @returns {Promise<*>} The return value of |func|. + */ +function assert_promise_resolves_after_event( + object, func, event, num_listeners = 1) { + return assert_promise_event_order_( + ShouldBeFirst.EVENT, object, func, event, num_listeners); +} + +/** + * Returns a promise that resolves after 100ms unless the the event is fired on + * the object in which case the promise rejects. + * @param {EventTarget} object The target object to listen for events. + * @param {string} event_name The event type to listen for. + * @returns {Promise<void>} Resolves if no events were fired. + */ +function assert_no_events(object, event_name) { + return new Promise((resolve) => { + let event_listener = (e) => { + object.removeEventListener(event_name, event_listener); + assert_unreached('Object should not fire an event.'); + }; + object.addEventListener(event_name, event_listener); + // TODO: Remove timeout. + // http://crbug.com/543884 + step_timeout(() => { + object.removeEventListener(event_name, event_listener); + resolve(); + }, 100); + }); +} + +/** + * Asserts that |properties| contains the same properties in + * |expected_properties| with equivalent values. + * @param {object} properties Actual object to compare. + * @param {object} expected_properties Expected object to compare with. + */ +function assert_properties_equal(properties, expected_properties) { + for (let key in expected_properties) { + assert_equals(properties[key], expected_properties[key]); + } +} + +/** + * Asserts that |data_map| contains |expected_key|, and that the uint8 values + * for |expected_key| matches |expected_value|. + */ +function assert_data_maps_equal(data_map, expected_key, expected_value) { + assert_true(data_map.has(expected_key)); + + const value = new Uint8Array(data_map.get(expected_key).buffer); + assert_equals(value.length, expected_value.length); + for (let i = 0; i < value.length; ++i) { + assert_equals(value[i], expected_value[i]); + } +} diff --git a/testing/web-platform/tests/bluetooth/resources/health-thermometer-iframe.html b/testing/web-platform/tests/bluetooth/resources/health-thermometer-iframe.html new file mode 100644 index 0000000000..f9f7a6f0d7 --- /dev/null +++ b/testing/web-platform/tests/bluetooth/resources/health-thermometer-iframe.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<body> +<button>Click me!</button> +<script> +let device, gatt; + +test_driver.set_test_context(parent); + +function requestDeviceWithOptionsAndConnect(options) { + return test_driver.click(document.getElementsByTagName("button")[0]) + .then(() => navigator.bluetooth.requestDevice(options)) + .then(device => device.gatt.connect()); +} + +window.addEventListener('message', (messageEvent) => { + switch (messageEvent.data.type) { + case 'GetAvailability': + navigator.bluetooth.getAvailability() + .then(availability => parent.postMessage(availability, '*')) + .catch(err => parent.postMessage(`FAIL: ${err}`, '*')); + break; + case 'GetDevices': + navigator.bluetooth.getDevices() + .then(devices => parent.postMessage('Success', '*')) + .catch(err => parent.postMessage(`FAIL: ${err}`, '*')); + break; + case 'RequestDevice': + test_driver.click(document.getElementsByTagName('button')[0]) + .then( + () => navigator.bluetooth.requestDevice( + {filters: [{services: ['generic_access']}]})) + .then(device => { + if (device.constructor.name === 'BluetoothDevice') { + parent.postMessage('Success', '*'); + } else { + parent.postMessage( + `FAIL: requestDevice in iframe returned ${device.name}`, '*'); + } + }) + .catch(err => parent.postMessage(`FAIL: ${err.name}: ${err.message}`, '*')); + break; + case 'RequestLEScan': + test_driver.click(document.getElementsByTagName('button')[0]) + .then( + () => navigator.bluetooth.requestLEScan( + {filters: [{name: 'Health Thermometer'}]})) + .then(leScan => { + if (leScan.active) { + parent.postMessage('Success', '*'); + leScan.stop(); + } else { + parent.postMessage(`FAIL: the LE scan hasn't been initiated.`, '*'); + } + }) + .catch(err => parent.postMessage(`FAIL: ${err.name}: ${err.message}`, '*')); + break; + case 'RequestAndConnect': + requestDeviceWithOptionsAndConnect(messageEvent.data.options) + .then(_ => { + gatt = _; + device = gatt.device; + parent.postMessage('Connected', '*'); + }) + .catch(err => { + parent.postMessage(`FAIL: ${err}`, '*'); + }); + break; + case 'DiscoverServices': + requestDeviceWithOptionsAndConnect(messageEvent.data.options) + .then(gatt => gatt.getPrimaryServices()) + .then(() => parent.postMessage('DiscoveryComplete', '*')) + .catch(err => { + parent.postMessage(`FAIL: ${err}`, '*'); + }); + break; + case 'GetService': + if (typeof gatt === 'undefined') { + parent.postMessage('FAIL: no GATT server', '*'); + break; + } + gatt.getPrimaryService(messageEvent.data.options) + .then(() => parent.postMessage('ServiceReceived', '*')) + .catch(err => parent.postMessage(`FAIL: ${err}`, '*')); + break; + default: + parent.postMessage( + `FAIL: Bad message type: ${messageEvent.data.type}`, '*'); + } +}); +</script> |