diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/resources/chromium/web-bluetooth-test.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/resources/chromium/web-bluetooth-test.js')
-rw-r--r-- | testing/web-platform/tests/resources/chromium/web-bluetooth-test.js | 629 |
1 files changed, 629 insertions, 0 deletions
diff --git a/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js new file mode 100644 index 0000000000..ecea5e760c --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js @@ -0,0 +1,629 @@ +'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<DOMString, BufferSource> to a map<K, array<uint8>> 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<DOMString, BufferSource> into a + // map<uint8, array<uint8>> for Mojo. + if ('manufacturerData' in scanResult.scanRecord) { + clonedScanResult.scanRecord.manufacturerData = convertToMojoMap( + scanResult.scanRecord.manufacturerData, Number, + true /* isNumberKey */); + } + + // Convert serviceData from a record<DOMString, BufferSource> into a + // map<string, array<uint8>> 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'; + } +} |