| 'use strict'; |
| |
| // Bluetooth UUID constants: |
| // Services: |
| var blacklist_test_service_uuid = "611c954a-263b-4f4a-aab6-01ddb953f985"; |
| var request_disconnection_service_uuid = "01d7d889-7451-419f-aeb8-d65e7b9277af"; |
| // Characteristics: |
| var blacklist_exclude_reads_characteristic_uuid = |
| "bad1c9a2-9a5b-4015-8b60-1579bbbf2135"; |
| var request_disconnection_characteristic_uuid = |
| "01d7d88a-7451-419f-aeb8-d65e7b9277af"; |
| |
| // 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 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' |
| }; |
| |
| // 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') |
| }]; |
| |
| // TODO(jyasskin): Upstream this to testharness.js: https://crbug.com/509058. |
| function callWithKeyDown(functionCalledOnKeyPress) { |
| return new Promise(resolve => { |
| function onKeyPress() { |
| document.removeEventListener('keypress', onKeyPress, false); |
| resolve(functionCalledOnKeyPress()); |
| } |
| document.addEventListener('keypress', onKeyPress, false); |
| |
| eventSender.keyDown(' ', []); |
| }); |
| } |
| |
| // Calls requestDevice() in a context that's 'allowed to show a popup'. |
| function requestDeviceWithKeyDown() { |
| let args = arguments; |
| return callWithKeyDown(() => navigator.bluetooth.requestDevice.apply(navigator.bluetooth, args)); |
| } |
| |
| // Calls testRunner.getBluetoothManualChooserEvents() until it's returned |
| // |expected_count| events. Or just once if |expected_count| is undefined. |
| function getBluetoothManualChooserEvents(expected_count) { |
| return new Promise((resolve, reject) => { |
| let events = []; |
| let accumulate_events = new_events => { |
| events.push(...new_events); |
| if (events.length >= expected_count) { |
| resolve(events); |
| } else { |
| testRunner.getBluetoothManualChooserEvents(accumulate_events); |
| } |
| }; |
| testRunner.getBluetoothManualChooserEvents(accumulate_events); |
| }); |
| } |
| |
| function setBluetoothFakeAdapter(adapter_name) { |
| return new Promise(resolve => { |
| testRunner.setBluetoothFakeAdapter(adapter_name, resolve); |
| }); |
| } |
| |
| // 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:'); |
| } |
| }); |
| } |
| |
| // Parses add-device(name)=id lines in |
| // testRunner.getBluetoothManualChooserEvents() output, and exposes the name->id |
| // mapping. |
| class AddDeviceEventSet { |
| constructor() { |
| this._idsByName = new Map(); |
| this._addDeviceRegex = /^add-device\(([^)]+)\)=(.+)$/; |
| } |
| assert_add_device_event(event, description) { |
| let match = this._addDeviceRegex.exec(event); |
| assert_true(!!match, event + " isn't an add-device event: " + description); |
| this._idsByName.set(match[1], match[2]); |
| } |
| has(name) { |
| return this._idsByName.has(name); |
| } |
| get(name) { |
| return this._idsByName.get(name); |
| } |
| } |
| |
| function runGarbageCollection() |
| { |
| // Run gc() as a promise. |
| return new Promise( |
| function(resolve, reject) { |
| GCController.collect(); |
| setTimeout(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); |
| }); |
| } |
| |
| // Creates |num_listeners| promises. Each adds an event listener |
| // to object. The promises resolve once the object fires |event| but |
| // reject if the event is fired before |object|.|func|() resolves. |
| // Returns a promise that fulfills with the result of |object|.|func()| |
| // and |event.target.value| of each of the other promises. |
| function assert_event_fires_after_promise(object, func, event, num_listeners) { |
| num_listeners = num_listeners !== undefined ? num_listeners : 1; |
| |
| if (object[func] === undefined) { |
| return Promise.reject('Function \'' + func + '\' not available in object.'); |
| } |
| let should_resolve = false; |
| let event_promises = []; |
| for (let i = 0; i < num_listeners; i++) { |
| event_promises.push(new Promise((resolve, reject) => { |
| let event_listener = (e) => { |
| object.removeEventListener(event, event_listener); |
| if (should_resolve) { |
| resolve(e.target.value); |
| } else { |
| reject(event + ' was triggered before the promise resolved.'); |
| } |
| }; |
| object.addEventListener(event, event_listener); |
| })); |
| } |
| return object[func]().then(result => { |
| should_resolve = true; |
| return Promise.all([result, ...event_promises]); |
| }); |
| } |
| |
| // 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(ortuno): Remove timeout. |
| // http://crbug.com/543884 |
| setTimeout(() => { |
| object.removeEventListener(event_name, event_listener); |
| resolve(); |
| }, 100); |
| }); |
| } |
| |
| class TestCharacteristicProperties { |
| constructor(properties) { |
| this.broadcast = properties.broadcast || false; |
| this.read = properties.read || false; |
| this.writeWithoutResponse = properties.writeWithoutResponse || false; |
| this.write = properties.write || false; |
| this.notify = properties.notify || false; |
| this.indicate = properties.indicate || false; |
| this.authenticatedSignedWrites = properties.authenticatedSignedWrites || false; |
| this.reliableWrite = properties.reliableWrite || false; |
| this.writableAuxiliaries = properties.writableAuxiliaries || false; |
| } |
| } |
| |
| function assert_properties_equal(properties, expected_properties) { |
| for (let key in expected_properties) { |
| assert_equals(properties[key], expected_properties[key]); |
| } |
| } |
| |
| // Generates a string of size |size|. |
| function generate_string(size, char) { |
| // When passing an array of n undefined's to String the resulting string |
| // has size n - 1. |
| return char.repeat(size); |
| } |
| |
| class EventCatcher { |
| constructor(object, event) { |
| this.eventFired = false; |
| let event_listener = e => { |
| 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 disconnected while performing a GATT operation.', |
| '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'] |
| }]; |
| } |