diff options
Diffstat (limited to 'tests/wpt/web-platform-tests/bluetooth/resources/bluetooth-helpers.js')
-rw-r--r-- | tests/wpt/web-platform-tests/bluetooth/resources/bluetooth-helpers.js | 800 |
1 files changed, 800 insertions, 0 deletions
diff --git a/tests/wpt/web-platform-tests/bluetooth/resources/bluetooth-helpers.js b/tests/wpt/web-platform-tests/bluetooth/resources/bluetooth-helpers.js new file mode 100644 index 00000000000..3f3ff52189f --- /dev/null +++ b/tests/wpt/web-platform-tests/bluetooth/resources/bluetooth-helpers.js @@ -0,0 +1,800 @@ +'use strict'; + +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; +} + +function loadScripts(paths) { + let chain = Promise.resolve(); + for (let path of paths) { + chain = chain.then(() => loadScript(path)); + } + return chain; +} + +function performChromiumSetup() { + // Make sure we are actually on Chromium. + if (!Mojo) { + return; + } + + // Load the Chromium-specific resources. + let prefix = '/resources/chromium'; + let extra = []; + if (window.location.pathname.includes('/LayoutTests/')) { + let root = window.location.pathname.match(/.*LayoutTests/); + prefix = `${root}/external/wpt/resources/chromium`; + extra = [ + `${root}/resources/bluetooth/bluetooth-fake-adapter.js`, + ]; + } else if (window.location.pathname.startsWith('/bluetooth/https/')) { + extra = [ + '/js-test-resources/bluetooth/bluetooth-fake-adapter.js', + ]; + } + return loadScripts([ + `${prefix}/mojo_bindings.js`, + `${prefix}/mojo_layouttest_test.mojom.js`, + `${prefix}/uuid.mojom.js`, + `${prefix}/fake_bluetooth.mojom.js`, + `${prefix}/web-bluetooth-test.js`, + ].concat(extra)) + // 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(crbug.com/569709): Remove once setBluetoothFakeAdapter is no + // longer used. + .then(() => setBluetoothFakeAdapter ? setBluetoothFakeAdapter('') + : undefined); +} + + +// 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 +function bluetooth_test(func, name, properties) { + Promise.resolve() + .then(() => promise_test(t => Promise.resolve() + // Trigger Chromium-specific setup. + .then(performChromiumSetup) + .then(() => func(t)), name, properties)); +} + +// 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: +// Services: +var blocklist_test_service_uuid = "611c954a-263b-4f4a-aab6-01ddb953f985"; +var request_disconnection_service_uuid = "01d7d889-7451-419f-aeb8-d65e7b9277af"; +// Characteristics: +var blocklist_exclude_reads_characteristic_uuid = + "bad1c9a2-9a5b-4015-8b60-1579bbbf2135"; +var request_disconnection_characteristic_uuid = + "01d7d88a-7451-419f-aeb8-d65e7b9277af"; +// Descriptors: +var blocklist_test_descriptor_uuid = "bad2ddcf-60db-45cd-bef9-fd72b153cf7c"; + +// Sometimes we need to test that using either the name, alias, or UUID +// produces the same result. The following objects help us do that. +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' +}; + +// The following tests make sure the Web Bluetooth implementation +// responds correctly to the different types of errors the +// underlying platform might return for GATT operations. + +// Each browser should map these characteristics to specific code paths +// that result in different errors thus increasing code coverage +// when testing. Therefore some of these characteristics might not be useful +// for all browsers. +// +// TODO(ortuno): According to the testing spec errorUUID(0x101) to +// errorUUID(0x1ff) should be use for the uuids of the characteristics. +var gatt_errors_tests = [{ + testName: 'GATT Error: Unknown.', + uuid: errorUUID(0xA1), + error: new DOMException( + 'GATT Error Unknown.', + 'NotSupportedError') +}, { + testName: 'GATT Error: Failed.', + uuid: errorUUID(0xA2), + error: new DOMException( + 'GATT operation failed for unknown reason.', + 'NotSupportedError') +}, { + testName: 'GATT Error: In Progress.', + uuid: errorUUID(0xA3), + error: new DOMException( + 'GATT operation already in progress.', + 'NetworkError') +}, { + testName: 'GATT Error: Invalid Length.', + uuid: errorUUID(0xA4), + error: new DOMException( + 'GATT Error: invalid attribute length.', + 'InvalidModificationError') +}, { + testName: 'GATT Error: Not Permitted.', + uuid: errorUUID(0xA5), + error: new DOMException( + 'GATT operation not permitted.', + 'NotSupportedError') +}, { + testName: 'GATT Error: Not Authorized.', + uuid: errorUUID(0xA6), + error: new DOMException( + 'GATT operation not authorized.', + 'SecurityError') +}, { + testName: 'GATT Error: Not Paired.', + uuid: errorUUID(0xA7), + // TODO(ortuno): Change to InsufficientAuthenticationError or similiar + // once https://github.com/WebBluetoothCG/web-bluetooth/issues/137 is + // resolved. + error: new DOMException( + 'GATT Error: Not paired.', + 'NetworkError') +}, { + testName: 'GATT Error: Not Supported.', + uuid: errorUUID(0xA8), + error: new DOMException( + 'GATT Error: Not supported.', + 'NotSupportedError') +}]; + +function callWithTrustedClick(callback) { + 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'. +function requestDeviceWithTrustedClick() { + let args = arguments; + return callWithTrustedClick( + () => navigator.bluetooth.requestDevice.apply(navigator.bluetooth, args)); +} + +// errorUUID(alias) returns a UUID with the top 32 bits of +// '00000000-97e5-4cd7-b9f1-f5a427670c59' replaced with the bits of |alias|. +// For example, errorUUID(0xDEADBEEF) returns +// 'deadbeef-97e5-4cd7-b9f1-f5a427670c59'. The bottom 96 bits of error UUIDs +// were generated as a type 4 (random) UUID. +function errorUUID(uuidAlias) { + // Make the number positive. + uuidAlias >>>= 0; + // Append the alias as a hex number. + var strAlias = '0000000' + uuidAlias.toString(16); + // Get last 8 digits of strAlias. + strAlias = strAlias.substr(-8); + // Append Base Error UUID + return strAlias + '-97e5-4cd7-b9f1-f5a427670c59'; +} + +// Function to test that a promise rejects with the expected error type and +// message. +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:'); + } + }); +} + +function runGarbageCollection() +{ + // Run gc() as a promise. + return new Promise( + function(resolve, reject) { + GCController.collect(); + step_timeout(resolve, 0); + }); +} + +function eventPromise(target, type, options) { + return new Promise(resolve => { + let wrapper = function(event) { + target.removeEventListener(type, wrapper); + resolve(event); + }; + target.addEventListener(type, wrapper, options); + }); +} + +// 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. +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('event'); + resolve(e.target.value); + }; + object.addEventListener(event, event_listener); + })); + } + + let func_promise = object[func]().then(result => { + order.push('promiseresolved'); + return result; + }); + + return Promise.all([func_promise, ...event_promises]) + .then((result) => { + if (should_be_first !== order[0]) { + throw should_be_first === 'promiseresolved' ? + `'${event}' was fired before promise resolved.` : + `Promise resolved before '${event}' was fired.`; + } + + if (order[0] !== 'promiseresolved' && + order[order.length - 1] !== 'promiseresolved') { + throw 'Promise resolved in between event listeners.'; + } + + return result; + }); +} + +// See assert_promise_event_order_ above. +function assert_promise_resolves_before_event( + object, func, event, num_listeners=1) { + return assert_promise_event_order_( + 'promiseresolved', object, func, event, num_listeners); +} + +// See assert_promise_event_order_ above. +function assert_promise_resolves_after_event( + object, func, event, num_listeners=1) { + return assert_promise_event_order_( + '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. +function assert_no_events(object, event_name) { + return new Promise((resolve, reject) => { + 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); + }); +} + +class TestCharacteristicProperties { + // |properties| is an array of strings for property bits to be set + // as true. + 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}'`; + }); + } +} + +function assert_properties_equal(properties, expected_properties) { + for (let key in expected_properties) { + assert_equals(properties[key], expected_properties[key]); + } +} + +class EventCatcher { + constructor(object, event) { + this.eventFired = false; + let event_listener = () => { + object.removeEventListener(event, event_listener); + this.eventFired = true; + }; + object.addEventListener(event, event_listener); + } +} + +// Returns a function that when called returns a promise that resolves when +// the device has disconnected. Example: +// device.gatt.connect() +// .then(gatt => get_request_disconnection(gatt)) +// .then(requestDisconnection => requestDisconnection()) +// .then(() => // device is now disconnected) +function get_request_disconnection(gattServer) { + return gattServer.getPrimaryService(request_disconnection_service_uuid) + .then(service => service.getCharacteristic(request_disconnection_characteristic_uuid)) + .then(characteristic => { + return () => assert_promise_rejects_with_message( + characteristic.writeValue(new Uint8Array([0])), + new DOMException( + 'GATT Server is disconnected. Cannot perform GATT operations. ' + + '(Re)connect first with `device.gatt.connect`.', + 'NetworkError')); + }); +} + +function generateRequestDeviceArgsWithServices(services = ['heart_rate']) { + return [{ + filters: [{ services: services }] + }, { + filters: [{ services: services, name: 'Name' }] + }, { + filters: [{ services: services, namePrefix: 'Pre' }] + }, { + filters: [{ services: services, name: 'Name', namePrefix: 'Pre' }] + }, { + 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, name: 'Name', namePrefix: 'Pre' }], + optionalServices: ['heart_rate'] + }]; +} + +// Simulates a pre-connected device with |address|, |name| and +// |knownServiceUUIDs|. +function setUpPreconnectedDevice({ + address = '00:00:00:00:00:00', name = 'LE Device', knownServiceUUIDs = []}) { + return navigator.bluetooth.test.simulateCentral({state: 'powered-on'}) + .then(fake_central => fake_central.simulatePreconnectedPeripheral({ + address: address, + name: name, + knownServiceUUIDs: knownServiceUUIDs, + })); +} + +// Returns a FakePeripheral that corresponds to a simulated pre-connected device +// called 'Health Thermometer'. The device has two known serviceUUIDs: +// 'generic_access' and 'health_thermometer'. +function setUpHealthThermometerDevice() { + return setUpPreconnectedDevice({ + address: '09:09:09:09:09:09', + name: 'Health Thermometer', + knownServiceUUIDs: ['generic_access', 'health_thermometer'], + }); +} + +// Returns an array containing two FakePeripherals corresponding +// to the simulated devices. +function setUpHealthThermometerAndHeartRateDevices() { + return navigator.bluetooth.test.simulateCentral({state: 'powered-on'}) + .then(fake_central => Promise.all([ + fake_central.simulatePreconnectedPeripheral({ + address: '09:09:09:09:09:09', + name: 'Health Thermometer', + knownServiceUUIDs: ['generic_access', 'health_thermometer'], + }), + fake_central.simulatePreconnectedPeripheral({ + address: '08:08:08:08:08:08', + name: 'Heart Rate', + knownServiceUUIDs: ['generic_access', 'heart_rate'], + })])); +} + +// Returns the same fake peripheral as setUpHealthThermometerDevice() except +// that connecting to the peripheral will succeed. +function setUpConnectableHealthThermometerDevice() { + let fake_peripheral; + return setUpHealthThermometerDevice() + .then(_ => fake_peripheral = _) + .then(() => fake_peripheral.setNextGATTConnectionResponse({ + code: HCI_SUCCESS, + })) + .then(() => fake_peripheral); +} + +// 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. +function getHealthThermometerDevice(options) { + let result; + return getConnectedHealthThermometerDevice(options) + .then(_ => result = _) + .then(() => result.fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS, + })) + .then(() => result); +} + +// Similar to getHealthThermometerDevice except that the peripheral has +// two 'health_thermometer' services. +function getTwoHealthThermometerServicesDevice(options) { + let device; + let fake_peripheral; + let fake_generic_access; + let fake_health_thermometer1; + let fake_health_thermometer2; + + return getConnectedHealthThermometerDevice(options) + .then(result => { + ({ + device, + fake_peripheral, + fake_generic_access, + fake_health_thermometer: fake_health_thermometer1, + } = result); + }) + .then(() => fake_peripheral.addFakeService({uuid: 'health_thermometer'})) + .then(s => fake_health_thermometer2 = s) + .then(() => fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS})) + .then(() => ({ + device: device, + fake_peripheral: fake_peripheral, + fake_generic_access: fake_generic_access, + fake_health_thermometer1: fake_health_thermometer1, + fake_health_thermometer2: fake_health_thermometer2 + })); +} + +// Returns an object containing a Health Thermometer BluetoothRemoteGattService +// and its corresponding FakeRemoteGATTService. +function getHealthThermometerService() { + let result; + return getHealthThermometerDevice() + .then(r => result = r) + .then(() => result.device.gatt.getPrimaryService('health_thermometer')) + .then(service => Object.assign(result, { + service, + fake_service: result.fake_health_thermometer, + })); +} + +// Returns an object containing a Measurement Interval +// BluetoothRemoteGATTCharacteristic and its corresponding +// FakeRemoteGATTCharacteristic. +function getMeasurementIntervalCharacteristic() { + let result; + return getHealthThermometerService() + .then(r => result = r) + .then(() => result.service.getCharacteristic('measurement_interval')) + .then(characteristic => Object.assign(result, { + characteristic, + fake_characteristic: result.fake_measurement_interval, + })); +} + +function getUserDescriptionDescriptor() { + let result; + return getMeasurementIntervalCharacteristic() + .then(r => result = r) + .then(() => result.characteristic.getDescriptor( + 'gatt.characteristic_user_description')) + .then(descriptor => Object.assign(result, { + descriptor, + fake_descriptor: result.fake_user_description, + })); +} + +// Populates a fake_peripheral with various fakes appropriate for a health +// thermometer. This resolves to an associative array composed of the fakes, +// including the |fake_peripheral|. +function populateHealthThermometerFakes(fake_peripheral) { + let fake_generic_access, fake_health_thermometer, fake_measurement_interval, + fake_user_description, fake_cccd, fake_temperature_measurement, + fake_temperature_type; + return fake_peripheral.addFakeService({uuid: 'generic_access'}) + .then(_ => fake_generic_access = _) + .then(() => fake_peripheral.addFakeService({ + uuid: 'health_thermometer', + })) + .then(_ => fake_health_thermometer = _) + .then(() => fake_health_thermometer.addFakeCharacteristic({ + uuid: 'measurement_interval', + properties: ['read', 'write', 'indicate'], + })) + .then(_ => fake_measurement_interval = _) + .then(() => fake_measurement_interval.addFakeDescriptor({ + uuid: 'gatt.characteristic_user_description', + })) + .then(_ => fake_user_description = _) + .then(() => fake_measurement_interval.addFakeDescriptor({ + uuid: 'gatt.client_characteristic_configuration', + })) + .then(_ => fake_cccd = _) + .then(() => fake_health_thermometer.addFakeCharacteristic({ + uuid: 'temperature_measurement', + properties: ['indicate'], + })) + .then(_ => fake_temperature_measurement = _) + .then(() => fake_health_thermometer.addFakeCharacteristic({ + uuid: 'temperature_type', + properties: ['read'], + })) + .then(_ => fake_temperature_type = _) + .then(() => ({ + fake_peripheral, + fake_generic_access, + fake_health_thermometer, + fake_measurement_interval, + fake_cccd, + fake_user_description, + fake_temperature_measurement, + fake_temperature_type, + })); +} + +// Similar to getHealthThermometerDevice except the GATT discovery +// response has not been set yet so more attributes can still be added. +function getConnectedHealthThermometerDevice(options) { + let device, fake_peripheral, fakes; + return getDiscoveredHealthThermometerDevice(options) + .then(_ => ({device, fake_peripheral} = _)) + .then(() => fake_peripheral.setNextGATTConnectionResponse({ + code: HCI_SUCCESS, + })) + .then(() => populateHealthThermometerFakes(fake_peripheral)) + .then(_ => fakes = _) + .then(() => device.gatt.connect()) + .then(() => Object.assign({device}, fakes)); +} + +// 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. +function getHealthThermometerDeviceWithServicesDiscovered(options) { + let device, fake_peripheral, fakes; + let iframe = document.createElement('iframe'); + return setUpConnectableHealthThermometerDevice() + .then(_ => fake_peripheral = _) + .then(() => populateHealthThermometerFakes(fake_peripheral)) + .then(_ => fakes = _) + .then(() => fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS, + })) + .then(() => new Promise(resolve => { + iframe.src = '../../../resources/bluetooth/health-thermometer-iframe.html'; + document.body.appendChild(iframe); + iframe.addEventListener('load', resolve); + })) + .then(() => 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); + })) + .then(() => requestDeviceWithTrustedClick(options)) + .then(_ => device = _) + .then(device => device.gatt.connect()) + .then(_ => Object.assign({device}, fakes)); +} + +// Similar to getHealthThermometerDevice() except the device has no services, +// characteristics, or descriptors. +function getEmptyHealthThermometerDevice(options) { + return getDiscoveredHealthThermometerDevice(options) + .then(({device, fake_peripheral}) => { + return fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS}) + .then(() => device.gatt.connect()) + .then(() => fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS})) + .then(() => ({ + device: device, + fake_peripheral: fake_peripheral + })); + }); +} + +// Similar to getHealthThermometerService() except the service has no +// characteristics or included services. +function getEmptyHealthThermometerService(options) { + let device; + let fake_peripheral; + let fake_health_thermometer; + return getDiscoveredHealthThermometerDevice(options) + .then(result => ({device, fake_peripheral} = result)) + .then(() => fake_peripheral.setNextGATTConnectionResponse({ + code: HCI_SUCCESS})) + .then(() => device.gatt.connect()) + .then(() => fake_peripheral.addFakeService({uuid: 'health_thermometer'})) + .then(s => fake_health_thermometer = s) + .then(() => fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS})) + .then(() => device.gatt.getPrimaryService('health_thermometer')) + .then(service => ({ + service: service, + fake_health_thermometer: fake_health_thermometer, + })); +} + +// 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. +// TODO(crbug.com/719816): Add descriptors. +function getHIDDevice(options) { + return setUpPreconnectedDevice({ + address: '10:10:10:10:10:10', + name: 'HID Device', + knownServiceUUIDs: [ + 'generic_access', + 'device_information', + 'human_interface_device' + ], + }) + .then(fake_peripheral => { + return requestDeviceWithTrustedClick(options) + .then(device => { + return fake_peripheral + .setNextGATTConnectionResponse({ + code: HCI_SUCCESS}) + .then(() => device.gatt.connect()) + .then(() => fake_peripheral.addFakeService({ + uuid: 'generic_access'})) + .then(() => fake_peripheral.addFakeService({ + uuid: 'device_information'})) + // Blocklisted Characteristic: + // https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt + .then(dev_info => dev_info.addFakeCharacteristic({ + uuid: 'serial_number_string', properties: ['read']})) + .then(() => fake_peripheral.addFakeService({ + uuid: 'human_interface_device'})) + .then(() => fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS})) + .then(() => ({ + device: device, + fake_peripheral: fake_peripheral + })); + }); + }); +} + +// Similar to getHealthThermometerDevice() except the device +// is not connected and thus its services have not been +// discovered. +function getDiscoveredHealthThermometerDevice( + options = {filters: [{services: ['health_thermometer']}]}) { + return setUpHealthThermometerDevice() + .then(fake_peripheral => { + return requestDeviceWithTrustedClick(options) + .then(device => ({ + device: device, + fake_peripheral: fake_peripheral + })); + }); +} |