ヘッドレスChromeライブラリpuppeteerの全てのサンプルプログラムを試してみた

puppeteerでどんなことができるかをざっくりと知るため、[puppeteerの公式サンプルプログラムたち](https://github.com/GoogleChrome/puppeteer/tree/master/examples/)を1つずつ紹介してみようと思います。

## block-images.js
```js
'use strict';

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setRequestInterception(true);
  page.on('request', request => {
    if (request.resourceType() === 'image')
      request.abort();
    else
      request.continue();
  });
  await page.goto('https://news.google.com/news/');
  await page.screenshot({path: 'news.png', fullPage: true});

  await browser.close();
})();
```

[Googleニュース](https://news.google.com/news/)の画面スクリーンショットを取得するプログラムです。
以下のようなスクリーンショットが取得できました（実際には画面下まであるのでもっと長いです）。

![news.png](https://qiita-image-store.s3.amazonaws.com/0/81715/e2db4d1b-7bb4-16ef-2c76-501c1510de8f.png)

スクリーンショットは`page.screenshot([options])`というAPIを実行するだけで取得できちゃいます。

このプログラムでのポイントは`page.setRequestInterception(true)`というAPIです。
HTTPリクエストをインターセプトして、リクエストを継続する(`continue()`)か中止する(`abort()`)かを指定できるようになります。

ここではリクエストするリソースの種類が画像の場合、リクエストを中止するという処理をしています。
なので取得したスクリーンショットに画像が表示されていません。

## custom-event.js
```js
'use strict';

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // Define a window.onCustomEvent function on the page.
  await page.exposeFunction('onCustomEvent', e => {
    console.log(`${e.type} fired`, e.detail || '');
  });

  /**
   * Attach an event listener to page to capture a custom event on page load/navigation.
   * @param {string} type Event name.
   * @return {!Promise}
   */
  function listenFor(type) {
    return page.evaluateOnNewDocument(type => {
      document.addEventListener(type, e => {
        window.onCustomEvent({type, detail: e.detail});
      });
    }, type);
  }

  await listenFor('app-ready'); // Listen for "app-ready" custom event on page load.

  await page.goto('https://www.chromestatus.com/features', {waitUntil: 'networkidle2'});

  await browser.close();
})();
```

[Chrome Platform Status](https://www.chromestatus.com/features)というサイトのロード時に発火される`app-ready`というカスタムイベントを取得して表示するプログラムです。

スクリプトを実行してみると、以下のような出力がされたりされなかったりしました。

```
$ node custom-event.js
app-ready fired {}
```

まず気になるAPIは`page.exposeFunction(name, puppeteerFunction)`。
これはページの`window`オブジェクトに`name`という関数を定義します。
関数本体は`puppeteerFunction`。
`puppeteerFunction`はブラウザではなくNode.jsのコンテキスト内で実行されます。
ここでは`window.onCustomEvent`関数を定義しています。

次は`page.evaluateOnNewDocument(pageFunction, ...args)`というAPIです。
ページがロードされたときに`pageFunction`を実行します。
`pageFunction`はページ内のJavaScriptファイルが実行される前に実行されます。
ここでは`app-ready`イベントに対するイベントリスナーを設定しており、`app-ready`イベントが発火したら`window.onCustomEvent()`を呼ぶようにしています。

## detect-sniff.js
```js
'use strict';

const puppeteer = require('puppeteer');

function sniffDetector() {
  const userAgent = window.navigator.userAgent;
  const platform = window.navigator.platform;

  window.navigator.__defineGetter__('userAgent', function() {
    window.navigator.sniffed = true;
    return userAgent;
  });

  window.navigator.__defineGetter__('platform', function() {
    window.navigator.sniffed = true;
    return platform;
  });
}

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.evaluateOnNewDocument(sniffDetector);
  await page.goto('https://www.google.com', {waitUntil: 'networkidle2'});
  console.log('Sniffed: ' + (await page.evaluate(() => !!navigator.sniffed)));

  await browser.close();
})();
```

https://www.google.com をロードした時に、JavaScriptから`window.navigator.userAgent`もしくは`window.navigator.platform`が参照されているかどうかを出力するプログラムです。

```
$ node detect-sniff.js
Sniffed: true
```

すでに出てきた`page.evaluateOnNewDocument(pageFunction, ...args)`というAPIが使われています。
ページがロードされたときに、`sniffDetector`関数を実行するように設定しています。

`sniffDetector`は、`window.navigator.userAgent`プロパティと`window.navigator.platform`プロパティに関数を設定しています。
それらのプロパティが参照されたときに、`window.navigator.sniffed`を`true`にするようにしています。

`page.evaluate(pageFunction, ...args)`は、`pageFunction`をそのページのコンテキストで実行します。
`pageFunction`の戻り値を取得することができるので、ここでは`!!navigator.sniffed`つまり`true`が出力されています。

## pdf.js
```js
'use strict';

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'});
  await page.pdf({
    path: 'hn.pdf',
    format: 'letter'
  });

  await browser.close();
})();
```

[Y Combinator | Hacker News](https://news.ycombinator.com/)のページをPDF化するプログラムです。
以下のようなPDFファイルが生成されました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/9a83cc71-2b77-884f-bb96-49897c46a7dd.png)

`page.pdf(options)`という一つのAPIだけでPDFが作成できます。
ここではファイル名とフォーマット形式のみ指定されていますが、他にも幅や高さの指定、ヘッダーやフッターのカスタマイズなどもできるようです。

## proxy.js
```js
'use strict';

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch({
    args: [ '--proxy-server=127.0.0.1:9876' ]
  });
  const page = await browser.newPage();
  await page.goto('https://google.com');
  await browser.close();
})();
```

ブラウザにプロキシを設定した状態でサイトにアクセスするプログラムです。
実際にプロキシ経由でアクセスしているかを確認するために、
[ここ](https://github.com/nodejitsu/node-http-proxy/blob/master/examples/http/reverse-proxy.js)からプロキシサーバのプログラムを拝借して試してみます（ポート番号だけ変更しました）。

```js:proxy-server.js
var http = require('http'),
    net = require('net'),
    httpProxy = require('http-proxy'),
    url = require('url'),
    util = require('util');

var proxy = httpProxy.createServer();

var server = http.createServer(function (req, res) {
  util.puts('Receiving reverse proxy request for:' + req.url);

  proxy.web(req, res, {target: req.url, secure: false});
}).listen(9876);

server.on('connect', function (req, socket) {
  util.puts('Receiving reverse proxy request for:' + req.url);

  var serverUrl = url.parse('https://' + req.url);

  var srvSocket = net.connect(serverUrl.port, serverUrl.hostname, function() {
    socket.write('HTTP/1.1 200 Connection Established\r\n' +
    'Proxy-agent: Node-Proxy\r\n' +
    '\r\n');
    srvSocket.pipe(socket);
    socket.pipe(srvSocket);
  });
});
```

`proxy.js`を実行すると、プロキシサーバのログが以下の通り出力されました。
プロキシ経由でGoogleにアクセスできているようです。

```
$ node proxy-server.js
Receiving reverse proxy request for:http://google.com/
Receiving reverse proxy request for:http://www.google.co.jp/?gfe_rd=cr&dcr=0&ei=-czVWsTxFKauX-bAl9AE
Receiving reverse proxy request for:www.google.co.jp:443
Receiving reverse proxy request for:ssl.gstatic.com:443
Receiving reverse proxy request for:www.google.com:443
Receiving reverse proxy request for:adservice.google.co.jp:443
```

## screenshot-fullpage.js
```
'use strict';

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.emulate(devices['iPhone 6']);
  await page.goto('https://www.nytimes.com/');
  await page.screenshot({path: 'full.png', fullPage: true});
  await browser.close();
})();
```

block-images.jsと同じく、画面スクリーンショットを取得するプログラムです。
`page.screenshot([options])`というAPIを使用しています。

ここで注目すべきは`page.emulate(options)`というAPIです。
`options`には`viewport`や`userAgent`などを渡すことができ、モバイルデバイスなどをエミュレートできるようです。
メジャーなデバイスについては`puppeteer/DeviceDescriptors`モジュールを使用することで細かなパラメータの指定をせずに済みます。
ここではiPhone6をエミュレートしてページにアクセスしています。

実際に取得した画像は以下のようなものです。
[New Yourk Times](https://www.nytimes.com/)がモバイル用のレイアウトで表示されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/7368cc50-b5e0-bc76-35d0-6a130376cfa7.png)

## screenshot.js
```js
'use strict';

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://example.com');
  await page.screenshot({path: 'example.png'});
  await browser.close();
})();
```

同じくスクリーンショットを取るプログラムです。
http://example.com の画面スクリーンショットを取得しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/18270cd4-3f38-01c6-f1dc-983dea9d3ea8.png)

## search.js
```js
'use strict';

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto('https://developers.google.com/web/');

  // Type into search box.
  await page.type('#searchbox input', 'Headless Chrome');

  // Wait for suggest overlay to appear and click "show all results".
  const allResultsSelector = '.devsite-suggest-all-results';
  await page.waitForSelector(allResultsSelector);
  await page.click(allResultsSelector);

  // Wait for the results page to load and display the results.
  const resultsSelector = '.gsc-results .gsc-thumbnail-inside a.gs-title';
  await page.waitForSelector(resultsSelector);

  // Extract the results from the page.
  const links = await page.evaluate(resultsSelector => {
    const anchors = Array.from(document.querySelectorAll(resultsSelector));
    return anchors.map(anchor => {
      const title = anchor.textContent.split('|')[0].trim();
      return `${title} - ${anchor.href}`;
    });
  }, resultsSelector);
  console.log(links.join('\n'));

  await browser.close();
})();
```

[Google Developers](https://developers.google.com/web/)で検索を実行し、検索結果のタイトルを取得するプログラムです。
実行結果は以下のとおりです。

```
$ node search.js
Getting Started with Headless Chrome - https://developers.google.com/web/updates/2017/04/headless-chrome
Automated testing with Headless Chrome - https://developers.google.com/web/updates/2017/06/headless-karma-mocha-chai
Headless Chrome: an answer to server-side rendering JS sites ... - https://developers.google.com/web/tools/puppeteer/articles/ssr
New in Chrome 59 - https://developers.google.com/web/updates/2017/05/nic59
All Updates tagged: headless - https://developers.google.com/web/updates/tags/headless
Puppeteer - https://developers.google.com/web/tools/puppeteer/
Examples - https://developers.google.com/web/tools/puppeteer/examples
ヘッドレス Chrome ことはじめ - https://developers.google.com/web/updates/2017/04/headless-chrome?hl=ja
FAQ - https://developers.google.com/web/tools/puppeteer/faq
Articles by Eric Bidelman - https://developers.google.com/web/resources/contributors/ericbidelman
```

実際にChromeでアクセスして、`Headless Chrome`で検索してみました。
上記で出力されているものと一致しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/90f84398-76fb-3cd2-0348-9bc716585a15.png)

`page.type(selector, text[, options])`は`selector`で指定された要素に`text`を入力するAPIです。
ここでは`Headless Chrome`という文字列を検索ボックスに入力しています。

`page.waitForSelector(selector[, options])`は、`selector`で指定した要素が表示されるまで待つAPIです。
ここで指定している`.devsite-suggest-all-results`は、検索候補を表示する部分です。

`page.click(selector[, options])`というAPIで検索候補の一番目をクリックします。

クリックしたら、`.gsc-results .gsc-thumbnail-inside a.gs-title`が表示されるまで待ちます。
これは検索結果を表示する部分です。

検索結果が表示されたら、`page.evaluate(pageFunction, ...args)`で検索結果のタイトルを取得する関数を実行します。
関数はページのコンテキストで実行されるので、`document.querySelectorAll()`が使えます。



## まとめ
以上、全8つのプログラムを紹介しました。
スクリーンショットやPDFがたった1行のプログラムで取得できたり、画面入力も手軽にできるなど、E2Eテスト目的だけでなくいろいろなことに使えそうです。

ここで紹介されているAPIはほんのごくわずかで、puppeteerにはかなりの数のAPIが用意されています。
是非チェックしてみてください。
[puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md)