mirror of
https://github.com/servo/servo.git
synced 2025-07-02 21:13:39 +01:00
1002 lines
35 KiB
JavaScript
1002 lines
35 KiB
JavaScript
'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
|
|
}));
|
|
});
|
|
}
|