summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/bluetooth/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/bluetooth/resources')
-rw-r--r--testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js1203
-rw-r--r--testing/web-platform/tests/bluetooth/resources/bluetooth-scanning-helpers.js42
-rw-r--r--testing/web-platform/tests/bluetooth/resources/bluetooth-test.js363
-rw-r--r--testing/web-platform/tests/bluetooth/resources/health-thermometer-iframe.html92
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>