マイク入力をモックする
ユースケース: アプリがユーザーの声を録音する — 音声認識(Speech-to-Text)、ボイスメモ、発音を採点する語学学習アプリ、音声レベルメーター。マイクに向かって話す自動テストを書きたい。
▶ 動くサンプル:
mock-microphone-input.spec.ts/ デモを触る(実行方法は examples/README)
なぜ難しいのか
Section titled “なぜ難しいのか”Playwrightにはマイク入力のためのAPIがありません。そのため、どのソリューションもスタックのどこかで「マイクの差し替え」を行うことになります:
- Chromiumの起動フラグ(
--use-file-for-fake-audio-capture)— ブラウザ組み込みで依存関係も不要ですが、Chromium専用・WAVのみ・ブラウザセッション全体で固定です。 - OSレベルの仮想オーディオデバイス — クロスブラウザですが、CIでのセットアップは非常につらいです。
- ページ内で
getUserMediaをパッチする — クロスブラウザかつ実行時に制御可能。後述のライブラリはこの方式です。
Chromium: フェイクデバイス + 音声ファイル
Section titled “Chromium: フェイクデバイス + 音声ファイル”Chromiumは起動時にマイクをWAVファイルへ差し替えられます。追加の依存関係は不要です:
export default defineConfig({ projects: [ { name: 'chromium', use: { launchOptions: { args: [ '--use-fake-ui-for-media-stream', // 許可ダイアログを自動承認 '--use-fake-device-for-media-stream', // フェイクのキャプチャデバイスを提供 '--use-file-for-fake-audio-capture=tests/fixtures/hello-world.wav', ], }, }, }, ],});ファイルは WAV である必要があります。ffmpegで変換できます:
ffmpeg -i hello-world.mp3 tests/fixtures/hello-world.wavデフォルトではファイルは無限ループで再生されます。%noloop を付けると1回だけ再生し、その後は無音になります:
--use-file-for-fake-audio-capture=tests/fixtures/hello-world.wav%noloopブラウザセッションごとに1つの発話で足り、テスト対象がChromiumだけなら、これで十分です。ただし制約は明確です: ファイルは起動時に固定されるため、最初の画面ではAと言い、次の画面ではBと言う、といったことができません。また、Firefox・WebKitでは何も再生されません。
もう1つ大きな注意点があります: macOSではフェイクキャプチャデバイスが不安定で、getUserMedia() が解決しないままハングすることがあります(--no-sandbox の有無やヘッドレスかどうかに関係なく発生。関連: crbug.com/1032604)。この方式はLinux/CI専用と考え、Macでのローカル実行には後述のライブラリを使ってください。
▶ 動くサンプル:
mock-microphone-input-flag.spec.ts(上記の理由によりmacOSではスキップされます)
クロスブラウザ + 実行時制御: playwright-audio-mocking
Section titled “クロスブラウザ + 実行時制御: playwright-audio-mocking”playwright-audio-mocking は、ページ読み込み前に navigator.mediaDevices.getUserMedia を置き換え、実際のWeb Audioベースの MediaStream を返します。その先にあるもの — MediaRecorder、AnalyserNode、WebRTC、音声認識API — はすべて無改造で動作し、Chromium・Firefox・WebKitに対応しています。
npm install -D playwright-audio-mockingimport { test, expect } from '@playwright/test';import { mockMicrophone } from 'playwright-audio-mocking';
test('transcribes speech', async ({ page }) => { const mic = await mockMicrophone(page); // page.goto() より前にインストールする 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');});実行時の制御
Section titled “実行時の制御”起動時フラグと違い、再生はテストの途中でも完全にスクリプトから制御できます:
await mic.play('fixtures/greeting.mp3'); // ブラウザがデコードできる形式なら何でもawait mic.play('fixtures/question.mp3'); // ストリーミング中のファイル切り替えawait mic.play('fixtures/hold-music.mp3', { loop: true });await mic.pause(); // 無音になり、再生位置は保持await mic.resume();await mic.stop(); // 無音になり、再生位置はリセット
const { playing, position, duration } = await mic.status();フィクスチャファイルなしで音声を生成する
Section titled “フィクスチャファイルなしで音声を生成する”ファイルパスの代わりにバイト列を渡せます — たとえばTTS APIの出力を直接流し込めば、テストごとに違う内容を話させることができます:
await mic.play(Buffer.from(await synthesizeSpeech('add milk to my shopping list')));mockMicrophone(page)は 必ずpage.goto()より前に呼んでください — initスクリプトとして注入されるためです。- ライブラリをChromiumで使う場合は、ライブラリが提供する
RECOMMENDED_CHROMIUM_ARGS(--autoplay-policy=no-user-gesture-required)を付けて起動すると、ユーザー操作なしでWeb Audioを開始できます。 - モックが置き換えるのは音声トラックのみです。
getUserMedia({ audio, video })の場合、videoの制約はデフォルトで本来の実装にパススルーされます。
- WebRTC通話をテストする — モックしたマイクを発信者の声として使う。
- 音が鳴っていることを検証する — 同じ問題の出力側。
- カメラ入力をモックする — 映像版。