I built 118+ device tests that run entirely in your browser โ€” no server, no uploads
DEV Community

I built 118+ device tests that run entirely in your browser - no server, no uploads

Every time I needed to check whether my microphone actually worked before a call, or whether a key on a used keyboard was dead, I ended up on some sketchy "free online tester" that wanted an account, showed six ad banners, and - the worst part - happily streamed my webcam to a server I knew nothing about. So I built the thing I actually wanted: TestOnDevice - 118+ device tests that run 100% in the browser. No installs, no account, and nothing you record ever leaves your machine. Here's what I learned building it.

The core constraint: the network is off-limits

The single rule that shaped the whole project: no device data touches a server. No webcam frames, no audio buffers, no keystrokes, no sensor readings. That constraint turned out to be freeing rather than limiting, because modern browser APIs are genuinely powerful. Almost every "device test" is just a Web API plus a bit of rendering:

What you're testing The API doing the work
Microphone / speakers MediaDevices.getUserMedia, Web Audio (AnalyserNode)
Webcam getUserMedia, <video>, MediaStreamTrack.getSettings()
Keyboard keydown / keyup, KeyboardEvent.code
Mouse / touch / stylus Pointer Events, PointerEvent.pressure
Gamepads Gamepad API
MIDI keyboards Web MIDI API
Display (dead pixels, refresh rate) Fullscreen + requestAnimationFrame
Motion / orientation DeviceOrientationEvent, DeviceMotionEvent

A live mic meter, entirely local

The microphone test is a good example of how little you need. Grab a stream, wire it into an AnalyserNode, and compute RMS on each frame for a level meter - no recording, no upload:

const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
ctx.createMediaStreamSource(stream).connect(analyser);
const data = new Uint8Array(analyser.frequencyBinCount);
function tick() {
  analyser.getByteTimeDomainData(data);
  let sum = 0;
  for (const v of data) {
    const x = (v - 128) / 128;
    sum += x * x;
  }
  drawMeter(Math.sqrt(sum / data.length)); // RMS -> level bar
  requestAnimationFrame(tick);
}
tick();

The most important line, though, is the cleanup. The moment the user stops or leaves, the hardware indicator light should go off:

stream.getTracks().forEach((track) => track.stop());

Releasing tracks aggressively is what makes a diagnostics tool feel trustworthy instead of creepy.

Gamepads: no events, only polling

The Gamepad API surprised me. You get a gamepadconnected event, but after that there are no button events at all - you have to poll the current state every frame:

window.addEventListener("gamepadconnected", (e) => {
  console.log("connected:", e.gamepad.id);
});
function poll() {
  for (const pad of navigator.getGamepads()) {
    if (!pad) continue;
    pad.buttons.forEach((b, i) => b.pressed && highlight(i));
    // Analog sticks: pad.axes[0], pad.axes[1], ...
  }
  requestAnimationFrame(poll);
}
poll();

That polling model is exactly why it's great for detecting stick drift: read pad.axes while the sticks are untouched and watch whether the values sit at zero or quietly wander. If you want to see it raw, the controller input viewer is just a live JSON dump of every button and axis.

The permissions gotcha that bites everyone

enumerateDevices() will happily list your cameras and microphones before you grant permission - but the label field is an empty string until you do:

const devices = await navigator.mediaDevices.enumerateDevices();
// device.label is "" until the user grants permission to that kind of device

This is a deliberate anti-fingerprinting measure. The UX fix is simple: show generic entries ("Microphone 1", "Camera 1") first, then re-enumerate and reveal real names after the user opts in. I ended up building a whole device enumerator around exactly this behavior.

Privacy as an architecture, not a promise

"We respect your privacy" in a footer is worthless. What actually earns trust:

  • No backend for device data. There's no endpoint to upload frames to, so there's nothing to leak.
  • Late permission requests. Access is only ever asked for after an explicit click on that specific test.
  • Immediate teardown. Streams are released on stop and on page unload.
  • No account, no fingerprint. Nothing to correlate you across sessions.

Because there's no server round-trip, most tests also work as a PWA offline - handy on a fresh machine with no drivers yet installed.

Takeaways

If you're building anything hardware- or media-adjacent for the web:

  • Reach for platform APIs before libraries - getUserMedia, Gamepad, Web MIDI, Pointer Events and the sensor APIs cover an enormous amount of ground.
  • Treat stream cleanup as a first-class feature, not an afterthought.
  • Let real privacy fall out of the architecture. "No server" is a stronger guarantee than any policy page.

You can try the whole thing here: testondevice.com. Start with the Quick Check if you just want a fast pass over everything, or browse all tests - mic, camera, keyboard, mouse, gamepad, MIDI, dead-pixel and refresh-rate - all running locally in your tab. If you want a deep dive into any single one (the refresh-rate detector and the dead-pixel finder both have some fun edge cases), let me know in the comments and I'll write it up.

Comments

No comments yet. Start the discussion.