bluetooth-test.js (15318B)
1 'use strict'; 2 3 // A flag indicating whether to use Web Bluetooth BiDi commands for Bluetooth 4 // emulation. 5 let useBidi = false; 6 7 /** 8 * Test Setup Helpers 9 */ 10 11 /** 12 * Loads a script by creating a <script> element pointing to |path|. 13 * @param {string} path The path of the script to load. 14 * @returns {Promise<void>} Resolves when the script has finished loading. 15 */ 16 function loadScript(path) { 17 let script = document.createElement('script'); 18 let promise = new Promise(resolve => script.onload = resolve); 19 script.src = path; 20 script.async = false; 21 document.head.appendChild(script); 22 return promise; 23 } 24 25 /** 26 * Performs the Chromium specific setup necessary to run the tests in the 27 * Chromium browser. This test file is shared between Web Platform Tests and 28 * Blink Web Tests, so this method figures out the correct paths to use for 29 * loading scripts. 30 * 31 * TODO(https://crbug.com/569709): Update this description when all Web 32 * Bluetooth Blink Web Tests have been migrated into this repository. 33 * @returns {Promise<void>} Resolves when Chromium specific setup is complete. 34 */ 35 async function performChromiumSetup() { 36 // Determine path prefixes. 37 let resPrefix = '/resources'; 38 const chromiumResources = ['/resources/chromium/web-bluetooth-test.js']; 39 const pathname = window.location.pathname; 40 if (pathname.includes('/wpt_internal/')) { 41 chromiumResources.push( 42 '/wpt_internal/bluetooth/resources/bluetooth-fake-adapter.js'); 43 } 44 45 await loadScript(`${resPrefix}/test-only-api.js`); 46 if (!isChromiumBased) { 47 return; 48 } 49 50 for (const path of chromiumResources) { 51 await loadScript(path); 52 } 53 54 await initializeChromiumResources(); 55 56 // Call setBluetoothFakeAdapter() to clean up any fake adapters left over by 57 // legacy tests. Legacy tests that use setBluetoothFakeAdapter() sometimes 58 // fail to clean their fake adapter. This is not a problem for these tests 59 // because the next setBluetoothFakeAdapter() will clean it up anyway but it 60 // is a problem for the new tests that do not use setBluetoothFakeAdapter(). 61 // TODO(https://crbug.com/569709): Remove once setBluetoothFakeAdapter is no 62 // longer used. 63 if (typeof setBluetoothFakeAdapter !== 'undefined') { 64 setBluetoothFakeAdapter(''); 65 } 66 } 67 68 /** 69 * These tests rely on the User Agent providing an implementation of the Web 70 * Bluetooth Testing API. 71 * https://docs.google.com/document/d/1Nhv_oVDCodd1pEH_jj9k8gF4rPGb_84VYaZ9IG8M_WY/edit?ts=59b6d823#heading=h.7nki9mck5t64 72 * @param {function{*}: Promise<*>} test_function The Web Bluetooth test to run. 73 * @param {string} name The name or description of the test. 74 * @param {object} properties An object containing extra options for the test. 75 * @param {Boolean} validate_response_consumed Whether to validate all response 76 * consumed or not. 77 * @returns {Promise<void>} Resolves if Web Bluetooth test ran successfully, or 78 * rejects if the test failed. 79 */ 80 function bluetooth_test( 81 test_function, name, properties, validate_response_consumed = true) { 82 return promise_test(async (t) => { 83 assert_implements(navigator.bluetooth, 'missing navigator.bluetooth'); 84 // Trigger Chromium-specific setup. 85 await performChromiumSetup(); 86 assert_implements( 87 navigator.bluetooth.test, 'missing navigator.bluetooth.test'); 88 await test_function(t); 89 if (validate_response_consumed) { 90 let consumed = await navigator.bluetooth.test.allResponsesConsumed(); 91 assert_true(consumed); 92 } 93 }, name, properties); 94 } 95 96 /** 97 * These tests rely on the User Agent providing an implementation of the 98 * WebDriver-Bidi for testing Web Bluetooth 99 * https://webbluetoothcg.github.io/web-bluetooth/#automated-testing 100 * @param {function{*}: Promise<*>} test_function The Web Bluetooth test to run. 101 * @param {string} name The name or description of the test. 102 * @param {object} properties An object containing extra options for the test. 103 * @param {Boolean} validate_response_consumed Whether to validate all response 104 * consumed or not. 105 * @returns {Promise<void>} Resolves if Web Bluetooth test ran successfully, or 106 * rejects if the test failed. 107 */ 108 function bluetooth_bidi_test( 109 test_function, name, properties, validate_response_consumed = true) { 110 return promise_test(async (t) => { 111 assert_implements(navigator.bluetooth, 'missing navigator.bluetooth'); 112 113 // Necessary setup for Bluetooth emulation using WebDriver Bidi commands. 114 useBidi = true; 115 await loadScript('/resources/web-bluetooth-bidi-test.js'); 116 await initializeBluetoothBidiResources(); 117 assert_implements( 118 navigator.bluetooth.test, 'missing navigator.bluetooth.test'); 119 await test_driver.bidi.bluetooth.request_device_prompt_updated.subscribe(); 120 await test_driver.bidi.bluetooth.gatt_connection_attempted.subscribe(); 121 await test_driver.bidi.bluetooth.characteristic_event_generated.subscribe(); 122 await test_driver.bidi.bluetooth.descriptor_event_generated.subscribe(); 123 try { 124 await test_function(t); 125 } finally { 126 await test_driver.bidi.bluetooth.disable_simulation(); 127 } 128 }, name, properties); 129 } 130 131 /** 132 * Test Helpers 133 */ 134 135 /** 136 * Waits until the document has finished loading. 137 * @returns {Promise<void>} Resolves if the document is already completely 138 * loaded or when the 'onload' event is fired. 139 */ 140 function waitForDocumentReady() { 141 return new Promise(resolve => { 142 if (document.readyState === 'complete') { 143 resolve(); 144 } 145 146 window.addEventListener('load', () => { 147 resolve(); 148 }, {once: true}); 149 }); 150 } 151 152 /** 153 * Simulates a user activation prior to running |callback|. 154 * @param {Function} callback The function to run after the user activation. 155 * @returns {Promise<*>} Resolves when the user activation has been simulated 156 * with the result of |callback|. 157 */ 158 async function callWithTrustedClick(callback) { 159 await waitForDocumentReady(); 160 return new Promise(resolve => { 161 let button = document.createElement('button'); 162 button.textContent = 'click to continue test'; 163 button.style.display = 'block'; 164 button.style.fontSize = '20px'; 165 button.style.padding = '10px'; 166 button.onclick = () => { 167 document.body.removeChild(button); 168 resolve(callback()); 169 }; 170 document.body.appendChild(button); 171 test_driver.click(button); 172 }); 173 } 174 175 /** 176 * Registers a one-time handler that selects the first device in the device 177 * prompt upon a device prompt updated event. 178 * @returns {Promise<void>} Fulfilled after the Bluetooth device prompt 179 * is handled, or rejected if the operation fails. 180 */ 181 function selectFirstDeviceOnDevicePromptUpdated() { 182 if (!useBidi) { 183 // Return a resolved promise when there is no bidi support. 184 return Promise.resolve(); 185 } 186 test_driver.bidi.bluetooth.request_device_prompt_updated.once().then( 187 (promptEvent) => { 188 assert_greater_than(promptEvent.devices.length, 0); 189 return test_driver.bidi.bluetooth.handle_request_device_prompt({ 190 prompt: promptEvent.prompt, 191 accept: true, 192 device: promptEvent.devices[0].id 193 }); 194 }); 195 } 196 197 /** 198 * Calls requestDevice() in a context that's 'allowed to show a popup'. 199 * @returns {Promise<BluetoothDevice>} Resolves with a Bluetooth device if 200 * successful or rejects with an error. 201 */ 202 function requestDeviceWithTrustedClick() { 203 selectFirstDeviceOnDevicePromptUpdated(); 204 let args = arguments; 205 return callWithTrustedClick( 206 () => navigator.bluetooth.requestDevice.apply(navigator.bluetooth, args)); 207 } 208 209 /** 210 * Calls requestLEScan() in a context that's 'allowed to show a popup'. 211 * @returns {Promise<BluetoothLEScan>} Resolves with the properties of the scan 212 * if successful or rejects with an error. 213 */ 214 function requestLEScanWithTrustedClick() { 215 let args = arguments; 216 return callWithTrustedClick( 217 () => navigator.bluetooth.requestLEScan.apply(navigator.bluetooth, args)); 218 } 219 220 /** 221 * Function to test that a promise rejects with the expected error type and 222 * message. 223 * @param {Promise} promise 224 * @param {object} expected 225 * @param {string} description 226 * @returns {Promise<void>} Resolves if |promise| rejected with |expected| 227 * error. 228 */ 229 function assert_promise_rejects_with_message(promise, expected, description) { 230 return promise.then( 231 () => { 232 assert_unreached('Promise should have rejected: ' + description); 233 }, 234 error => { 235 assert_equals(error.name, expected.name, 'Unexpected Error Name:'); 236 if (expected.message) { 237 assert_true( 238 error.message.includes(expected.message), 239 'Unexpected Error Message:'); 240 } 241 }); 242 } 243 244 /** 245 * Helper class that can be created to check that an event has fired. 246 */ 247 class EventCatcher { 248 /** 249 * @param {EventTarget} object The object to listen for events on. 250 * @param {string} event The type of event to listen for. 251 */ 252 constructor(object, event) { 253 /** @type {boolean} */ 254 this.eventFired = false; 255 256 /** @type {function()} */ 257 let event_listener = () => { 258 object.removeEventListener(event, event_listener); 259 this.eventFired = true; 260 }; 261 object.addEventListener(event, event_listener); 262 } 263 } 264 265 /** 266 * Notifies when the event |type| has fired. 267 * @param {EventTarget} target The object to listen for the event. 268 * @param {string} type The type of event to listen for. 269 * @param {object} options Characteristics about the event listener. 270 * @returns {Promise<Event>} Resolves when an event of |type| has fired. 271 */ 272 function eventPromise(target, type, options) { 273 return new Promise(resolve => { 274 let wrapper = function(event) { 275 target.removeEventListener(type, wrapper); 276 resolve(event); 277 }; 278 target.addEventListener(type, wrapper, options); 279 }); 280 } 281 282 /** 283 * The action that should occur first in assert_promise_event_order_(). 284 * @enum {string} 285 */ 286 const ShouldBeFirst = { 287 EVENT: 'event', 288 PROMISE_RESOLUTION: 'promiseresolved', 289 }; 290 291 /** 292 * Helper function to assert that events are fired and a promise resolved 293 * in the correct order. 294 * 'event' should be passed as |should_be_first| to indicate that the events 295 * should be fired first, otherwise 'promiseresolved' should be passed. 296 * Attaches |num_listeners| |event| listeners to |object|. If all events have 297 * been fired and the promise resolved in the correct order, returns a promise 298 * that fulfills with the result of |object|.|func()| and |event.target.value| 299 * of each of event listeners. Otherwise throws an error. 300 * @param {ShouldBeFirst} should_be_first Indicates whether |func| should 301 * resolve before |event| is fired. 302 * @param {EventTarget} object The target object to add event listeners to. 303 * @param {function(*): Promise<*>} func The function to test the resolution 304 * order for. 305 * @param {string} event The event type to listen for. 306 * @param {number} num_listeners The number of events to listen for. 307 * @returns {Promise<*>} The return value of |func|. 308 */ 309 function assert_promise_event_order_( 310 should_be_first, object, func, event, num_listeners) { 311 let order = []; 312 let event_promises = []; 313 for (let i = 0; i < num_listeners; i++) { 314 event_promises.push(new Promise(resolve => { 315 let event_listener = (e) => { 316 object.removeEventListener(event, event_listener); 317 order.push(ShouldBeFirst.EVENT); 318 resolve(e.target.value); 319 }; 320 object.addEventListener(event, event_listener); 321 })); 322 } 323 324 let func_promise = object[func]().then(result => { 325 order.push(ShouldBeFirst.PROMISE_RESOLUTION); 326 return result; 327 }); 328 329 return Promise.all([func_promise, ...event_promises]).then((result) => { 330 if (should_be_first !== order[0]) { 331 throw should_be_first === ShouldBeFirst.PROMISE_RESOLUTION ? 332 `'${event}' was fired before promise resolved.` : 333 `Promise resolved before '${event}' was fired.`; 334 } 335 336 if (order[0] !== ShouldBeFirst.PROMISE_RESOLUTION && 337 order[order.length - 1] !== ShouldBeFirst.PROMISE_RESOLUTION) { 338 throw 'Promise resolved in between event listeners.'; 339 } 340 341 return result; 342 }); 343 } 344 345 /** 346 * Asserts that the promise returned by |func| resolves before events of type 347 * |event| are fired |num_listeners| times on |object|. See 348 * assert_promise_event_order_ above for more details. 349 * @param {EventTarget} object The target object to add event listeners to. 350 * @param {function(*): Promise<*>} func The function whose promise should 351 * resolve first. 352 * @param {string} event The event type to listen for. 353 * @param {number} num_listeners The number of events to listen for. 354 * @returns {Promise<*>} The return value of |func|. 355 */ 356 function assert_promise_resolves_before_event( 357 object, func, event, num_listeners = 1) { 358 return assert_promise_event_order_( 359 ShouldBeFirst.PROMISE_RESOLUTION, object, func, event, num_listeners); 360 } 361 362 /** 363 * Asserts that the promise returned by |func| resolves after events of type 364 * |event| are fired |num_listeners| times on |object|. See 365 * assert_promise_event_order_ above for more details. 366 * @param {EventTarget} object The target object to add event listeners to. 367 * @param {function(*): Promise<*>} func The function whose promise should 368 * resolve first. 369 * @param {string} event The event type to listen for. 370 * @param {number} num_listeners The number of events to listen for. 371 * @returns {Promise<*>} The return value of |func|. 372 */ 373 function assert_promise_resolves_after_event( 374 object, func, event, num_listeners = 1) { 375 return assert_promise_event_order_( 376 ShouldBeFirst.EVENT, object, func, event, num_listeners); 377 } 378 379 /** 380 * Returns a promise that resolves after 100ms unless the event is fired on 381 * the object in which case the promise rejects. 382 * @param {EventTarget} object The target object to listen for events. 383 * @param {string} event_name The event type to listen for. 384 * @returns {Promise<void>} Resolves if no events were fired. 385 */ 386 function assert_no_events(object, event_name) { 387 return new Promise((resolve) => { 388 let event_listener = (e) => { 389 object.removeEventListener(event_name, event_listener); 390 assert_unreached('Object should not fire an event.'); 391 }; 392 object.addEventListener(event_name, event_listener); 393 // TODO: Remove timeout. 394 // http://crbug.com/543884 395 step_timeout(() => { 396 object.removeEventListener(event_name, event_listener); 397 resolve(); 398 }, 100); 399 }); 400 } 401 402 /** 403 * Asserts that |properties| contains the same properties in 404 * |expected_properties| with equivalent values. 405 * @param {object} properties Actual object to compare. 406 * @param {object} expected_properties Expected object to compare with. 407 */ 408 function assert_properties_equal(properties, expected_properties) { 409 for (let key in expected_properties) { 410 assert_equals(properties[key], expected_properties[key]); 411 } 412 } 413 414 /** 415 * Asserts that |data_map| contains |expected_key|, and that the uint8 values 416 * for |expected_key| matches |expected_value|. 417 */ 418 function assert_data_maps_equal(data_map, expected_key, expected_value) { 419 assert_true(data_map.has(expected_key)); 420 421 const value = new Uint8Array(data_map.get(expected_key).buffer); 422 assert_equals(value.length, expected_value.length); 423 for (let i = 0; i < value.length; ++i) { 424 assert_equals(value[i], expected_value[i]); 425 } 426 }