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 img
Code 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-bold
Code 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をコピーしました