From f042657c735c94efdd537f1fb74415e70ee39a6a Mon Sep 17 00:00:00 2001 From: Chengwei Hsieh Date: Fri, 21 Feb 2025 15:07:32 -0800 Subject: [PATCH] bluetooth: Migrate some WPT tests to use bidi commands This CL migrates some Web Bluetooth WPT tests to use WebDriver Bidi bluetooth.simulatePreconnectedPeripheral command. It also creates a new file web-bluetooth-bidi-test.js, which is a counterpart of web-bluetooth-test.js. The following CLs will incrementally implement other necessary functionalities in the file for fully migrating Web Bluetooth WPT tests to run in headless shell. Bug: 41484719 Change-Id: Ia19e8f4b6f637a913f48ae8d3445299ac336d919 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6203507 Commit-Queue: Jack Hsieh Reviewed-by: Matt Reynolds Cr-Commit-Position: refs/heads/main@{#1423424} --- ...ter-absent-getAvailability.https.window.js | 2 +- ...owered-off-getAvailability.https.window.js | 2 +- ...powered-on-getAvailability.https.window.js | 2 +- ...iframe-getAvailability.sub.https.window.js | 2 +- .../device-with-empty-name.https.window.js | 14 +-- .../device-with-name.https.window.js | 14 +-- .../radio-not-present.https.window.js | 4 +- bluetooth/resources/bluetooth-fake-devices.js | 7 +- bluetooth/resources/bluetooth-test.js | 35 +++++++ resources/web-bluetooth-bidi-test.js | 91 +++++++++++++++++++ 10 files changed, 147 insertions(+), 26 deletions(-) rename bluetooth/{legacy => bidi}/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js (60%) rename bluetooth/{legacy => bidi}/requestDevice/acceptAllDevices/device-with-name.https.window.js (60%) rename bluetooth/{legacy => bidi}/requestDevice/radio-not-present.https.window.js (90%) create mode 100644 resources/web-bluetooth-bidi-test.js diff --git a/bluetooth/bidi/adapter/adapter-absent-getAvailability.https.window.js b/bluetooth/bidi/adapter/adapter-absent-getAvailability.https.window.js index 419a9d411dfed5..ba58cc83b6ae88 100644 --- a/bluetooth/bidi/adapter/adapter-absent-getAvailability.https.window.js +++ b/bluetooth/bidi/adapter/adapter-absent-getAvailability.https.window.js @@ -7,7 +7,7 @@ const test_desc = 'getAvailability() resolves with false if the system does ' + 'not have an adapter.'; bluetooth_bidi_test(async () => { - await test_driver.bidi.bluetooth.simulate_adapter({state: "absent"}); + await navigator.bluetooth.test.simulateCentral({state: 'absent'}); let availability = await navigator.bluetooth.getAvailability(); assert_false( availability, diff --git a/bluetooth/bidi/adapter/adapter-powered-off-getAvailability.https.window.js b/bluetooth/bidi/adapter/adapter-powered-off-getAvailability.https.window.js index 9bf10801bb72f5..ce99800cbbdbbe 100644 --- a/bluetooth/bidi/adapter/adapter-powered-off-getAvailability.https.window.js +++ b/bluetooth/bidi/adapter/adapter-powered-off-getAvailability.https.window.js @@ -7,7 +7,7 @@ const test_desc = 'getAvailability() resolves with true if the Bluetooth ' + 'radio is powered off, but the platform that supports Bluetooth LE.'; bluetooth_bidi_test(async () => { - await test_driver.bidi.bluetooth.simulate_adapter({state: "powered-off"}); + await navigator.bluetooth.test.simulateCentral({state: 'powered-off'}); let availability = await navigator.bluetooth.getAvailability(); assert_true( availability, diff --git a/bluetooth/bidi/adapter/adapter-powered-on-getAvailability.https.window.js b/bluetooth/bidi/adapter/adapter-powered-on-getAvailability.https.window.js index 6ffba254d8a80c..fd91c9fa04b01a 100644 --- a/bluetooth/bidi/adapter/adapter-powered-on-getAvailability.https.window.js +++ b/bluetooth/bidi/adapter/adapter-powered-on-getAvailability.https.window.js @@ -7,7 +7,7 @@ const test_desc = 'getAvailability() resolves with true if the Bluetooth ' + 'radio is powered on and the platform supports Bluetooth LE.'; bluetooth_bidi_test(async () => { - await test_driver.bidi.bluetooth.simulate_adapter({state: "powered-on"}); + await navigator.bluetooth.test.simulateCentral({state: 'powered-on'}); let availability = await navigator.bluetooth.getAvailability(); assert_true( availability, diff --git a/bluetooth/bidi/adapter/cross-origin-iframe-getAvailability.sub.https.window.js b/bluetooth/bidi/adapter/cross-origin-iframe-getAvailability.sub.https.window.js index fd638b55561d38..319aca32b1f76d 100644 --- a/bluetooth/bidi/adapter/cross-origin-iframe-getAvailability.sub.https.window.js +++ b/bluetooth/bidi/adapter/cross-origin-iframe-getAvailability.sub.https.window.js @@ -10,7 +10,7 @@ const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' + let iframe = document.createElement('iframe'); bluetooth_bidi_test(async () => { - await test_driver.bidi.bluetooth.simulate_adapter({state: "powered-on"}); + await navigator.bluetooth.test.simulateCentral({state: 'powered-on'}); await new Promise(resolve => { iframe.src = cross_origin_src; document.body.appendChild(iframe); diff --git a/bluetooth/legacy/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js b/bluetooth/bidi/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js similarity index 60% rename from bluetooth/legacy/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js rename to bluetooth/bidi/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js index 15bde6a9336c9e..0fa52cd553babc 100644 --- a/bluetooth/legacy/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js +++ b/bluetooth/bidi/requestDevice/acceptAllDevices/device-with-empty-name.https.window.js @@ -1,4 +1,4 @@ -// META: script=/resources/testdriver.js +// META: script=/resources/testdriver.js?feature=bidi // META: script=/resources/testdriver-vendor.js // META: script=/bluetooth/resources/bluetooth-test.js // META: script=/bluetooth/resources/bluetooth-fake-devices.js @@ -6,14 +6,10 @@ const test_desc = 'Device with empty name and no UUIDs nearby. Should be ' + 'found if acceptAllDevices is true.'; -bluetooth_test(async () => { - let { device } = await setUpPreconnectedFakeDevice({ - fakeDeviceOptions: { - name: '' - }, - requestDeviceOptions: { - acceptAllDevices: true - } +bluetooth_bidi_test(async () => { + let {device} = await setUpPreconnectedFakeDevice({ + fakeDeviceOptions: {name: ''}, + requestDeviceOptions: {acceptAllDevices: true} }); assert_equals(device.name, ''); }, test_desc); diff --git a/bluetooth/legacy/requestDevice/acceptAllDevices/device-with-name.https.window.js b/bluetooth/bidi/requestDevice/acceptAllDevices/device-with-name.https.window.js similarity index 60% rename from bluetooth/legacy/requestDevice/acceptAllDevices/device-with-name.https.window.js rename to bluetooth/bidi/requestDevice/acceptAllDevices/device-with-name.https.window.js index f3373a6bb64f51..537d01f4f388ec 100644 --- a/bluetooth/legacy/requestDevice/acceptAllDevices/device-with-name.https.window.js +++ b/bluetooth/bidi/requestDevice/acceptAllDevices/device-with-name.https.window.js @@ -1,4 +1,4 @@ -// META: script=/resources/testdriver.js +// META: script=/resources/testdriver.js?feature=bidi // META: script=/resources/testdriver-vendor.js // META: script=/bluetooth/resources/bluetooth-test.js // META: script=/bluetooth/resources/bluetooth-fake-devices.js @@ -7,14 +7,10 @@ const test_desc = 'acceptAllDevices is true.'; const name = 'LE Device'; -bluetooth_test(async () => { - let { device } = await setUpPreconnectedFakeDevice({ - fakeDeviceOptions: { - name: name - }, - requestDeviceOptions: { - acceptAllDevices: true - } +bluetooth_bidi_test(async () => { + let {device} = await setUpPreconnectedFakeDevice({ + fakeDeviceOptions: {name: name}, + requestDeviceOptions: {acceptAllDevices: true} }); assert_equals(device.name, name); }, test_desc); diff --git a/bluetooth/legacy/requestDevice/radio-not-present.https.window.js b/bluetooth/bidi/requestDevice/radio-not-present.https.window.js similarity index 90% rename from bluetooth/legacy/requestDevice/radio-not-present.https.window.js rename to bluetooth/bidi/requestDevice/radio-not-present.https.window.js index b55d63c6ff45fa..370ededf222a59 100644 --- a/bluetooth/legacy/requestDevice/radio-not-present.https.window.js +++ b/bluetooth/bidi/requestDevice/radio-not-present.https.window.js @@ -1,4 +1,4 @@ -// META: script=/resources/testdriver.js +// META: script=/resources/testdriver.js?feature=bidi // META: script=/resources/testdriver-vendor.js // META: script=/bluetooth/resources/bluetooth-test.js // META: script=/bluetooth/resources/bluetooth-fake-devices.js @@ -7,7 +7,7 @@ const test_desc = 'Reject with NotFoundError if there is no BT radio present.'; const expected = new DOMException('Bluetooth adapter not available.', 'NotFoundError'); -bluetooth_test( +bluetooth_bidi_test( () => navigator.bluetooth.test.simulateCentral({state: 'absent'}) .then( () => assert_promise_rejects_with_message( diff --git a/bluetooth/resources/bluetooth-fake-devices.js b/bluetooth/resources/bluetooth-fake-devices.js index c503bf790c4b4a..1c8e24f19d1bc9 100644 --- a/bluetooth/resources/bluetooth-fake-devices.js +++ b/bluetooth/resources/bluetooth-fake-devices.js @@ -398,8 +398,11 @@ async function setUpPreconnectedFakeDevice(setupOptionsOverride) { // Request the device if the request option isn't empty. if (Object.keys(setupOptions.requestDeviceOptions).length !== 0) { - preconnectedDevice.device = - await requestDeviceWithTrustedClick(setupOptions.requestDeviceOptions); + const prompt_promise = selectFirstDeviceOnDevicePromptUpdated(); + [preconnectedDevice.device] = await Promise.all([ + requestDeviceWithTrustedClick(setupOptions.requestDeviceOptions), + prompt_promise + ]); } // Set up services discovered state. diff --git a/bluetooth/resources/bluetooth-test.js b/bluetooth/resources/bluetooth-test.js index a771abbd51b625..bd597efd01b601 100644 --- a/bluetooth/resources/bluetooth-test.js +++ b/bluetooth/resources/bluetooth-test.js @@ -1,5 +1,9 @@ 'use strict'; +// A flag indicating whether to use Web Bluetooth BiDi commands for Bluetooth +// emulation. +let useBidi = false; + /** * Test Setup Helpers */ @@ -105,6 +109,15 @@ function bluetooth_bidi_test( test_function, name, properties, validate_response_consumed = true) { return promise_test(async (t) => { assert_implements(navigator.bluetooth, 'missing navigator.bluetooth'); + + // Necessary setup for Bluetooth emulation using WebDriver Bidi commands. + useBidi = true; + await loadScript('/resources/web-bluetooth-bidi-test.js'); + await initializeBluetoothBidiResources(); + assert_implements( + navigator.bluetooth.test, 'missing navigator.bluetooth.test'); + await test_driver.bidi.bluetooth.request_device_prompt_updated.subscribe(); + await test_function(t); }, name, properties); } @@ -153,6 +166,28 @@ async function callWithTrustedClick(callback) { }); } +/** + * Registers a one-time handler that selects the first device in the device + * prompt upon a device prompt updated event. + * @returns {Promise} Fulfilled after the Bluetooth device prompt + * is handled, or rejected if the operation fails. + */ +function selectFirstDeviceOnDevicePromptUpdated() { + if (!useBidi) { + // Return a resolved promise when there is no bidi support. + return Promise.resolve(); + } + test_driver.bidi.bluetooth.request_device_prompt_updated.once().then( + (promptEvent) => { + assert_greater_than_equal(promptEvent.devices.length, 0); + return test_driver.bidi.bluetooth.handle_request_device_prompt({ + prompt: promptEvent.prompt, + accept: true, + device: promptEvent.devices[0].id + }); + }); +} + /** * Calls requestDevice() in a context that's 'allowed to show a popup'. * @returns {Promise} Resolves with a Bluetooth device if diff --git a/resources/web-bluetooth-bidi-test.js b/resources/web-bluetooth-bidi-test.js new file mode 100644 index 00000000000000..044ad1a43aeccf --- /dev/null +++ b/resources/web-bluetooth-bidi-test.js @@ -0,0 +1,91 @@ +'use strict' + +// Convert `manufacturerData` to an array of bluetooth.BluetoothManufacturerData +// defined in +// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-bidi-definitions. +function convertToBidiManufacturerData(manufacturerData) { + const bidiManufacturerData = []; + for (const key in manufacturerData) { + bidiManufacturerData.push( + {key: parseInt(key), data: btoa(manufacturerData[key].buffer)}) + } + return bidiManufacturerData; +} + +class FakeBluetooth { + constructor() { + this.fake_central_ = null; + } + + // Returns a promise that resolves with a FakeCentral that clients can use + // to simulate events that a device in the Central/Observer role would + // receive as well as monitor the operations performed by the device in the + // Central/Observer role. + // + // A "Central" object would allow its clients to receive advertising events + // and initiate connections to peripherals i.e. operations of two roles + // defined by the Bluetooth Spec: Observer and Central. + // See Bluetooth 4.2 Vol 3 Part C 2.2.2 "Roles when Operating over an + // LE Physical Transport". + async simulateCentral({state}) { + if (this.fake_central_) { + throw 'simulateCentral() should only be called once'; + } + + await test_driver.bidi.bluetooth.simulate_adapter({state: state}); + this.fake_central_ = new FakeCentral(); + return this.fake_central_; + } +} + +// FakeCentral allows clients to simulate events that a device in the +// Central/Observer role would receive as well as monitor the operations +// performed by the device in the Central/Observer role. +class FakeCentral { + constructor() { + this.peripherals_ = new Map(); + } + + // Simulates a peripheral with |address|, |name|, |manufacturerData| and + // |known_service_uuids| that has already been connected to the system. If the + // peripheral existed already it updates its name, manufacturer data, and + // known UUIDs. |known_service_uuids| should be an array of + // BluetoothServiceUUIDs + // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid + // + // Platforms offer methods to retrieve devices that have already been + // connected to the system or weren't connected through the UA e.g. a user + // connected a peripheral through the system's settings. This method is + // intended to simulate peripherals that those methods would return. + async simulatePreconnectedPeripheral( + {address, name, manufacturerData = {}, knownServiceUUIDs = []}) { + await test_driver.bidi.bluetooth.simulate_preconnected_peripheral({ + address: address, + name: name, + manufacturerData: convertToBidiManufacturerData(manufacturerData), + knownServiceUuids: knownServiceUUIDs + }); + + return this.fetchOrCreatePeripheral_(address); + } + + // Create a fake_peripheral object from the given address. + fetchOrCreatePeripheral_(address) { + let peripheral = this.peripherals_.get(address); + if (peripheral === undefined) { + peripheral = new FakePeripheral(address); + this.peripherals_.set(address, peripheral); + } + return peripheral; + } +} + +class FakePeripheral { + constructor(address) { + this.address = address; + } +} + +function initializeBluetoothBidiResources() { + navigator.bluetooth.test = new FakeBluetooth(); +}