playwright-audio-mocking
v0.1.1
Published
Stream arbitrary audio files into getUserMedia() microphone input from Playwright. Works on Chromium, Firefox and WebKit, with runtime play/pause/stop/loop control.
Downloads
300
Maintainers
Readme
playwright-audio-mocking
Stream any audio file into a website's microphone (getUserMedia()) from Playwright.
Test voice-driven UIs — speech recognition, voice memos, audio meters, WebRTC calls — by feeding them real audio, on Chromium, Firefox and WebKit, with full runtime control.
import { test, expect } from '@playwright/test';
import { mockMicrophone } from 'playwright-audio-mocking';
test('voice input', async ({ page }) => {
const mic = await mockMicrophone(page); // install BEFORE page.goto()
await page.goto('https://example.com/voice');
await page.click('#record'); // the app calls getUserMedia()
await mic.play('fixtures/hello.mp3'); // stream the file as mic input
await mic.waitForEnd();
await expect(page.locator('#transcript')).toHaveText('hello');
});Why not --use-file-for-fake-audio-capture?
Chromium has a built-in flag for fake audio capture, but it is limited:
| | --use-file-for-fake-audio-capture | playwright-audio-mocking |
| --- | --- | --- |
| Browsers | Chromium only | Chromium, Firefox, WebKit |
| Formats | WAV only | Anything the browser can decode (WAV, MP3, OGG, FLAC, …) |
| Switch files at runtime | ✗ (fixed at launch) | ✓ (play() any time, any file) |
| Playback control | ✗ | ✓ play / pause / resume / stop / loop |
| Wait for playback to finish | ✗ | ✓ waitForEnd() |
Installation
npm install -D playwright-audio-mockingRequires playwright-core >= 1.30 as a peer dependency (already present if you use @playwright/test or playwright).
How it works
mockMicrophone(page) registers an init script that replaces navigator.mediaDevices.getUserMedia before the page loads. When the site requests audio, it receives a real MediaStream backed by a Web Audio MediaStreamAudioDestinationNode. mic.play(file) sends the file's bytes into the page, decodes them with AudioContext.decodeAudioData, and plays them into that stream.
Because the site gets a genuine MediaStream, everything downstream — MediaRecorder, AnalyserNode, WebRTC addTrack, speech recognition sending audio to a server — works as usual. The browser's real microphone is never touched, no permission prompt is triggered for audio, and the fake device shows up in enumerateDevices().
Usage
With @playwright/test
import { test, expect } from '@playwright/test';
import { mockMicrophone } from 'playwright-audio-mocking';
test('transcribes speech', async ({ page }) => {
const mic = await mockMicrophone(page);
await page.goto('/voice-memo');
await page.getByRole('button', { name: 'Record' }).click();
await mic.play('tests/fixtures/hello-world.wav');
await mic.waitForEnd();
await page.getByRole('button', { name: 'Stop' }).click();
await expect(page.getByTestId('transcript')).toContainText('hello world');
});Installing on a whole browser context
import { installAudioMock, microphone } from 'playwright-audio-mocking';
const context = await browser.newContext();
await installAudioMock(context); // every page in this context is mocked
const page = await context.newPage();
await page.goto('https://example.com/voice');
const mic = microphone(page);
await mic.play('fixtures/hello.wav');Runtime control
const mic = await mockMicrophone(page);
await page.goto('/call');
await mic.play('fixtures/greeting.mp3'); // stream a file
await mic.play('fixtures/question.mp3'); // switch files mid-stream
await mic.play('fixtures/hold-music.mp3', { loop: true }); // loop forever
await mic.pause(); // silence, position kept
await mic.resume(); // continue from position
await mic.stop(); // silence, position reset
await mic.load('fixtures/answer.mp3'); // pre-decode without playing
await mic.play(); // replay the loaded audio
const { playing, paused, position, duration } = await mic.status();Audio can also be passed as bytes instead of a file path:
await mic.play(Buffer.from(await synthesizeSpeech('hello'))); // e.g. from a TTS APIAPI
mockMicrophone(page, options?): Promise<Microphone>
Installs the mock on the page and returns its controller. Must be called before page.goto() — the mock is injected as an init script and takes effect on the next navigation.
installAudioMock(pageOrContext, options?): Promise<void>
Installs the mock only. Accepts a Page or a BrowserContext (mocks every page in the context).
microphone(page): Microphone
Returns a controller for a page whose mock was installed via installAudioMock.
Options (AudioMockOptions)
| Option | Default | Description |
| --- | --- | --- |
| deviceLabel | 'Mock Microphone (playwright-audio-mocking)' | Label reported by enumerateDevices() and MediaStreamTrack.label |
| deviceId | 'playwright-audio-mocking' | deviceId reported by enumerateDevices() / track.getSettings() |
| groupId | 'playwright-audio-mocking' | groupId reported by enumerateDevices() |
| passThroughVideo | true | For getUserMedia({ audio, video }), forward the video constraint to the real getUserMedia and merge the video tracks in (only audio is mocked) |
Microphone
| Method | Description |
| --- | --- |
| play(input?, { loop? }) | Decode and stream a file path / Uint8Array / Buffer. Without input, replays the last loaded audio. Resolves with { duration } once playback starts. |
| load(input) | Decode without playing. Resolves with the duration in seconds. |
| pause() / resume() | Pause keeping the position / continue from it. |
| stop() | Stop and reset the position. |
| waitForEnd({ timeout? }) | Resolve when playback finishes. Never resolves for loop: true — call stop(). |
| status() | { installed, playing, paused, position, duration } |
| isPlaying() | Shorthand for (await status()).playing. |
RECOMMENDED_CHROMIUM_ARGS
['--autoplay-policy=no-user-gesture-required'] — see below.
Notes & caveats
Install before navigation. The mock takes effect on the next navigation after
mockMicrophone()/installAudioMock(). If the page is already on the target site,page.reload()after installing.Navigation resets playback state. Loaded audio lives in the document; after a navigation, call
play()again.Start playing after the app starts listening. Like a real mic, audio played before the site calls
getUserMedia()(or before it starts consuming the stream) is simply not heard.Autoplay policy. Chromium may keep the
AudioContextsuspended until a user gesture. Playing after aclick()is usually enough; for gesture-less flows, launch with:import { RECOMMENDED_CHROMIUM_ARGS } from 'playwright-audio-mocking'; const browser = await chromium.launch({ args: RECOMMENDED_CHROMIUM_ARGS });Or in
playwright.config.ts:use: { launchOptions: { args: ['--autoplay-policy=no-user-gesture-required'] } }Firefox on headless Linux CI needs an audio server. Without an audio backend, Firefox never starts its
AudioContextand no audio flows. Install and start PulseAudio before running tests (see this repo's CI workflow):sudo apt-get install -y pulseaudio && pulseaudio --start --exit-idle-time=-1Secure context required.
navigator.mediaDevicesonly exists on HTTPS orlocalhost— same rule as for a real microphone.Supported formats are whatever the browser's
decodeAudioDatasupports. WAV and MP3 work everywhere; OGG/Opus is not supported by WebKit.No permission prompts for audio. The mocked
getUserMedianever reaches the browser's permission layer. If you usepassThroughVideowith real video constraints, grant camera permission as usual (context.grantPermissions(['camera'])).
