Puppeteer Tips

## Puppeteer？
読み方は「**ぱぺてぃあ**」。
​
Node.jsのライブラリでChromeを操作しDOMの要素を取得、ステータスコードを取得、レスポンスタイムを計測・・等々できます。
※Chromeのデベロッパーツールで見れる情報は（たぶん）全てpuppeteerで取得できる

## Sample
「サイト内の各ページのタイトルが予測した値になっているか？」という自動テストをPuppeteerを利用して処理してみます。
​
大まかな流れは下記のようになります。
​ 
1. テストデータをCSVファイルから読み込み
2. 1行ずつループし、取得した値と予測した値を比較
3. 結果表示

### Code

https://github.com/yusukeito58/puppeteer-template

```shell
$ tree -I node_modules
.
├── package.json
└── src
    ├── data
    │   └── title.test.csv
    ├── lib
    │   └── output.js
    └── test
        └── title          // テスト・処理内容応じてにディレクトリを切って、実行ファイル（index.js）と説明ファイル（README.md）を置くといい感じ
            ├── README.md  // テスト概要や実行方法を記載
            └── index.js   // 実行ファイル
```
​

```javascript:index.js
const puppeteer = require('puppeteer');
const papa = require('papaparse');
const assert = require('assert');
const fs = require('fs');
​
const root = '../../'
const { showTestStart, showResult  } = require(root + 'lib/output');
​
// メイン処理
(async () => {
  console.time('Processing time');
​
  // テスト対象のサイト
  const domain = 'https://www.google.com';
​
  // テスト対象のデータ ※ Listを直接コードに書く、CSVから読み込む etc...
  file = fs.readFileSync(root + 'data/title.test.csv', 'utf8')
  dataList = papa.parse(file, {
    header: true,
    skipEmptyLines: true
  }).data;
​
  // カウンタ初期化
  let count = 0;
  // エラー一覧
  let errorList = [];
​
  // ブラウザ起動
  const browser = await puppeteer.launch();
  for (const data of dataList) {
    count += 1;
​
    // アクセス先のURLを生成
    const url = domain + data.path;
​
    // 進捗を表示
    showTestStart(url, count, dataList);
​
    // ページ生成
    const page = await browser.newPage();
​
    // JSやCSSの読み込みを無視
    await page.setRequestInterception(true);
    page.on('request', (interceptedRequest) => {
      if (url === interceptedRequest.url()) {
        interceptedRequest.continue();
      } else {
        interceptedRequest.abort();
      }
    });
​
    // テスト対象のURLにアクセス（返り値にresponseが返る）
    await page.goto(url);
​
    // ページタイトル取得
    const title = await page.title();
​
    try {
      // 予期された結果と比較
      assert.equal(title, data.title);
      console.log('✅  ' + 'Expected result');
    } catch (err) {
      console.log('❌  ' + 'Unexpected result');
      console.log(err.message);
​
      errorList.push(err.message);
    }
    console.log('\n');
​
    // ページ閉じる
    await page.close();
  }
​
  // ブラウザ閉じる
  await browser.close()
​
  showResult(errorList);
  console.timeEnd('Processing time');
})();
```
​

```javascript:lib/output.js
exports.showTestStart = (currentUrl, index, urls) => {
  const color = '\u001b[44m\u001b[37m';
  const reset = '\u001b[0m';
​
  console.log(`${color} 🖥  ${currentUrl} | ${index} / ${urls.length} ${reset}`);
};
​
exports.showResult = (errorList) => {
  let msg = '';
  if (errorList.length === 0) {
    msg = '✅  🎉🎉🎉Congratulation for passing!!🎉🎉🎉';
  } else {
    msg = '❌  Failed the test...😭';
  }
​
  console.log('\n' + msg + '\n');
};
```
​

```data/title.test.csv
url,title
/,Google
/search/howsearchworks/,Google Search - Discover How Google Search Works
```


```json:package.json
{
  "name": "puppeteer-template",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "yusukeito58",
  "license": "MIT",
  "dependencies": {
    "assert": "^2.0.0",
    "papaparse": "^5.1.1",
    "puppeteer": "^2.1.1"
  }
}
```

​
### 実行
サンプルで用意したプログラムを実行してみます。

```shell
$ git clone https://github.com/yusukeito58/puppeteer-template.git

$ npm i
$ cd src/test/title

# 実行
$ node index.js
```

### 結果
うまくいくと下記のように表示されます。
<img width="472" alt="スクリーンショット 2020-02-15 19.37.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116168/5f50f317-6c94-ef5e-a7ba-dbeedd30b7ab.png">

## Tips
個人的に利用頻度が多い処理をまとめておきます。

### 特定要素の有無を判定
例. 検索ボタンがあるか確認

```javascript
// 要素を取得
const hasElement = async (page, selector) => {
  const item = await page.$(); // selectorを引数で受け取るようにするともっと汎用的に使える
  if (item) {
    return true;
  } else {
    return false;
  }
};

(async () => {   
  for (...) {
    :
    const selector = 'center > input.gNO89b';
    ret = await hasElement(page, selector);
    :
  }
}();
```

### UserAgentを指定
例．デバイスを「iPhone X」に設定

```javascript
const devices = require('puppeteer/DeviceDescriptors');
// テストデバイス
const device = devices['iPhone X'];
​
(async () => {   
  for (...) {
    :
    // デバイス設定
    await page.emulate(device);
    :
  }
}();
```

### JSやCSSなどの読み込みを無視
処理速度が数倍違ってきます。

```javascript
(async () => {   
  for (...) {
    :
    await page.setRequestInterception(true);
​
    page.on('request', (interceptedRequest) => {
    if (url === interceptedRequest.url()) {
        interceptedRequest.continue();
    } else {
        interceptedRequest.abort();
    }
    });
    :
  }
}();​
```

### テストデータをCSVファイルから取得​

```
url,title
/,Google
/search/howsearchworks/,Google Search - Discover How Google Search Works
```

```javascript
const papa = require('papaparse');
const fs = require('fs');
:
​
(async () => { 
  :
  // テストデータ取得
  file = fs.readFileSync(root + 'data/title.test.csv', 'utf8')
  dataList = papa.parse(file, {
    header: true,
    skipEmptyLines: true
  }).data;
  :
)();
```

### 非同期に対象ページにアクセス​
データが大量にあった場合、直列的に対象ページにアクセスすると処理時間を要するため、非同期に同時処理すると処理時間が短縮されます。
​
***安定しないケースもあるので、検証が必要かも知れません。（もっと良い方法ありそう）***
​
例. 大量の対象URLが全て正常（ステータスコードが200である）か確認する

```javascript
// ステータスコードを取得
const getStatusCode = (browser, url) => {
  return new Promise(async (resolve) => {
    const page = await browser.newPage();
​
    await page.setDefaultNavigationTimeout(0);
​
    await page.setRequestInterception(true);
    page.on('request', (interceptedRequest) => {
      if (url === interceptedRequest.url()) {
        interceptedRequest.continue();
      } else {
        interceptedRequest.abort();
      }
    });
​
    const response = await page.goto(url);
​
    // ステータスコードを返却（true:200台）
    const result = {
      'url': url,
      'status': response.ok()
    }
​
    await page.close();
​
    resolve(result);
  })
}
​
:
​
(async () => { 
  :
  for (let url of urls) {
    allResponse.push(getStatusCode(browser, url))  // あまり大量だとPCがが唸る・・・
  }
​
  errors = await Promise.all(allResponse) // 非同期実行
    .then(results => {
       // エラーとなった情報だけにフィルタリング
       return results.filter(result => !result.status)
  })
  :
)();
```

### 配列を〇〇個ごとに分割
Puppeteerから少し外れますが、前項の非同期処理を行う前処理です。同時実行する個数に調整する際に利用します。


```javascript
const divideArrIntoPieces = (arr, n) => {
  let arrList = [];
  let idx = 0;
  while(idx < arr.length){
    arrList.push(arr.splice(idx, idx + n));
  }
  return arrList;
}
​
(async () => {
  :
  urlList = divideArrIntoPieces(allUrl, 10);
  
  for (let urls of urlList) {
    for (let url of urls) {
      :
    }
  }
}();
```

### 画像を保存​

```javascript

// ダウンロード対象
imgUrl = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png';
​
// ローカルの保存先
const imgPath = './images/image.jpg';
​
// 画像ダウンロード
const viewSource = await page.goto(imgUrl);
​
// ローカルに保存
fs.writeFileSync(imgPath, await viewSource.buffer());
```

## 参考​

- Puppeteer : https://github.com/puppeteer/puppeteer
    - Examples : https://github.com/puppeteer/examples
    - API Document : https://github.com/puppeteer/puppeteer/blob/master/docs/api.md
- Assert : https://nodejs.org/api/assert.html
- Papaparse : https://www.papaparse.com