| 'use strict'; |
| |
| /** |
| * Test Setup Helpers |
| */ |
| |
| /** |
| * Loads a script by creating a <script> element pointing to |path|. |
| * @param {string} path The path of the script to load. |
| * @returns {Promise<void>} Resolves when the script has finished loading. |
| */ |
| 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; |
| } |
| |
| /** |
| * Performs the Chromium specific setup necessary to run the tests in the |
| * Chromium browser. This test file is shared between Web Platform Tests and |
| * Blink Web Tests, so this method figures out the correct paths to use for |
| * loading scripts. |
| * |
| * TODO(https://crbug.com/569709): Update this description when all Web |
| * Bluetooth Blink Web Tests have been migrated into this repository. |
| * @returns {Promise<void>} Resolves when Chromium specific setup is complete. |
| */ |
| async function performChromiumSetup() { |
| // Determine path prefixes. |
| let resPrefix = '/resources'; |
| const chromiumResources = ['/resources/chromium/web-bluetooth-test.js']; |
| const pathname = window.location.pathname; |
| if (pathname.includes('/wpt_internal/')) { |
| chromiumResources.push( |
| '/wpt_internal/bluetooth/resources/bluetooth-fake-adapter.js'); |
| } |
| |
| await loadScript(`${resPrefix}/test-only-api.js`); |
| if (!isChromiumBased) { |
| return; |
| } |
| |
| for (const path of chromiumResources) { |
| await loadScript(path); |
| } |
| |
| await initializeChromiumResources(); |
| |
| // 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(https://crbug.com/569709): Remove once setBluetoothFakeAdapter is no |
| // longer used. |
| if (typeof setBluetoothFakeAdapter !== '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 |
| * @param {function{*}: Promise<*>} test_function The Web Bluetooth test to run. |
| * @param {string} name The name or description of the test. |
| * @param {object} properties An object containing extra options for the test. |
| * @returns {Promise<void>} Resolves if Web Bluetooth test ran successfully, or |
| * rejects if the test failed. |
| */ |
| function bluetooth_test(test_function, name, properties) { |
| return promise_test(async (t) => { |
| assert_implements(navigator.bluetooth, 'missing navigator.bluetooth'); |
| // Trigger Chromium-specific setup. |
| await performChromiumSetup(); |
| assert_implements(navigator.bluetooth.test, 'missing navigator.bluetooth.test'); |
| await test_function(t); |
| let consumed = await navigator.bluetooth.test.allResponsesConsumed(); |
| assert_true(consumed); |
| }, name, properties); |
| } |
| |
| /** |
| * Test Helpers |
| */ |
| |
| /** |
| * Waits until the document has finished loading. |
| * @returns {Promise<void>} Resolves if the document is already completely |
| * loaded or when the 'onload' event is fired. |
| */ |
| function waitForDocumentReady() { |
| return new Promise(resolve => { |
| if (document.readyState === 'complete') { |
| resolve(); |
| } |
| |
| window.addEventListener('load', () => { |
| resolve(); |
| }, {once: true}); |
| }); |
| } |
| |
| /** |
| * Simulates a user activation prior to running |callback|. |
| * @param {Function} callback The function to run after the user activation. |
| * @returns {Promise<*>} Resolves when the user activation has been simulated |
| * with the result of |callback|. |
| */ |
| async function callWithTrustedClick(callback) { |
| await waitForDocumentReady(); |
| return 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'. |
| * @returns {Promise<BluetoothDevice>} Resolves with a Bluetooth device if |
| * successful or rejects with an error. |
| */ |
| function requestDeviceWithTrustedClick() { |
| let args = arguments; |
| return callWithTrustedClick( |
| () => navigator.bluetooth.requestDevice.apply(navigator.bluetooth, args)); |
| } |
| |
| /** |
| * Calls requestLEScan() in a context that's 'allowed to show a popup'. |
| * @returns {Promise<BluetoothLEScan>} Resolves with the properties of the scan |
| * if successful or rejects with an error. |
| */ |
| function requestLEScanWithTrustedClick() { |
| let args = arguments; |
| return callWithTrustedClick( |
| () => navigator.bluetooth.requestLEScan.apply(navigator.bluetooth, args)); |
| } |
| |
| /** |
| * Function to test that a promise rejects with the expected error type and |
| * message. |
| * @param {Promise} promise |
| * @param {object} expected |
| * @param {string} description |
| * @returns {Promise<void>} Resolves if |promise| rejected with |expected| |
| * error. |
| */ |
| 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:'); |
| } |
| }); |
| } |
| |
| /** |
| * Runs the garbage collection. |
| * @returns {Promise<void>} Resolves when garbage collection has finished. |
| */ |
| function runGarbageCollection() { |
| // Run gc() as a promise. |
| return new Promise(function(resolve, reject) { |
| GCController.collect(); |
| step_timeout(resolve, 0); |
| }); |
| } |
| |
| /** |
| * Helper class that can be created to check that an event has fired. |
| */ |
| class EventCatcher { |
| /** |
| * @param {EventTarget} object The object to listen for events on. |
| * @param {string} event The type of event to listen for. |
| */ |
| constructor(object, event) { |
| /** @type {boolean} */ |
| this.eventFired = false; |
| |
| /** @type {function()} */ |
| let event_listener = () => { |
| object.removeEventListener(event, event_listener); |
| this.eventFired = true; |
| }; |
| object.addEventListener(event, event_listener); |
| } |
| } |
| |
| /** |
| * Notifies when the event |type| has fired. |
| * @param {EventTarget} target The object to listen for the event. |
| * @param {string} type The type of event to listen for. |
| * @param {object} options Characteristics about the event listener. |
| * @returns {Promise<Event>} Resolves when an event of |type| has fired. |
| */ |
| function eventPromise(target, type, options) { |
| return new Promise(resolve => { |
| let wrapper = function(event) { |
| target.removeEventListener(type, wrapper); |
| resolve(event); |
| }; |
| target.addEventListener(type, wrapper, options); |
| }); |
| } |
| |
| /** |
| * The action that should occur first in assert_promise_event_order_(). |
| * @enum {string} |
| */ |
| const ShouldBeFirst = { |
| EVENT: 'event', |
| PROMISE_RESOLUTION: 'promiseresolved', |
| }; |
| |
| /** |
| * 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. |
| * @param {ShouldBeFirst} should_be_first Indicates whether |func| should |
| * resolve before |event| is fired. |
| * @param {EventTarget} object The target object to add event listeners to. |
| * @param {function(*): Promise<*>} func The function to test the resolution |
| * order for. |
| * @param {string} event The event type to listen for. |
| * @param {number} num_listeners The number of events to listen for. |
| * @returns {Promise<*>} The return value of |func|. |
| */ |
| 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(ShouldBeFirst.EVENT); |
| resolve(e.target.value); |
| }; |
| object.addEventListener(event, event_listener); |
| })); |
| } |
| |
| let func_promise = object[func]().then(result => { |
| order.push(ShouldBeFirst.PROMISE_RESOLUTION); |
| return result; |
| }); |
| |
| return Promise.all([func_promise, ...event_promises]).then((result) => { |
| if (should_be_first !== order[0]) { |
| throw should_be_first === ShouldBeFirst.PROMISE_RESOLUTION ? |
| `'${event}' was fired before promise resolved.` : |
| `Promise resolved before '${event}' was fired.`; |
| } |
| |
| if (order[0] !== ShouldBeFirst.PROMISE_RESOLUTION && |
| order[order.length - 1] !== ShouldBeFirst.PROMISE_RESOLUTION) { |
| throw 'Promise resolved in between event listeners.'; |
| } |
| |
| return result; |
| }); |
| } |
| |
| /** |
| * Asserts that the promise returned by |func| resolves before events of type |
| * |event| are fired |num_listeners| times on |object|. See |
| * assert_promise_event_order_ above for more details. |
| * @param {EventTarget} object The target object to add event listeners to. |
| * @param {function(*): Promise<*>} func The function whose promise should |
| * resolve first. |
| * @param {string} event The event type to listen for. |
| * @param {number} num_listeners The number of events to listen for. |
| * @returns {Promise<*>} The return value of |func|. |
| */ |
| function assert_promise_resolves_before_event( |
| object, func, event, num_listeners = 1) { |
| return assert_promise_event_order_( |
| ShouldBeFirst.PROMISE_RESOLUTION, object, func, event, num_listeners); |
| } |
| |
| /** |
| * Asserts that the promise returned by |func| resolves after events of type |
| * |event| are fired |num_listeners| times on |object|. See |
| * assert_promise_event_order_ above for more details. |
| * @param {EventTarget} object The target object to add event listeners to. |
| * @param {function(*): Promise<*>} func The function whose promise should |
| * resolve first. |
| * @param {string} event The event type to listen for. |
| * @param {number} num_listeners The number of events to listen for. |
| * @returns {Promise<*>} The return value of |func|. |
| */ |
| function assert_promise_resolves_after_event( |
| object, func, event, num_listeners = 1) { |
| return assert_promise_event_order_( |
| ShouldBeFirst.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. |
| * @param {EventTarget} object The target object to listen for events. |
| * @param {string} event_name The event type to listen for. |
| * @returns {Promise<void>} Resolves if no events were fired. |
| */ |
| function assert_no_events(object, event_name) { |
| return new Promise((resolve) => { |
| 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); |
| }); |
| } |
| |
| /** |
| * Asserts that |properties| contains the same properties in |
| * |expected_properties| with equivalent values. |
| * @param {object} properties Actual object to compare. |
| * @param {object} expected_properties Expected object to compare with. |
| */ |
| function assert_properties_equal(properties, expected_properties) { |
| for (let key in expected_properties) { |
| assert_equals(properties[key], expected_properties[key]); |
| } |
| } |
| |
| /** |
| * Asserts that |data_map| contains |expected_key|, and that the uint8 values |
| * for |expected_key| matches |expected_value|. |
| */ |
| function assert_data_maps_equal(data_map, expected_key, expected_value) { |
| assert_true(data_map.has(expected_key)); |
| |
| const value = new Uint8Array(data_map.get(expected_key).buffer); |
| assert_equals(value.length, expected_value.length); |
| for (let i = 0; i < value.length; ++i) { |
| assert_equals(value[i], expected_value[i]); |
| } |
| } |