DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

Puppeteerによるフルページスクリーンショットを画像遅延読み込みに対応させる

こんにちは、薦田です(@toshiya_komoda)。 今回はChromeブラウザのブラウザテスト自動化ツールであるPuppeteerに関するTipsを紹介します。

Puppeteerによるフルページスクリーンショット

Puppeteerは、Chrome DevTools ProtocolのNode.jsクライアントであり、Chrome DevToolsチームがメンテナンスしています。 その主な用途の1つにブラウザ自動テストが挙げられます。 PuppeteerはChromeの操作に特化しており、操作対象がChromeに限定されるもののSeleniumにはない便利なAPIを利用できます。

Puppeteerで利用できる便利なAPIの1つに、フルページスクリーンショットがあります。 フルページスクリーンショットとは、ウェブページの上端から下端までを1つなぎの画像としてスクリーンショットを撮ったものです。

例えば、以下の画像はフルページスクリーンショットの例です。DeNA Testing Blogの過去の記事です。

DeNA Testing Blog記事のフルページスクリーンショット

Seleniumでもスクリーンショットを取得可能ですが、取得対象の領域がviewportに制限されます。 このためSeleniumでフルページスクリーンショットを実現しようとすると、スクロールしながら複数枚のスクリーンショットを撮った後、1枚につなぎ合わせるといった実装が必要になります。

Puppeteerを利用した場合の実装は以下のように簡潔なものになります。 Node.js v8.11.1, Puppeteer 1.3.0で動作確認しています。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: true});
  const page = await browser.newPage();
  page.setViewport({width: 1200, height: 800})
  const url = 'http://swet.dena.com/entry/2018/02/16/104605'
  await page.goto(url);
  await page.waitForNavigation({waitUntil:'networkidle2', timeout:5000})
            .catch(e => console.log('timeout exceed. proceed to next operation'))
  await page.screenshot({path: 'testing-blog.png', fullPage:true})
  console.log("save screenshot: " + url)
  await browser.close()
})();

補足:Puppeteerの以前のバージョンではフルページスクリーンショットの高さに16384pxの制限がありましたが、1.2.0以降のバージョンではこちらの制限はなくなっているようです。

遅延読み込み対策

Puppeteerでは、前述のようにフルページスクリーンショットを簡単に利用することができ、Visual Regression Testなどに利用することが可能です。 しかし、画像の遅延読み込みに対応したウェブページでは、フルページスクリーンショットをうまく取得できないことがあります。

こちらの画像がそのような例です。 lazysizesという画像遅延読み込みライブラリのデモページで別に画像を用意して試しています。

遅延読み込みページのフルページスクリーンショット

使用画像: Test by Ray Bouknight is licensed under CC BY 2.0

ページ中にロードしきれていない画像が存在しており、正しくフルページスクリーンショット画像が撮れていません。

この問題はPuppeteerのissueにも報告されており、 不具合扱いではありませんが、Feature RequestとしてPuppeteer側での対応が提案されている状況です。 現時点では以下のように一度ページ下端までウィンドウをスクロールさせることで問題を回避できます。

const puppeteer = require('puppeteer');

async function scrollToBottom(page, viewportHeight) {
  const getScrollHeight = () => {
    return Promise.resolve(document.documentElement.scrollHeight) }

  let scrollHeight = await page.evaluate(getScrollHeight)
  let currentPosition = 0
  let scrollNumber = 0

  while (currentPosition < scrollHeight) {
    scrollNumber += 1
    const nextPosition = scrollNumber * viewportHeight
    await page.evaluate(function (scrollTo) {
      return Promise.resolve(window.scrollTo(0, scrollTo))
    }, nextPosition)
    await page.waitForNavigation({waitUntil: 'networkidle2', timeout: 5000})
              .catch(e => console.log('timeout exceed. proceed to next operation'));

    currentPosition = nextPosition;
    console.log(`scrollNumber: ${scrollNumber}`)
    console.log(`currentPosition: ${currentPosition}`)

    // 2
    scrollHeight = await page.evaluate(getScrollHeight)
    console.log(`ScrollHeight ${scrollHeight}`)
  }
}

(async () => {
  const viewportHeight = 1200
  const viewportWidth = 1600
  const browser = await puppeteer.launch({headless: true});
  const page = await browser.newPage();
  page.setViewport({width: viewportWidth, height: viewportHeight})
  const url = 'http://afarkas.github.io/lazysizes/#examples'
  await page.goto(url);

  // 1
  await page.waitForNavigation({waitUntil: 'networkidle2', timeout: 5000})
            .catch(e => console.log('timeout exceed. proceed to next operation'));

  await scrollToBottom(page, viewportHeight)
  await page.screenshot({path:'scroll-sc.png', fullPage: true})

  console.log("save screenshot: " + url)
  await browser.close()
})();

こちらのコードは、Vasilev氏のPuppeteer Screenshotsの実装を参考にしつつ、以下2点の改良を加えたものです。

  1. page.waitForNavigation()関数の条件として、networkidle2(ネットワーク接続が2本以下の状態が500ms以上続く状態)を 指定しています。 ただしウェブサイトによっては、 画像ロード以外のネットワーク接続の影響でこの条件が恒久的に満たされないため、timeoutオプション(デフォルトは30sec)を明示的に指定しています。
  2. また、scrollToBottom()関数の中ではwhileループの中でスクロールしながら毎回scrollHeightの値を更新しています。 これは、スクロール操作によってページのscrollHeightの値が動的に変わる場合があるためです。

ページを一度下端までスクロールしておくことで、さきほどのページのフルページスクリーンショットは以下のように改善されます。

遅延読み込みページのフルページスクリーンショット(遅延読み込み対応版)

画像が全てロードされ、きれいなフルページスクリーンショットが取得されています。

補足Tips

以前のバージョンにあった高さ制限の問題とは別に、Puppeteerでは非常に大きなフルページスクリーンショット画像を取得しようとすると、その下端がかけてしまうことがあります。 こちらの現象は遅延読み込みとは別の問題ですが、こちらのissueで説明されています。 こちらの問題はChromium側での対応が必要ということで、Chromiumの不具合としてあがっていますが、Puppeteer1.3.0にバンドルされたChromiumでは未対応です。

この現象が発生した場合には、スクロールしながらスクリーンショットを複数枚取得し最後につなぎ合わせる、といったSeleniumの場合と同様のワークアラウンドが必要になります。