Selenideノウハウ

# 概要
Selenideとは、Junitベースのテストの中で
ブラウザを操作し想定の画面遷移やデータが表示されるかの確認を行えるもの

# 初期設定
環境
言語:JDK 1.8

## Mavenの記載
pom.xmlに以下の記載を追加

~~~ xml
    <dependencies>
    　　<!-- 中略 -->
        <dependency>
            <groupId>com.codeborne</groupId>
            <artifactId>selenide</artifactId>
            <version>4.9</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
~~~ 
4.9:2018/3/12時点でのSelenideの最新バージョン

### SpringBootの場合
SpringBootを導入している場合、Selenideが参照しているライブラリ(Selenium)のバージョンが古いため
selenideを動作させるためには以下の設定が追加で必要となる

<details><summary>追加設定内容</summary><div>

~~~ xml
    <dependencies>
    　<!-- 中略 -->
      <dependency>
          <groupId>org.seleniumhq.selenium</groupId>
          <artifactId>selenium-api</artifactId>
          <version>3.8.1</version>
      </dependency>
      <dependency>
          <groupId>org.seleniumhq.selenium</groupId>
          <artifactId>selenium-chrome-driver</artifactId>
          <version>3.8.1</version>
      </dependency>
      <dependency>
          <groupId>org.seleniumhq.selenium</groupId>
          <artifactId>selenium-firefox-driver</artifactId>
          <version>3.8.1</version>
      </dependency>
      <dependency>
          <groupId>org.seleniumhq.selenium</groupId>
          <artifactId>selenium-remote-driver</artifactId>
          <version>3.8.1</version>
      </dependency>

      <dependency>
          <groupId>org.seleniumhq.selenium</groupId>
          <artifactId>selenium-support</artifactId>
          <version>3.8.1</version>
      </dependency>
    </dependencies>
~~~ 

FireFox及びChromeで動作させる前提であれば上記設定のみで動作しました
その他`NoClassDefFound`や`NoSuchMethod`の類のエラーが出た際には
[selenium-apiの依存関係](https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java/3.8.1)を確認し、ローカルで有効になっているバージョンと比較を行うこと
</div></details>

## Driverの準備
### ChromeDriverの準備
起動させるブラウザが、FireFox以外の場合にはDriverファイル(Selenideで記載している内容を受け、ブラウザの操作/情報取得を行ってくれる実行ファイル)が必要となります
[ダウンロードサイト](https://sites.google.com/a/chromium.org/chromedriver/downloads)から環境にあったファイルを取得し、Junitで指定できる場所に配置してください

<details><summary>独り言1</summary>
selenide4.8以降は不要という記事を読んだのですが、私が実行したときはダウンロードが実施されなかったので
ドライバを準備しておきました。
</details>
<details><summary>独り言2</summary>
ドライバをローカルに配置した場合は、対象のファイルに対して実行権限があるか確認が必要  
ない場合は、別途実行権限を設定する必要がある
Docker等Windows以外の環境で実施する場合は注意が必要
</details>

# ドライバ起動～終了まで

## Driverの初期設定
以下の手順でDriverの設定を行う
`@Before`メソッドの中で設定を行う

### Chromeの場合
~~~java
    @Before
    public void before() {
        Configuration.browser = WebDriverRunner.CHROME;
        //Chromeドライバのパスを設定(ここでは設定ファイルから取得)
        System.setProperty("webdriver.chrome.driver", getClass().getResource(properties.getChromeDriver()).getPath());

        ChromeOptions chromeOptions = new ChromeOptions();
        //HeadLessモード指定
        if (properties.isHeadless()) {
            chromeOptions.addArguments("--headless");
        }
        Map<String, Object> chromePrefs = new HashMap<>();
        //PopUp表示を抑制
        chromePrefs.put("profile.default_content_settings.popups", 0);
        //ダウンロードフォルダ指定(テストメソッドごとに格納パスを切り替える等任意で設定)
        chromePrefs.put("download.default_directory", "/User/Temp/result/DownloadFile");
        //ダウンロード先指定ダイアログ表示抑制
        chromePrefs.put("download.prompt_for_download", false);
        chromeOptions.setExperimentalOption("prefs", chromePrefs);
        driver = new ChromeDriver(chromeOptions);

        //WebDriverRunnerにDriverを指定
        WebDriverRunner.setWebDriver(driver);
        
        Configuration.fastSetValue = true;
        //スクリーンショットの設定ディレクトリ設定
        Configuration.reportsFolder = "/User/Temp/result/ScreenShot"; 
    }  
~~~

以下で各指定内容について記載します

### ヘッドレスモードの指定
以下の指定を行うと、HeadlessModeでChromeを起動し、ブラウザでの確認を行えます。
この指定を行うことによりUI無しの環境でも自動検証を行うことが出来ます

~~~ java
            chromeOptions.addArguments("--headless");
~~~

### ファイルダウンロードパスの指定
以下2行でファイルダウンロード時のディレクトリ指定と
ダウンロード時にダイアログが表示されるのを抑制します。

~~~java
        //ダウンロードフォルダ指定(テストメソッドごとに格納パスを切り替える等任意で設定)
        chromePrefs.put("download.default_directory", "/User/Temp/result/DownloadFile");
        //ダウンロード先指定ダイアログ表示抑制
        chromePrefs.put("download.prompt_for_download", false);
~~~
Chrome設定画面の以下の箇所に反映されます
![Chrome設定.JPG](https://qiita-image-store.s3.amazonaws.com/0/242808/fb11d410-ee57-adaf-eeaf-eab6f1a5a403.jpeg)

### スクリーンショット保存先
以下でスクリーンショットの保存先指定を行います
(スクリーンショットについては後述)

~~~java
        //スクリーンショットの設定ディレクトリ設定
        Configuration.reportsFolder = "/User/Temp/result/ScreenShot";
~~~

### 文字入力指定
以下指定を行うことにより、inputタグに文字を入力する際に、
指定した文字を一括で指定します
(以下の指定を行わない場合、1文字づつ画面に入力されます)

~~~java
        Configuration.fastSetValue = true;
~~~

## Driver終了
`@After`メソッドの中で実施します。
以下メソッドで、Driverを終了します

~~~java
    @After
    public void after() {
        driver.close();
    }
~~~

# テスト

`@Test`のメソッド内で実施(普通のJunitと同様)
まずざっくりの流れとしては、
1. `Selenide#open(String,Class)`で指定のURLを起動
2. 起動したページ要素を取得し、想定の要素が表示されているか確認
3. ボタン等をクリックして想定通りの遷移になるか、想定通りの動きをするの確認
"2-3"の繰り返しで複数画面の確認も可能

## URLを指定して画面起動
以下のように指定ページを開く

~~~ java
ItemsPageObject itemsPage = Selenide.open("http://localhost:8080/items", ItemsPageObject.class);
~~~

### PageObjectについて
第二引数及び返却値となるクラス(Object)は`PageObject`と呼び
このPageObject内でSelenideの要素の取得や操作を行い
テストメソッド内では取得結果の判定やPageObject経由での画面操作を行うことが推奨されている

~~~ java
    @Test
    public void testItemList() throws Exception {
        ItemspageObject itemsPage = Selenide.open("/items", ItemspageObject.class);
        List<String> ret = itemsPage.getItemNames();
        assertEquals("ItemCount", 2, ret.size());
    }
~~~
上記の例だと`ItemspageObject#getItemNames()`内でHTMLの要素を取得し
取得した内容の確認はTestメソッド内で実施している

## 要素の取得

Pageオブジェクト(クラス)に取得メソッドを記載することにより要素を取得する
### 基本的な要素取得方法
`Selenide#$(String)`を使用することにより対象の要素を取得することが出来る
以降こんな簡単なページを例に記載していく

~~~html
<table>
 <tr>
   <th>Header1</th>
 </tr>
 <tr>
   <td>Data1</td>
 </tr>
 <tr>
   <td>Data1</td>
 </tr>
</table>
~~~

以下のように取得すると`<table>`タグ全体を取得することが出来る

~~~ java
public SelenideElement getTable(){
   return Selenide.$("table");
}
~~~

さらに子要素を取得したい場合は以下のように取得できる

~~~ java
public ElementsCollection getTableRows(){
   //Tableの要素を取得
   SelenideElement table = Selenide.$("table");
   //子要素の一覧を取得
   return table.$$("tr");
}
~~~

`SelenideElement Selenide#$(String)`で指定した文字列で要素を検索し、最初に取得できた要素を取得でき

`ElementsCollection Selenide#$$(String)`で指定した文字列で要素を検索し、最初に取得できた要素一覧で取得できる
`ElementsCollection`は、`AbstarctList<SelenideElement>`の継承クラスなので、取得した要素数の確認や、Listの中身の検査も実施可能(詳細略)

### 要素取得方法(PageObjectのフィールドに設定)

PageObjectのフィールドを以下のよう`@FindBy`に取得方法を記載することにより要素をInjectionすることが出来ます

~~~java
public class ItemspageObject extends SelenideBasePageObject {

    @FindBy(how = How.TAG_NAME, using = "table")
    private SelenideElement table;


    @FindBy(how = How.TAG_NAME, using = "tr")
    private ElementsCollection rows;

~~~

上記の場合`ItemspageObject`のインスタンス生成時にtag名称="table"で起動ページから要素を取得し値を設定します
上記`table`の子要素を取得したい場合等、`SelenideElement`ではなく独自クラスを指定することが出来ます

~~~ Java 
public class ItemspageObject extends SelenideBasePageObject {

    @FindBy(how = How.TAG_NAME, using = "table")
    private OriginalTable table;
~~~

`OriginalTable`は以下のように`ElementsContainer`を継承して作成

~~~java
public class OriginalTable extends ElementsContainer {
    @FindBy(how = How.TAG_NAME,using = "tr")
    private List<TableRow> tableRows;
~~~

複数の要素をインジェクションしたい場合で独自クラスを指定したい場合は`List<T>`のように宣言しておく
`T`は`ElementsContainer`を継承したクラスであること

~~~java
public class TableRow extends ElementsContainer {
    /**
     * TableHeader
     */
    @FindBy(how = How.TAG_NAME, using = "th")
    private ElementsCollection headers;
~~~

## 要素の操作
取得した要素に対して操作について説明します

### 値の取得
指定要素のタグで囲まれた値を取得するためには
`String SelenideElement#getText()`を使用します

~~~html
   <th>Header1</th>
~~~
上記の"th"タグに相応するElemetに対して実行した場合は"Header1"という文字列が取得できます

### 属性値の取得
指定要素の属性値を取得する場合には、`String SelenideElement#getAttribute(String)`を使用します。

~~~ html
<a href="/users">Data2</a>
~~~

上記の"href"属性を取得したい場合は以下のように取得します。

~~~ java
SelenideElement aTag;
//中略
String hrefStr = aTag.getAttribute("href");
~~~

**注意点**
href属性の場合、取得してくる値は以下のようになります
`http://loalhost:8080/users`(相対パスではなく解釈された絶対パスが返却される)

## 要素に値を設定

### 文字列設定
以下のような文字列入力項目に対して文字列を指定する

~~~ html
<a>Input:<input type="text" name="NAME"></a>
~~~
`SelenideElemnt#setValue(String)`を使用して実施

~~~java
//InputTagに文字列を設定
inputTag.setValue("aaaaa");
//設定した文字列は"SelenideElemnt#getValue()"で取得できる
assertEquals("InputTagValue", "aaaaa", inputTag.getValue());
~~~

### ファイル指定
以下のようなファイル選択項目に対してファイルパスを指定する

~~~ html
<a>FileUp:<input type="file" name="FILE"></a>
~~~

~~~java
//InputTagにファイルパスを設定
fileUpTag.uploadFile(new File("/User/Text.txt"));
~~~

上記処理を実施した際に実際にファイルがアップロードされるわけでなく、項目にファイルパスを設定するのみ
(サブミット処理を行って初めてアップロードがなされる)

### hidden項目に対して値設定
`type='hidden'`と指定されている項目に対して値を設定したい場合は、`SelenideElemnt#setValue(String)`を使用するとエラーが出ます。
そのため、JavaScript経由で値の設定を行います。

~~~ html
<input type='hidden' name='hiddenItem' value='HiddenValue' />
~~~

上記例の'hiddenItem'の項目に値を設定するためには以下のように記載すれば値を設定できます。

~~~java
String value = "設定値"
((JavascriptExecutor) webDriver).executeScript("document.getElementsByName('hiddenItem').item(0).value = '" + value + "';");
~~~
IE/FireFox/Chrome用のWebDriverはともに`RemoteWebDriver`を継承しており、
`RemoteWebDriver`が`JavascriptExecutor`を実装しているので、一般的なブラウザでなら対応できます。

### ファイルダウンロード
#### Selenideで用意されているダウンロード処理
`File SelenideElemnt#download()`を使用するとhref属性の参照先のファイルを取得する。
JavaScriptでの動作等々、途中の処理が含まれるような処理には対応できない

#### 必要と思われるダウンロード処理
ボタンやリンク等をクリックした後に、ダウンロードフォルダ([リンク先](#ファイルダウンロードパスの指定)での設定で指定)を確認して新規に作成されたファイルをダウンロードファイルとして取得する。

以下注意点

1. クリック前に指定フォルダのファイル数を保持しておく([リンク先](#リスナーの設定)参照)
1. ダウンロード処理実施時には以下の順番でフォルダの中身が変わるので最終的にファイルダウンロード完了を検知してファイルの精査を行う必要がある
 1. ファイル増加なし(ダウンロード開始されていない)
 2. 作成されたファイルの拡張子が`.tmp`(Chromeによるダウンロード中状態-1)
 3. 作成されたファイルの拡張子が`.crdownload`(Chromeによるダウンロード中状態-2)
 4. 作成されたファイル名称が正規の名称になる(ダウンロード完了)

以下のソースで上記1-4の判定実施して取得ができます

~~~ java
    /**
     * 最新のダウンロードファイルを取得します
     *
     * @return 最新のダウンロードファイル
     */
    public File getLatestDownloadFile() {
        final File downloadDir = new File(getFileDownloadPath());
        
        //ダウンロード完了まで10秒待機
        WebDriverWait waitForUpload = new WebDriverWait(driver, 10);

        waitForUpload.until(new ExpectedCondition<Boolean>() {
            //ここの判定処理は、デフォルトで500ミリ秒ごとに実施
            public Boolean apply(WebDriver _driver) {
                //ファイル名からダウンロード中か否かを判定
                return isDownloadFinish(getLatestFile(downloadDir));
            }
        });

        //最新ファイルを取得
        File retFile = getLatestFile(downloadDir);
        //最新ファイルが取得できたら対象のファイルを返却
        if (isDownloadFinish(retFile)) {
            return retFile;
        }
        //出来なかったらNull返却
        return null;
    }

    /**
     * ダウンロード中か否かを判定します
     *
     * @param target 対象ファイル
     * @return ダウンロード完了か否か
     */
    private static boolean isDownloadFinish(File target) {
        return target != null && !target.getName().endsWith(".crdownload") && !target.getName().endsWith(".tmp");
    }

    /**
     * 最新ファイルを取得します
     *
     * @param targetDir ダウンロードディレクトリ
     * @return 最新ダウンロードファイル
     */
    private File getLatestFile(File targetDir) {
        File[] downLoaededFiles = targetDir.listFiles();
        
        //クリック前に保持したファイル数と変わっていなかったらNullを返却
        if (downLoaededFiles == null || downLoaededFiles.length == resultFileCount) {
            return null;
        }

        //ファイルの更新時間で比較し、最新のファイルを返却
        Arrays.sort(downLoaededFiles, new Comparator<File>() {
            @Override
            public int compare(File file1, File file2) {
                return file1.lastModified() <= file2.lastModified() ? 1 : -1;
            }
        });

        return downLoaededFiles[0];
    }
~~~

#### リスナーの設定
以下のように、WebDriverRunnerに対してリスナー(`WebDriverEventListener`の実装クラス)を指定することでクリック前等のタイミングで処理を実行することが出来る

~~~java
       WebDriverRunner.addListener(new WebDriverListenerImpl());
~~~

`WebDriverEventListener`の実装クラスはこんな感じで実装(クリック前に指定フォルダのファイル数を保持しておく)

~~~ java
/**
 * WebDriverEventListenerの実装クラス
 */
public class WebDriverListenerImpl implements WebDriverEventListener {

    //その他のメソッド群は省略

    @Override
    public void beforeClickOn(WebElement webElement, WebDriver webDriver) {
        //クリック前に実績フォルダ配下のファイル数を取得する
        baseClass.setResultFileCount(new File(" "/User/Temp/result/DownloadFile"").listFiles().length);
    }
}