summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/bluetooth/resources/bluetooth-test.js
blob: 7ad1b937e1f5d1ca4a1e20da410e969ba229227e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
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]);
  }
}