まとめ
Puppeteer + Intro.js = E2E + Manual Capture
モチベーション
puppeteerを利用するとGoogleChromeのheadlessモードを制御して簡単に画面キャプチャを取ることが出来ます。この機能を使うとreg-suitなどと連携して、E2Eのテストを比較的簡単に実現できます。
ところでE2Eの本来の目的とは何でしょうか。
- E2Eのシナリオはアプリケーション本体の仕様であり
- その確認するための動線は実際のマニュアルに等しい
と私は考えています。
そのため、同時にマニュル用画面キャプチャも一緒に行いたいというのが今回の話の発端です。
話さないこと
Puppeteerの詳し使い方や、reg-suitに関してはこの記事では言及しません。
E2EをどうやってCIで行うかに関しても同様です。
使うツール


Intro.jsはデモとかチュートリアル的なものを簡単に作れるライブラリです。
スポットライトのような感じで、サービスの使い方を教えてくれる機能を提供してくれます。
準備
シナリオの作成
まずはpuppeteerを使ったシナリオテストがないと意味がありません。
今回は、以下のような流れのシナリオを作ります。
- Google検索を表示
- 検索結果の一覧からconnpassのサイトを探索
- Burikaigi2018のエントリーサイトを表示
// node capture.js で実行するとactual_images/以下にそれぞれのスナプショットが保存される。
const puppeteer = require("puppeteer");
(async () => {
// ブラウザを起動
// わかりやすいように、ヘッドレスモードをOFFにして、各アクションに300msec遅延させる
const browser = await puppeteer.launch({
headless: false,
slowMo: 300,
});
// ブラウザの新規ウィンドウ作成
const page = await browser.newPage();
// ウィンドウサイズを調整
await page.setViewport({ width: 1600, height: 1200 });
// Googleへ遷移し、ネットワーク通信が終わるまで待つ
const URL = "http://google.co.jp";
await page.goto(URL, {waitUntil: "networkidle2"});
await page.screenshot({path: "./actual_images/1.png"});
// 入力フィールドに検索文字列を入力
await page.type("#lst-ib", "burikaigi2018 connpass");
await page.screenshot({path: "./actual_images/2.png"});
// 検索実行し検索結果が表示されるまで待つ
await page.click("input[type=submit]");
await page.waitFor("#resultStats");
await page.screenshot({path: "./actual_images/3.png"});
// 検索結果のタイトル一覧を取得し、connpass.comを含むコンテンツへ遷移
const aHandles = await page.$$("h3 > a");
for (var i = 0; i < aHandles.length; i++) {
const href = await page.evaluate((handle) => handle.href, aHandles[i]);
if (href.includes("connpass.com")) {
await page.screenshot({path: "./actual_images/4.png"});
await aHandles[i].click();
break;
}
}
// ナビゲーションが完了するまで待つ
await page.waitForNavigation({ waitUntil: "networkidle2" });
await page.screenshot({path: "./actual_images/5.png"});
// ブラウザを閉じる
await browser.close();
})();
ヘルプ用の画像取得
上記のシナリオにヘルプ用の画像取得の処理を組み込んでいきます。
ヘルプ生成のIntro.jsを動的に読み込むメソッドを定義します。
async function appendIntro(page, setup) {
// Intro.jsをCDNから読み込み
await page.evaluate(() => {
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/intro.js@2.7.0/intro.min.js';
document.body.appendChild(script);
var link = document.createElement('link');
link.rel = "stylesheet";
link.href = "https://cdnjs.cloudflare.com/ajax/libs/intro.js/2.7.0/introjs.min.css"
document.body.appendChild(link);
});
// IntroJSがアクセス可能になるまで待つ
await page.waitForFunction("window.introJs");
// 対象のセレクタにIntro.jsのデータ属性を設定する
await setup(page);
}
上記メソッドを、画面遷移時に読み込ませます(一部抜粋)。
const URL = "http://google.co.jp";
await page.goto(URL, {waitUntil: "networkidle2"});
await appendIntro(page, setupGoogleTop);
appendIntroで定義している setupGoogleTop
は、各セレクタにIntro.jsの設定を組み込むためのものです。
data-step
はチュートリアルの順番、 data-intro
は実際に表示されるツールチップを示します。
詳細はIntro.jsのAPIを参照してください。
function setupGoogleTop(page) {
return page.evaluate(() => {
const textInput = document.getElementById("lst-ib");
textInput.setAttribute('data-step', 1);
textInput.setAttribute('data-intro', '入力フィールドを選択し、burikaigi2018 connpassと入力してください');
const submitButton = document.querySelector("input[name=btnK]");
submitButton.setAttribute('data-step', 2);
submitButton.setAttribute('data-intro', '検索ボタンを押します');
});
}
これでIntro.jsを実行される仕組みの準備は完了です。
以下のようにメソッドを実行して画面遷移時に、チュートリアルを起動します。
// チュートリアル開始
await page.evaluate(() => window._intro = introJs().start());
// 次のヘルプを表示
await page.evaluate(() => window._intro.nextStep());
完成形はGistにUpしています。
実際に実行した場合のキャプチャは以下の通りです。
- Googleを表示





結論
E2Eテストを作る際に頑張って取得したセレクタを、それ以外にも有効活用したいと考えて、実践した案の一つです。
この記事では、マニュアルのためのキャプチャでしたが、puppeteerはDevToolにもアクセス出来ます。
例えば、パフォーマンス測定なども組み込むことができると考えています。
おまけ
Q. 直接Intro.jsだけ読み込んで、リリース時だけ外せばいいんじゃね?
はい、その通りです。
Q. Intro.jsのプレフィックス最初からつけとけばいいんじゃね?
はい、その通りです。
Q. キャプチャだけ取れてもマニュアルじゃないよね?
data-step/data-introの部分を外部化して生成出来るように考えてますがまだやっていません。
Q. 完成系のGist見ると、E2E用のキャプチャが消えてるんだけど?
コードを単純化するために、一旦マニュアルのためのキャプチャ以外の要素は消しました。