【Selenide】Web自動テスト　マニアックなつまづきポイントと対処方法まとめ

## はじめに
Selenideは、JavaベースのWebテスト自動化フレームワークです。
Seleniumをより使いやすく、簡潔なコードでWebUIテストを記述できるようにしたラッパーです。
今回は自動化対象システムがJava製のため、開発ツール類の親和性とメンバーのJavaスキルアップのためPython+SeleniumではなくJava+Selenideを採用しました。

基本的な使い方の記事はたくさんあるので、ここでは少しマニアック(?)なつまづきポイントと対処方法をまとめてみます。

## テストを連続実行する場合に、前のテストのキャッシュ(cookie,localstrage)によりテストが失敗する。
@Before（各ケースの直前に実行）クラスでキャッシュクリア
```java
    @Before
    public void beforeTest() {
    	clearBrowserCookies();
		clearBrowserLocalStorage();
    }
```

## テスト環境の自己証明書エラー回避
```java
    DesiredCapabilities dc = new DesiredCapabilities();
    dc.setCapability("acceptSslCerts", true);
    ChromeOptions opts = new ChromeOptions();
    opts.merge(dc);
        Configuration.browserCapabilities.setCapability(ChromeOptions.CAPABILITY, opts);
```

## 画面サイズを"縦幅だけ"画面いっぱいに広げる
スクリーンショット画像のサイズ節約などのため「最大化」はしたくない場合。
```java
    Selenide.open("http://xxxxxxxxx");
    driver = WebDriverRunner.getWebDriver();
    driver.manage().window().maximize();//いったんmaxサイズにして縦幅を取得
    int width = 1280;//横幅は固定
    int height = ((Number) Selenide.executeJavaScript("return document.body.clientHeight")).intValue();
    driver.manage().window().setSize(new Dimension(width,height));
    //Configuration.browserSize = width + "x" + height;//←なぜかこれは効かない
```

## エラー発生時に処理を入れる
カスタムのTestWatcherを定義して、
```java
class SelenideTestWatcher extends TestWatcher{
    @Override
    public void failed(Throwable e, Description description){
        super.failed(e, description);
        // エラー発生時にやりたい処理（後処理やスクショ撮影など）
    }
}
```
@Ruleとしてテストクラス内で読み込む。
```java
    @Rule
    public SelenideTestWatcher watcher = new SelenideTestWatcher();
```


## プロキシ設定
```java
    Proxy p = new Proxy();
    p.setHttpProxy("xxx.xxx.xxx.xxx:8080");
    p.setSslProxy("xxx.xxx.xxx.xxx:8080");
    WebDriverRunner.setProxy(p);
```
## Chrome111以降で起動時にConnectionFailedExceptionが発生する場合
```java
    System.setProperty("chromeoptions.args", "--remote-allow-origins=*");
```

## 自分で用意したchromedriverを使用する
基本はSelenideが自動でブラウザバージョンにあったchromedriverを入れてくれるため不要。
ただしChrome115以降はchromedriverの配布方法が変わったため、Selenide6.16.1以降でないと自動DLはできない。
~~Selenide6.16.1のwebdrivermanagerはJava8では動作しない（Java11以降のみ）ため、環境起因でJava8までしか利用できない場合は以下の設定により自前でDLしたchromedriverを利用する必要がある。~~
→(追記)Selenide6.17.0でJava8にも対応した。
ただし起動時に「https://googlechromelabs.github.io/chrome-for-testing/」にアクセスして最新のdriverを取得できる環境でないと自動での最新化は不可のため、引き続き以下の方法でchromedriverを自前で調達＋指定する必要がある。
※chromedriverのDLページは[こちら](https://googlechromelabs.github.io/chrome-for-testing/)
```java
System.setProperty("webdriver.chrome.driver", "lib\\chromedriver.exe");
```

## テスト終了後もブラウザを保持
```java
Configuration.holdBrowserOpen = true;
```

## 画面全体のスクリーンショットを取得する（ファイル名は画面URLから取得）
```java
    String ssFolderPath = "スクリーンショットを保存するフォルダパス";
    String url = (String)Selenide.executeJavaScript("return location.href");
    String fileName =　url.substring(url.lastIndexOf("/")+1,url.indexOf("?")==-1 ? url.length() : url.indexOf("?"));//URLの最後のスラッシュ以降をファイル名とする
    FullSizePhotographer photographer;
    Path imagePath = Paths.get(ssFolderPath + fileName + ".jpg");
    try {
        Files.write(imagePath, photographer.takeScreenshot(WebDriverRunner.driver(),OutputType.BYTES).get());
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
```

## 要素が見つからない（NoSuchElementException）
基本は[CSSセレクタの指定方法](https://webliker.info/css-selector-cheat-sheet/)を確認する。
よくあるのは以下４パターン。
1. シンプルに指定方を間違えている。
``class="aaa bbb"``みたいな要素のclassをそのままコピって
``$(".aaa bbb")``のようにしてしまいがち。正しくは``$(".aaa.bbb")``
2. まだ読み込まれていない
商品検索など時間がかかる画面の場合、デフォルトの待機時間(4秒)では足りない場合がある。
その場合はelementが表示されるまで明示的に待機処理を入れる。
``$(".className").should(Condition.enabled,Duration.ofSeconds(60));``
[待機できるConditionの種類はこちら](https://selenide.org/javadoc/current/com/codeborne/selenide/Condition.html)
大体、対象がinputならenabled、そうでなければappearを使用すれば上手くいく。
3. iframe配下の要素を取得しようとしている
iframe配下の要素は直接指定できず、frameの切り替えをしてから要素にアクセスする必要がある。
``Selenide.swicthTo().frame("FrameName");``
4. 別ウィンドウ（別タブ）の要素を取得しようとしている
新しいウィンドウやタブを開いた後、そのウィンドウの中身にアクセスする場合は、windowの切り替えをしてから要素にアクセスする必要がある。
``Selenide.swicthTo().window(1);``
※windowの番号は古いものから0,1,2,3...と採番される。

## 要素がクリックできない(ElementClickInterceptedException)
1. 別要素に邪魔されている
フローティングバナーなど、スクロールに追従して移動する要素に阻まれてクリックできない場合、目的の要素をクリック可能な位置までスクロールさせてからクリックする。
``$("button#id").scrollIntoView(true);//画面の最上部に来るようにスクロール``
※falseで画面最下部に来るようにスクロール。
2. 画面範囲外
画面の描画範囲外にある要素はクリックできない。スクロールでも対応できない場合はjavascriptでclick()イベントを直接呼び出す。
```java
    WebElement element = $("button#id").toWebElement();
	executeJavaScript("arguments[0].click();", element);
```
## タブ切り替え操作が多く、何番目のタブかわからなくなる
タブが一つになるまで閉じる共通処理を用意しておき、タブを開く操作の終了時点で必ず呼び出すようにする。
```java
    public void closeTabs() {
    	while(driver.getWindowHandles().size() > 1) {
        	driver.close();
        	switchTo().window(driver.getWindowHandles().size() - 1);
        }
    }
```

## hidden項目を取得
hidden項目はSelenideElementとしてアクセスできないため、javascript経由でアクセスする。
```java
    String val = (String)Selenide.executeJavaScript("return document.getElementById('id').value");
```

## IEのレガシーシステムも自動化したい
IE（EdgeのIEモード）を自動化する場合は色々環境設定が必要。
1. iedriverのレジストリ設定
32bit:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BFCACH
64bit:HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BFCACHE
→値0の iexplore.exe という名前のDWORD値を作成
2. インターネットオプションで全てのゾーンで「保護モードを有効にする」チェックON
3. レイアウトが崩れる（ドキュメントモードが「ie7」になってしまう）場合は以下レジストリ編集
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION
→値11001(10進数。16進数では0x2AF9))の iexplore.exe という名前のDWORD値を作成
→ドキュメントモードを「ie11」に固定。
4. Edgeのサイドバーが出てしまう場合は以下レジストリを追加（Edgeが無ければキーを新規作成）
HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge
→HubsSidebarEnabledという名前のDWORD値、値は0(0x00000001)
※Edgeでもサイドバーが出なくなるので注意。
5. [ここ](https://www.selenium.dev/downloads/)からIEドライバーを入手し、任意ディレクトリに配置。

テストコード内の設定は以下の通り。
```java
    Configuration.browser = "ie";
    Configuration.holdBrowserOpen = true;
    System.setProperty("webdriver.ie.driver", "lib\\IEDriverServer.exe");
    InternetExplorerOptions opts = new InternetExplorerOptions();
    opts.setCapability("initialBrowserUrl", "https://IE起動時に読み込まれる初期URL");
    opts.setBrowserVersion("11");
    opts.setCapability("show_hub_apps_tower", "false");
    InternetExplorerDriver driver = new InternetExplorerDriver( opts );
    driver.manage().window().setSize(new Dimension(1280, 1000));
    WebDriverRunner.setWebDriver( driver );
```

## 印刷ダイアログのスクリーンショットを取得したい
普通にwindow.printするとhangしてtimeout終了※するため、意図的にjsのtimeoutを発生させ握り潰す。
※[こちら](https://github.com/mozilla/geckodriver/issues/925)によると、webdriverはOSレベルのダイアログに干渉できない模様。
```java
    try {
        Selenide.executeJavaScript("setTimeout(window.print, 5);");
    }catch(Exception e) {
        //何もしない
    }
    switchTo().window(1);// 印刷ダイアログへの切り替え
    screenShot();
    switchTo().window(0);// 印刷ダイアログに対してdriver.closeができないため、いったん元のタブに戻る
```

## 番外編
Selenideは関係ないですが、E2Eテストを書くにあたって実現した付随機能についてもメモしておきます。

## テストコードに外部から値を渡す
自動テストにおける画面への入力値やテスト対象環境など、テスト実行時に外部から渡したい項目を読み込む方法。
実行時引数(システムプロパティ)に値が設定されていればそれを、設定されなければプロパティファイルから値を読み込む。

```test.properties
env=staging
```
```java
    String env = "";
    Properties properties = new Properties();
    try {
        InputStream istream = new FileInputStream(new File("test.properties"));
        properties.load(istream);
        env = System.getProperty("env", properties.getProperty("env"));
    } catch (IOException e) {
        e.printStackTrace();
    }
```
→実行時、システムプロパティに「env」という引数があればそれを、なければtest.propertiesのenvを読み込む。
## テストメソッドのメソッド名を取得する
スクリーンショットのファイル名にテストメソッド名を付与したい場合など。
```java
    String methodName = Arrays.stream(Thread.currentThread().getStackTrace())
        		.filter(st -> st.getClassName().equals("com.test.AppTest"))//パッケージ名.クラス名
        		.map(st -> st.getMethodName())
        		.findFirst().orElse("");
```
## テストをバッチ実行する
自動テストツールをチームメンバーにバッチファイルとして配布する際など
（テストシナリオはmavenプロジェクトとして開発。）
```AppTest.java
    @Test
    public void testMethod(){
        //テストシナリオ
    }
```
テストメソッドにはJunitの@Testアノテーションをつける。
```自動テスト実行.bat
call mvn test -Dtest=AppTest#testMethod > log¥自動テスト実行.log 2>&1
type log¥自動テスト実行.log
pause
```
実行しつつ実行結果をログ出力し、標準出力にも表示する。
※「-Dtest=テストクラス名#テストメソッド名」の形式で記述。
※mvnコマンドはcallで呼ばないとpauseで止まってくれず、テスト結果の確認ができない。