canvas描画をテストする
ユースケース: チャートライブラリ、ダイアグラムエディタ、WebGLの商品コンフィギュレータ、ドローイングアプリ。検証すべきものは描画されていて、DOMには存在しない — page.locator() に見えるのは大きな <canvas> がひとつだけで、その中身は見えません。
なぜ難しいのか
Section titled “なぜ難しいのか”- 要素もテキストもARIAツリーも(たいてい)ない — 標準のツールキットが機能しなくなります。
- canvasの出力は環境依存です。GPU、アンチエイリアス、フォントのラスタライズがマシンごとに異なるため、素朴なスクリーンショット比較はフレーキーになります。
- アニメーションとトランジションがあるため、どのピクセルが見えるかはいつ見るかに依存します。
1. まずレンダリングを決定論的にする
Section titled “1. まずレンダリングを決定論的にする”スクリーンショットのアサーションは、同じ入力が同じピクセルを描く場合にしか機能しません。
// 時間駆動のアニメーションを固定する(Playwright組み込みのフェイククロック)await page.clock.install({ time: new Date('2026-01-01T00:00:00Z') });
// アプリ読み込み前に乱数をシードするawait page.addInitScript(() => { let seed = 42; Math.random = () => (seed = (seed * 16807) % 2147483647) / 2147483647;});
// CSSアニメーション・トランジションのノイズを無効化するawait page.emulateMedia({ reducedMotion: 'reduce' });WebGLでは、全マシンで同一のラスタライズ結果を得るためにCIではソフトウェアレンダリングを使ってください: --use-gl=swiftshader(Chromium)。
2. 許容差とマスクつきのスクリーンショットアサーション
Section titled “2. 許容差とマスクつきのスクリーンショットアサーション”await expect(page.locator('#chart')).toHaveScreenshot('revenue-chart.png', { maxDiffPixelRatio: 0.01, // アンチエイリアス差を吸収する mask: [page.locator('.live-timestamp')],});ベースラインは手元のマシンではなくCIと同じ環境で生成してください(例: Dockerイメージ経由)。プラットフォーム間のフォントレンダリング差は、偽陽性の差分の筆頭原因です。
3. 全体比較ではなくピクセルを突く
Section titled “3. 全体比較ではなくピクセルを突く”「3本目のバーが赤くて2本目より高い」のような検証では、canvasを直接サンプリングするほうが全画像比較よりはるかに堅牢です。
const barColor = await page.evaluate(() => { const canvas = document.querySelector('#chart canvas') as HTMLCanvasElement; const ctx = canvas.getContext('2d')!; const [r, g, b] = ctx.getImageData(260, 180, 1, 1).data; return { r, g, b };});expect(barColor.r).toBeGreaterThan(200); // 赤系であることWebGLのcanvasをこの方法で読むには preserveDrawingBuffer: true が必要です。あるいは要素のスクリーンショットを撮り、Node側でPNGをデコードしてください。
4. データレイヤーがあるならそちらで検証する
Section titled “4. データレイヤーがあるならそちらで検証する”多くのチャートライブラリは内部モデルを公開しています(chart.getDatasetMeta(...)、three.jsのシーングラフなど)。「モデルが描画されていること」はピクセルでテストし、「値が正しいこと」はモデルで検証する — 「正しく描けているか」と「データが正しいか」を分離すれば、フレーキーさの大半は消えます。
動くcanvas: フレームではなく動きを計測する
Section titled “動くcanvas: フレームではなく動きを計測する”動くcanvas(パン・ズーム、物理演算)では単発のスクリーンショットは不適切です。代わりに位相相関で画像の移動量を計測し、視覚的な安定を待ってください。それをパッケージにしたのが playwright-interactive-ui-test です — 慣性つき地図UIをテストするを参照してください。
getImageDataは汚染された(tainted)canvasでは失敗します(CORSヘッダなしのクロスオリジン画像)。- ヘッドレスとヘッドありでレンダリングが異なることがあります。ベースラインはどちらか一方に決めて固定してください。
- ピクセルパーフェクトを追いかけないこと。許容差+狙い撃ちのサンプリングは、
maxDiffPixels: 0と古くなったベースラインの墓場に勝ります。
- 慣性つき地図UIをテストする — インタラクティブ・慣性ありの場合。
- トーストとちらつきを捕まえる — ピクセルが陰で変わっている場合。