ピクセルをテストする。小さなチームのためのスクリーンショットQA
テストはすべて緑なのに、ドロップダウンはクリックできませんでした。ユニットテストには、重なり順のバグも、ダークテーマの崩れも、スマートフォンでのはみ出しも見えません。いまはすべての画面を代わりに見てくれている小さなスクリーンショットの仕組みと、テストでは決して見つからない見た目のバグを紹介します。
私たちがこれを取り入れたのは、正直に言えば痛い目を見た後でした。スクリーンショットが1枚あれば気づけたはずの見た目の不具合をリリースしてしまったのです。暗い背景で色が抜けて見えるすりガラスのパネル。描画はされるのにクリックできないメニュー。スマートフォンの幅では65ピクセルの細い切れ端になっていた検索欄。どれも緑のテストを難なく通り抜けました。テストが見ていたのは関数で、バグはピクセルの中に住んでいたからです。
その仕組みは思ったより小さい
私たちのものは、それらしい架空のデータで満たしたローカル環境、セッションをディスクに保存して制限にかからないようにしたヘッドレスのログインが1つ、そしてページ、テーマ、画面幅を掛け合わせて回すループです。デスクトップとスマートフォン、明るいテーマと暗いテーマ、大事な画面はすべて対象にします。全体でも短いスクリプトで、ひと通り回すと数分で画像のフォルダができあがります。
import { chromium } from 'playwright'; const browser = await chromium.launch();const ctx = await browser.newContext({ storageState: 'auth.json', // login once, reuse everywhere viewport: { width: 1440, height: 900 },});const page = await ctx.newPage(); for (const [name, url] of PAGES) { await page.goto(BASE + url, { waitUntil: 'load' }); for (const theme of ['light', 'dark']) { await page.evaluate((t) => document.documentElement.setAttribute('data-theme', t), theme); await page.screenshot({ path: `shots/${name}-${theme}.png`, fullPage: true }); }}スクリーンショットに証拠を添える
画像は目に見えるものを捕まえます。操作にまつわるバグについては、ブラウザ自身に証言してもらうのが確実です。ほとんどの仕事は2行で片づきます。部品の中心で elementFromPoint を呼べば、そのクリックを実際に受け取るのが何かが分かります。getBoundingClientRect を見れば、その部品が対応をうたっている画面幅に本当に収まっているかが分かります。いまでは、一度でも痛い目を見た箇所については両方を必ず確認しています。
// Is the menu item really clickable, or is something invisible covering it?const hit = await page.evaluate(() => { const el = document.querySelector('[role="menuitem"]'); const r = el.getBoundingClientRect(); const top = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2); return el === top || el.contains(top);}); // Does the search input actually fit a 390px phone?const fits = await page.evaluate(() => { const r = document.querySelector('input[name="q"]').getBoundingClientRect(); return r.right <= window.innerWidth && r.width > 200;});毎回、両方のテーマで
ダークモードのバグはそれだけで一つの分野です。片方のテーマだけで作業している開発者からは、うまく隠れてしまうからです。直書きされたほとんど白の塗りは、明るいページでは意図どおりに見えて、暗いページでは目に刺さります。同じページを見ている間にテーマの属性を切り替えるのは1行で済み、それだけで確認できる範囲が倍になります。私たちは、両方のテーマで撮っていない画面はテストしていないものとして扱っています。


これで見つかるもの
- スタッキングコンテキストのバグ。描画され、見えていて、それでもクリックできない状態です。当たり判定を調べないと見えません。
- テーマの漏れ。暗いページで光ってしまう明るい面、消えてしまうグラフ、背景が焼き込まれたイラスト。
- スマートフォンでのはみ出し。固定バーが内容を覆う、入力欄が細い切れ端に切り取られる、見出しが文の途中で切れる。
- 文脈の中の文章。切れた語尾、前後の矛盾、リンターでは決して指摘されない一文。
これでユニットテストが不要になるわけではありません。テストは金額計算の正しさを証明し、スクリーンショットはそれを実行するボタンに人の手が届くことを証明します。小さなチームにQA部門はありませんが、リリースのたびに新しいスクリーンショットのフォルダを見返すことは、なかなか良い代わりになります。テストとは、見ることです。
テストが通るというのは、関数についての言明です。スクリーンショットは、製品についての言明です。