'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}/fake_bluetooth_chooser.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(() => typeof setBluetoothFakeAdapter === 'undefined' ? 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 function bluetooth_test(func, name, properties) { Promise.resolve() .then(() => promise_test(t => Promise.resolve() // Trigger Chromium-specific setup. .then(performChromiumSetup) .then(() => func(t)) .then(() => navigator.bluetooth.test.allResponsesConsumed()) .then(consumed => assert_true(consumed)), 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"; var blocklist_exclude_reads_descriptor_uuid = "bad3ec61-3cc3-4954-9702-7977df514114"; // 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') }]; // Waits until the document has finished loading. function waitForDocumentReady() { return new Promise(resolve => { if (document.readyState === 'complete') { resolve(); } window.addEventListener('load', () => { resolve(); }, {once: true}); }); } function callWithTrustedClick(callback) { return waitForDocumentReady() .then(() => 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'] }]; } // Causes |fake_peripheral| to disconnect and returns a promise that resolves // once `gattserverdisconnected` has been fired on |device|. function simulateGATTDisconnectionAndWait(device, fake_peripheral) { return Promise.all([ eventPromise(device, 'gattserverdisconnected'), fake_peripheral.simulateGATTDisconnection(), ]); } // 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 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 UUIDs |blocklist_test_service_uuid| which // correspond to a service with the same 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. function getBlocklistDevice( options = {filters: [{services: [blocklist_test_service_uuid]}]}) { let device, 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; return setUpPreconnectedDevice({ address: '11:11:11:11:11:11', name: 'Blocklist Device', knownServiceUUIDs: ['generic_access', blocklist_test_service_uuid], }) .then(_ => fake_peripheral = _) .then(() => requestDeviceWithTrustedClick(options)) .then(_ => device = _) .then(() => fake_peripheral.setNextGATTConnectionResponse({ code: HCI_SUCCESS, })) .then(() => device.gatt.connect()) .then(() => fake_peripheral.addFakeService({ uuid: blocklist_test_service_uuid, })) .then(_ => fake_blocklist_test_service = _) .then(() => fake_blocklist_test_service.addFakeCharacteristic({ uuid: blocklist_exclude_reads_characteristic_uuid, properties: ['read', 'write'], })) .then(_ => fake_blocklist_exclude_reads_characteristic = _) .then(() => fake_blocklist_test_service.addFakeCharacteristic({ uuid: 'gap.peripheral_privacy_flag', properties: ['read', 'write'], })) .then(_ => fake_blocklist_exclude_writes_characteristic = _) .then(() => fake_blocklist_exclude_writes_characteristic .addFakeDescriptor({uuid: blocklist_test_descriptor_uuid})) .then(_ => fake_blocklist_descriptor = _) .then(() => fake_blocklist_exclude_writes_characteristic .addFakeDescriptor({uuid: blocklist_exclude_reads_descriptor_uuid})) .then(_ => fake_blocklist_exclude_reads_descriptor = _) .then(() => fake_blocklist_exclude_writes_characteristic .addFakeDescriptor({ uuid: 'gatt.client_characteristic_configuration' })) .then(_ => fake_blocklist_exclude_writes_descriptor = _) .then(() => fake_peripheral.setNextGATTDiscoveryResponse({ code: HCI_SUCCESS, })) .then(() => ({ device, 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 BluetoothRemoveGattService and // its corresponding FakeRemoteGATTService. function getBlocklistTestService() { let result; return getBlocklistDevice() .then(_ => result = _) .then(() => result.device.gatt.getPrimaryService(blocklist_test_service_uuid)) .then(service => 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. function getBlocklistExcludeReadsCharacteristic() { let result, fake_characteristic; return getBlocklistTestService() .then(_ => result = _) .then(() => result.service.getCharacteristic( blocklist_exclude_reads_characteristic_uuid)) .then(characteristic => 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. function getBlocklistExcludeWritesCharacteristic() { let result, fake_characteristic; return getBlocklistTestService() .then(_ => result = _) .then(() => result.service.getCharacteristic( 'gap.peripheral_privacy_flag')) .then(characteristic => 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. function getBlocklistExcludeReadsDescriptor() { let result; return getBlocklistExcludeWritesCharacteristic() .then(_ => result = _) .then(() => result.characteristic.getDescriptor( blocklist_exclude_reads_descriptor_uuid)) .then(descriptor => 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. function getBlocklistExcludeWritesDescriptor() { let result; return getBlocklistExcludeWritesCharacteristic() .then(_ => result = _) .then(() => result.characteristic.getDescriptor( 'gatt.client_characteristic_configuration')) .then(descriptor => Object.assign( result, { descriptor: descriptor, fake_descriptor: result.fake_blocklist_exclude_writes_descriptor, })); } // 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 => { 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); })) .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. function getHIDDevice(options) { let device, fake_peripheral; return getConnectedHIDDevice(options) .then(_ => ({device, fake_peripheral} = _)) .then(() => fake_peripheral.setNextGATTDiscoveryResponse({ code: HCI_SUCCESS, })) .then(() => ({device, fake_peripheral})); } // 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. function getConnectedHIDDevice(options) { let device, fake_peripheral; return setUpPreconnectedDevice({ address: '10:10:10:10:10:10', name: 'HID Device', knownServiceUUIDs: [ 'generic_access', 'device_information', 'human_interface_device', ], }) .then(_ => (fake_peripheral = _)) .then(() => requestDeviceWithTrustedClick(options)) .then(_ => (device = _)) .then(() => 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(() => ({device, 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 })); }); }