コンテンツにスキップ

音が鳴っていることを検証する

ユースケース: 通知チャイム、メディアプレイヤー、ゲーム。恐れるべきリグレッションは無音です — 要素は「再生中」なのに音が出ていない(ミュートされたトラック、壊れたコーデック、自動再生ポリシー、音量0)。

ヘッドレスブラウザにはスピーカーがなく、Playwrightには「音が出ているか」を調べるAPIがありません。expect(el).toHaveJSProperty('paused', false) は出力が無音でも通ります — paused === false は要素が再生しようとしていることしか意味しないのです。

captureStream() はメディア要素の出力を MediaStream として複製し、AnalyserNode で波形を取得できます。RMSエネルギーが非ゼロ = 聞こえる信号がある、ということです:

import { test, expect } from '@playwright/test';
async function audioRms(page, selector: string) {
return page.evaluate(async (sel) => {
const el = document.querySelector(sel) as HTMLMediaElement;
const stream = (el as any).captureStream?.() ?? (el as any).mozCaptureStream();
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
ctx.createMediaStreamSource(stream).connect(analyser);
await new Promise((r) => setTimeout(r, 300));
const data = new Float32Array(analyser.fftSize);
analyser.getFloatTimeDomainData(data);
return Math.sqrt(data.reduce((s, v) => s + v * v, 0) / data.length);
}, selector);
}
test('notification chime is audible', async ({ page }) => {
await page.goto('/inbox');
await page.getByRole('button', { name: 'Send' }).click();
await expect.poll(() => audioRms(page, '#chime')).toBeGreaterThan(0.01);
});

鳴っていることだけでなく、何が鳴っているかを検証する

Section titled “鳴っていることだけでなく、何が鳴っているかを検証する”

周波数データを見ればチャイムとブザーを区別できます。1つの要素を複数の音が共有している場合に便利です:

const dominantHz = await page.evaluate(() => {
const analyser = (window as any).__analyser; // 上記と同様にセットアップしておく
const bins = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(bins);
const peak = bins.indexOf(Math.max(...bins));
return (peak * analyser.context.sampleRate) / 2 / bins.length;
});
expect(dominantHz).toBeGreaterThan(800); // チャイムは約880Hz
expect(dominantHz).toBeLessThan(950);

Web Audioアプリ(メディア要素なし)

Section titled “Web Audioアプリ(メディア要素なし)”

音が AudioContext から直接出る場合(ゲーム、シンセサイザー)は、initスクリプトでアプリのコンテキストを捕まえ、そのdestinationにアナライザーを接続します:

await page.addInitScript(() => {
const Orig = window.AudioContext;
window.AudioContext = class extends Orig {
constructor(...args: any[]) {
super(...args);
(window as any).__audioCtx = this;
}
};
});

その後 page.evaluate 内でアナライザーを作り、__audioCtx.destination への入力を経由させます。

  • Chromiumは --autoplay-policy=no-user-gesture-required を付けて起動してください。そうしないとユーザー操作の前にコンテキストがサスペンドされることがあります。
  • メディア要素の captureStream() はWebKitでは実装されていません — この検証はChromium/Firefoxで実行するか、Web Audio経由にしてください。
  • ミュートされた要素(el.muted = true)でも、一部のブラウザでは captureStream() にエネルギーが乗ります — ミュートこそが追いたいバグなら el.muted === false を別途検証しましょう。