mirror of
https://github.com/servo/servo.git
synced 2025-06-25 09:34:32 +01:00
Auto merge of #14277 - szeged:service-and-manufacturer-data, r=jdm
serviceData and manufacturerData support <!-- Please describe your changes on the following line: --> Allow requesting for BluetoothDevices with service and manufacturer specific data. --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `__` with appropriate data: --> - [x] `./mach build -d` does not report any errors - [x] `./mach test-tidy` does not report any errors <!-- Either: --> - [x] There are tests for these changes <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. --> <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/14277) <!-- Reviewable:end -->
This commit is contained in:
commit
5946f756d7
19 changed files with 830 additions and 83 deletions
|
@ -131,25 +131,62 @@ fn matches_filter(device: &BluetoothDevice, filter: &BluetoothScanfilter) -> boo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4.
|
// Step 4.
|
||||||
// TODO: Implement get_manufacturer_data in device crate.
|
if let Some(ref manufacturer_data) = filter.get_manufacturer_data() {
|
||||||
// if let Some(manufacturer_id) = filter.get_manufacturer_id() {
|
let advertised_manufacturer_data = match device.get_manufacturer_data() {
|
||||||
// if !device.get_manufacturer_data().contains_key(manufacturer_id) {
|
Ok(data) => data,
|
||||||
// return false;
|
Err(_) => return false,
|
||||||
// }
|
};
|
||||||
// }
|
for (ref id, &(ref prefix, ref mask)) in manufacturer_data.iter() {
|
||||||
//
|
if let Some(advertised_data) = advertised_manufacturer_data.get(id) {
|
||||||
|
if !data_filter_matches(advertised_data, prefix, mask) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 5.
|
// Step 5.
|
||||||
// TODO: Implement get_device_data in device crate.
|
if let Some(ref service_data) = filter.get_service_data() {
|
||||||
// if !filter.get_service_data_uuid().is_empty() {
|
let advertised_service_data = match device.get_service_data() {
|
||||||
// if !device.get_service_data().contains_key(filter.get_service_data_uuid()) {
|
Ok(data) => data,
|
||||||
// return false;
|
Err(_) => return false,
|
||||||
// }
|
};
|
||||||
// }
|
for (uuid, &(ref prefix, ref mask)) in service_data.iter() {
|
||||||
|
if let Some(advertised_data) = advertised_service_data.get(uuid.as_str()) {
|
||||||
|
if !data_filter_matches(advertised_data, prefix, mask) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 6.
|
// Step 6.
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://webbluetoothcg.github.io/web-bluetooth/#bluetoothdatafilterinit-matches
|
||||||
|
fn data_filter_matches(data: &[u8], prefix: &[u8], mask: &[u8]) -> bool {
|
||||||
|
// Step 1-2: No need to copy the bytes here.
|
||||||
|
// Step 3.
|
||||||
|
if data.len() < prefix.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4.
|
||||||
|
for ((data, mask), prefix) in data.iter().zip(mask.iter()).zip(prefix.iter()) {
|
||||||
|
if data & mask != prefix & mask {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5.
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn matches_filters(device: &BluetoothDevice, filters: &BluetoothScanfilterSequence) -> bool {
|
fn matches_filters(device: &BluetoothDevice, filters: &BluetoothScanfilterSequence) -> bool {
|
||||||
if filters.has_empty_or_invalid_filter() {
|
if filters.has_empty_or_invalid_filter() {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -7,7 +7,7 @@ use device::bluetooth::{BluetoothAdapter, BluetoothDevice};
|
||||||
use device::bluetooth::{BluetoothGATTCharacteristic, BluetoothGATTDescriptor, BluetoothGATTService};
|
use device::bluetooth::{BluetoothGATTCharacteristic, BluetoothGATTDescriptor, BluetoothGATTService};
|
||||||
use std::borrow::ToOwned;
|
use std::borrow::ToOwned;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashSet, HashMap};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::string::String;
|
use std::string::String;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -417,6 +417,29 @@ fn create_blocklisted_device(adapter: &BluetoothAdapter) -> Result<(), Box<Error
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_glucose_heart_rate_devices(adapter: &BluetoothAdapter) -> Result<(), Box<Error>> {
|
||||||
|
let glucose_devie = try!(create_device_with_uuids(adapter,
|
||||||
|
GLUCOSE_DEVICE_NAME.to_owned(),
|
||||||
|
GLUCOSE_DEVICE_ADDRESS.to_owned(),
|
||||||
|
vec![GLUCOSE_SERVICE_UUID.to_owned(),
|
||||||
|
TX_POWER_SERVICE_UUID.to_owned()]));
|
||||||
|
|
||||||
|
let heart_rate_device_empty = try!(create_heart_rate_device(adapter, true));
|
||||||
|
|
||||||
|
let mut manufacturer_dta = HashMap::new();
|
||||||
|
manufacturer_dta.insert(17, vec![1, 2, 3]);
|
||||||
|
try!(glucose_devie.set_manufacturer_data(manufacturer_dta));
|
||||||
|
|
||||||
|
let mut service_data = HashMap::new();
|
||||||
|
service_data.insert(GLUCOSE_SERVICE_UUID.to_owned(), vec![1, 2, 3]);
|
||||||
|
try!(glucose_devie.set_service_data(service_data));
|
||||||
|
|
||||||
|
service_data = HashMap::new();
|
||||||
|
service_data.insert(HEART_RATE_SERVICE_UUID.to_owned(), vec![1, 2, 3]);
|
||||||
|
try!(heart_rate_device_empty.set_service_data(service_data));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn test(manager: &mut BluetoothManager, data_set_name: String) -> Result<(), Box<Error>> {
|
pub fn test(manager: &mut BluetoothManager, data_set_name: String) -> Result<(), Box<Error>> {
|
||||||
let may_existing_adapter = manager.get_or_create_adapter();
|
let may_existing_adapter = manager.get_or_create_adapter();
|
||||||
let adapter = match may_existing_adapter.as_ref() {
|
let adapter = match may_existing_adapter.as_ref() {
|
||||||
|
@ -437,14 +460,7 @@ pub fn test(manager: &mut BluetoothManager, data_set_name: String) -> Result<(),
|
||||||
},
|
},
|
||||||
GLUCOSE_HEART_RATE_ADAPTER => {
|
GLUCOSE_HEART_RATE_ADAPTER => {
|
||||||
try!(set_adapter(adapter, GLUCOSE_HEART_RATE_ADAPTER.to_owned()));
|
try!(set_adapter(adapter, GLUCOSE_HEART_RATE_ADAPTER.to_owned()));
|
||||||
|
let _ = try!(create_glucose_heart_rate_devices(adapter));
|
||||||
let _glucose_devie = try!(create_device_with_uuids(adapter,
|
|
||||||
GLUCOSE_DEVICE_NAME.to_owned(),
|
|
||||||
GLUCOSE_DEVICE_ADDRESS.to_owned(),
|
|
||||||
vec![GLUCOSE_SERVICE_UUID.to_owned(),
|
|
||||||
TX_POWER_SERVICE_UUID.to_owned()]));
|
|
||||||
|
|
||||||
let _heart_rate_device_empty = try!(create_heart_rate_device(adapter, true));
|
|
||||||
},
|
},
|
||||||
UNICODE_DEVICE_ADAPTER => {
|
UNICODE_DEVICE_ADAPTER => {
|
||||||
try!(set_adapter(adapter, UNICODE_DEVICE_ADAPTER.to_owned()));
|
try!(set_adapter(adapter, UNICODE_DEVICE_ADAPTER.to_owned()));
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::slice::Iter;
|
use std::slice::Iter;
|
||||||
|
|
||||||
// A device name can never be longer than 29 bytes. An adv packet is at most
|
// A device name can never be longer than 29 bytes. An adv packet is at most
|
||||||
|
@ -23,28 +23,31 @@ impl ServiceUUIDSequence {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ManufacturerData = HashMap<u16, (Vec<u8>, Vec<u8>)>;
|
||||||
|
type ServiceData = HashMap<String, (Vec<u8>, Vec<u8>)>;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct BluetoothScanfilter {
|
pub struct BluetoothScanfilter {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
name_prefix: String,
|
name_prefix: String,
|
||||||
services: ServiceUUIDSequence,
|
services: ServiceUUIDSequence,
|
||||||
manufacturer_id: Option<u16>,
|
manufacturer_data: Option<ManufacturerData>,
|
||||||
service_data_uuid: String,
|
service_data: Option<ServiceData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BluetoothScanfilter {
|
impl BluetoothScanfilter {
|
||||||
pub fn new(name: Option<String>,
|
pub fn new(name: Option<String>,
|
||||||
name_prefix: String,
|
name_prefix: String,
|
||||||
services: Vec<String>,
|
services: Vec<String>,
|
||||||
manufacturer_id: Option<u16>,
|
manufacturer_data: Option<ManufacturerData>,
|
||||||
service_data_uuid: String)
|
service_data: Option<ServiceData>)
|
||||||
-> BluetoothScanfilter {
|
-> BluetoothScanfilter {
|
||||||
BluetoothScanfilter {
|
BluetoothScanfilter {
|
||||||
name: name,
|
name: name,
|
||||||
name_prefix: name_prefix,
|
name_prefix: name_prefix,
|
||||||
services: ServiceUUIDSequence::new(services),
|
services: ServiceUUIDSequence::new(services),
|
||||||
manufacturer_id: manufacturer_id,
|
manufacturer_data: manufacturer_data,
|
||||||
service_data_uuid: service_data_uuid,
|
service_data: service_data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,20 +63,20 @@ impl BluetoothScanfilter {
|
||||||
&self.services.0
|
&self.services.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_manufacturer_id(&self) -> Option<u16> {
|
pub fn get_manufacturer_data(&self) -> Option<&ManufacturerData> {
|
||||||
self.manufacturer_id
|
self.manufacturer_data.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_service_data_uuid(&self) -> &str {
|
pub fn get_service_data(&self) -> Option<&ServiceData> {
|
||||||
&self.service_data_uuid
|
self.service_data.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty_or_invalid(&self) -> bool {
|
pub fn is_empty_or_invalid(&self) -> bool {
|
||||||
(self.name.is_none() &&
|
(self.name.is_none() &&
|
||||||
self.name_prefix.is_empty() &&
|
self.name_prefix.is_empty() &&
|
||||||
self.get_services().is_empty() &&
|
self.get_services().is_empty() &&
|
||||||
self.manufacturer_id.is_none() &&
|
self.manufacturer_data.is_none() &&
|
||||||
self.service_data_uuid.is_empty()) ||
|
self.service_data.is_none()) ||
|
||||||
self.get_name().unwrap_or("").len() > MAX_NAME_LENGTH ||
|
self.get_name().unwrap_or("").len() > MAX_NAME_LENGTH ||
|
||||||
self.name_prefix.len() > MAX_NAME_LENGTH
|
self.name_prefix.len() > MAX_NAME_LENGTH
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,9 @@ use bluetooth_traits::scanfilter::{BluetoothScanfilter, BluetoothScanfilterSeque
|
||||||
use bluetooth_traits::scanfilter::{RequestDeviceoptions, ServiceUUIDSequence};
|
use bluetooth_traits::scanfilter::{RequestDeviceoptions, ServiceUUIDSequence};
|
||||||
use core::clone::Clone;
|
use core::clone::Clone;
|
||||||
use dom::bindings::cell::DOMRefCell;
|
use dom::bindings::cell::DOMRefCell;
|
||||||
use dom::bindings::codegen::Bindings::BluetoothBinding::{self, BluetoothMethods, BluetoothRequestDeviceFilter};
|
use dom::bindings::codegen::Bindings::BluetoothBinding::{self, BluetoothDataFilterInit, BluetoothLEScanFilterInit};
|
||||||
use dom::bindings::codegen::Bindings::BluetoothBinding::RequestDeviceOptions;
|
use dom::bindings::codegen::Bindings::BluetoothBinding::{BluetoothMethods, RequestDeviceOptions};
|
||||||
|
use dom::bindings::codegen::UnionTypes::StringOrUnsignedLong;
|
||||||
use dom::bindings::error::Error::{self, NotFound, Security, Type};
|
use dom::bindings::error::Error::{self, NotFound, Security, Type};
|
||||||
use dom::bindings::error::Fallible;
|
use dom::bindings::error::Fallible;
|
||||||
use dom::bindings::js::{JS, MutHeap, Root};
|
use dom::bindings::js::{JS, MutHeap, Root};
|
||||||
|
@ -31,10 +32,14 @@ use js::jsapi::{JSAutoCompartment, JSContext};
|
||||||
use network_listener::{NetworkListener, PreInvoke};
|
use network_listener::{NetworkListener, PreInvoke};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
const KEY_CONVERSION_ERROR: &'static str = "This `manufacturerData` key can not be parsed as unsigned short:";
|
||||||
const FILTER_EMPTY_ERROR: &'static str = "'filters' member, if present, must be nonempty to find any devices.";
|
const FILTER_EMPTY_ERROR: &'static str = "'filters' member, if present, must be nonempty to find any devices.";
|
||||||
const FILTER_ERROR: &'static str = "A filter must restrict the devices in some way.";
|
const FILTER_ERROR: &'static str = "A filter must restrict the devices in some way.";
|
||||||
|
const MANUFACTURER_DATA_ERROR: &'static str = "'manufacturerData', if present, must be non-empty to filter devices.";
|
||||||
|
const MASK_LENGTH_ERROR: &'static str = "`mask`, if present, must have the same length as `dataPrefix`.";
|
||||||
// 248 is the maximum number of UTF-8 code units in a Bluetooth Device Name.
|
// 248 is the maximum number of UTF-8 code units in a Bluetooth Device Name.
|
||||||
const MAX_DEVICE_NAME_LENGTH: usize = 248;
|
const MAX_DEVICE_NAME_LENGTH: usize = 248;
|
||||||
// A device name can never be longer than 29 bytes.
|
// A device name can never be longer than 29 bytes.
|
||||||
|
@ -44,6 +49,7 @@ const MAX_DEVICE_NAME_LENGTH: usize = 248;
|
||||||
const MAX_FILTER_NAME_LENGTH: usize = 29;
|
const MAX_FILTER_NAME_LENGTH: usize = 29;
|
||||||
const NAME_PREFIX_ERROR: &'static str = "'namePrefix', if present, must be nonempty.";
|
const NAME_PREFIX_ERROR: &'static str = "'namePrefix', if present, must be nonempty.";
|
||||||
const NAME_TOO_LONG_ERROR: &'static str = "A device name can't be longer than 248 bytes.";
|
const NAME_TOO_LONG_ERROR: &'static str = "A device name can't be longer than 248 bytes.";
|
||||||
|
const SERVICE_DATA_ERROR: &'static str = "'serviceData', if present, must be non-empty to filter devices.";
|
||||||
const SERVICE_ERROR: &'static str = "'services', if present, must contain at least one service.";
|
const SERVICE_ERROR: &'static str = "'services', if present, must contain at least one service.";
|
||||||
const OPTIONS_ERROR: &'static str = "Fields of 'options' conflict with each other.
|
const OPTIONS_ERROR: &'static str = "Fields of 'options' conflict with each other.
|
||||||
Either 'acceptAllDevices' member must be true, or 'filters' member must be set to a value.";
|
Either 'acceptAllDevices' member must be true, or 'filters' member must be set to a value.";
|
||||||
|
@ -122,7 +128,7 @@ impl Bluetooth {
|
||||||
// https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices
|
// https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices
|
||||||
fn request_bluetooth_devices(&self,
|
fn request_bluetooth_devices(&self,
|
||||||
p: &Rc<Promise>,
|
p: &Rc<Promise>,
|
||||||
filters: &Option<Vec<BluetoothRequestDeviceFilter>>,
|
filters: &Option<Vec<BluetoothLEScanFilterInit>>,
|
||||||
optional_services: &Option<Vec<BluetoothServiceUUID>>) {
|
optional_services: &Option<Vec<BluetoothServiceUUID>>) {
|
||||||
// TODO: Step 1: Triggered by user activation.
|
// TODO: Step 1: Triggered by user activation.
|
||||||
|
|
||||||
|
@ -165,7 +171,7 @@ pub fn response_async<T: AsyncBluetoothListener + Reflectable + 'static>(
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices
|
// https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices
|
||||||
fn convert_request_device_options(filters: &Option<Vec<BluetoothRequestDeviceFilter>>,
|
fn convert_request_device_options(filters: &Option<Vec<BluetoothLEScanFilterInit>>,
|
||||||
optional_services: &Option<Vec<BluetoothServiceUUID>>)
|
optional_services: &Option<Vec<BluetoothServiceUUID>>)
|
||||||
-> Fallible<RequestDeviceoptions> {
|
-> Fallible<RequestDeviceoptions> {
|
||||||
// Step 2.2: There is no requiredServiceUUIDS, we scan for all devices.
|
// Step 2.2: There is no requiredServiceUUIDS, we scan for all devices.
|
||||||
|
@ -206,13 +212,13 @@ fn convert_request_device_options(filters: &Option<Vec<BluetoothRequestDeviceFil
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices
|
// https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices
|
||||||
fn canonicalize_filter(filter: &BluetoothRequestDeviceFilter) -> Fallible<BluetoothScanfilter> {
|
fn canonicalize_filter(filter: &BluetoothLEScanFilterInit) -> Fallible<BluetoothScanfilter> {
|
||||||
// Step 2.4.1.
|
// Step 2.4.1.
|
||||||
if filter.services.is_none() &&
|
if filter.services.is_none() &&
|
||||||
filter.name.is_none() &&
|
filter.name.is_none() &&
|
||||||
filter.namePrefix.is_none() &&
|
filter.namePrefix.is_none() &&
|
||||||
filter.manufacturerId.is_none() &&
|
filter.manufacturerData.is_none() &&
|
||||||
filter.serviceDataUUID.is_none() {
|
filter.serviceData.is_none() {
|
||||||
return Err(Type(FILTER_ERROR.to_owned()));
|
return Err(Type(FILTER_ERROR.to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,31 +291,66 @@ fn canonicalize_filter(filter: &BluetoothRequestDeviceFilter) -> Fallible<Blueto
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Step 2.4.6.
|
// Step 2.4.6 - 2.4.7
|
||||||
let manufacturer_id = filter.manufacturerId;
|
let manufacturer_data = match filter.manufacturerData {
|
||||||
|
Some(ref manufacturer_data_map) => {
|
||||||
// Step 2.4.7.
|
if manufacturer_data_map.is_empty() {
|
||||||
let service_data_uuid = match filter.serviceDataUUID {
|
return Err(Type(MANUFACTURER_DATA_ERROR.to_owned()));
|
||||||
Some(ref service_data_uuid) => {
|
|
||||||
// Step 2.4.7.1 - 2.4.7.2.
|
|
||||||
let uuid = try!(BluetoothUUID::service(service_data_uuid.clone())).to_string();
|
|
||||||
|
|
||||||
// Step 2.4.7.3.
|
|
||||||
if uuid_is_blocklisted(uuid.as_ref(), Blocklist::All) {
|
|
||||||
return Err(Security)
|
|
||||||
}
|
}
|
||||||
|
let mut map = HashMap::new();
|
||||||
// Step 2.4.7.4.
|
for (key, bdfi) in manufacturer_data_map.iter() {
|
||||||
uuid
|
let manufacturer_id = match u16::from_str(key.as_ref()) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(err) => return Err(Type(format!("{} {} {}", KEY_CONVERSION_ERROR, key, err))),
|
||||||
|
};
|
||||||
|
map.insert(manufacturer_id, try!(canonicalize_bluetooth_data_filter_init(bdfi)));
|
||||||
|
}
|
||||||
|
Some(map)
|
||||||
},
|
},
|
||||||
None => String::new(),
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(BluetoothScanfilter::new(name,
|
// Step 2.4.8 -2.4.9
|
||||||
name_prefix,
|
let service_data = match filter.serviceData {
|
||||||
services_vec,
|
Some(ref service_data_map) => {
|
||||||
manufacturer_id,
|
if service_data_map.is_empty() {
|
||||||
service_data_uuid))
|
return Err(Type(SERVICE_DATA_ERROR.to_owned()));
|
||||||
|
}
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for (key, bdfi) in service_data_map.iter() {
|
||||||
|
let service_name = match u32::from_str(key.as_ref()) {
|
||||||
|
Ok(number) => StringOrUnsignedLong::UnsignedLong(number),
|
||||||
|
_ => StringOrUnsignedLong::String(key.clone())
|
||||||
|
};
|
||||||
|
let service = try!(BluetoothUUID::service(service_name)).to_string();
|
||||||
|
if uuid_is_blocklisted(service.as_ref(), Blocklist::All) {
|
||||||
|
return Err(Security);
|
||||||
|
}
|
||||||
|
map.insert(service, try!(canonicalize_bluetooth_data_filter_init(bdfi)));
|
||||||
|
}
|
||||||
|
Some(map)
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 10.
|
||||||
|
Ok(BluetoothScanfilter::new(name, name_prefix, services_vec, manufacturer_data, service_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://webbluetoothcg.github.io/web-bluetooth/#bluetoothdatafilterinit-canonicalizing
|
||||||
|
fn canonicalize_bluetooth_data_filter_init(bdfi: &BluetoothDataFilterInit) -> Fallible<(Vec<u8>, Vec<u8>)> {
|
||||||
|
// Step 1.
|
||||||
|
let data_prefix = bdfi.dataPrefix.clone().unwrap_or(vec![]);
|
||||||
|
// Step 2.
|
||||||
|
// If no mask present, mask will be a sequence of 0xFF bytes the same length as dataPrefix.
|
||||||
|
// Masking dataPrefix with this, leaves dataPrefix untouched.
|
||||||
|
let mask = bdfi.mask.clone().unwrap_or(vec![0xFF; data_prefix.len()]);
|
||||||
|
// Step 3.
|
||||||
|
if mask.len() != data_prefix.len() {
|
||||||
|
return Err(Type(MASK_LENGTH_ERROR.to_owned()));
|
||||||
|
}
|
||||||
|
// Step 4.
|
||||||
|
Ok((data_prefix, mask))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BluetoothError> for Error {
|
impl From<BluetoothError> for Error {
|
||||||
|
|
|
@ -4,16 +4,25 @@
|
||||||
|
|
||||||
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth
|
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth
|
||||||
|
|
||||||
dictionary BluetoothRequestDeviceFilter {
|
dictionary BluetoothDataFilterInit {
|
||||||
|
// BufferSource dataPrefix;
|
||||||
|
sequence<octet> dataPrefix;
|
||||||
|
// BufferSource mask;
|
||||||
|
sequence<octet> mask;
|
||||||
|
};
|
||||||
|
|
||||||
|
dictionary BluetoothLEScanFilterInit {
|
||||||
sequence<BluetoothServiceUUID> services;
|
sequence<BluetoothServiceUUID> services;
|
||||||
DOMString name;
|
DOMString name;
|
||||||
DOMString namePrefix;
|
DOMString namePrefix;
|
||||||
unsigned short manufacturerId;
|
// Maps unsigned shorts to BluetoothDataFilters.
|
||||||
BluetoothServiceUUID serviceDataUUID;
|
MozMap<BluetoothDataFilterInit> manufacturerData;
|
||||||
|
// Maps BluetoothServiceUUIDs to BluetoothDataFilters.
|
||||||
|
MozMap<BluetoothDataFilterInit> serviceData;
|
||||||
};
|
};
|
||||||
|
|
||||||
dictionary RequestDeviceOptions {
|
dictionary RequestDeviceOptions {
|
||||||
sequence<BluetoothRequestDeviceFilter> filters;
|
sequence<BluetoothLEScanFilterInit> filters;
|
||||||
sequence<BluetoothServiceUUID> optionalServices /*= []*/;
|
sequence<BluetoothServiceUUID> optionalServices /*= []*/;
|
||||||
boolean acceptAllDevices = false;
|
boolean acceptAllDevices = false;
|
||||||
};
|
};
|
||||||
|
@ -23,6 +32,12 @@ interface Bluetooth {
|
||||||
// [SecureContext]
|
// [SecureContext]
|
||||||
// readonly attribute BluetoothDevice? referringDevice;
|
// readonly attribute BluetoothDevice? referringDevice;
|
||||||
// [SecureContext]
|
// [SecureContext]
|
||||||
|
// Promise<boolean> getAvailability();
|
||||||
|
// [SecureContext]
|
||||||
|
// attribute EventHandler onavailabilitychanged;
|
||||||
|
// [SecureContext]
|
||||||
|
// readonly attribute BluetoothDevice? referringDevice;
|
||||||
|
[SecureContext]
|
||||||
Promise<BluetoothDevice> requestDevice(optional RequestDeviceOptions options);
|
Promise<BluetoothDevice> requestDevice(optional RequestDeviceOptions options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
14
ports/cef/Cargo.lock
generated
14
ports/cef/Cargo.lock
generated
|
@ -191,12 +191,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blurdroid"
|
name = "blurdroid"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blurmock"
|
name = "blurmock"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -493,10 +493,10 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "device"
|
name = "device"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
source = "git+https://github.com/servo/devices#4a6ab4be0de229fafa6aa3657a5702646832ba08"
|
source = "git+https://github.com/servo/devices#1bb5a200c7ae1f42ddf3c42b235b3db66226aabf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"blurdroid 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"blurdroid 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"blurmock 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"blurmock 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"blurz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"blurz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2947,8 +2947,8 @@ dependencies = [
|
||||||
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
|
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
|
||||||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||||
"checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
"checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
"checksum blurdroid 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5fce4ea3366b583e9d49e1aa3a42252e53b42911bccd06f31c3e81c48ccfc79e"
|
"checksum blurdroid 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4a86fbb3818e7f850410e026bfac7742fe86cbf4acf49f5752936b32d1f7eb8"
|
||||||
"checksum blurmock 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c3034c7372bc7951e0a916b7e952b0043cd4ccb5112cd30827f0e1708e05c2b1"
|
"checksum blurmock 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "68dd72da3a3bb40f3d3bdd366c4cf8e2b1d208c366304f382c80cef8126ca8da"
|
||||||
"checksum blurz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d49796c8d5a1b5f6b2b8686e46ed4ab842987c477f765b69f1d3e8df6072608"
|
"checksum blurz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d49796c8d5a1b5f6b2b8686e46ed4ab842987c477f765b69f1d3e8df6072608"
|
||||||
"checksum brotli 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "bff2d5511b5ba5840f46cc3f9c0c3ab09db20e9b9a4db344ef7df3fb547a627a"
|
"checksum brotli 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "bff2d5511b5ba5840f46cc3f9c0c3ab09db20e9b9a4db344ef7df3fb547a627a"
|
||||||
"checksum browserhtml 0.1.17 (git+https://github.com/browserhtml/browserhtml?branch=crate)" = "<none>"
|
"checksum browserhtml 0.1.17 (git+https://github.com/browserhtml/browserhtml?branch=crate)" = "<none>"
|
||||||
|
|
14
ports/servo/Cargo.lock
generated
14
ports/servo/Cargo.lock
generated
|
@ -190,12 +190,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blurdroid"
|
name = "blurdroid"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blurmock"
|
name = "blurmock"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -522,10 +522,10 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "device"
|
name = "device"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
source = "git+https://github.com/servo/devices#4a6ab4be0de229fafa6aa3657a5702646832ba08"
|
source = "git+https://github.com/servo/devices#1bb5a200c7ae1f42ddf3c42b235b3db66226aabf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"blurdroid 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"blurdroid 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"blurmock 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"blurmock 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"blurz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"blurz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3148,8 +3148,8 @@ dependencies = [
|
||||||
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
|
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
|
||||||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||||
"checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
"checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
"checksum blurdroid 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5fce4ea3366b583e9d49e1aa3a42252e53b42911bccd06f31c3e81c48ccfc79e"
|
"checksum blurdroid 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4a86fbb3818e7f850410e026bfac7742fe86cbf4acf49f5752936b32d1f7eb8"
|
||||||
"checksum blurmock 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c3034c7372bc7951e0a916b7e952b0043cd4ccb5112cd30827f0e1708e05c2b1"
|
"checksum blurmock 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "68dd72da3a3bb40f3d3bdd366c4cf8e2b1d208c366304f382c80cef8126ca8da"
|
||||||
"checksum blurz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d49796c8d5a1b5f6b2b8686e46ed4ab842987c477f765b69f1d3e8df6072608"
|
"checksum blurz 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d49796c8d5a1b5f6b2b8686e46ed4ab842987c477f765b69f1d3e8df6072608"
|
||||||
"checksum brotli 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "bff2d5511b5ba5840f46cc3f9c0c3ab09db20e9b9a4db344ef7df3fb547a627a"
|
"checksum brotli 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "bff2d5511b5ba5840f46cc3f9c0c3ab09db20e9b9a4db344ef7df3fb547a627a"
|
||||||
"checksum browserhtml 0.1.17 (git+https://github.com/browserhtml/browserhtml?branch=crate)" = "<none>"
|
"checksum browserhtml 0.1.17 (git+https://github.com/browserhtml/browserhtml?branch=crate)" = "<none>"
|
||||||
|
|
|
@ -7370,6 +7370,12 @@
|
||||||
"url": "/_mozilla/mozilla/bluetooth/requestDevice/blocklisted-service-in-optionalServices.html"
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/blocklisted-service-in-optionalServices.html"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/canonicalizeFilter/blocklisted-service-data-key.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/canonicalizeFilter/blocklisted-service-data-key.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/canonicalizeFilter/blocklisted-service-data-key.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
"mozilla/bluetooth/requestDevice/canonicalizeFilter/empty-filter.html": [
|
"mozilla/bluetooth/requestDevice/canonicalizeFilter/empty-filter.html": [
|
||||||
{
|
{
|
||||||
"path": "mozilla/bluetooth/requestDevice/canonicalizeFilter/empty-filter.html",
|
"path": "mozilla/bluetooth/requestDevice/canonicalizeFilter/empty-filter.html",
|
||||||
|
@ -7466,6 +7472,24 @@
|
||||||
"url": "/_mozilla/mozilla/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-namePrefix.html"
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/canonicalizeFilter/unicode-valid-length-name-namePrefix.html"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-manufacturer-data-key.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-manufacturer-data-key.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-manufacturer-data-key.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-mask-length.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-mask-length.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-mask-length.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-service-data-key.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-service-data-key.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-service-data-key.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
"mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-optionalServices-member.html": [
|
"mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-optionalServices-member.html": [
|
||||||
{
|
{
|
||||||
"path": "mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-optionalServices-member.html",
|
"path": "mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-optionalServices-member.html",
|
||||||
|
@ -7478,6 +7502,42 @@
|
||||||
"url": "/_mozilla/mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-services-member.html"
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/canonicalizeFilter/wrong-service-in-services-member.html"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/device-found-using-mask.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/device-found-using-mask.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/device-found-using-mask.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/device-found-with-key-and-value.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/device-found-with-key-and-value.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/device-found-with-key-and-value.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/device-found-with-key-only.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/device-found-with-key-only.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/device-found-with-key-only.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/device-found-with-service-and-manufacturer-data.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/device-found-with-service-and-manufacturer-data.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/device-found-with-service-and-manufacturer-data.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/device-not-found-with-extra-data.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/device-not-found-with-extra-data.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/device-not-found-with-extra-data.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mozilla/bluetooth/requestDevice/device-not-found-with-service-and-manufacturer-data.html": [
|
||||||
|
{
|
||||||
|
"path": "mozilla/bluetooth/requestDevice/device-not-found-with-service-and-manufacturer-data.html",
|
||||||
|
"url": "/_mozilla/mozilla/bluetooth/requestDevice/device-not-found-with-service-and-manufacturer-data.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
"mozilla/bluetooth/requestDevice/discovery-succeeds.html": [
|
"mozilla/bluetooth/requestDevice/discovery-succeeds.html": [
|
||||||
{
|
{
|
||||||
"path": "mozilla/bluetooth/requestDevice/discovery-succeeds.html",
|
"path": "mozilla/bluetooth/requestDevice/discovery-succeeds.html",
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
let blocklisted_keys = [{
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'heart_rate': {},
|
||||||
|
'00001812-0000-1000-8000-00805f9b34fb': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'heart_rate': {},
|
||||||
|
'00001530-1212-efde-1523-785feabcd123': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'heart_rate': {},
|
||||||
|
'f000ffc0-0451-4000-b000-000000000000': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'heart_rate': {},
|
||||||
|
'human_interface_device': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'heart_rate': {},
|
||||||
|
0x1812: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
|
||||||
|
promise_test(t => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
let promises = [];
|
||||||
|
blocklisted_keys.forEach(args => {
|
||||||
|
promises.push(promise_rejects(t, 'SecurityError', window.navigator.bluetooth.requestDevice(args)));
|
||||||
|
});
|
||||||
|
return Promise.all(promises);
|
||||||
|
}, 'Rejects with SecurityError, if a serviceData key is blocklisted.');
|
||||||
|
</script>
|
|
@ -0,0 +1,74 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
let wrong_manufacturer_data_keys = [{
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
'A': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
undefined: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
null: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
'': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
'-0': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
'-17': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
65536: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
'65536': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
|
||||||
|
promise_test(t => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
let promises = [];
|
||||||
|
wrong_manufacturer_data_keys.forEach(args => {
|
||||||
|
promises.push(promise_rejects(t, new TypeError(), window.navigator.bluetooth.requestDevice(args)));
|
||||||
|
});
|
||||||
|
return Promise.all(promises);
|
||||||
|
}, 'Rejects with TypeError, if a manufacturerData key is not an unsigned short.');
|
||||||
|
</script>
|
|
@ -0,0 +1,70 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
let length_error_cases = [{
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {
|
||||||
|
dataPrefix: new Uint8Array([0x91, 0xAA, 0x78]),
|
||||||
|
mask: new Uint8Array([0x0F, 0x57])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {
|
||||||
|
dataPrefix: new Uint8Array([0x91]),
|
||||||
|
mask: new Uint8Array([0x0F, 0x57])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {
|
||||||
|
mask: new Uint8Array([0x0F, 0x57])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'heart_rate': {
|
||||||
|
dataPrefix: new Uint8Array([0x91, 0xAA, 0x78]),
|
||||||
|
mask: new Uint8Array([0x0F, 0x57])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'heart_rate': {
|
||||||
|
dataPrefix: new Uint8Array([0x91]),
|
||||||
|
mask: new Uint8Array([0x0F, 0x57])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'heart_rate': {
|
||||||
|
mask: new Uint8Array([0x0F, 0x57])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
|
||||||
|
promise_test(t => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
let promises = [];
|
||||||
|
length_error_cases.forEach(args => {
|
||||||
|
promises.push(promise_rejects(t, new TypeError(), window.navigator.bluetooth.requestDevice(args)));
|
||||||
|
});
|
||||||
|
return Promise.all(promises);
|
||||||
|
}, 'Rejects with TypeError, if mask present, and does not have the same length as dataPrefix.');
|
||||||
|
</script>
|
|
@ -0,0 +1,88 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
let wrong_service_data_keys = [{
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
'wrong_service': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
'-240': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
'': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
'-0': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
null: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
undefined: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
'4294967296': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
4294967296: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
'123456789-0000-1000-8000-00805f9b34fb': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {},
|
||||||
|
'0x180d': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
|
||||||
|
promise_test(t => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
let promises = [];
|
||||||
|
wrong_service_data_keys.forEach(args => {
|
||||||
|
promises.push(promise_rejects(t, 'SyntaxError', window.navigator.bluetooth.requestDevice(args)));
|
||||||
|
});
|
||||||
|
return Promise.all(promises);
|
||||||
|
}, 'Rejects with SyntaxError, if a serviceData key does not convert to a valid uuid.');
|
||||||
|
</script>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
promise_test(() => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
return Promise.all([
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {
|
||||||
|
dataPrefix: new Uint8Array([0x91, 0xAA, 0x03]),
|
||||||
|
mask: new Uint8Array([0x0F, 0x57, 0x0F])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
0x1808: {
|
||||||
|
dataPrefix: new Uint8Array([0x91, 0xAA, 0x03]),
|
||||||
|
mask: new Uint8Array([0x0F, 0x57, 0x0F])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {
|
||||||
|
dataPrefix: new Uint8Array([0x91, 0xAA, 0x03]),
|
||||||
|
mask: new Uint8Array([0x0F, 0x57, 0x0F])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
serviceData: {
|
||||||
|
0x1808: {
|
||||||
|
dataPrefix: new Uint8Array([0x91, 0xAA, 0x03]),
|
||||||
|
mask: new Uint8Array([0x0F, 0x57, 0x0F])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.then(devices => {
|
||||||
|
devices.forEach(device => assert_equals(device.name, mock_device_name.glucose));
|
||||||
|
});
|
||||||
|
}, 'A device can be found by requesting service/manufacturerData with a dataPrefix and an appropriate mask');
|
||||||
|
</script>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
promise_test(() => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
return Promise.all([
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {dataPrefix: new Uint8Array([1, 2, 3])},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {dataPrefix: new Uint8Array([1, 2])},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {dataPrefix: new Uint8Array([1])},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {dataPrefix: new Uint8Array([1, 2, 3])},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'00001808-0000-1000-8000-00805f9b34fb': {dataPrefix: new Uint8Array([1, 2])},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
0x1808: {dataPrefix: new Uint8Array([1])},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {dataPrefix: new Uint8Array([1])},
|
||||||
|
},
|
||||||
|
serviceData: {
|
||||||
|
0x1808: {dataPrefix: new Uint8Array([1])},
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.then(devices => {
|
||||||
|
devices.forEach(device => assert_equals(device.name, mock_device_name.glucose));
|
||||||
|
});
|
||||||
|
}, 'A device can be found by requesting with a service/manufacturerData.dataPrefix, which contains the starting or all bytes of device\'s advertised service/manufacturer specific data bytes.');
|
||||||
|
</script>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
promise_test(() => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
return Promise.all([
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
'17': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'00001808-0000-1000-8000-00805f9b34fb': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
0x1808: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.then(devices => {
|
||||||
|
devices.forEach(device => assert_equals(device.name, mock_device_name.glucose));
|
||||||
|
});
|
||||||
|
}, 'Only using the correct service/manufacturerData key, a device can be found.');
|
||||||
|
</script>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
promise_test(() => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
return window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {dataPrefix: new Uint8Array([1, 2, 3])},
|
||||||
|
},
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {dataPrefix: new Uint8Array([1, 2, 3])}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.then(device => assert_equals(device.name, mock_device_name.glucose));
|
||||||
|
}, 'Requesting with manufacturerData and serviceData in the same filter, will find the device which advertises both.');
|
||||||
|
</script>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
let extra_data_filters = [{
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
18: {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
serviceData: {
|
||||||
|
'battery_service': {},
|
||||||
|
'glucose': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
18: {}
|
||||||
|
},
|
||||||
|
serviceData: {
|
||||||
|
'battery_service': {},
|
||||||
|
'glucose': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {}
|
||||||
|
},
|
||||||
|
serviceData: {
|
||||||
|
'battery_service': {},
|
||||||
|
'glucose': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {},
|
||||||
|
18: {}
|
||||||
|
},
|
||||||
|
serviceData: {
|
||||||
|
'glucose': {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
|
||||||
|
promise_test(t => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
let promises = [];
|
||||||
|
extra_data_filters.forEach(args => {
|
||||||
|
promises.push(promise_rejects(t, 'NotFoundError', window.navigator.bluetooth.requestDevice(args)));
|
||||||
|
});
|
||||||
|
return Promise.all(promises);
|
||||||
|
}, 'Requesting with an extra service/manufacturerData, which is not advertised, rejects with NotFoundError.');
|
||||||
|
</script>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/_mozilla/mozilla/bluetooth/bluetooth-helpers.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
promise_test(t => {
|
||||||
|
window.testRunner.setBluetoothMockDataSet(adapter_type.glucose_heart_rate);
|
||||||
|
return promise_rejects(
|
||||||
|
t, 'NotFoundError',
|
||||||
|
window.navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{
|
||||||
|
manufacturerData: {
|
||||||
|
17: {dataPrefix: new Uint8Array([1, 2, 3])},
|
||||||
|
},
|
||||||
|
serviceData: {
|
||||||
|
'heart_rate': {dataPrefix: new Uint8Array([1, 2, 3])}
|
||||||
|
}
|
||||||
|
}]}));
|
||||||
|
}, 'Requesting with manufacturerData and serviceData in the same filter, will not find a device which does not advertise both.');
|
||||||
|
</script>
|
|
@ -11,6 +11,8 @@ let matching_namePrefix = 'Heart';
|
||||||
let non_matching_services = [battery_service.name];
|
let non_matching_services = [battery_service.name];
|
||||||
let non_matching_name = 'Some Device';
|
let non_matching_name = 'Some Device';
|
||||||
let non_matching_namePrefix = 'Some';
|
let non_matching_namePrefix = 'Some';
|
||||||
|
let non_matching_manufacturer_data = {18: {}};
|
||||||
|
let non_matching_service_data = {'battery_service': {}};
|
||||||
|
|
||||||
let test_specs = [{
|
let test_specs = [{
|
||||||
filters: [{
|
filters: [{
|
||||||
|
@ -81,6 +83,16 @@ let test_specs = [{
|
||||||
filters: [{
|
filters: [{
|
||||||
namePrefix: non_matching_namePrefix
|
namePrefix: non_matching_namePrefix
|
||||||
}]
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
name: matching_name,
|
||||||
|
manufacturerData: non_matching_manufacturer_data
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
filters: [{
|
||||||
|
namePrefix: matching_namePrefix,
|
||||||
|
serviceData: non_matching_service_data
|
||||||
|
}]
|
||||||
}];
|
}];
|
||||||
|
|
||||||
promise_test(t => {
|
promise_test(t => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue