4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社スカラパートナーズAdvent Calendar 2023

Day 17

PuppeteerをChromeのレコーダーツールを使って活用してみた

Last updated at Posted at 2023-12-16

はじめに

今回はPuppeteer(パペティア?)について取り上げます。

公式サイト

Puppeteer is a Node.js library which provides a high-level API to control Chrome/Chromium over the DevTools Protocol. Puppeteer runs in headless mode by default, but can be configured to run in full ("headful") Chrome/Chromium.

Puppeteerは、Chrome/Chromiumを制御してくれるNode.jsライブラリで、ヘッドレスでもフッドフルでも利用可能とのこと。

画面操作などの自動操作はもちろん、スクリーンショットの取得やパフォーマンス診断など、E2Eで必要そうな機能は揃っているようです。

記事の環境

下記環境でおこなっております。

  • MacOS 12.1
  • Node.js 21.1.0
  • Google Chrome 120.0.6099.71

下記については本記事で扱わないため、他の方の詳しい記事を参照ください。

  • Puppeteerの書き方
  • E2Eテスト関連
    • PuppeteerとJESTの連携
    • 他のツールとの比較検討

Puppeteerの前に

いきなり脱線しますが、Chromeのデベロッパーツールはみなさん使っておりますでしょうか。
何気なくツールの三点リーダーをクリックしたら、「その他のツール」の項目があり、なかなか面白そうなツールがあります。その中の「レコーダー※」という項目がありますので、こちらを触っていきましょう。

※レコーダーの横にフラスコのアイコンがあるので、プレビュー版のようです

レコーダーを使ってみよう

テスト用にGoogleフォームでページを作りましたのでこちらのページで試していきます。

レコーダーの使い方(記録)

WEBページを開いて、「レコーダー」ツールを開きます。
説明が表示されますので、「新しい記録を作成」をクリックします。

スクリーンショット 2023-12-15 14.59.57.png

「記録名」を入力して、「セレクタの属性」、「記録するセレクタの種類」はデフォルトのまま、「記録を開始」をクリックしていきます。

スクリーンショット 2023-12-15 15.02.18.png

記録を開始した状態で、画面操作をします。

スクリーンショット 2023-12-15 15.04.59.png

レコーダーツールを見ると記録がされています。操作が終わったら「記録を終了」をクリックしてください。

スクリーンショット 2023-12-15 15.04.24.png

レコーダーの使い方(再生)

記録後は、リプレイをして同じ操作を行うことが可能です。

スクリーンショット 2023-12-15 15.07.42.png

リプレイするとエラーが出たりします。一旦、該当ステップの三点リーダーから「ステップを削除」しましょう。
※添付の場合は、次のキーアップもエラーになりますので、こちらも削除します。

スクリーンショット 2023-12-15 15.09.50.png

リプレイでフォームの回答ができました!

スクリーンショット 2023-12-15 15.04.39.png

レコーダーの使い方(エクスポート)

ツール内の上部にある、「下矢印と受け皿のアイコン」(どう書いたら伝わるんだろう?ゴミ箱アイコンの左)を押すと、エクスポートメニューが出てきますので、「Puppeteer(including Lighthouse analysis)」をクリックしましょう。

スクリーンショット 2023-12-15 15.14.33.png

jsファイルがダウンロードできますので、これを使って本題であるPuppeteerの話に入りたいと思います。
(いつも前置きが長い!!)

レコーダーの使い方(補足)

※まだ続きます。
記録している際に「アサーションを追加」や、記録後に「ステップを追加」から、手順の追加や編集も可能です。

スクリーンショット 2023-12-15 15.17.50.png

selectorsのアイコンから、画面のマウスクリックで要素を選択できたり、その要素に対してイベントを追加できたり、値を変更できたりします。

本題)Puppeteerを触ってみよう

環境準備

作業フォルダを作って、ターミナルで開きます。
Puppeteerの公式サイトにあるコマンドを実行します。今回はyarnを使います。
レコーダーで、Lighthouse付きのものをエクスポートしたのでこちらも入れておきます。

yarn add puppeteer lighthouse

動作確認

レコーダーからエクスポートしたファイル「test_20231215_1500.js」を同じフォルダにおいて実行してみましょう。

% ls -la
total 96
drwxr-xr-x    6 kimura  staff    192 12 15 15:35 .
drwxr-xr-x    8 kimura  staff    256 12 15 15:34 ..
drwxr-xr-x  108 kimura  staff   3456 12 15 15:35 node_modules
-rw-r--r--    1 kimura  staff     55 12 15 15:35 package.json
-rw-------@   1 kimura  staff   6278 12 15 15:16 test_20231215_1500.js
-rw-r--r--    1 kimura  staff  33958 12 15 15:35 yarn.lock

% node test_20231215_1500.js 
% 

はい、何も起きません。エラーも起きません。
(実際はlighthouseの生成ファイルができています。)

エクスポートされたコードを見てみましょう。
test_20231215_1500.js
const fs = require('fs');
const puppeteer = require('puppeteer'); // v20.7.4 or later

(async () => {
    const browser = await puppeteer.launch({headless: 'new'});
    const page = await browser.newPage();
    const timeout = 5000;
    page.setDefaultTimeout(timeout);

    const lhApi = await import('lighthouse'); // v10.0.0 or later
    const flags = {
        screenEmulation: {
            disabled: true
        }
    }
    const config = lhApi.desktopConfig;
    const lhFlow = await lhApi.startFlow(page, {name: 'test_20231215_1500', config, flags});
    {
        const targetPage = page;
        await targetPage.setViewport({
            width: 773,
            height: 717
        })
    }
    await lhFlow.startNavigation();
    {
        const targetPage = page;
        const promises = [];
        const startWaitingForEvents = () => {
            promises.push(targetPage.waitForNavigation());
        }
        startWaitingForEvents();
        await targetPage.goto('https://docs.google.com/forms/d/e/1FAIpQLSf38o8dKt1WlLenQUOw0MsPsUQN-bqM_CA1XE5LGMEd2HknzA/viewform');
        await Promise.all(promises);
    }
    await lhFlow.endNavigation();
    await lhFlow.startTimespan();
    {
        const targetPage = page;
        await puppeteer.Locator.race([
            targetPage.locator('::-p-aria(ニックネームを教えてください。[role=\\"textbox\\"])'),
            targetPage.locator('div.o3Dpx > div:nth-of-type(1) input'),
            targetPage.locator('::-p-xpath(//*[@id=\\"mG61Hd\\"]/div[2]/div/div[2]/div[1]/div/div/div[2]/div/div[1]/div/div[1]/input)'),
            targetPage.locator(':scope >>> div.o3Dpx > div:nth-of-type(1) input')
        ])
            .setTimeout(timeout)
            .click({
              offset: {
                x: 78.66666412353516,
                y: 9.80206298828125,
              },
            });
    }
    {
        const targetPage = page;
        await puppeteer.Locator.race([
            targetPage.locator('::-p-aria(ニックネームを教えてください。[role=\\"textbox\\"])'),
            targetPage.locator('div.o3Dpx > div:nth-of-type(1) input'),
            targetPage.locator('::-p-xpath(//*[@id=\\"mG61Hd\\"]/div[2]/div/div[2]/div[1]/div/div/div[2]/div/div[1]/div/div[1]/input)'),
            targetPage.locator(':scope >>> div.o3Dpx > div:nth-of-type(1) input')
        ])
            .setTimeout(timeout)
            .fill('キムラ');
    }
    {
        const targetPage = page;
        await targetPage.keyboard.down('Enter');
    }
    {
        const targetPage = page;
        await targetPage.keyboard.up('Enter');
    }
    {
        const targetPage = page;
        await puppeteer.Locator.race([
            targetPage.locator('::-p-aria(ニックネームを教えてください。[role=\\"textbox\\"])'),
            targetPage.locator('div.o3Dpx > div:nth-of-type(1) input'),
            targetPage.locator('::-p-xpath(//*[@id=\\"mG61Hd\\"]/div[2]/div/div[2]/div[1]/div/div/div[2]/div/div[1]/div/div[1]/input)'),
            targetPage.locator(':scope >>> div.o3Dpx > div:nth-of-type(1) input')
        ])
            .setTimeout(timeout)
            .fill('キムラ');
    }
    {
        const targetPage = page;
        await puppeteer.Locator.race([
            targetPage.locator('div.o3Dpx > div:nth-of-type(2) > div > div'),
            targetPage.locator('::-p-xpath(//*[@id=\\"mG61Hd\\"]/div[2]/div/div[2]/div[2]/div/div)'),
            targetPage.locator(':scope >>> div.o3Dpx > div:nth-of-type(2) > div > div'),
            targetPage.locator('::-p-text(Qiitaアカウントは持っていますか?持っている持っていない選択を解除)')
        ])
            .setTimeout(timeout)
            .click({
              offset: {
                x: 82.33332824707031,
                y: 61.135406494140625,
              },
            });
    }
    {
        const targetPage = page;
        await puppeteer.Locator.race([
            targetPage.locator('span > div > div:nth-of-type(1) span'),
            targetPage.locator('::-p-xpath(//*[@id=\\"mG61Hd\\"]/div[2]/div/div[2]/div[2]/div/div/div[2]/div[1]/div/span/div/div[1]/label/div/div[2]/div/span)'),
            targetPage.locator(':scope >>> span > div > div:nth-of-type(1) span'),
            targetPage.locator('::-p-text(持っている)')
        ])
            .setTimeout(timeout)
            .click({
              offset: {
                x: 32.666664123535156,
                y: 13.46875,
              },
            });
    }
    {
        const targetPage = page;
        await puppeteer.Locator.race([
            targetPage.locator('div.o3Dpx div:nth-of-type(4) span'),
            targetPage.locator('::-p-xpath(//*[@id=\\"mG61Hd\\"]/div[2]/div/div[2]/div[3]/div/div/div[2]/div[1]/div[4]/label/div/div[2]/div/span)'),
            targetPage.locator(':scope >>> div.o3Dpx div:nth-of-type(4) span'),
            targetPage.locator('::-p-text(知識になった/ためになった)')
        ])
            .setTimeout(timeout)
            .click({
              offset: {
                x: 58.666664123535156,
                y: 16.46875,
              },
            });
    }
    await lhFlow.endTimespan();
    await lhFlow.startNavigation();
    {
        const targetPage = page;
        const promises = [];
        const startWaitingForEvents = () => {
            promises.push(targetPage.waitForNavigation());
        }
        await puppeteer.Locator.race([
            targetPage.locator('div.lRwqcd > div > span'),
            targetPage.locator('::-p-xpath(//*[@id=\\"mG61Hd\\"]/div[2]/div/div[3]/div[1]/div[1]/div/span)'),
            targetPage.locator(':scope >>> div.lRwqcd > div > span')
        ])
            .setTimeout(timeout)
            .on('action', () => startWaitingForEvents())
            .click({
              offset: {
                x: 54.33332824707031,
                y: 12.80206298828125,
              },
            });
        await Promise.all(promises);
    }
    await lhFlow.endNavigation();
    const lhFlowReport = await lhFlow.generateReport();
    fs.writeFileSync(__dirname + '/flow.report.html', lhFlowReport)

    await browser.close();

})().catch(err => {
    console.error(err);
    process.exit(1);
});

レコーダーからのエクスポートだと、デフォルトがヘッドレスで起動する設定になっていますので、下記を変更します。

test_20231215_1500.js
// [変更前]
//    const browser = await puppeteer.launch({headless: 'new'});
//[変更前]
    const browser = await puppeteer.launch({headless: false});

再度実行します。

% node test_20231215_1500.js 
% 

Chromeが起動し、レコーダーで記録した同じ操作を実行し、Chromeが終了しました。(ヘッドフルでテストしている様子が見れました!)

サイトの分析・診断

実行後に、実は「flow.report.html」というファイルが生成されています。
こちらをブラウザで開いてみてください。
テストした対象ページについての分析・診断レポートが確認できます。

スクリーンショット 2023-12-15 15.57.31.png

スクリーンショット 2023-12-15 15.57.37.png

Performance、Accessibility、Best Practices、SEO、PWAの5つの項目で診断した結果が表示されておりますので、ページの品質向上やSEO改善に役立てられると思います。

E2Eテストとしての利用

ここまでpuppeteerの簡単に使うための部分を説明してきましたが、E2Eテストで使うには、jsを書いて検証コードを書いたり、JESTと連携する方法が良いそうです。
今回の記事では対象外とさせていただきます。

おまけ

別で、AWSのCloudwatch Synthetics(E2Eの監視サービス)を触る機会があり、SyntheticsがPuppeteerのRuntimeをサポートしているということで、そこまで活用するような可能性を探りたかったのですが、残念ながらPuppeteerのバージョンの不一致で、簡単な活用(貼り付けてすぐ動かす)はできませんでした。

  • Chrome 「レコーダー」ツールからエクスポートされるjsファイル
    • v20.7.4 or later
  • SyntheticsのRuntime
    • Puppeteer-core 19.7.0

でも、Syntheticsの活用も紹介したいと思いますので簡単に触れておきます。

AWS Syntheticsで遊んでみる

AWSマネコンのCloudwatchのページにSyntheticsがあります。

スクリーンショット 2023-12-15 19.05.52.png

「Create canary」から設定していきましょう。
簡単に使ってみるのであれば、blueprintを活用して、画面操作での設定もできますので、そちらを紹介します。

スクリーンショット 2023-12-15 19.08.28.png

Chromeの拡張機能が必要なので、インストールしてください。

URLを入れて、「Open URL in new tab」をクリックします。

スクリーンショット 2023-12-15 19.09.44.png

対象ページで、拡張機能を起動して、「Start recording」をクリックして操作を記録します。

スクリーンショット 2023-12-15 19.10.52.png

操作完了したら、「End recording」をクリックすると、コードが表示されます。

スクリーンショット 2023-12-15 19.11.48.png

「Copy to clipboard」をクリックしてコードをコピーします。

スクリーンショット 2023-12-15 19.11.52.png

Syntheticsに戻って、Canary builderの「Name」に任意の名前、Script editorのエディタにコピーしたコードを貼り付けます。

スクリーンショット 2023-12-15 19.13.22.png

その他の設定は適宜、設定してください。
Scheduleのところだけ、テスト利用であれば、「Run once」にしておくと1回だけしか動かないので安心です。

設定が終わったら、ページ下部の「Create canary」をクリックしてください。

canary作成後、チェックが走り、ダッシュボードに結果が表示されます。

スクリーンショット 2023-12-15 19.17.58.png

詳細に入ってみると、画面で記録した操作がStepとしてチェックされていることがわかります。

スクリーンショット 2023-12-15 19.18.59.png

Syntheticsの詳しい使い方に関しては、ぜひ色々調べてみてください。

まとめ

Chromeの「レコーダー」ツールで、画面操作しながらテストケースを作成して、手元のPuppeteerで実行し、分析まで行うということをやってみました。
他のE2Eツールでもっと良いものもあると思いますが、サクッと触ってみたり、検証データ作成する、WEBサイトの品質向上で、簡単に使ってみるのはアリかもしれません。
※ビジネスシーンで信頼できるかは自己判断にてお願いいたします。

また、画面操作で記録して監視できるSyntheticsも紹介させていただきました。

どちらも、画面操作でテストケースが作れるというところが便利であり、エンジニアでなくても活用できる点が特徴かなと思います。

ぜひ、触ったことないよーという方がいれば、簡単なので、触ってみるキッカケになれば幸いです。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?