フロントエンド・テストツール比較 Puppeteer #02テスト編

## はじめに
[前回](https://qiita.com/creaith/items/212d5533f07f600b4f2a)でpuppetterの環境構築が完了しました。
[Seleniumの環境構築](https://qiita.com/creaith/items/c138c0417470a17cc4b4)より劇的に簡単で感動しましたね。
今回は引き続き[上田](https://qiita.com/uedashuhei)、[曽宮](https://qiita.com/ryosomiya)が、
基本的なブラウザの機能をpuppeteerで実装して動かしてみます。
合わせてSelenium班も同一テストを行います。

### 対するSelenium班
[フロントエンド・テストツール比較 Selenium #02テスト編](https://qiita.com/creaith/items/e654fae65811a0df74f3)
* [志村](https://qiita.com/naoto1222)、[モトキ](https://qiita.com/pickles) 

## 実施する操作手順
[検証用のソース](https://github.com/naoto1222/tech-blog)
今回はサンプルでReactで作ったSPAのテスト用ページを用意しました。
リンク先の構築手順に従って用意してください。

### ログイン画面の操作

|ログイン画面|テストケース|
|---|---|
|![01_home_first.png](https://qiita-image-store.s3.amazonaws.com/0/240629/2a1481e6-d2ed-c2db-9403-590095b12646.png)|・ログイン画面を開く<br>・タイトルのアサーション、キャプチャ<br>・入力フォームにonfocus、背景色変更後にキャプチャ<br>・IDとパスワードを入力せず、ログインボタンをクリック<br>・エラーモーダルをキャプチャ<br>・エラーモーダルの閉じるボタンをクリック<br>・IDとパスワードを入力、キャプチャ<br><br>入力値<br>ID:takeda<br>Password:shinji<br><br>・ログインボタンをクリック|

### ログイン後の一覧画面の操作
|一覧画面|テストケース|
|---|---|
|![12_list_addlist.png](https://qiita-image-store.s3.amazonaws.com/0/240629/3de1f677-eec2-acfb-7161-08bba94f2105.png)|・ログイン後の一覧画面に遷移して、ID/パスワードのアサーションとキャプチャ<br>・リストで以下の要素を登録してキャプチャ(入力したものが、画面にリンクとして反映される)<br><br>入力値:javascript, ruby, scala<br><br>・リストの順番を入れ替えてキャプチャ|

### 別タブの操作
* 別タブを開くリンクをクリックする
* (別タブの操作)タイトルをアサーション・キャプチャ
* (別タブの操作)タブを閉じる
* 元のタブでキャプチャする

### ブラウザバック・フォワードの操作
* リストの任意の項目(同タブリンク)をクリックする
* タイトルをアサーションして、キャプチャ
* 一覧画面に戻る、進むを行い、それぞれでキャプチャする

## テストコード
上記で用意したテストケースを網羅できるテストコードを書いていきます。

```script.js
const puppeteer = require('puppeteer');

const loginUrl = 'http://***/' // テストするURL
const pageTitle = 'React Test';
const id = 'takeda';
const pw = 'shinji';
const inputList = ['javascript', 'ruby', 'scala'];

// ログイン画面の操作
async function phase1(page) {
  console.log('start phase1');

  // ログインページに遷移する
  await page.goto(loginUrl);

  // @TODO titleのアサーション →puppeteerではアサーションは未実装
  await page.screenshot({ path: 'image/01_home_first.png' });

  // input#idにフォーカスを当てる
  await page.click('#id_input');
  await page.screenshot({ path: 'image/02_home_focused.png' });

  // ログインエラーでモーダルを出し、閉じる
  await page.click('#login_button');
  await page.waitForSelector('#error_label');
  await page.screenshot({ path: 'image/03_login_failed.png' });
  await page.click('#close_button');

  // id,passwordを入力する
  await page.type('#id_input', id);
  await page.type('#pass_input', pw);
  await page.screenshot({ path: 'image/04_home_input.png' });

  // loginボタンを押下してページ遷移
  await page.click('#login_button');

  // 特定のDOMレンダリングを待つ
  await page.waitForSelector('#text_input');
  console.log('ログインに成功しました');
}

// リスト画面の操作
async function phase2(page) {
  console.log('start phese2');

  // ログイン完了後スクリーンショット
  await page.screenshot({ path: 'image/11_list_first.png' });

  // リストを登録
  for (let value of inputList) {
    await page.type('#text_input', value);
    await page.click('#regist_button');
  }
  await page.waitFor(500);
  // リストを追加後スクリーンショットを撮る
  await page.screenshot({ path: 'image/12_list_addlist.png' });

  // @TODO リストの一番上を一番したに移動させる
}

// 別タブの操作
async function phase3(page, browser) {
  console.log('start phase3');

  // 別タブリンクをクリック
  await page.click('#blank_link');
  await page.waitFor(1000);

  // 新しいタブを操作
  const pages = await browser.pages();
  const page2 = pages[2];
  await page2.bringToFront();
  await page2.waitFor(500);

  // 新しく開いたページをスクリーンショット
  await page2.screenshot({ path: 'image/21_tab_first.png' });

  // 新しく開いたタブを閉じて、スクリーンショット
  await page2.close();
  await page.screenshot({ path: 'image/22_tab_return.png' });
}

// ブラウザバックの操作
async function phase4(page) {
  console.log('start phase4');

  // リストのリンクをクリック
  await page.click('#list_label3 h3 a');
  await page.waitFor(500);
  await page.screenshot({ path: 'image/31_list_link.png' });

  // @TODO リスト画面に戻る
  // @TODO 再度リンク先へ進む
}

(async () => {
  const browser = await puppeteer.launch({
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ]
  });

  // 新規ページを開く
  const page = await browser.newPage();
  // 画面サイズの指定
  page.setViewport({ width: 500, height: 500 });

  await phase1(page);
  await phase2(page);
  await phase3(page, browser);
  await phase4(page);

  // ページを閉じる
  browser.close();
  console.log('end puppeter');
})();
```

## 結果
大きくわけて以下３つのテストについて実施することができませんでした。  

1. アサーションについてはpuppetterの機能ではできないので省略。nodeのパッケージを別途用意してください。（Seleniumのようなテストツールじゃないので仕方なしか？）
* ドラッグ＆ドロップについては、puppetterのAPIである`mouse.up()`の不具合（？）により実装不可。[関連issue](https://github.com/GoogleChrome/puppeteer/issues/1265)
* ブラウザバックについては、`page.goBack()`が用意されているがこれを実装するとブラウザバックはしてくれるが、その後puppeteerが終了しなくなる。[関連issue](https://github.com/GoogleChrome/puppeteer/issues/1166)

これらが解決できる方、もっと良い実装方法を知ってる方、**ご教授ください！！** 
Puppeteerはまだまだ最近メジャーバージョンが1に乗ったので今後に期待ですね！！

## Creaithメンバー
この記事の著者：[上田](https://qiita.com/uedashuhei)、[曽宮](https://qiita.com/ryosomiya)
その他メンバー：[志村](https://qiita.com/naoto1222)、[モトキ](https://qiita.com/pickles)