ScreenshotNeo

BlogHow-to

Emulate Bluetooth Devices with Puppeteer

Use Puppeteer’s experimental Bluetooth API to simulate adapter state and devices, handle chooser prompts, and write reliable Web Bluetooth tests.

By the ScreenshotNeo team4 October 20266 min read

Puppeteer can emulate Bluetooth adapter state and simulate preconnected peripherals through the experimental page.bluetooth API. A typical test powers on the emulated adapter, adds a device with a name and service UUIDs, exercises the page’s Web Bluetooth flow, and disables emulation during cleanup. For a chooser flow, register waitForDevicePrompt() before triggering the page’s device request, then select the matching simulated device.

This is browser simulation. It does not control a physical Bluetooth radio or peripheral. The API is experimental, and Chromium’s emulation state is currently tied to a browser context, so tests that need independent state should use separate contexts.

1. Set up Puppeteer

Use a Puppeteer release whose bundled browser and protocol path match the environment you intend to test. Puppeteer tightly couples releases to browser builds for protocol compatibility. Record the Puppeteer version, browser build, and protocol path in CI logs or test documentation when reproducibility matters.

npm install --save-dev puppeteer

The examples below use JavaScript modules and Puppeteer’s documented API shape. Check the API reference for the version installed in your project because Bluetooth emulation is experimental and its details may change.

2. Emulate an adapter and add a simulated peripheral

This runnable example opens a local test page, emulates a powered-on adapter, adds one preconnected peripheral, and performs cleanup even if the test action throws. Replace the page URL and the assertions or actions with your application’s flow.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
const context = await browser.createBrowserContext();
const page = await context.newPage();

try {
  await page.goto('http://localhost:3000', { waitUntil: 'domcontentloaded' });

  await page.bluetooth.emulateAdapter('powered-on');
  await page.bluetooth.simulatePreconnectedPeripheral({
    address: '09:09:09:09:09:09',
    name: 'SOME_NAME',
    manufacturerData: [{ key: 17, data: 'AP8BAX8=' }],
    knownServiceUuids: ['12345678-1234-5678-9abc-def123456789'],
  });

  // Exercise the app's Web Bluetooth flow here.
  // Example: await page.click('#connect-bluetooth');
} finally {
  await page.bluetooth.disableEmulation();
  await context.close();
  await browser.close();
}

emulateAdapter() takes the desired adapter state and an optional low-energy support flag. The simulated peripheral description can include an address, name, manufacturer data, and known service UUIDs. Use values that match what the application expects; a mismatch in a service UUID or device predicate can make a correct simulation appear unavailable.

3. Handle a device chooser prompt

If the page calls navigator.bluetooth.requestDevice() from a button or other user action, start waiting for the prompt before triggering that action. The wait does not return a prompt that is already active.

const [devicePrompt] = await Promise.all([
  page.waitForDevicePrompt(),
  page.click('#connect-bluetooth'),
]);

const device = await devicePrompt.waitForDevice(({ name }) =>
  name.includes('My Device')
);

await devicePrompt.select(device);

Use a predicate that identifies the intended device reliably. If multiple simulated devices could match, narrow the predicate with a stable name or other identifying field exposed by the prompt. Keep the simulated peripheral’s known service UUIDs consistent with the application’s chooser filters.

4. Choose the right test shape

Test need Approach
Check behavior when the adapter is available Emulate a powered-on adapter, add the simulated peripheral if the flow needs one, and exercise the page.
Check a chooser-based connection flow Start waitForDevicePrompt() before the UI action, then find and select the intended device.
Keep test state independent Use a separate browser context for each test requiring isolated Bluetooth state.
Make a run reproducible Record Puppeteer version, browser build, and protocol path; use a release compatible with the browser under test.

5. Reliability, performance, and limitations

  • Experimental API: Puppeteer labels the Bluetooth emulation methods experimental. Pin your dependency and confirm the relevant API against that release.
  • Browser-context scope: Although the Web Bluetooth specification expects emulated adapters to be isolated per top-level navigable, Chromium’s implementation is tied to the browser context according to Puppeteer’s documentation. Pages sharing a context can therefore interfere. Create a separate context when tests require isolation.
  • Cleanup: Call disableEmulation() in a finally block so an assertion failure does not leave emulation enabled for later work.
  • Performance: The dossier provides no benchmark for Bluetooth emulation. Keep setup focused on the adapter and peripherals each test needs, and avoid sharing a context across tests that depend on independent Bluetooth state.
  • Reliability: This simulates browser-visible state; it is not a substitute for validating behavior against real Bluetooth hardware, operating-system permissions, radio conditions, or a physical peripheral.
  • Cost: The Puppeteer emulation workflow itself has no product price stated in the referenced documentation. Your runtime and CI costs depend on where and how you run the browser.

6. Troubleshooting

Symptom Likely cause Fix
page.bluetooth or a method is unavailable The installed Puppeteer version or bundled browser does not expose the documented experimental API. Check the installed version’s Bluetooth API reference and use a compatible Puppeteer/browser release.
The chooser wait hangs or misses the prompt The wait began after the page already requested a device, or the UI action did not trigger requestDevice(). Call waitForDevicePrompt() before clicking or otherwise triggering the request, and confirm the page action actually invokes the Web Bluetooth flow.
No simulated device matches The device predicate, name, service filter, or simulated peripheral fields do not match. Inspect the predicate and align the simulated name and known service UUIDs with the page’s request and chooser logic.
Bluetooth state leaks between pages or tests Pages share a browser context, while Chromium’s emulation scope is context-level. Run independent tests in separate browser contexts and disable emulation during cleanup.
The test changes behavior after an upgrade Puppeteer and browser protocol compatibility is release-sensitive, and this API is experimental. Pin versions, record the browser build and protocol path, then check the API reference for the exact release in use.

7. Or skip the browser setup

If your goal is to capture how a page looks rather than test its Web Bluetooth behavior, ScreenshotNeo is a website screenshot API and MCP server. It cannot emulate Bluetooth or replace this Puppeteer test. It can return a page screenshot or PDF with a single request. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Responses include X-Page-Verdict and X-Billed headers.
  • An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
  • The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.

8. Frequently asked questions

Does this connect to a real Bluetooth device?

No. Puppeteer simulates adapter and peripheral state inside the browser for testing.

Can I call waitForDevicePrompt() after opening the chooser?

No. Start waiting before the action that triggers the Web Bluetooth device request.

Can two pages in one context use independent emulated adapters?

Do not rely on that isolation. Puppeteer documents Chromium’s implementation as browser-context scoped; use separate contexts for independent state.

Is Bluetooth emulation stable across Puppeteer upgrades?

The API is experimental, and browser protocol compatibility depends on the Puppeteer and browser release. Check the version-specific reference and pin versions for repeatable runs.

References