![Evelyn Masso](https://web.dev/images/authors/evelynmasso.jpg) Evelyn Masso [X](https://twitter.com/outofambit) [GitHub](https://github.com/outofambit) [LinkedIn](https://www.linkedin.com/in/evelynmasso) ![François Beaufort](https://web.dev/images/authors/beaufortfrancois.jpg) François Beaufort [GitHub](https://github.com/beaufortfrancois)

<br />

Web Bluetooth has been supported since Chrome 56, and lets developers write web apps that talk directly to users' Bluetooth devices. The [Espruino web editor's](https://www.espruino.com/ide/) ability to upload code to compatible Bluetooth devices is one such example. Testing these applications is now possible with [Puppeteer](https://pptr.dev/).

This blog post walks through how to use Puppeteer to operate and test a Bluetooth-enabled web app. The key part of this is Puppeteer's ability to operate Chrome's Bluetooth device chooser.

If you aren't familiar with using Web Bluetooth in Chrome, the following video shows the Bluetooth prompt in Espruino web editor:
User selects a Puck.js bluetooth device in the Espruino web editor.

To follow this blog post, you'll need a Bluetooth-enabled web app, a Bluetooth device it can communicate with, and be using Puppeteer [v21.4.0](https://github.com/puppeteer/puppeteer/releases/tag/puppeteer-v21.4.0) or later.

## Launch the browser

As with most Puppeteer scripts, start by launching the browser with [`Puppeteer.launch()`](https://pptr.dev/api/puppeteer.puppeteernode.launch). In order to access Bluetooth features, you need to provide a few extra arguments:

- Disable headless mode: This means Puppeteer will open a visible Chrome browser window to run the test. Use the [new headless mode](https://developer.chrome.com/docs/chromium/new-headless) if you prefer to run it without UI. The [legacy headless mode](https://developer.chrome.com/blog/headless-chrome) doesn't support showing Bluetooth prompts.
- Additional arguments to Chromium: Pass an ["enable Web Bluetooth" argument](https://peter.sh/experiments/chromium-command-line-switches/#enable-features) for Linux environments.

```javascript
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: false,
  args: ["--enable-features=WebBluetooth"],
});
```

When opening the first page, it's generally recommended to use an [incognito browser context](https://pptr.dev/api/puppeteer.browsercontext/#example). This helps prevent leaking permissions between the tests run with your script (though there is some OS level shared state that cannot be prevented by Puppeteer). The following code demonstrates this:

```javascript
const browserContext = await browser.createIncognitoBrowserContext();
const page = await browserContext.newPage();
```

You can then navigate to the URL of the web app you are testing with [`Page.goto()`](https://pptr.dev/api/puppeteer.page.goto).

## Open the Bluetooth device prompt

Once you've used Puppeteer to open the web app's page with Puppeteer, you can connect to the Bluetooth device to read data. This next step assumes you have a button on your web app that runs some JavaScript including a call to [`navigator.bluetooth.requestDevice()`](https://developer.mozilla.org/docs/Web/API/Bluetooth/requestDevice).

Use [`Page.locator().click()`](https://pptr.dev/api/puppeteer.locator.click) to press that button, and [`Page.waitForDevicePrompt()`](https://pptr.dev/api/puppeteer.page.waitfordeviceprompt/) to recognize when the Bluetooth device chooser appears. You *must* call `waitForDevicePrompt()` before clicking the button, otherwise, the prompt will have already opened up, and it won't be able to detect it.

Since both of these Puppeteer methods return promises, [`Promise.all()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) is a convenient way to call them in the right order together:

```javascript
const [devicePrompt] = await Promise.all([
  page.waitForDevicePrompt(),
  page.locator("#start-test-button").click(),
]);
```

The promise returned by `waitForDevicePrompt()` resolves to a [`DeviceRequestPrompt`](https://pptr.dev/api/puppeteer.devicerequestprompt) object which you'll use next to select the Bluetooth device you want to connect to.

## Select a device

Use [`DeviceRequestPrompt.waitForDevice()`](https://pptr.dev/api/puppeteer.devicerequestprompt.waitfordevice) and [`DeviceRequestPrompt.select()`](https://pptr.dev/api/puppeteer.devicerequestprompt.select) to find and connect to the correct Bluetooth device.

`DeviceRequestPrompt.waitForDevice()` calls the supplied callback each time Chrome finds a Bluetooth device with some [basic info about the device](https://pptr.dev/api/puppeteer.devicerequestpromptdevice#properties). The first time the callback returns true, `waitForDevice()` resolves to the matched `DeviceRequestPromptDevice`. Pass that device to `DeviceRequestPrompt.select()` to select and connect to that Bluetooth device.

```javascript
const bluetoothDevice = await devicePrompt.waitForDevice(
  (d) => d.name == wantedDeviceName,
);
await devicePrompt.select(bluetoothDevice);
```

Once `DeviceRequestPrompt.select()` resolves, Chrome is connected to the device, and the web page is able to access it.

## Read from the device

At this point, your web app will be connected to the chosen Bluetooth device and be able to read information from it. This might look like:

```javascript
const serviceId = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";

const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: [serviceId] }],
});
const gattServer = await device.gatt.connect();
const service = await gattServer.getPrimaryService(serviceId);
const characteristic = await service.getCharacteristic(
  "0b30afd0-193e-11eb-adc1-0242ac120002",
);
const dataView = await characteristic.readValue();
```

For a more in-depth walkthrough of this sequence of API calls, see [Communicating with Bluetooth devices over JavaScript](https://developer.chrome.com/articles/bluetooth).

At this point, you know how to use Puppeteer to automate the use of a Bluetooth-enabled web app by replacing the human step of selecting a device from the Bluetooth device chooser menu. While this might be generally useful, it is directly applicable to writing an end-to-end test for such a web app.

## Create a test

The missing piece from taking the code so far to writing a full test is getting information out of the web app and into your Puppeteer script. Once you have this, it's fairly straightforward to use a testing library (like [TAP](https://node-tap.org/) or [mocha](https://mochajs.org/)) to verify the correct data was read and reported.

One of the easiest ways to do this is to write data to the DOM. JavaScript has plenty of ways to do this without additional libraries. Going back to your hypothetical web app, it might change the color of a status indicator when it reads data from the Bluetooth device or print the literal data in a field. For example:

```javascript
const dataDisplayElement = document.querySelector('#data-display');
dataDisplayElement.innerText = dataView.getUint8();
```

From Puppeteer, [`Page.$eval()`](https://pptr.dev/api/puppeteer.page._eval/) gives you a way to pull this data out of the page's DOM and into a test script. `$eval()` uses the same logic as [`document.querySelector()`](https://developer.mozilla.org/docs/Web/API/Document/querySelector) to find an element and then runs the supplied callback function with that element as the argument. Once you have this as a variable, use your assertion library to test if the data is what we expect.

```javascript
const dataText = await page.$eval('#data-display', (el) => el.innerText);
equal(17, dataText);
```

## Additional resources

To see more complex examples of writing tests for Bluetooth-enabled web apps with Puppeteer, see this repository: <https://github.com/WebBluetoothCG/manual-tests/>. The [Web Bluetooth Community Group](https://www.w3.org/community/web-bluetooth/) maintains this suite of tests, all of which can be run from a browser or locally. The ["Read-only Characteristic" test](https://github.com/WebBluetoothCG/manual-tests/tree/main/characteristic_read) is most similar to the example used in this blog post.

## Acknowledgments

Thanks to Vincent Scheib for kicking off this project and providing valuable feedback on this post.