blob: 993ffbcc3de8ae12dcbb61d4592e8f1d5c04c2e3 [file] [log] [blame]
'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']
}];
}