PuPHPeteerでE2Eテスト(5) amazonのカート

今回はページオブジェクトモデルで、amazonで「テスト駆動開発」をカートに入れるテストプログラムを作ります。(ログインはせず、購入手続きまで進みませんので、心配はいりません)

PuPHPeteerとは

WebアプリケーションのE2Eテストフレームワークの一つに、Puppeteer(パペッティア)があります。Puppeteerはブラウザを起動し、ブラウザを操作してE2Eテストをします。

PuPHPeteerは、PHPからPuppeteerを使うためのライブラリです。

前回

前々回のJpostalを使った郵便番号から住所を自動入力するページのテストプログラムをページオブジェクトモデルでリファクタしました。

今回のテスト

今回は、amazon.co.jpで「テスト駆動開発」を検索し、カートに入れて、3080円であることをテストします。

まず、手でブラウザを操作します。ねんのため、ブラウザをシークレットモードで開くか、amazonからログアウトしてください。

テストしたいシナリオです。

  • amazon.co.jpトップページを表示します(https://www.amazon.co.jp/)
  • 検索ワード欄で「テスト駆動開発」を入力します。
  • 検索ボタンをクリックます。
  • 検索結果ページで、1番目のサムネイル画像をクリックします。
  • 商品詳細ページで、「カートに入れる」ボタンをクリックします。
  • カートの小計に「3080」と表示されているはずです。

amazon.co.jpトップを表示します

tests/AmazonTest.phpを新規作成します。

<?php

namespace Ninton\Test;

use Ninton\Test\Libs\Amazon\TopPage;

class AmazonTest extends BasePuppeteerTestCase
{
    public function test_テスト駆動開発を購入(): void
    {
        $topPage = TopPage::goto($this->page);
        $this->screenShot();

        $this->assertTrue(true);
    }
}
Code language: PHP (php)

tests/Libs/Amazon/TopPage.phpを新規作成します。

<?php

namespace Ninton\Test\Libs\Amazon;

use Nesk\Puphpeteer\Resources\Page;
use Ninton\Test\Libs\BasePage;

class TopPage extends BasePage
{
    public static function goto(Page $page): TopPage
    {
        $page->goto('https://www.amazon.co.jp');
        return new TopPage($page);
    }
}
Code language: PHP (php)

ここでテストを実行します。

$ ./phpunit.sh tests/AmazonTest.php 
+ ./vendor/bin/phpunit --configuration=phpunit.xml tests/AmazonTest.php
PHPUnit 8.5.14 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 8.46 seconds, Memory: 6.00 MB

OK (1 test, 1 assertion)
Code language: Bash (bash)

スクリーンショット画像も保存されました。

上部の検索欄で「テスト駆動開発」を入力します。

tests/AmazonTest.phpのテストメソッド内に追加します。

        $topPage->type検索ワード('テスト駆動開発');
Code language: plaintext (plaintext)

検索ワード欄のHTMLソースを見ると、id="twotabsearchtextbox" が設定されていました。CSSセレクターは"#twotabsearchtextbox"です。

<input type="text" id="twotabsearchtextbox" value="" name="field-keywords" autocomplete="off" placeholder="" class="nav-input nav-progressive-attribute" dir="auto" tabindex="0" aria-label="検索">Code language: HTML, XML (xml)

tests/Lib/Amazon/TopPage.phpに、type検索ワード()メソッドを追加します。

    public function type検索ワード(string $text): void
    {
        $this->pageType('#twotabsearchtextbox', $text);
    }
Code language: PHP (php)

ここでテストを実行すると、検索ワード欄に「テスト駆動開発」と入力されたのが、一瞬見えました。

検索ボタンをクリックます。

tests/AmazonTest.phpの テストメソッド内に追加します。

        $topPage->click検索();
        $this->waitForPageLoad();
Code language: plaintext (plaintext)

検索ボタンのHTMLソースを見ると、id="nav-search-submit-button" が設定されていました。CSSセレクターは、"#nav-search-submit-button" です。

<input id="nav-search-submit-button" type="submit" class="nav-input nav-progressive-attribute" value="検索" tabindex="0">Code language: HTML, XML (xml)

tests/Lib/Amazon/TopPage.phpに、click検索()メソッドを追加します。

    public function click検索(): void
    {
        $this->page->click('#nav-search-submit-button');
    }
Code language: PHP (php)

ここでテストを実行すると、検索結果のページが表示されました。

検索結果ページです。1番目のサムネイル画像をクリックします。

tests/AmazonTest.phpに追加します。

use Ninton\Test\Libs\Amazon\SearchResultPage;
...

    public function test_テスト駆動開発を購入(): void
    {
        ...

        $searchResultPage = new SearchResultPage($this->page);
        $searchResultPage->click最初のサムネイル画像();
        $this->waitForPageLoad();

Code language: plaintext (plaintext)

検索結果のサムネイル画像まわりのHTMLソースを見ると、DOM idは設定されていません。

まず、サムネイル画像を囲むAタグを探して、右クリック>Copy>Copy SelectorでコピーしたCSSセレクターを試してみます。

tests/Lib/Amazon/SearchResultPage.phpを新規作成します。

<?php

namespace Ninton\Test\Libs\Amazon;

use Ninton\Test\Libs\BasePage;

class SearchResultPage extends BasePage
{
    public function click最初のサムネイル画像(): void
    {
        $selector = 'div.sg-col-4-of-12:nth-child(1) > div:nth-child(1) > span:nth-child(1) > div:nth-child(1) > div:nth-child(1) > span:nth-child(2) > a:nth-child(1)';
        $this->page->click($selector);
    }
}

Code language: PHP (php)

ここでテストを実行します。

検索結果に「テスト駆動開発」は表示されますが、検索結果の手前にPR領域が表示されて、別の本をクリックしてしまいました。何回か実行してみると、PR領域に表示される本の個数が変化したり、PR領域自体が表示されないこともありました。

最初のバージョンのCSSセレクターは「検索結果の1番目」を特定できないようです。DOM idではないCSSセレクターは、DOM構造やDOM順序の変化に弱いのが難点です。

HTMLソースを調べていると、次のようなDOM構造がわかりました。

<div class="s-search-results">

  <!-- 1冊目 -->
  <div class="s-result-item">
    <a href=""><img src="" /></a>
  </div>

  <!-- 2冊目 -->
  <div class="s-result-item">
    <a href=""><img src="" /></a>
  </div>
</div>Code language: HTML, XML (xml)

CSSセレクターとして使えそうな箇所を取り出すと、

.s-search-results
  .s-result-item
    a
      imgCode language: HTML, XML (xml)

そこで、CSSセレクターを次のようにしたところ、1冊目をクリックすることができました。

    public function click最初のサムネイル画像(): void
    {
        $selector = '.s-search-results .s-result-item a img';
        $this->page->click($selector);
    }
Code language: PHP (php)

ところが、新たな問題です。

サムネイル画像をクリックすると、新しいタブに商品詳細ページが表示されてしまいました。$this->pageは現在のタブを表していて、新しいタブを操作できません。PuPHPeteerから、target="_blank"で開いた新しいタブにアクセスする方法がわかりませんでした。

レビューの★の下の「単行本(ソフトカバー)」をクリックしてみると、現在のタブに商品詳細ページが表示されました。サムネイル画像ではなく、これをクリックすることにします。

レビューの★の下の「単行本(ソフトカバー)」のHTMLソースを見ると、次のDOM構造でした。

<div class="s-search-results">

  <!-- 1冊目 -->
  <div class="s-result-item">
    <a class="a-text-bold" href="">単行本(ソフトカバー)</a>
  </div>

  <!-- 2冊目 -->
  <div class="s-result-item">
    <a class="a-text-bold" href="">大型本</a>
  </div>
</div>Code language: HTML, XML (xml)

CSSセレクターとして使えそうな箇所を取り出すと、

.s-search-results
  .s-result-item
    a.a-text-boldCode language: HTML, XML (xml)

そこで、CSSセレクターを次のようにしました。

    public function click最初のサムネイル画像(): void
    {
        $selector = '.s-search-results .s-result-item a.a-text-bold';
        $this->page->click($selector);
    }

Code language: PHP (php)

テストを実行してみると、現在のタブに商品詳細ページを表示できるようになりました。

検索結果に10冊表示されていたとしてさ、このCSSセレクターをクリックしたら、10冊すべてのAタグをクリックしないのかな?

Puppeteerのpage.clickのドキュメントに書いてあったわ

page.click(selector[, opions])
selector: A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.

selector: クリックする要素を検索するためのセレクター。セレクターを満たす要素が複数ある場合は、最初の要素がクリックされます。

https://pptr.dev/#?product=Puppeteer&version=v8.0.0&show=api-pageclickselector-options

page.clickは見つかった最初の要素をクリックするから、心配ないわね

ところが、別の問題です。

click後のwaitForPageLoad()でタイムアウトしてしまいます。

        $searchResultPage->click最初のサムネイル画像();
        $this->waitForPageLoad(); // ここでタイムアウトしてしまう...
        $this->screenShot();
Code language: plaintext (plaintext)

waitForPageLoad()で待つのはあきらめて、別の方法でページ表示の完了を待つことにします。

waitForPageLoad()を削除します。

        $searchResultPage->click最初のサムネイル画像();
        // $this->waitForPageLoad();
        // $this->screenShot();
Code language: plaintext (plaintext)

商品詳細ページです。「カートに入れる」ボタンをクリックします。

tests/AmazonTest.phpのテストメソッドに次のコードを追加します。

use Ninton\Test\Libs\Amazon\DetailPage;
...

    public function test_テスト駆動開発を購入(): void
    {
        ...

        $detailPage = new DetailPage($this->page);
        $detailPage->waitForカートに入れるボタン();
        $this->screenShot();
Code language: plaintext (plaintext)

tests/Libs/Amazon/DetailPage.phpを新規作成します。

click後のwaitForPageLoadでタイムアウトしてしまうので、その代わりに、「カートに入れる」ボタンが表示されたら、ページ表示が完了したと判断することにしました。

<?php

namespace Ninton\Test\Libs\Amazon;

use Ninton\Test\Libs\BasePage;

class DetailPage extends BasePage
{
    private const ADD_TO_CART_BUTTON = '#add-to-cart-button';

    public function waitForカートに入れるボタン(): void
    {
        $this->page->waitForSelector(self::ADD_TO_CART_BUTTON);
    }
}
Code language: PHP (php)

「カートに入れる」ボタンのHTMLソースを見ると、id="add-to-cart-button"なので、CSSセレクターは"#add-to-cart-button"を設定しました。

<input id="add-to-cart-button" name="submit.add-to-cart" title="カートに入れる" data-hover="左から<b>__dims__</b>を選択して<br>ショッピングカートに追加" class="a-button-input" type="submit" value="カートに入れる" aria-labelledby="submit.add-to-cart-announce">Code language: HTML, XML (xml)

ここで、テストを実行すると、無事OKでした。

つづけて、tests/AmazonTest.phpのテストメソッドに次のコードを追加します。click後、すぐにブラウザを閉じてしまうので、sleep(5); で待つことにします。

        $detailPage->clickカートに入れるボタン();
        sleep(5);
Code language: plaintext (plaintext)

tests/Libs/Amazon/DetailPage.phpに「カートに入れる」ボタンをクリックするメソッドを追加します。

    public function clickカートに入れるボタン(): void
    {
        $this->page->click(self::ADD_TO_CART_BUTTON);
    }
Code language: PHP (php)

常にwaitForSelectorでページ表示を待てばいいんじゃないの?

ブラウザ表示を見ていると、そのDOM要素が表示されていても、ときどきwaitForSelectorもタイムアウトしてしまうことがあるの

あとね、このページにしかないDOM要素だ!と思っていると、条件によっては表示されないことがあったりするのよ

難しいんだね

sleep、waitForNavigation、watiForSelectorを試したり、組み合わせてみてね

カートの小計に「3080」と表示されているはずです。

tests/AmazonTest.phpのテストメソッドから、さきほどのsleep(5); を削除して、次のコードを追加します。

use Ninton\Test\Libs\Amazon\AddedToCartPage;
...

    public function test_テスト駆動開発を購入(): void
    {
        ...

        // sleep(5);

        $addedToCartPage = new AddedToCartPage($this->page);
        $addedToCartPage->waitForレジに進むボタン();
        $this->screenShot();
Code language: plaintext (plaintext)

tests/Libs/Amazon/AddedToCart.phpを新規作成します。

「レジに進む」ボタンが表示されたら、ページ表示が完了したと判断することにします。

<?php

namespace Ninton\Test\Libs\Amazon;

use Ninton\Test\Libs\BasePage;

class AddedToCartPage extends BasePage
{
    private const CHECKOUT_BUTTON = '#hlb-ptc-btn-native';

    public function waitForレジに進むボタン(): void
    {
        $this->page->waitForSelector(self::CHECKOUT_BUTTON);
    }
}
Code language: PHP (php)

「レジに進む」ボタンのHTMLソースを見ると、id="hlb-ptc-btn-native"なので、CSSセレクターは"#hlb-ptc-btn-native"を設定しました。

<a id="hlb-ptc-btn-native" href="https://www.amazon.co.jp/ap/signin?_encoding=UTF8..." class="a-button-text a-text-center" role="button">
      レジに進む
    </a>Code language: HTML, XML (xml)

ここで、テストを実行すると、無事OKでした。

最後に、カートの小計に、"3080"が表示されていることを検証します。

tests/AmazonTest.phpのテストメソッドに、次のコードを追加します。

        $subTotal = $addedToCartPage->getカートの小計();
        $this->assertEquals('3080', $subTotal);
Code language: plaintext (plaintext)

tests/Libs/Amazon/AddedToCartPage.php に次のメソッドを追加します。

取り出した文字列の先頭と末尾のスペースや改行は、ltrim関数、rtrim関数で削除します。

数字とマイナス以外を削除して、「¥3,080」を「3080」にします。

    public function getカートの小計(): string
    {
        $selector = '#hlb-subcart  span.hlb-price';
        $text = $this->pageGetText($selector);
        $text = ltrim(rtrim($text));
        $text = preg_replace('/[^-0-9]/', '', $text);
        return $text;
    }
Code language: plaintext (plaintext)

Libs/BasePage.phpに、次のメソッドを追加します。

pageGetValueメソッドとほとんど同じですが、getProperty('value')ではなく、getProperty('textContent')とします。

    public function pageGetText(string $selector): string
    {
        $el = $this->page->querySelector($selector);
        $value = $el->getProperty('textContent')->jsonValue();
        return $value;
    }
Code language: plaintext (plaintext)

テストを実行すると、無事OKです。約20秒かかりました。

$ ./phpunit.sh tests/AmazonTest.php 
+ ./vendor/bin/phpunit --configuration=phpunit.xml tests/AmazonTest.php
PHPUnit 8.5.14 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 20.14 seconds, Memory: 6.00 MB
Code language: Bash (bash)

まとめ

tests/AmazonTest.php

<?php

namespace Ninton\Test;

use Ninton\Test\Libs\Amazon\AddedToCartPage;
use Ninton\Test\Libs\Amazon\DetailPage;
use Ninton\Test\Libs\Amazon\SearchResultPage;
use Ninton\Test\Libs\Amazon\TopPage;

class AmazonTest extends BasePuppeteerTestCase
{
    public function test_テスト駆動開発を購入(): void
    {
        $topPage = TopPage::goto($this->page);
        $this->screenShot();

        $topPage->type検索ワード('テスト駆動開発');
        $topPage->click検索();
        $this->waitForPageLoad();
        $this->screenShot();

        $searchResultPage = new SearchResultPage($this->page);
        $searchResultPage->click最初のサムネイル画像();

        $detailPage = new DetailPage($this->page);
        $detailPage->waitForカートに入れるボタン();
        $this->screenShot();

        $detailPage->clickカートに入れるボタン();

        $addedToCartPage = new AddedToCartPage($this->page);
        $addedToCartPage->waitForレジに進むボタン();
        $this->screenShot();

        $subTotal = $addedToCartPage->getカートの小計();
        $this->assertEquals('3080', $subTotal);
    }
}Code language: plaintext (plaintext)

githubへのリンク

最初のテストを作るのは手間がかかるね

そうね、ページを表示して、HTMLソースを探して、CSSセレクターを決めるところが時間がかかるわね。

つづく(シナリオトレイト)

タイトルとURLをコピーしました