'use strict'; const content = {}; const bluetooth = {}; const MOJO_CHOOSER_EVENT_TYPE_MAP = {}; function toMojoCentralState(state) { switch (state) { case 'absent': return bluetooth.mojom.CentralState.ABSENT; case 'powered-off': return bluetooth.mojom.CentralState.POWERED_OFF; case 'powered-on': return bluetooth.mojom.CentralState.POWERED_ON; default: throw `Unsupported value ${state} for state.`; } } // Converts bluetooth.mojom.WriteType to a string. If |writeType| is // invalid, this method will throw. function writeTypeToString(writeType) { switch (writeType) { case bluetooth.mojom.WriteType.kNone: return 'none'; case bluetooth.mojom.WriteType.kWriteDefaultDeprecated: return 'default-deprecated'; case bluetooth.mojom.WriteType.kWriteWithResponse: return 'with-response'; case bluetooth.mojom.WriteType.kWriteWithoutResponse: return 'without-response'; default: throw `Unknown bluetooth.mojom.WriteType: ${writeType}`; } } // Canonicalizes UUIDs and converts them to Mojo UUIDs. function canonicalizeAndConvertToMojoUUID(uuids) { let canonicalUUIDs = uuids.map(val => ({uuid: BluetoothUUID.getService(val)})); return canonicalUUIDs; } // Converts WebIDL a record to a map> to // use for Mojo, where the value for K is calculated using keyFn. function convertToMojoMap(record, keyFn, isNumberKey = false) { let map = new Map(); for (const [key, value] of Object.entries(record)) { let buffer = ArrayBuffer.isView(value) ? value.buffer : value; if (isNumberKey) { let numberKey = parseInt(key); if (Number.isNaN(numberKey)) throw `Map key ${key} is not a number`; map.set(keyFn(numberKey), Array.from(new Uint8Array(buffer))); continue; } map.set(keyFn(key), Array.from(new Uint8Array(buffer))); } return map; } function ArrayToMojoCharacteristicProperties(arr) { const struct = {}; arr.forEach(property => { struct[property] = true; }); return struct; } class FakeBluetooth { constructor() { this.fake_bluetooth_ptr_ = new bluetooth.mojom.FakeBluetoothRemote(); this.fake_bluetooth_ptr_.$.bindNewPipeAndPassReceiver().bindInBrowser('process'); this.fake_central_ = null; } // Set it to indicate whether the platform supports BLE. For example, // Windows 7 is a platform that doesn't support Low Energy. On the other // hand Windows 10 is a platform that does support LE, even if there is no // Bluetooth radio present. async setLESupported(supported) { if (typeof supported !== 'boolean') throw 'Type Not Supported'; await this.fake_bluetooth_ptr_.setLESupported(supported); } // Returns a promise that resolves with a FakeCentral that clients can use // to simulate events that a device in the Central/Observer role would // receive as well as monitor the operations performed by the device in the // Central/Observer role. // Calls sets LE as supported. // // A "Central" object would allow its clients to receive advertising events // and initiate connections to peripherals i.e. operations of two roles // defined by the Bluetooth Spec: Observer and Central. // See Bluetooth 4.2 Vol 3 Part C 2.2.2 "Roles when Operating over an // LE Physical Transport". async simulateCentral({state}) { if (this.fake_central_) throw 'simulateCentral() should only be called once'; await this.setLESupported(true); let {fakeCentral: fake_central_ptr} = await this.fake_bluetooth_ptr_.simulateCentral( toMojoCentralState(state)); this.fake_central_ = new FakeCentral(fake_central_ptr); return this.fake_central_; } // Returns true if there are no pending responses. async allResponsesConsumed() { let {consumed} = await this.fake_bluetooth_ptr_.allResponsesConsumed(); return consumed; } // Returns a promise that resolves with a FakeChooser that clients can use to // simulate chooser events. async getManualChooser() { if (typeof this.fake_chooser_ === 'undefined') { this.fake_chooser_ = new FakeChooser(); } return this.fake_chooser_; } } // FakeCentral allows clients to simulate events that a device in the // Central/Observer role would receive as well as monitor the operations // performed by the device in the Central/Observer role. class FakeCentral { constructor(fake_central_ptr) { this.fake_central_ptr_ = fake_central_ptr; this.peripherals_ = new Map(); } // Simulates a peripheral with |address|, |name|, |manufacturerData| and // |known_service_uuids| that has already been connected to the system. If the // peripheral existed already it updates its name, manufacturer data, and // known UUIDs. |known_service_uuids| should be an array of // BluetoothServiceUUIDs // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid // // Platforms offer methods to retrieve devices that have already been // connected to the system or weren't connected through the UA e.g. a user // connected a peripheral through the system's settings. This method is // intended to simulate peripherals that those methods would return. async simulatePreconnectedPeripheral( {address, name, manufacturerData = {}, knownServiceUUIDs = []}) { await this.fake_central_ptr_.simulatePreconnectedPeripheral( address, name, convertToMojoMap(manufacturerData, Number, true /* isNumberKey */), canonicalizeAndConvertToMojoUUID(knownServiceUUIDs)); return this.fetchOrCreatePeripheral_(address); } // Simulates an advertisement packet described by |scanResult| being received // from a device. If central is currently scanning, the device will appear on // the list of discovered devices. async simulateAdvertisementReceived(scanResult) { // Create a deep-copy to prevent the original |scanResult| from being // modified when the UUIDs, manufacturer, and service data are converted. let clonedScanResult = JSON.parse(JSON.stringify(scanResult)); if ('uuids' in scanResult.scanRecord) { clonedScanResult.scanRecord.uuids = canonicalizeAndConvertToMojoUUID(scanResult.scanRecord.uuids); } // Convert the optional appearance and txPower fields to the corresponding // Mojo structures, since Mojo does not support optional interger values. If // the fields are undefined, set the hasValue field as false and value as 0. // Otherwise, set the hasValue field as true and value with the field value. const has_appearance = 'appearance' in scanResult.scanRecord; clonedScanResult.scanRecord.appearance = { hasValue: has_appearance, value: (has_appearance ? scanResult.scanRecord.appearance : 0) } const has_tx_power = 'txPower' in scanResult.scanRecord; clonedScanResult.scanRecord.txPower = { hasValue: has_tx_power, value: (has_tx_power ? scanResult.scanRecord.txPower : 0) } // Convert manufacturerData from a record into a // map> for Mojo. if ('manufacturerData' in scanResult.scanRecord) { clonedScanResult.scanRecord.manufacturerData = convertToMojoMap( scanResult.scanRecord.manufacturerData, Number, true /* isNumberKey */); } // Convert serviceData from a record into a // map> for Mojo. if ('serviceData' in scanResult.scanRecord) { clonedScanResult.scanRecord.serviceData.serviceData = convertToMojoMap( scanResult.scanRecord.serviceData, BluetoothUUID.getService, false /* isNumberKey */); } await this.fake_central_ptr_.simulateAdvertisementReceived( clonedScanResult); return this.fetchOrCreatePeripheral_(clonedScanResult.deviceAddress); } // Simulates a change in the central device described by |state|. For example, // setState('powered-off') can be used to simulate the central device powering // off. // // This method should be used for any central state changes after // simulateCentral() has been called to create a FakeCentral object. async setState(state) { await this.fake_central_ptr_.setState(toMojoCentralState(state)); } // Create a fake_peripheral object from the given address. fetchOrCreatePeripheral_(address) { let peripheral = this.peripherals_.get(address); if (peripheral === undefined) { peripheral = new FakePeripheral(address, this.fake_central_ptr_); this.peripherals_.set(address, peripheral); } return peripheral; } } class FakePeripheral { constructor(address, fake_central_ptr) { this.address = address; this.fake_central_ptr_ = fake_central_ptr; } // Adds a fake GATT Service with |uuid| to be discovered when discovering // the peripheral's GATT Attributes. Returns a FakeRemoteGATTService // corresponding to this service. |uuid| should be a BluetoothServiceUUIDs // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid async addFakeService({uuid}) { let {serviceId: service_id} = await this.fake_central_ptr_.addFakeService( this.address, {uuid: BluetoothUUID.getService(uuid)}); if (service_id === null) throw 'addFakeService failed'; return new FakeRemoteGATTService( service_id, this.address, this.fake_central_ptr_); } // Sets the next GATT Connection request response to |code|. |code| could be // an HCI Error Code from BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a // number outside that range returned by specific platforms e.g. Android // returns 0x101 to signal a GATT failure // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE async setNextGATTConnectionResponse({code}) { let {success} = await this.fake_central_ptr_.setNextGATTConnectionResponse( this.address, code); if (success !== true) throw 'setNextGATTConnectionResponse failed.'; } // Sets the next GATT Discovery request response for peripheral with // |address| to |code|. |code| could be an HCI Error Code from // BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a number outside that // range returned by specific platforms e.g. Android returns 0x101 to signal // a GATT failure // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE // // The following procedures defined at BT 4.2 Vol 3 Part G Section 4. // "GATT Feature Requirements" are used to discover attributes of the // GATT Server: // - Primary Service Discovery // - Relationship Discovery // - Characteristic Discovery // - Characteristic Descriptor Discovery // This method aims to simulate the response once all of these procedures // have completed or if there was an error during any of them. async setNextGATTDiscoveryResponse({code}) { let {success} = await this.fake_central_ptr_.setNextGATTDiscoveryResponse( this.address, code); if (success !== true) throw 'setNextGATTDiscoveryResponse failed.'; } // Simulates a GATT disconnection from the peripheral with |address|. async simulateGATTDisconnection() { let {success} = await this.fake_central_ptr_.simulateGATTDisconnection(this.address); if (success !== true) throw 'simulateGATTDisconnection failed.'; } // Simulates an Indication from the peripheral's GATT `Service Changed` // Characteristic from BT 4.2 Vol 3 Part G 7.1. This Indication is signaled // when services, characteristics, or descriptors are changed, added, or // removed. // // The value for `Service Changed` is a range of attribute handles that have // changed. However, this testing specification works at an abstracted // level and does not expose setting attribute handles when adding // attributes. Consequently, this simulate method should include the full // range of all the peripheral's attribute handle values. async simulateGATTServicesChanged() { let {success} = await this.fake_central_ptr_.simulateGATTServicesChanged(this.address); if (success !== true) throw 'simulateGATTServicesChanged failed.'; } } class FakeRemoteGATTService { constructor(service_id, peripheral_address, fake_central_ptr) { this.service_id_ = service_id; this.peripheral_address_ = peripheral_address; this.fake_central_ptr_ = fake_central_ptr; } // Adds a fake GATT Characteristic with |uuid| and |properties| // to this fake service. The characteristic will be found when discovering // the peripheral's GATT Attributes. Returns a FakeRemoteGATTCharacteristic // corresponding to the added characteristic. async addFakeCharacteristic({uuid, properties}) { let {characteristicId: characteristic_id} = await this.fake_central_ptr_.addFakeCharacteristic( {uuid: BluetoothUUID.getCharacteristic(uuid)}, ArrayToMojoCharacteristicProperties(properties), this.service_id_, this.peripheral_address_); if (characteristic_id === null) throw 'addFakeCharacteristic failed'; return new FakeRemoteGATTCharacteristic( characteristic_id, this.service_id_, this.peripheral_address_, this.fake_central_ptr_); } // Removes the fake GATT service from its fake peripheral. async remove() { let {success} = await this.fake_central_ptr_.removeFakeService( this.service_id_, this.peripheral_address_); if (!success) throw 'remove failed'; } } class FakeRemoteGATTCharacteristic { constructor(characteristic_id, service_id, peripheral_address, fake_central_ptr) { this.ids_ = [characteristic_id, service_id, peripheral_address]; this.descriptors_ = []; this.fake_central_ptr_ = fake_central_ptr; } // Adds a fake GATT Descriptor with |uuid| to be discovered when // discovering the peripheral's GATT Attributes. Returns a // FakeRemoteGATTDescriptor corresponding to this descriptor. |uuid| should // be a BluetoothDescriptorUUID // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothdescriptoruuid async addFakeDescriptor({uuid}) { let {descriptorId: descriptor_id} = await this.fake_central_ptr_.addFakeDescriptor( {uuid: BluetoothUUID.getDescriptor(uuid)}, ...this.ids_); if (descriptor_id === null) throw 'addFakeDescriptor failed'; let fake_descriptor = new FakeRemoteGATTDescriptor( descriptor_id, ...this.ids_, this.fake_central_ptr_); this.descriptors_.push(fake_descriptor); return fake_descriptor; } // Sets the next read response for characteristic to |code| and |value|. // |code| could be a GATT Error Response from // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range // returned by specific platforms e.g. Android returns 0x101 to signal a GATT // failure. // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE async setNextReadResponse(gatt_code, value=null) { if (gatt_code === 0 && value === null) { throw '|value| can\'t be null if read should success.'; } if (gatt_code !== 0 && value !== null) { throw '|value| must be null if read should fail.'; } let {success} = await this.fake_central_ptr_.setNextReadCharacteristicResponse( gatt_code, value, ...this.ids_); if (!success) throw 'setNextReadCharacteristicResponse failed'; } // Sets the next write response for this characteristic to |code|. If // writing to a characteristic that only supports 'write_without_response' // the set response will be ignored. // |code| could be a GATT Error Response from // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range // returned by specific platforms e.g. Android returns 0x101 to signal a GATT // failure. async setNextWriteResponse(gatt_code) { let {success} = await this.fake_central_ptr_.setNextWriteCharacteristicResponse( gatt_code, ...this.ids_); if (!success) throw 'setNextWriteCharacteristicResponse failed'; } // Sets the next subscribe to notifications response for characteristic with // |characteristic_id| in |service_id| and in |peripheral_address| to // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F // 3.4.1.1 Error Response or a number outside that range returned by // specific platforms e.g. Android returns 0x101 to signal a GATT failure. async setNextSubscribeToNotificationsResponse(gatt_code) { let {success} = await this.fake_central_ptr_.setNextSubscribeToNotificationsResponse( gatt_code, ...this.ids_); if (!success) throw 'setNextSubscribeToNotificationsResponse failed'; } // Sets the next unsubscribe to notifications response for characteristic with // |characteristic_id| in |service_id| and in |peripheral_address| to // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F // 3.4.1.1 Error Response or a number outside that range returned by // specific platforms e.g. Android returns 0x101 to signal a GATT failure. async setNextUnsubscribeFromNotificationsResponse(gatt_code) { let {success} = await this.fake_central_ptr_.setNextUnsubscribeFromNotificationsResponse( gatt_code, ...this.ids_); if (!success) throw 'setNextUnsubscribeToNotificationsResponse failed'; } // Returns true if notifications from the characteristic have been subscribed // to. async isNotifying() { let {success, isNotifying} = await this.fake_central_ptr_.isNotifying(...this.ids_); if (!success) throw 'isNotifying failed'; return isNotifying; } // Gets the last successfully written value to the characteristic and its // write type. Write type is one of 'none', 'default-deprecated', // 'with-response', 'without-response'. Returns {lastValue: null, // lastWriteType: 'none'} if no value has yet been written to the // characteristic. async getLastWrittenValue() { let {success, value, writeType} = await this.fake_central_ptr_.getLastWrittenCharacteristicValue( ...this.ids_); if (!success) throw 'getLastWrittenCharacteristicValue failed'; return {lastValue: value, lastWriteType: writeTypeToString(writeType)}; } // Removes the fake GATT Characteristic from its fake service. async remove() { let {success} = await this.fake_central_ptr_.removeFakeCharacteristic(...this.ids_); if (!success) throw 'remove failed'; } } class FakeRemoteGATTDescriptor { constructor(descriptor_id, characteristic_id, service_id, peripheral_address, fake_central_ptr) { this.ids_ = [ descriptor_id, characteristic_id, service_id, peripheral_address]; this.fake_central_ptr_ = fake_central_ptr; } // Sets the next read response for descriptor to |code| and |value|. // |code| could be a GATT Error Response from // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range // returned by specific platforms e.g. Android returns 0x101 to signal a GATT // failure. // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE async setNextReadResponse(gatt_code, value=null) { if (gatt_code === 0 && value === null) { throw '|value| cannot be null if read should succeed.'; } if (gatt_code !== 0 && value !== null) { throw '|value| must be null if read should fail.'; } let {success} = await this.fake_central_ptr_.setNextReadDescriptorResponse( gatt_code, value, ...this.ids_); if (!success) throw 'setNextReadDescriptorResponse failed'; } // Sets the next write response for this descriptor to |code|. // |code| could be a GATT Error Response from // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range // returned by specific platforms e.g. Android returns 0x101 to signal a GATT // failure. async setNextWriteResponse(gatt_code) { let {success} = await this.fake_central_ptr_.setNextWriteDescriptorResponse( gatt_code, ...this.ids_); if (!success) throw 'setNextWriteDescriptorResponse failed'; } // Gets the last successfully written value to the descriptor. // Returns null if no value has yet been written to the descriptor. async getLastWrittenValue() { let {success, value} = await this.fake_central_ptr_.getLastWrittenDescriptorValue( ...this.ids_); if (!success) throw 'getLastWrittenDescriptorValue failed'; return value; } // Removes the fake GATT Descriptor from its fake characteristic. async remove() { let {success} = await this.fake_central_ptr_.removeFakeDescriptor(...this.ids_); if (!success) throw 'remove failed'; } } // FakeChooser allows clients to simulate user actions on a Bluetooth chooser, // and records the events produced by the Bluetooth chooser. class FakeChooser { constructor() { let fakeBluetoothChooserFactoryRemote = new content.mojom.FakeBluetoothChooserFactoryRemote(); fakeBluetoothChooserFactoryRemote.$.bindNewPipeAndPassReceiver().bindInBrowser('process'); this.fake_bluetooth_chooser_ptr_ = new content.mojom.FakeBluetoothChooserRemote(); this.fake_bluetooth_chooser_client_receiver_ = new content.mojom.FakeBluetoothChooserClientReceiver(this); fakeBluetoothChooserFactoryRemote.createFakeBluetoothChooser( this.fake_bluetooth_chooser_ptr_.$.bindNewPipeAndPassReceiver(), this.fake_bluetooth_chooser_client_receiver_.$.associateAndPassRemote()); this.events_ = new Array(); this.event_listener_ = null; } // If the chooser has received more events than |numOfEvents| this function // will reject the promise, else it will wait until |numOfEvents| events are // received before resolving with an array of |FakeBluetoothChooserEvent| // objects. async waitForEvents(numOfEvents) { return new Promise(resolve => { if (this.events_.length > numOfEvents) { throw `Asked for ${numOfEvents} event(s), but received ` + `${this.events_.length}.`; } this.event_listener_ = () => { if (this.events_.length === numOfEvents) { let result = Array.from(this.events_); this.event_listener_ = null; this.events_ = []; resolve(result); } }; this.event_listener_(); }); } async selectPeripheral(peripheral) { if (!(peripheral instanceof FakePeripheral)) { throw '|peripheral| must be an instance of FakePeripheral'; } await this.fake_bluetooth_chooser_ptr_.selectPeripheral(peripheral.address); } async cancel() { await this.fake_bluetooth_chooser_ptr_.cancel(); } async rescan() { await this.fake_bluetooth_chooser_ptr_.rescan(); } onEvent(chooserEvent) { chooserEvent.type = MOJO_CHOOSER_EVENT_TYPE_MAP[chooserEvent.type]; this.events_.push(chooserEvent); if (this.event_listener_ !== null) { this.event_listener_(); } } } async function initializeChromiumResources() { content.mojom = await import( '/gen/content/web_test/common/fake_bluetooth_chooser.mojom.m.js'); bluetooth.mojom = await import( '/gen/device/bluetooth/public/mojom/test/fake_bluetooth.mojom.m.js'); const map = MOJO_CHOOSER_EVENT_TYPE_MAP; const types = content.mojom.ChooserEventType; map[types.CHOOSER_OPENED] = 'chooser-opened'; map[types.CHOOSER_CLOSED] = 'chooser-closed'; map[types.ADAPTER_REMOVED] = 'adapter-removed'; map[types.ADAPTER_DISABLED] = 'adapter-disabled'; map[types.ADAPTER_ENABLED] = 'adapter-enabled'; map[types.DISCOVERY_FAILED_TO_START] = 'discovery-failed-to-start'; map[types.DISCOVERING] = 'discovering'; map[types.DISCOVERY_IDLE] = 'discovery-idle'; map[types.ADD_OR_UPDATE_DEVICE] = 'add-or-update-device'; // If this line fails, it means that current environment does not support the // Web Bluetooth Test API. try { navigator.bluetooth.test = new FakeBluetooth(); } catch { throw 'Web Bluetooth Test API is not implemented on this ' + 'environment. See the bluetooth README at ' + 'https://github.com/web-platform-tests/wpt/blob/master/bluetooth/README.md#web-bluetooth-testing'; } }