CodeceptJSのhelper、playWright vs WebDriver

# 概要
またまた対決シリーズ、且つ、どっちが良いとも結論付けないのですが、
[Selenium学習サイト](https://hotel.testplanisphere.dev/ja/)の学習中に遭遇した、helperにplayWrightを利用した場合とWebDriverを利用した場合とで気がついた違いについてです。どちらが良いとか悪いとかでは無く、適宜選択して使えればと思います。その際の判断材料になれば。
表にまとめてみました。

|helper|playWright|WebDriver|
|:---|:---|:---|
|Popup続けて2つ|1個目がスルーされる。Popup already exists and was not closed. Popups must always be closed by calling either I.acceptPopup() or I.cancelPopup()|個々に検出|
|waitForClickable|I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable. Remove usage of this function|waitForClickableで待てる|
|waitForClickable|ボタンのテキストで指定可|CSSで指定する必要があった|
|seeInField|検出可能|Element input[name=tel] has no value attribute. だが、grabValueFromでは検出可能。たぶん[これ](https://github.com/codeceptjs/CodeceptJS/issues/2965)|
|対応ブラウザ|Chrome / Firefox / Safari|WebDriverがあるもの全て。EDGEも可|
画面遷移やメニュー出現を待つ処理がplayWrightではautomaticalyということでclickableを待つのを不要という割りには、画面遷移で（ログアウト後の再ログインでよく発生しました）テストが止まってしまうのは困りましたが、他のwait（waitForElementとか）試したわけでは無いので旨く使えばplayWrightでも良かったかもしれません。wait(秒)も、今後、絶対使わないで済ませる自信ありません。Popupも2つとも検証が絶対必要？
と必ずしもplayWrightをhelperに使わない理由にはならないような気もします。決め手としてはEDGEも確認できるというものですが、EDGEだって最近のはchromium EDGEなので、絶対必要かと尋ねられれば　？です。
helperにWebDriverを使うの初でしたので、動くまでいろいろ壁ありました。（Selenium Serverの件とか）seeInField使えなかったのもショックだったし。
結果的にSelenium学習サイトでは、WebDriverの採用となりました。

実行環境は
codeceptVersion:  "3.1.1"
nodeInfo:  15.8.0
osInfo:  Windows 10 10.0.19043
cpuInfo:  (16) x64 Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz
です。
# CodeceptJSで、helperにWebDriverを使用する手順
WebDriverをhelperに使うの初めてだったので備忘録。
まず、プロジェクトフォルダにて、CodeceptJSをインストール

```
npm install codeceptjs --save
```
次に、selenium standalone serverとドライバ一式をインストール

```
npx selenium-standalone install
```
npx codeceptjs initで、helperには WebDriver を選択する。
Codeceptjsのサイトでは、webdriverioをcodeceptjs内で起動するため、以下をhelperのWebDriverに追加するように紹介されていますが、

```codecept.conf.js
host: '127.0.0.1',
port: 4444,
restart: false
```
また、pluginsにも追加するように紹介されていますが、

```codecept.conf.js
wdio: {
  enabled: true,
  services: ['selenium-standalone']
}
```

その場合、テスト実行後にDriverプロセスが起動したままになってしまい、次々にプロジェクトを切替えてテスト実行することができません。以下のブログに事例がありました。
[CodeceptJSを動かしてみる](https://kkayataka.hatenablog.com/entry/2019/09/07/155717)
なので、上記2つはcodecept.conf.jsに**追記しません。**
何も追記しないので、**npx codeceptjs init でWebDriverを選択しただけ**ということになります。
注）allure等のプラグインは必要に応じて追記ください。
ブラウザ毎にプロジェクトを設置して、PowerShellで一気に実行したかったので、各ブラウザでのテストが終了したら、起動した各ブラウザのDriverプロセスが終了しない点で、CodeceptJSからDriverを起動せずに、standalone serverを、テスト前に予め起動することにしました。

```
npx selenium-standalone start
```
で起動できます。AppiumでAppiumサーバ起動しておくのに似てますね。

# helper に playWrightを使うべきかWebDriverを使うべきか
Selenium学習サイトで会員削除操作をすると、2つのPopup「退会すると全ての情報が削除されます。」と「退会処理を完了しました。ご利用ありがとうございました。」というPopupが現れて、それぞれacceptのボタンがあるのですが、どうしても最初のPopupで一旦停止せず、こんな感じでエラーになります。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">helperがplayWrightだと、Popupスルーしてしまう <a href="https://t.co/9s0nVdsZoX">pic.twitter.com/9s0nVdsZoX</a></p>&mdash; kazuhiroYoshino (@XSyrIQwKY7Yuy6m) <a href="https://twitter.com/XSyrIQwKY7Yuy6m/status/1430060791546322952?ref_src=twsrc%5Etfw">August 24, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

```
   I.click('退会する');
   I.seeInPopup('退会すると全ての情報が削除されます。');
   I.acceptPopup();
   I.seeInPopup('退会処理を完了しました。ご利用ありがとうございました。');
   I.acceptPopup();
```
としているので、acceptPopupはしているつもり。waitを行間に入れてもダメでしたので、どうやらplayWrightをhelperに利用した場合、Popupが連続するのが苦手なように見えます。
同じテストシナリオでも、helperにWebDriverを利用した場合には問題なく各ポップアップの文言を検証できるので、敢えて苦手なものに時間費やすより、できるものを使えばいいと考えます。WebDriverをhelperに使うの初だったので、不安はありましたが。
さぁこれで先に進めると思った矢先、次の壁。WebDriverをhelperに利用した場合、テキストボックス内の文言検証が、seeInField 使えませんでした。playWrightでは発生しませんし、Appiumでも問題なくseeInField使えていました。
「Element input[name=tel] has no value attribute」
attribute を持ってませんよ。みたいな。
ダメ元で grabValueFrom を使ってみたら、値取得できました。同じattribute（value）だと思うのですが、挙動が異なるように見えます。値取得できたのでassert使って検証しました。本当はseeの類いで済ませられるとCodeceptJSの良いところが発揮できるのですが。
繰り返しになりますが、playWrightよりWebDriverを使った方が良いという話ではありません。両方使えるのがベストかと思います。WebDriverは初と言っても、Seleniumでは利用実績有りましたので、seeInFieldがダメでもgrabValueFromだったらいけるかな？　といったのも生Seleniumでの要素取得に苦労した経験が活かせたのかもと思っています。
# テストシナリオ
Appiumで作成したシナリオがほとんどそのまま使えました。会員登録検証は、登録とマイページの検証、会員種別毎に宿泊予約ページに表示されるメニューの違いを検証しています。テストデータは省いてますが、テストシナリオは、

```ReserveTestByChrome_test.js
Feature('会員登録機能_By_Chrome');

Data(reserveTestTable).Scenario('会員登録_ログイン_Planメニュー確認_退会', async({I , current}) => {
    I.amOnPage('https://hotel.testplanisphere.dev/ja/');
//会員登録
    I.click('会員登録');
    I.fillField('email', current.メルアド);
    I.fillField('password', current.パスワード);
    I.fillField('password-confirmation', current.パスワード);
    I.fillField('username', current.氏名);
    if(current.ランク == 'プレミアム会員'){
        I.click('input[name="rank"]');
    }
    if(current.ランク == '一般会員'){
        I.click('#rank-normal');
    }
    if((current.住所).length != 0){
        I.fillField('address', current.住所)
    }
    if((current.電話).length != 0){
        I.fillField('tel', current.電話);
    }
    if(current.性別 == '男性'){
        I.selectOption('gender', '男性');
    }
    if(current.性別 == '女性'){
        I.selectOption('gender', '女性');
    }
    if(current.性別 == 'その他'){
        I.selectOption('gender', 'その他');
    }
    if(current.性別 == '回答しない'){
        I.selectOption('gender', '回答しない');
    }
    if((current.生年月日).length != 0){
        let birth = current.生年月日;
        let bDay = await I.executeScript(function(birth) {
//            var bDay = birth;   # 未だに引数に渡せてません
            var bDay = '1960-12-11';
            $(birthday).val(bDay);
        }, '#birthday');
    }
    if(current.お知らせ == '受け取る'){
        I.checkOption('notification');
    }
    I.waitForClickable('#signup-form > button');
    I.click('登録');
    I.waitForClickable('#logout-form > button');
    I.click('ログアウト');

//登録したアカウントで再ログイン。登録内容の検証
    I.waitForClickable('#login-holder > a');
    I.click('ログイン');
    I.fillField('email', current.メルアド);
    I.fillField('password', current.パスワード);
    I.waitForClickable('#login-button');
    I.click('#login-button');
    I.see(current.メルアド);
    I.see(current.氏名);
    I.see(current.ランク)
    if((current.住所).length != 0){
        I.see(current.住所);
    }
    if((current.電話).length != 0){
        I.see(current.電話);
    }
    if(current.性別 == '男性'){
        I.see(current.性別);
    }
    if(current.性別 == '女性'){
        I.see(current.性別);
    }
    if(current.性別 == 'その他'){
        I.see(current.性別);
    }
    if(current.性別 == '回答しない'){
        I.see('未登録');
    }
    if((current.生年月日).length != 0){
        I.checkBirthday(current.生年月日);
    }
    I.see(current.お知らせ);
//宿泊予約メニュー確認
    I.click('宿泊予約');
    I.scrollPageToBottom();
    if(current.ランク == '一般会員'){
        I.dontSee('プレミアムプラン');
        I.see('ディナー付きプラン');
        I.see('お得なプラン');
    }
    if(current.ランク == 'プレミアム会員'){
        I.see('プレミアムプラン');
        I.see('ディナー付きプラン');
        I.see('お得なプラン');
    }

//退会
    I.click('マイページ');
    I.waitForClickable('#delete-form > button');
    I.click('退会する');
   I.seeInPopup('退会すると全ての情報が削除されます。');
    I.acceptPopup();
    I.seeInPopup('退会処理を完了しました。ご利用ありがとうございました。');
    I.acceptPopup();
});
```
次に会員登録後の宿泊予約検証を、Appiumでの検証で作成済みのテストシナリオをほぼ流用できますが、前述のようにテキストボックス内の初期値検証（会員登録時の氏名や電話番号が初期値になる）において、修正しています。
以下がテストシナリオです。
宿泊料金、部屋タイプ、会員で予約した場合の氏名と連絡先の初期値を検証しています。こちらもテストデータを省いていますが、

```ReserveByMemberChrome_test.js
Feature('Member予約機能_By_Chrome');

Data(reserveTestTable).Scenario('Member予約_部屋タイプ確認_料金確認_By_Chrome', async({I , current}) => {
    I.amOnPage('https://hotel.testplanisphere.dev/ja/');
//会員登録
    I.waitForClickable('#navbarNav > ul > li:nth-child(3) > a');
    I.click('会員登録');
    I.fillField('email', current.メルアド);
    I.fillField('password', current.パスワード);
    I.fillField('password-confirmation', current.パスワード);
    I.fillField('username', current.氏名);
    if(current.ランク == 'プレミアム会員'){
        I.click('input[name="rank"]');
    }
    if(current.ランク == '一般会員'){
        I.click('#rank-normal');
    }
    if((current.住所).length != 0){
        I.fillField('address', current.住所)
    }
    if((current.電話).length != 0){
        I.fillField('tel', current.電話);
    }
    if(current.性別 == '男性'){
        I.selectOption('gender', '男性');
    }
    if(current.性別 == '女性'){
        I.selectOption('gender', '女性');
    }
    if(current.性別 == 'その他'){
        I.selectOption('gender', 'その他');
    }
    if(current.性別 == '回答しない'){
        I.selectOption('gender', '回答しない');
    }
    if((current.生年月日).length != 0){
        let birth = current.生年月日;
        let bDay = await I.executeScript(function(birth) {
//            var bDay = birth;
            var bDay = '1960-12-11';
            $(birthday).val(bDay);
        }, '#birthday');
    }
    if(current.お知らせ == '受け取る'){
        I.checkOption('notification');
    }
    I.waitForClickable('#signup-form > button');
    I.click('登録');

//会員で予約
    I.waitForClickable('#navbarNav > ul > li:nth-child(3) > a');
    I.click('宿泊予約');
    I.clickPlan(current.宿泊プラン);
    I.switchToNextTab(1);
    I.see(current.宿泊プラン);
    await I.grabValueFrom('#date');
    I.fromDay(current.宿泊初日);
    I.click('閉じる');
    I.fillField('term', current.連泊数);
    I.fillField('head-count', current.宿泊人数);
    if(current.朝食 == 'on'){
        I.checkOption('#breakfast');
    }
    if(current.昼からチェックインプラン == 'on'){
        I.checkOption('#early-check-in');
    }
    if(current.お得な観光プラン == 'on'){
        I.checkOption('#sightseeing');
    }
//会員は氏名が初期値として入力される
     let regName = await I.grabValueFrom('input[name=username]');
     I.assertEqual(regName, current.氏名);
//    I.seeInField('#username', current.氏名);
    I.selectOption('contact', current.確認のご連絡);
    if(current.確認のご連絡 == 'メールでのご連絡'){
        if((current.メルアド).length != 0) {
//会員はメールアドレスが登録されていたら初期値として入力済みになる
            let mail = await I.grabValueFrom('input[name=email]');
            I.assertEqual(mail, current.メルアド);
//            I.seeInField('#email', current.メルアド);
        }else{
            I.fillField('email', current.メルアド);
        }
    }
    if(current.確認のご連絡 == '電話でのご連絡'){
            if((current.電話).length != 0) {
//会員は電話番号が登録されていたら初期値として入力済みになる
                   let telephone = await I.grabValueFrom('input[name=tel]');
                   I.assertEqual(telephone, current.電話);
//                I.seeInField('input[name=tel]', current.電話);
            }else{
                I.fillField('tel', current.電話番号)
            }
    }
    if(current.ご要望 == '144文字'){
        I.fillField('comment', message);
    }
    if((current.宿泊プラン == 'お得な特典付きプラン') || (current.宿泊プラン == 'プレミアムプラン') || (current.宿泊プラン == '素泊まり') || (current.宿泊プラン == '出張ビジネスプラン') || (current.宿泊プラン == 'カップル限定プラン')){
        I.switchTo('#room-info > iframe');
        I.see(current.部屋タイプ);
        I.switchTo();
    }else{
        I.see(current.部屋タイプ);
    }
    let totalBill = await I.grabTextFrom('#total-bill');
    I.seeBill(current.合計料金, totalBill);
    let term = await I.grabValueFrom('#term');
    let headcount = await I.grabValueFrom('#head-count');
    I.click('予約内容を確認する');
//確認画面
    I.see(current.宿泊プラン);
    totalBill = await I.grabTextFrom('#total-bill');
    I.seeBill(current.合計料金, totalBill);
    I.seeTerm(term);
    I.seeHeadCount(headcount);
    if(current.朝食 == 'on'){
        I.see('朝食バイキング');
    }
    if(current.昼からチェックインプラン == 'on'){
        I.see('昼からチェックインプラン');
    }
    if(current.お得な観光プラン == 'on') {
        I.see('お得な観光プラン');
    }
    I.see(current.氏名);
    if(current.確認のご連絡 == '希望しない'){
        I.see('希望しない');
    }
    if(current.確認のご連絡 == 'メールでのご連絡'){
        I.see(current.メルアド);
    }
    if(current.確認のご連絡 == '電話でのご連絡'){
        I.see(current.電話番号);
    }
    if(current.ご要望 == '144文字'){
        I.see(message);
    }
    I.click('この内容で予約する');

//予約完了画面
    I.see('予約を完了しました');
    I.see('ご来館、心よりお待ちしております。');
    I.click('閉じる');
});
```
steps_file.jsや合計料金検証用のテストクラスはAppiumで使ったものがそのまま使えました。
やはりMethodを自前で準備せずに済むのは、大きいですね。
あと、fillFieldでテキストボックスにデータを流し込むMethodあるのですが、これ一旦Clearもしてからデータを送っています。初期値が入っている場合とか、そのままデータ入れると初期値に続いて入力されてしまう場合もあるので、生Seleniumでは、clearしてから流し込むMethodを自作することになるのですが、CodeceptJSでは、このあたりもきっちりできてます。
会員登録後の宿泊予約検証に用いるテストデータは、Appiumで使ったPictMasterを流用して、確認するブラウザ（Chrome / Firefox / EDGE）を項目に追加して、デバイスとデバイスの向きを削除しています。
![pict004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/102e97ac-f32a-7473-b4d2-59cad6f7c0af.png)
ALL Pairで生成した会員による予約テストのテストデータ件数はブラウザ3種類で82件でした。こんな感じ。（抜粋）

```ReserveByMemberChrome_test.js
let reserveTestTable = new DataTable(['メルアド','パスワード','氏名','ランク','住所','電話','性別','生年月日','お知らせ','宿泊プラン','宿泊初日','連泊数','宿泊人数','朝食','昼からチェックインプラン','お得な観光プラン','確認のご連絡','電話番号','ご要望','部屋タイプ','合計料金']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','貸し切り露天風呂プラン','Saturday','3','6','on','off','on','電話でのご連絡','','144文字','部屋指定なし','213000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','貸し切り露天風呂プラン','Saturday','3','6','on','off','on','電話でのご連絡','','144文字','部屋指定なし','210000']);
reserveTestTable.add(['kagetora@example.jp','pass1234','長尾景虎','一般会員','','','男性','1960-12-11','受け取らない','お得なプラン','Friday','1','9','off','off','off','電話でのご連絡','01234567890','off','部屋指定なし','54000']);
reserveTestTable.add(['kagetora@example.jp','pass1234','長尾景虎','一般会員','','','男性','1960-12-11','受け取らない','お得な特典付きプラン','Sunday','1','1','on','on','off','メールでのご連絡','','off','スタンダードツイン','10750']);
reserveTestTable.add(['masatora@example.jp','pass5678','真田昌虎','一般会員','京都府宇治市','','男性','1960-12-11','受け取る','エステ・マッサージプラン','Monday','9','6','on','on','off','電話でのご連絡','01234567890','off','部屋指定なし','573000']);
reserveTestTable.add(['masatora@example.jp','pass5678','真田昌虎','一般会員','京都府宇治市','','男性','1960-12-11','受け取る','お得な特典付きプラン','Saturday','1','1','on','off','on','電話でのご連絡','01234567890','144文字','スタンダードツイン','10750']);
reserveTestTable.add(['masatora@example.jp','pass5678','真田昌虎','一般会員','京都府宇治市','','男性','1960-12-11','受け取る','出張ビジネスプラン','Sunday','9','2','on','off','off','希望しない','','off','シングル','164250']);
reserveTestTable.add(['masatora@example.jp','pass5678','真田昌虎','一般会員','京都府宇治市','','男性','1960-12-11','受け取る','素泊まり','Wednesday','1','1','off','off','off','希望しない','','off','シングル','5500']);
```
Firefoxのテストデータ（抜粋）

```ReserveByMemberFirefox_test.js
let reserveTestTable = new DataTable(['メルアド','パスワード','氏名','ランク','住所','電話','性別','生年月日','お知らせ','宿泊プラン','宿泊初日','連泊数','宿泊人数','朝食','昼からチェックインプラン','お得な観光プラン','確認のご連絡','電話番号','ご要望','部屋タイプ','合計料金']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','お得な特典付きプラン','Tuesday','9','9','on','on','on','メールでのご連絡','','144文字','スタンダードツイン','697500']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','お得な特典付きプラン','Tuesday','9','9','on','on','on','メールでのご連絡','','144文字','スタンダードツイン','697000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','カップル限定プラン','Monday','2','2','on','on','off','メールでのご連絡','','144文字','プレミアムツイン','38000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','ディナー付きプラン','Sunday','3','4','off','on','off','電話でのご連絡','','144文字','部屋指定なし','114500']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','テーマパーク優待プラン','Wednesday','5','1','off','on','on','希望しない','','off','部屋指定なし','57000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','出張ビジネスプラン','Friday','9','1','off','on','on','電話でのご連絡','','144文字','シングル','75125']);
reserveTestTable.add(['kagetora@example.jp','pass1234','長尾景虎','一般会員','','','男性','1960-12-11','受け取らない','エステ・マッサージプラン','Thursday','1','6','off','on','on','希望しない','','144文字','部屋指定なし','66000']);
reserveTestTable.add(['kagetora@example.jp','pass1234','長尾景虎','一般会員','','','男性','1960-12-11','受け取らない','出張ビジネスプラン','Monday','9','2','on','off','on','メールでのご連絡','','144文字','シングル','162500']);
reserveTestTable.add(['kagetora@example.jp','pass1234','長尾景虎','一般会員','','','男性','1960-12-11','受け取らない','素泊まり','Saturday','1','1','on','off','off','電話でのご連絡','01234567890','144文字','シングル','7875']);
reserveTestTable.add(['masatora@example.jp','pass5678','真田昌虎','一般会員','京都府宇治市','','男性','1960-12-11','受け取る','カップル限定プラン','Thursday','2','2','off','off','on','電話でのご連絡','01234567890','off','プレミアムツイン','34000']);
reserveTestTable.add(['masatora@example.jp','pass5678','真田昌虎','一般会員','京都府宇治市','','男性','1960-12-11','受け取る','ディナー付きプラン','Friday','3','4','off','off','on','メールでのご連絡','','144文字','部屋指定なし','123000']);
reserveTestTable.add(['aiko@example.jp','passpass','直江愛子','一般会員','','01234567890','女性','1960-12-11','受け取らない','カップル限定プラン','Sunday','2','2','on','off','off','メールでのご連絡','','off','プレミアムツイン','40000']);
reserveTestTable.add(['aiko@example.jp','passpass','直江愛子','一般会員','','01234567890','女性','1960-12-11','受け取らない','ディナー付きプラン','Saturday','3','4','on','off','off','電話でのご連絡','','144文字','部屋指定なし','131000']);
reserveTestTable.add(['aiko@example.jp','passpass','直江愛子','一般会員','','01234567890','女性','1960-12-11','受け取らない','テーマパーク優待プラン','Monday','1','1','off','on','on','メールでのご連絡','','off','部屋指定なし','12000']);
reserveTestTable.add(['aiko@example.jp','passpass','直江愛子','一般会員','','01234567890','女性','1960-12-11','受け取らない','素泊まり','Monday','1','1','on','on','off','電話でのご連絡','','144文字','シングル','7500']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','プレミアム会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','エステ・マッサージプラン','Tuesday','9','6','on','on','on','希望しない','','144文字','部屋指定なし','579000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','プレミアム会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','お得なプラン','Monday','9','1','off','off','off','電話でのご連絡','','144文字','部屋指定なし','57000']);
```
EDGEは（抜粋）

```ReserveByMemberEdge_test.js
let reserveTestTable = new DataTable(['メルアド','パスワード','氏名','ランク','住所','電話','性別','生年月日','お知らせ','宿泊プラン','宿泊初日','連泊数','宿泊人数','朝食','昼からチェックインプラン','お得な観光プラン','確認のご連絡','電話番号','ご要望','部屋タイプ','合計料金']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','エステ・マッサージプラン','Friday','1','1','off','off','on','電話でのご連絡','','off','部屋指定なし','10000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','エステ・マッサージプラン','Friday','1','1','off','off','on','電話でのご連絡','','off','部屋指定なし','9000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','お得なプラン','Thursday','1','1','on','on','on','希望しない','','144文字','部屋指定なし','9000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','一般会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','素泊まり','Tuesday','1','2','off','off','off','電話でのご連絡','','off','シングル','11000']);
reserveTestTable.add(['kagetora@example.jp','pass1234','長尾景虎','一般会員','','','男性','1960-12-11','受け取らない','カップル限定プラン','Saturday','2','2','off','off','off','メールでのご連絡','','off','プレミアムツイン','40000']);
reserveTestTable.add(['kagetora@example.jp','pass1234','長尾景虎','一般会員','','','男性','1960-12-11','受け取らない','ディナー付きプラン','Wednesday','3','4','on','on','off','電話でのご連絡','01234567890','off','部屋指定なし','118000']);
reserveTestTable.add(['kagetora@example.jp','pass1234','長尾景虎','一般会員','','','男性','1960-12-11','受け取らない','テーマパーク優待プラン','Tuesday','5','1','off','off','on','希望しない','','off','部屋指定なし','53500']);
reserveTestTable.add(['kagetora@example.jp','pass1234','長尾景虎','一般会員','','','男性','1960-12-11','受け取らない','貸し切り露天風呂プラン','Monday','1','1','off','on','off','メールでのご連絡','','144文字','部屋指定なし','10000']);
reserveTestTable.add(['masatora@example.jp','pass5678','真田昌虎','一般会員','京都府宇治市','','男性','1960-12-11','受け取る','お得なプラン','Sunday','9','9','on','off','off','メールでのご連絡','','144文字','部屋指定なし','607500']);
reserveTestTable.add(['masatora@example.jp','pass5678','真田昌虎','一般会員','京都府宇治市','','男性','1960-12-11','受け取る','テーマパーク優待プラン','Thursday','5','1','on','off','on','電話でのご連絡','01234567890','144文字','部屋指定なし','61000']);
reserveTestTable.add(['masatora@example.jp','pass5678','真田昌虎','一般会員','京都府宇治市','','男性','1960-12-11','受け取る','貸し切り露天風呂プラン','Tuesday','1','1','on','on','off','メールでのご連絡','','off','部屋指定なし','11000']);
reserveTestTable.add(['aiko@example.jp','passpass','直江愛子','一般会員','','01234567890','女性','1960-12-11','受け取らない','エステ・マッサージプラン','Wednesday','1','6','off','off','on','メールでのご連絡','','144文字','部屋指定なし','60000']);
reserveTestTable.add(['aiko@example.jp','passpass','直江愛子','一般会員','','01234567890','女性','1960-12-11','受け取らない','お得なプラン','Tuesday','1','1','off','off','on','メールでのご連絡','','off','部屋指定なし','7000']);
reserveTestTable.add(['aiko@example.jp','passpass','直江愛子','一般会員','','01234567890','女性','1960-12-11','受け取らない','お得な特典付きプラン','Saturday','9','9','on','off','off','電話でのご連絡','','off','スタンダードツイン','711000']);
reserveTestTable.add(['aiko@example.jp','passpass','直江愛子','一般会員','','01234567890','女性','1960-12-11','受け取らない','貸し切り露天風呂プラン','Friday','1','6','on','off','on','電話でのご連絡','','144文字','部屋指定なし','66000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','プレミアム会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','お得な特典付きプラン','Wednesday','1','9','on','off','on','メールでのご連絡','','144文字','スタンダードツイン','81000']);
reserveTestTable.add(['harunobu@example.jp','password','武田晴信','プレミアム会員','京都府宇治市','01234567890','男性','1960-12-11','受け取る','テーマパーク優待プラン','Saturday','5','1','on','off','off','メールでのご連絡','','144文字','部屋指定なし','60000']);
```
ちなみに、仮にブラウザにOperaを追加したとしても、7件の増加になります。（20件弱の増加にはなりません。）
会員登録検証についてもAppiumで作成済みのPictmasterを改造して、こちらは3種類のブラウザで13件のテストデータを準備して臨みました。
ゲストによる宿泊予約についても同様に準備し、総テスト件数は全ブラウザで110件となりました。
実行時間は、10分弱くらいでした。

* 会員登録（一般会員 / プレミアム会員）とマイページ検証、会員種別による利用可能な宿泊プラン検証
* ゲストユーザーによる宿泊予約検証（プラン名、部屋タイプ、合計料金検証）
* 会員による宿泊予約検証（プラン名、部屋タイプ、合計料金検証）

ブラウザ3種類、10分で済みます。
生Seleniumでは、けっこうThread.sleep 入れてたので、もっとかかったかも。
# テストレポート
Allureでレポートもcodecept.conf.jsに記述して、allure serve ./output で表示できました。
![testResult009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/cd5057bf-90bc-dc73-5e52-9668f67a45ba.png)
意図的に各ブラウザで合計料金間違いのテストデータを混入させたのも、検出できています。
Chrome / Firefox / EDGEの各ブラウザで、会員登録検証（登録と宿泊プランの検証、退会操作）、ゲストユーザーによる宿泊予約の検証、会員登録後の宿泊予約検証が、テストスイートとしてレポートされました。
PowerShellのスクリプトを以下のように記述しておいて、一気にテスト実行します。

```planisphereTest.ps1
cd C:\works\PlanisphereTest\ChromeWebio
npx codeceptjs run
cd C:\works\PlanisphereTest
cd C:\works\PlanisphereTest\webioFirefox
npx codeceptjs run
cd C:\works\PlanisphereTest
cd C:\works\PlanisphereTest\Edge
npx codeceptjs run
cd C:\works\PlanisphereTest
```
テスト実行してレポート表示してしまえば、何の変哲も無いテスト結果ですが、生Seleniumで同じ事をしようとすれば、ブラウザ毎のDriverをテスト環境に置いて、テスト設定ファイルに各ドライバの所在を記述して、テスト実行時にそれを読み込んだり（codecept.conf.jsみたいな）を自前で準備するのに比べると、遙かに簡単にテストできました。
ブラウザ毎のプロジェクト・ディレクトリ構成は

```
PlanisphereTest
├── ChromeWebio                    # Chromeによるテストプロジェクト
│   ├── reserve_test           
│   │   ├── ReserveTestByChrome_test.js    # 会員登録・マイページ確認・退会テストシナリオ
│   │   ├── ReserveByGuestChrome_test.js    # ゲストユーザーによる宿泊予約テストシナリオ
│   │   └── ReserveByMemberChrome_test.js   # 登録会員による宿泊予約テストシナリオ
│   │
│   ├── codecept.conf.js            # 設定
│   ├── steps_file.js            # カスタムステップ
│   └── connector.js             # 利用料金、宿泊初日、宿泊最終日の計算クラス
│   
├── webioFirefox                    # Chromeによるテストプロジェクト
│   ├── reserve_test           
│   │   ├── ReserveTestByFirefox_test.js    # 会員登録・マイページ確認・退会テストシナリオ
│   │   ├── ReserveByGuestFirefox_test.js    # ゲストユーザーによる宿泊予約テストシナリオ
│   │   └── ReserveByMemberFirefox_test.js   # 登録会員による宿泊予約テストシナリオ
│   │
│   ├── codecept.conf.js            # 設定
│   ├── steps_file.js            # カスタムステップ
│   └── connector.js             # 利用料金、宿泊初日、宿泊最終日の計算クラス
│   
├── EDGE                    # EDGEによるテストプロジェクト
│   ├── reserve_test           
│   │   ├── ReserveTestByEdge_test.js    # 会員登録・マイページ確認・退会テストシナリオ
│   │   ├── ReserveByGuestEdge_test.js    # ゲストユーザーによる宿泊予約テストシナリオ
│   │   └── ReserveByMemberEdge_test.js   # 登録会員による宿泊予約テストシナリオ
│   │
│   ├── codecept.conf.js            # 設定
│   ├── steps_file.js            # カスタムステップ
│   └── connector.js             # 利用料金、宿泊初日、宿泊最終日の計算クラス
├── output               # ログ、スクリーンショットファイル出力先
└── planisphereTest.ps1        # PowerShell
```
# ブラウザ毎のcodecept.conf.js違い
WebDriver以下の1カ所、記述が異なるだけです。Chromeの場合

```codecept.conf.js
  helpers: {
    WebDriver: {
      url: 'https://hotel.testplanisphere.dev/ja/',
      browser: 'chrome'
    },
         "ChaiWrapper" : {
                "require": "codeceptjs-chai"
              }
  },
```
Firefoxの場合

```codecept.conf.js
  helpers: {
    WebDriver: {
      url: 'https://hotel.testplanisphere.dev/ja/',
      browser: 'firefox'
    },
              "ChaiWrapper" : {
                     "require": "codeceptjs-chai"
                   }
  },
```
Edgeの場合

```codecept.conf.js
  helpers: {
    WebDriver: {
      url: 'https://hotel.testplanisphere.dev/ja/',
      browser: 'MicrosoftEdge'
    },
              "ChaiWrapper" : {
                     "require": "codeceptjs-chai"
                   }
  },
```
# 最後に
永らくjavaで生Seleniumを扱ってきたので、CodeceptJSを扱うようになったことでJavaScriptに慣れるまでの時間が、一番かかったと思いますが、それ以上にCodeceptJSで自動テスト環境が簡単に構築できる恩恵は大きいと思います。
今回、helperにplayWrightを使った場合とWebDriverを使った場合とで同じ対象に対してテスト実装する手段に違いが認められました。テスト環境が手軽に構築できるCodeceptJSでも、テスト対象となるWebUIによっては、若干の壁を乗り越えなければならない場合もありそうです。Webアプリの自動テストに取り組んでいると、案外、テストに至るまでの操作と検証そのものは、そんなに種類が多くないことに気がつくかもしれません。Stepを全て自作すると言っても、そんなに大変なことでは無い場合もあるかもしれません。ロケータ取得にしてもCSSで旨くいくことが多いWebUIもあれば、classNameやIDの方が旨くいくことがほとんどのWebUIも実際ありますね。自動テストの壁としてよく挙がるReact等を用いたSPAで動的にUIが変化する場合や、画面での検証以外（例えば組込みに見られるようなListen portに着信するような仕組み）にどれだけ対応できるのと言った点もツール選定の材料かと思います。ローコードと言っても、少しはプログラミング・スキル要ると考えます。
ちいさくCodeceptJSで始めて、どうしても手に負えないときはガッツリとSeleniumに向き合うのが良いかもしれないと思いました。CodeceptJSを使えば（いろいろ癖もありそうだけど）、自作時間が削減できる点、簡単にテスト開始できる点で捻出した時間は、テスト計画やテスト設計に時間を使えてメリット大きいと感じました。