Playwright でマイクをモックする

に公開

Playwrightとは?

  • PlaywrightとはE2Eテストを行うためのライブラリです。
  • LLMの登場により、コーディング速度は飛躍的に上昇しましたが、コードの品質を保つためにはテストが不可欠です。
  • マイクを使うようなテストがしたかったのですが、サンプルコードがなかなか見つからなかったので、記事に残そうと思います。

https://playwright.dev/

ブラウザ上のマイクを使うまでの流れ

  • モックとは、あるオブジェクトの入力と出力を肩代わりすることです。
  • よって、モックを作成するにはモックするオブジェクトを十分に理解している必要があります。
  • 実際にマイクが利用される流れを見ていきましょう。

1. ブラウザ標準APIで、マイクへのアクセスをリクエストする

const userMedia = await navigator.mediaDevices.getUserMedia({
    audio: {
      noiseSuppression: true,      // ノイズ抑制ON
      echoCancellation: true,       // エコーキャンセルON
      autoGainControl: true,        // 自動ゲイン調整ON
      channelCount: 1,              // モノラル(1) or ステレオ(2)
      latency: { ideal: 0.1 },      // 目標遅延100ms
      sampleRate: { ideal: 48000 }, // 48kHz
      sampleSize: { exact: 16 },    // 16bit
    }
  });

https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_audio_tracks

上のページで、他にどのようなプロパティが指定できるかの記述があります。

2. 録音準備

const microphone = new MediaRecorder(userMedia);

3. dataavailable イベントの処理

const onData = (e: BlobEvent) => {
  if (e.data.size > 0) {
    // 受け取った音声の処理
  }
};
microphone.removeEventListener("dataavailable", onData);

4. 録音開始

microphone.start(250); // 0.25秒ごとに dataavailable イベント が発火
microphone.pause(); // 録音停止
microphone.resume(); // 録音再開

テストコード

  • 自動テストである都合上、ヘッドレスブラウザでテストを行う必要があります。
  • E2Eテストにおいては、モックはなるべく少ない方が良いとされていますが、マイクはどうしようもないのでモックします。ここで、マイクをモックするには、navigator.mediaDevices.getUserMediaをモックし、音声ストリームを返してやる必要があります。
  • マイクのモックのサンプルコードは以下です。
import { test } from "@playwright/test";

// HTMLMediaElementにcaptureStreamメソッドを追加する型定義
interface HTMLMediaElementWithCapture extends HTMLMediaElement {
  captureStream(): MediaStream;
}

// windowオブジェクトにstartTestAudio関数を追加する型定義
declare global {
  interface Window {
    startTestAudio?: () => Promise<boolean>;
    getUserMediaCalled?: boolean;
  }
}

test.skip(
  ({ browserName }) => browserName === "firefox",
  "firefoxでは実行しない",
);
test.skip(
  ({ browserName }) => browserName === "webkit",
  "webkitでは実行しない",
);

test("マイクストリーミングテスト", async ({ page }) => {
  // page.addInitScript でブラウザ起動直後にスクリプト注入
  await page.addInitScript(() => {
    let audioElement: HTMLAudioElement | null = null;
    window.getUserMediaCalled = false; 


    // テスト用WAVファイルを隠しオーディオ要素で再生 → マイク代わりにする
    navigator.mediaDevices.getUserMedia = async (_constraints) => {
      window.getUserMediaCalled = true;
      const url = "/fixtures/asano.wav"; // public/fixtures/asano.wav に配置

      // テスト用WAVを <audio> 要素で追加
      audioElement = new Audio(url);
      audioElement.crossOrigin = "anonymous";
      audioElement.style.display = "none";
      audioElement.loop = true; // ループ再生で長時間テスト可能
      document.body.appendChild(audioElement);

      // captureStream() でMediaStreamを返却
      return (
        audioElement as unknown as HTMLMediaElementWithCapture
      ).captureStream();
    };

    //  グローバルに音声再生関数を公開.テストコード内から page.evaluate(() => window.startTestAudio()) で呼び出せるようにする
    window.startTestAudio = async () => {
      if (audioElement && audioElement.paused) {
        try {
          await audioElement.play(); // さっき登録した <audio> 要素を再生
          return true;
        } catch {
          return false;
        }
      } else {
        if (!audioElement) {
          console.log("audioElementが存在しません");
        } else if (!audioElement.paused) {
          console.log("audioElementは既に再生中です");
        }
        return false;
      }
    };
  });

  /*
    音声を再生するページに移動し、navigator.mediaDevices.getUserMediaを呼び出すような操作をする
  */

  // 音声再生
  const audioStarted = await page.evaluate(async () => {
    return (await window.startTestAudio?.()) || false;
  });


});

  • firefox, webkit はcaptureStream() メソッド自体が未対応なのでスキップしています。

playwright.config.ts

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  globalSetup: "./src/tests/e2e/global-setup.ts",
  testDir: "./src/tests/e2e",
  timeout: 60_000, // 30秒から60秒に延長
  retries: process.env.CI ? 2 : 0,
  reporter: [["list"], ["html", { open: "never" }]],
  use: {
    headless: true,
    baseURL: "http://localhost:3010",
    trace: "on-first-retry",
    launchOptions: {
      slowMo: 250,
      args: [
        "--use-fake-device-for-media-stream", // 実際のマイクじゃなく偽デバイスを使う
        "--use-fake-ui-for-media-stream", // パーミッションダイアログを自動許可
        "--use-file-for-fake-audio-capture=tests/fixtures/asano.wav", // テスト用WAVをマイクに見せかける
      ],
    },
  },
  webServer: {
    command: "npx next dev --port 3010",
    url: "http://localhost:3010",
    timeout: 120_000,
    reuseExistingServer: !process.env.CI,
    env: {
      NODE_ENV: "test",
    },
  },

  // デバイスエミュレーション(必要に応じて)
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    // { name: "webkit", use: { ...devices["Desktop Safari"] } },
  ],
});

あとはnpx playwright testを実行するだけです。

まとめ

Playwright でマイクのモックを行ってみました。実装例を見つけることができず、cursor でガチャガチャしても上手くいかず、少し時間がかかってしまいました。
同じような状況の方の役に立てば幸いです。

Discussion