GitHub Actionsでブラウザ表示のスクリーンショットを撮る
背景
フロントエンド開発では、実装したWebページがどのように表示されているのか確認したい場面があると思います。
最近では、vue-cli-service serve や nuxi dev などのフレームワークを使うことで、手元で簡単にホスティングが可能になり、ホットリロードで変更を用意に確認できます。
一方でプロダクトによってはスマホサイズやPCサイズの両方の表示を確認する必要があったり、多言語対応をしたりすると1ページずつ確認していく作業は手間がかかります。
そこで GitHub Actions で様々なパターンのスクリーンショットを撮影するように実装します。
準備ステップ
アプリケーションのホスティングに関する実装は開発環境によって異なるため、今回は example.com
にアクセスします。
開発中のアプリケーションで試す場合は、起動のステップを挟んだうえで localhost:3000
などに置き換えてください。
Node
バージョン20で試しましたが、すでにActiveな22でも大丈夫なはずです。
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20.x'
Puppeteer
Puppeteerは、GoogleのChrome DevToolsチームが開発したNode.js用のライブラリーで、Chromiumベースのブラウザーを自動操作するためのツールです。
- name: Install puppeteer
run: |
npm install puppeteer
公式ドキュメントのトップページには以下のような表記があります。
npm i puppeteer # Downloads compatible Chrome during installation.
つまり、npm install puppeteer
のコマンドで、Chromeも同時にダウンロードされます。
日本語フォント
前のステップでダウンロードしたChromeには日本語フォントが入っておらず、日本語を表示する場合はフォントのインストールが必要です。
- name: Install Japanese fonts
run: |
sudo apt-get update
sudo apt-get install -y \
fonts-noto-cjk \
fonts-noto-cjk-extra
ちなみにfonts-noto-cjkのcjkは、Chinese, Japanese, Koreanの頭文字で、3言語が入ったパッケージです。
Noto CJKフォントは幅広い漢字表記をサポートしているため、日本語だけでなく中国語や韓国語のテキストも正しく表示されます。
スクショを撮影
- name: Run Puppeteer script to take a screenshot
run: |
node -e "
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({});
const page = await browser.newPage();
await page.setViewport({
width: 1200,
height: 800
});
await page.goto('https://example.com');
await page.screenshot({
path: "./screen_shot.png",
fullPage: true
});
const title = await page.title();
await browser.close();
})();
"
Puppeteerを使用してブラウザインスタンスを起動し、新しいタブを開き、指定されたURLのスクリーンショットを撮影します。
アーティファクトに保存
撮影後の確認を容易にするため、GitHub ActionsのArtifactとして保存します。
- name: Upload Screenshot Artifact
uses: actions/upload-artifact@v4
with:
name: screenshot
path: ./screenshot.png
retention-days: 1
if-no-files-found: error
compression-level: 6
Artifact download URL: https://github.com/{group}/{repository}/actions/runs/00000000/artifacts/0000000000
のような形で保存されたスクリーンショットをURLでGithub Actionの実行ログから確認できます。
ここまでの実装で十分に運用可能です。この先の情報は必要に応じて実装に組み込むと便利なものです。
Appendix
縦幅を動的にする
上の例ではViewPortを1200x800の範囲しか撮影できていません。
たいていのサイトは、より縦長のコンテンツを持っているので動的に縦幅を取得して描画範囲を拡大します。
const pageHeight = await page.evaluate(() => {
return Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.body.clientHeight,
document.documentElement.clientHeight
);
});
await page.setViewport({
width: 1200,
height: pageHeight
});
取得したスクリーンショットをPull Requestに貼る
GitHub ActionsのArtifacts v4では同じJob内でもArtifactが利用できるように緩和されています。
- name: Upload Screenshot Artifact
uses: actions/upload-artifact@v4
id: artifact
with:
name: screenshot
path: ./screenshot.png
retention-days: 1
if-no-files-found: error
compression-level: 6
- name: Comment PR
uses: thollander/actions-comment-pull-request@v2
with:
comment_tag: screenshot-artifact
mode: recreate
message: |
🎉 **スクリーンショット がアップロードされました!** 🎉
🔗 [スクリーンショットをダウンロードする](${{ steps.artifact.outputs.artifact-url }}) 👀✨
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
そのため、アップロードした直後のステップでコメントにURLを貼り付けることができます。
コミットするたびにコメントが増えることのないように、thollander/actions-comment-pull-request
に recreate
設定で過去のコメントを更新していくことができます。
多環境で撮影する
GitHub ActionsのMatrix Strategyを利用していくつかの環境を定義します。
jobs:
shot:
runs-on: ubuntu-latest
strategy:
matrix:
lang: ['ja', 'en', 'zh']
browser: ['chrome', 'edge']
device: ['desktop', 'mobile', 'tablet']
ここでは、3言語 x 2ブラウザー x 3画面サイズを定義しているので、合計18パターンのJobが立ち上がります。
- uses: browser-actions/setup-chrome@latest
if: matrix.browser == 'chrome'
- uses: browser-actions/setup-edge@latest
if: matrix.browser == 'edge'
- name: Install puppeteer-core
run: |
npm install puppeteer-core
2つのブラウザーにそれぞれダウンロードするステップを追加します。
元の npm install puppeteer
とは違い npm install puppeteer-core
を利用することで、ブラウザのインストールは行わないようにしています。
- name: Run Puppeteer script to take a screenshot
run: |
node -e "
const puppeteer = require('puppeteer-core');
(async () => {
let browser;
const launchOptions = {
headless: true, // Ensure headless mode is enabled
args: ['--no-sandbox', '--disable-setuid-sandbox']
};
switch ('${{ matrix.browser }}') {
case 'chrome':
launchOptions.executablePath = '/usr/bin/google-chrome-stable';
break;
case 'edge':
launchOptions.executablePath = '/usr/bin/microsoft-edge';
break;
}
let viewport;
switch ('${{ matrix.device }}') {
case 'desktop':
viewport = { width: 1200, height: 800 };
break;
case 'mobile':
viewport = { width: 375, height: 667 };
break;
case 'tablet':
viewport = { width: 768, height: 1024 };
break;
}
browser = await puppeteer.launch(launchOptions);
const page = await browser.newPage();
await page.setViewport(viewport);
await page.setExtraHTTPHeaders({
'Accept-Language': '${{ matrix.lang }}'
});
await page.goto(process.env.URL, { waitUntil: 'networkidle2' });
await page.screenshot({
path: process.env.FILENAME,
fullPage: true
});
await browser.close();
})();
"
env:
URL: 'https://example.com?lang=${{ matrix.lang }}'
FILENAME: "example_${{ matrix.lang }}_${{ matrix.browser }}_${{ matrix.device}}.png"
それぞれの環境毎に保存されるようにJavaScriptの実装を調整します。
このような実装でそれぞれ独立してアクセスを行いスクリーンショットの保存が行えます。
各スクリーンショットも以下のように環境毎に確認できるので、必要なものだけをダウンロードできます。
終わりに
この仕組みは、ブラウザーやライブラリのダウンロードが軽量で、GithubActionの実行時間も長くないので、費用負担や初期実装をかけずに検証環境を整備できます。
一般的に、UIテストはメンテナンスコストが高いといわれていますが、スクリーンショットを撮影するだけであれば手を加える場面も多くはないと考えています。
また、環境毎に差分を検知した場合にのみコメントが行われるようになると開発者が思わぬデグレに気づけたりします。
このように撮影されたスクリーンショットをQAチームが手動確認をするなどの分担もあり得るかもしれません。
この記事が、皆さんの個人や会社のプロダクトの品質保証の助けになれば幸いです。
Discussion