Puppeteerとは何者なのか少し触ってみた




# 今回やりたかったこと


puppeteerというGoogle製のツールが前から気になっていたのでいい機会だと思い触れてみました。

触ってみた感想、ES6に多少なり触れている方であれば非常に簡単に扱えるのではないかといった印象を受けました。


今回はpuppeteerがどんなアプリケーションなのか体験してみました

---

この記事はMake IT アドベントカレンダーの4日目の記事として投稿しています！
明日（２分後）は@haduki1208 が提供してくれます！




# Puppeteerとは

[パペティアとは](https://github.com/GoogleChrome/puppeteer)


>Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

>PuppeteerはDevToolsプロトコルでChromeまたはChromiumを制御するための高水準APIを提供するNodeライブラリです。 Puppeteerはデフォルトでヘッドレスで実行されますが、完全な（ヘッドレスではない）ChromeまたはChromiumを実行するように設定できます。


なにができるの

- 画面キャプチャ
- SPA, SSRのクローリング
- フォームの提出、UIテスト、キーボード入力などの自動化
- Iphoneなどのエミュレート
- サイトのパフォーマンスチェック



# 実際に動かしてみた

準備

```shell

yarn add --dev jest-puppeteer puppeteer jest
```


なにはともあれテストラン

```js:test_run.js

const puppeteer = require('puppeteer');

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

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

```shell

node test_run.js
```

example.comのスクリーンキャプチャを取得するテストランです
こんな感じでうごかします

サンプル集
[こちら](https://github.com/checkly/puppeteer-examples)のサイトが非常に参考になりました



### 疑似的にユーザーの操作を行い任意のページのスクショを撮る

```js:test_run.js
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const path = require('path');
const screenshotPath = path.resolve('./out/screenshot/') + '/';

try {
  (async () => {
    // init
    const browser = await puppeteer.launch({
      headless: true
    });
    const page = await browser.newPage();
    let targetElement = "";
    await page.emulate(devices['iPhone X']);
    await page.setUserAgent('Mozilla/5.0 ..');

    // Qiit Topページへ
    await page.goto('https://qiita.com/');
    targetElement = '#globalHeader>div>div.st-Header_start>div.st-Header_searchButton';
    await page.waitFor(targetElement);

    // 検索虫眼鏡をタップ
    await page.tap(targetElement);
    targetElement = '#globalHeader>div>form>input';
    await page.waitFor(targetElement);

    // 検索フォームにMakeITと入力して検索
    await page.type(targetElement, 'MakeIT');
    const p = await page.$(targetElement);
    await p.press('Enter');
    targetElement = '#main > div > div > div.searchResultContainer_main > div:nth-child(3) > div.searchResult_main > h1 > a';
    await page.waitFor(targetElement)

    // 一番上の記事をタップしスクショを撮影
    await page.tap(targetElement)
    for(let i = 0 ; i<3; i++){
      await page.waitForSelector('.p-items_main', {waitUntil: 'domcontentloaded'});
      await page.screenshot({path:`${screenshotPath}result_${i}.png`,fullPage: true});
    }

    await browser.close();
  })();
} catch (err) {
  console.error(err)
}
```


```js

const browser = await puppeteer.launch({
  headless: true
});
```
headless:falseにするとブラウザが立ち上がり自動操作の様子が見て取れます
面白いぐらい非常に軽快な操作でなにか別のことにも使えそうな気がしてきます。




```js
const devices = require('puppeteer/DeviceDescriptors');
await page.emulate(devices['iPhone X']);
```
iPhone Xをエミュレートできます
配列内に一度に複数の端末を同時テストできたはずです
AndroidやPCにも対応しています



### クローラーチックに回してみる

```js:test_run.js
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const path = require('path');
const screenshotPath = path.resolve('./out/screenshot/') + '/';

try {
  (async () => {
    // init
    const browser = await puppeteer.launch({
      headless: false
    });
    const page = await browser.newPage();
    let targetElement = "";
    await page.emulate(devices['iPhone X']);
    await page.setUserAgent('Mozilla/5.0 ..');

    await page.goto('https://qiita.com/search?q=makeit');
    targetElement = '#main > div > div > div.searchResultContainer_main > div > div.searchResult_main > h1 > a';
    await page.waitFor(targetElement)
    
    const list = await page.$$(targetElement);
    const datas = [];
    for (let i = 0; i < list.length; i++) {
      var data = {
        href: await (await list[i].getProperty('href')).jsonValue(),
        textContent: await (await list[i].getProperty('textContent')).jsonValue(),
        innerHTML: await (await list[i].getProperty('innerHTML')).jsonValue()
      };
      datas.push(data);
    }

    // 全記事を取得
    for (let item of datas) {
    await page.goto(item.href);
    for (let i = 0; i < 3; i++) {
      await page.waitForSelector('.p-items_main', {
        waitUntil: 'domcontentloaded'
      });
      await page.screenshot({
        path: `${screenshotPath}${item.textContent}_result_${i}.png`,
        fullPage: true
      });
    }
  }

    await browser.close();
  })();
} catch (err) {
  console.error(err)
}
```


途中のDOM解析部分は[こちら](https://qiita.com/go_sagawa/items/85f97deab7ccfdce53ea)を~~丸パクリ~~参考にさせていただきました







# まとめ

DOMのロードを完全に待つ方法がわかりませんでした。ガチャガチャやっているうちにスクショを三回取れば最後の一枚はきれいに撮れることが分かりました。
waitFor()やawait page.waitForSelector()で待ち構えていてもスルーしてしまったり...
結局うまいやり方が見つからず断念

思ったのですがpuppeteerの使いどころとして、画面全体の画像から差分を得るのではなく、もっと細かいコンポーネント単位で取得して比較するのが正解なのかもしれません。
今回取得した画面全体のスクショファイルが約2MBぐらいが平均になりました。中規模以上のアプリケーションに対して愚直に実行する場合、画像の取り回しに気を配らないとまずいかもしれません。


今回簡単でしたがPuppeteerの紹介を行いました
PuppeteerはGoogleの後ろ盾があるとは言えまだまだコミュニティが若く、アプリケーションとしてseleniumと比べ不安定な部分もあります

ただし、Node.jsの実行環境さえあればコマンド一発で立ち上がり別途ブラウザを用意する必要もないので非常にストレスなくはチャレンジできます。

また今回触れませんでしたがサイトの**パフォーマンスチェック**にも使えるそうなので、今度機会があればチャレンジしてみたいと思います