📷

GitHub Actionsでブラウザ表示のスクリーンショットを撮る

2024/12/03に公開

背景

フロントエンド開発では、実装した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-requestrecreate 設定で過去のコメントを更新していくことができます。

多環境で撮影する

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の実装を調整します。

このような実装でそれぞれ独立してアクセスを行いスクリーンショットの保存が行えます。

Builds

各スクリーンショットも以下のように環境毎に確認できるので、必要なものだけをダウンロードできます。

Artifacts

終わりに

この仕組みは、ブラウザーやライブラリのダウンロードが軽量で、GithubActionの実行時間も長くないので、費用負担や初期実装をかけずに検証環境を整備できます。

一般的に、UIテストはメンテナンスコストが高いといわれていますが、スクリーンショットを撮影するだけであれば手を加える場面も多くはないと考えています。

また、環境毎に差分を検知した場合にのみコメントが行われるようになると開発者が思わぬデグレに気づけたりします。

このように撮影されたスクリーンショットをQAチームが手動確認をするなどの分担もあり得るかもしれません。

この記事が、皆さんの個人や会社のプロダクトの品質保証の助けになれば幸いです。

Luup Developers Blog

Discussion