chromium/third_party/blink/web_tests/external/wpt/bluetooth/resources/bluetooth-fake-devices.js

'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'],
    })
  ]);
}