summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/bluetooth/resources/bluetooth-test.js')
-rw-r--r--testing/web-platform/tests/bluetooth/resources/bluetooth-test.js363
1 files changed, 363 insertions, 0 deletions
diff --git a/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js b/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js
new file mode 100644
index 0000000000..7ad1b937e1
--- /dev/null
+++ b/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js
@@ -0,0 +1,363 @@
+'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.
+ * @param {Boolean} validate_response_consumed Whether to validate all response
+ * consumed or not.
+ * @returns {Promise<void>} Resolves if Web Bluetooth test ran successfully, or
+ * rejects if the test failed.
+ */
+function bluetooth_test(
+ test_function, name, properties, validate_response_consumed = true) {
+ 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);
+ if (validate_response_consumed) {
+ 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:');
+ }
+ });
+}
+
+/**
+ * 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]);
+ }
+}