--headless時代の本命？ Chrome を Node.jsから操作するライブラリ puppeteer について

# --headless時代の本命？ Chrome を Node.jsから操作するライブラリ puppeteer について

[puppeteer](https://github.com/GoogleChrome/puppeteer) はHeadless Chrome をNode.jsで操作しやすくしたライブラリです。今日(※ 2017/8/17)一日で凄い勢いでGitHubのトレンド入りしており、TLでも話題になっていたので、早速触ってみました。

![Trending_repositories_on_GitHub_today.png](https://qiita-image-store.s3.amazonaws.com/0/8808/a326c39b-67ca-9dc0-3b39-dc87631aa370.png)

Node.jsでChromeを操作するというコンテキストにおいては、[Nightmare.js](https://github.com/segmentio/nightmare)と同じレイヤに属するプロダクトですね。Nightmare.jsはElectronを介在させることで、Chromeの操作を実現していましたが、今年の5月にChromeでheadlessモードが利用可能になって以降[^1]、headless Chromeを直接操作するライブラリが色々と出始めていますね。この系統は、[chromy](https://github.com/OnetapInc/chromy)や、やはり先日GitHubでトレンド入りしていた[chromeless](https://github.com/graphcool/chromeless)などが挙げられます。

puppeteerがこれらのライブラリと一線を画すのは、なんと言っても本家ChromeのDevTool開発チームが作成・メンテナンスしている、という点でしょう。

## 仕組み
puppeteerは、WebdriverIOのようなクロスブラウザ対応のツールとは異なり、Chrome DevTools Protocol[^2] を利用してNode.jsからChromeの開発者ツールへ接続して操作を行います。

## 触ってみる
この手のツールの定番、スクリーンショットの取得をやってみます。

まずはインストール。

```sh
npm i puppeteer
```

postinstallフックにより、chromiumもインストールされます。インストールされるchromiumのバージョンはpuppeteerのpackage.jsonで一意に定まるようになっているため、npmやyarnのロックファイルと併用することで、利用するchromiumのバージョンをコントロール可能です。

続いてブラウザを操作するスクリプトを書いていきます。

```javascript:script.js
const fs = require('fs');
const assert = require('assert');
const puppeteer = require('puppeteer');

(async() => {

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://github.com/Quramy');
  await page.screenshot({path: 'example.png'});

  browser.close();
  assert(fs.existsSync('example.png'));
  console.log(' 🎉 ');
})();
```

特に説明不要ですね。GitHubのプロフィールページを表示してキャプチャを撮るスクリプトです。

下記のコマンドで動作させ、example.pngに画像が出力されていれば成功です。

```sh
node script.js
```

## CIでの動作
実際のところ、ローカルでキャプチャを取得しても面白みもへったくれもありません。やはりCIで動かしてこそでしょう。

### TravisCI
特に難しい点はありません。通常のNode.jsプロジェクトと同様にymlを書くだけで簡単に動作します。

```yaml:.travis.yml
os:
- linux
language: node_js
node_js:
- '8'

script:
- node script.js
```

WebDriverやNightmare.jsの場合は仮想フレームバッファの設定など、ブラウザをCIで動作させるための特殊な設定が必要なのですが、それすらも不要なのは気分がいいですね。

### WerckerCI
折角なので、DockerベースのCIでも試してみました。普段から使っているという理由でOracle WerckerCIを選択。
Node.js公式のDocker imageを利用したところ、

```text
(node:227) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Failed to connect to chrome!
(node:227) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
npm info lifecycle pupperteer-example@1.0.0~posttest: pupperteer-example@1.0.0
```

というエラーが表示されてしまっため、多少のworkaroundを施しています[^3]。どうやら、Debian系のOSで動作させる場合には、追加でlibxcb関連をインストールした上で、SUID Sandboxを無効化[^4]する必要があるらしいです。

```yaml:wercker.yml
box: node:8

build:
  steps:
    - script:
        name: Workaround for GoogleChrome/puppeteer#290
        code: sh ./fix_290.sh
    - npm-install
    - npm-test
```

```sh:fix_290.sh
#!/bin/bash

apt-get update
apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
  libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
  libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
  libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
  ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
```

```js:script.js(変更部分)
  // argsを明示
  const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
```

とすることで、動作しました。Travisに比べると少しややこしいですね...。
真面目にやるのであれば、上記の依存関係を焼き付けたimageを作ってしまった方が良さそうな気もします[^5]。

Werckerだけでなく、CircleCI 2.xのようなDocker baseのCIでpuppeteerを動作させる際は同様の考慮が必要になりそうなので、何かの助けになれば幸いです。

なお、動作検証に利用したレポジトリは https://github.com/Quramy/puppeteer-example です。参考まで。

## おわりに

[API一覧](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md)をざっと眺めてみると、Nightmare.jsで実現できていたことはほぼ全てpuppeteerでも実現できそうな印象です。

スクリプトを送り込むinjectFileや、ブラウザ上でscriptを評価するevaluate、そして、Node.js上で定義した関数をブラウザ上に公開する exposeFunctionあたりを駆使すればほぼ何でも出来るんじゃないかこれは、という予感です。

飽くまでChrome専用の操作には限定されますが、e2eテスト環境であったり、お手軽なDOM環境が欲しいときには重宝しそうです。

僕のチームでは、開発しているWebアプリケーションについては、ブラウザで動作させて画像のキャプチャを取得し、前回との差分を検知することで回帰テストを回しており、これに特化した [reg-suit](https://reg-viz.github.io/reg-suit/) というツールを作成したりしています。
このテストフローについては、別途まとめた記事にしたいと思っていますが、最終的には「如何にCIでキャプチャ画像を取得するか」という点に難しさが収束していく感があり、puppeteerのようなブラウザ操作ツールを上手く活用できる色々と捗りそう。

それでは、また。

[^1]: v59 より. https://developers.google.com/web/updates/2017/05/nic59
[^2]: https://chromedevtools.github.io/devtools-protocol/
[^3]: https://github.com/GoogleChrome/puppeteer/issues/290#issuecomment-322921352
[^4]: https://chromium.googlesource.com/chromium/src/+/master/docs/linux_suid_sandbox_development.md
[^5]: Node.js 8ベースで作成して、https://hub.docker.com/r/regviz/node-xcb/ に公開しています