LoginSignup
4
8

More than 1 year has passed since last update.

Kotlin + Selenide実践入門

Last updated at Posted at 2021-04-19

はじめに

KotlinもSelenideも既に多くの方が利用されていると思いますが、Kotlin+Selenideの
実践入門的な内容の記事はあまり存在しないと思い、まとめてみようと思います。
Selenideを利用すると高い生産性でテストが実装可能です。本投稿では表現できておりませんが、Kotlinもとても生産性の高い言語です。
この2つを組み合わせて利用すること自体が大きな可能性を秘めていると確信しております。

開発環境はIntelliJでgradleプロジェクトとなります。

操作対象のHTML

操作対象のHTML概要

気軽に動かして確認いただけるように操作対象は単一ファイルのHTMLとなっております。
また、fileプロトコルでローカルパスを指定してテスト対象とできるので、実際に動かして確認する際のコストも低いと言えます。

HTMLですが、Selenideの処理対象として十分な物と思います。足りない部分はファイルアップロード系ぐらいでしょうか。

処理遅延対応

実際のプロダクトコードでは、ボタンクリック後に結果が反映されるまでに遅延がありますので

  • 遅延で要素が追加されるボタン
  • 遅延で要素が削除されるボタン

を含んでいます。

プロダクトコードの問題に対応するための例

表示要素だけを見ればidもCssSelectorも一意に要素を特定できるが、非表示な要素も考慮すると特定できない

  • idやCssSelectorが重複するテキストボックス
  • idやCssSelectorが重複するボタン

を含んでいます。idが重複し放題なのはそのためです。

クリック不可能な要素の例

親要素が表示状態になってから要素を操作すればいいだけなのですが、複雑な構造の場合は細かく制御すると生産性が下がってしまいますので

  • visibility:hiddenのdivの内部に存在するボタン

も含んでおります。

操作対象の動作イメージ

操作対象サンプルの説明4.gif

操作対象のHTML

操作対象のHTMLは以下の通りです。中身はテスト実装のところでも記載いたしますので、サクッと目を通していただければと思います。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>操作対象サンプル</title>
  <style>
    div{
    border: 1px solid #666;
    }
  </style>
</head>
<body>
  <script type="text/javascript">
  function clickAddButton(){
    addDiv4Action(0);
  }

  function clickDelayAddButton(){
    addDiv4Action(5000);
  }

  function addDiv4Action(delay){
    let divRoot = document.getElementById("divRoot");
    // 要素の追加
    let div4 = document.createElement("div");
    div4.id = "div4"; 
    let text1 = document.createTextNode("4段目");
    div4.appendChild(text1);
    window.setTimeout( function() { divRoot.appendChild(div4); }, delay );
  }

  function clickDeleteButton(){
    deleteDiv4ActionButton(0);
  }

  function clickDelayDeleteButton(){
    deleteDiv4ActionButton(5000);
  }

  function deleteDiv4ActionButton(delay){
    // 要素の削除
    let divRoot = document.getElementById("divRoot");
    let div4 = document.getElementById("div4");
    if (div4){
      window.setTimeout( function() { divRoot.removeChild(divRoot.lastChild); }, delay );
    }
  }

  function clickHideButton(){
    // 要素の削除
    let divRoot = document.getElementById("divRoot");
    let div4 = document.getElementById("div4");
    if (div4){
      div4.style.display ="none";
    }
  }

  function setClickButton(buttonName){
    var buttonResult = document.getElementById('clickButtonResult');
    buttonResult.textContent = buttonName;
  }
  </script>

  <h1>操作対象サンプル</h1>

  <div id="divRoot">
    <div id="div1" class="div1Class" name="div1Name">1段目</div>
    <div id="div2" class="div2Class" name="div2Name">2段目</div>
    <div id="div3" class="div3Class" name="div3Name">3段目</div>
  </div>  
  <BR> 
  <input type="button" id="addButton" value="追加ボタン" onclick="clickAddButton()">
  <input type="button" id="delayAddButton" value="遅延追加ボタン" onclick="clickDelayAddButton()">
  <input type="button" id="deleteButton" value="削除ボタン" onclick="clickDeleteButton()">
  <input type="button" id="delayDeleteButton" value="遅延削除ボタン" onclick="clickDelayDeleteButton()">
  <input type="button" id="hideButton" value="非表示ボタン" onclick="clickHideButton()">
  <HR>

  <label>名前:</label>
  <input type="text" name="nameText" size="30" maxlength="20"></td>
  <HR>

  <label>血液型:</label>
  <select name="blood">
    <option value="A">A型</option>
    <option value="B">B型</option>
    <option value="O">O型</option>
    <option value="AB">AB型</option>
  </select>
  <HR>

  <label>学年:</label>
  <input type="radio" name="grade" id="gradle1" value="1"><label for="gradle1">1年生</label>
  <input type="radio" name="grade" id="gradle2" value="2"><label for="gradle2">2年生</label>
  <input type="radio" name="grade" id="gradle3" value="3"><label for="gradle3">3年生</label>
  <HR>

  <label>よく利用する言語:</label>
  <input type="checkbox" name="lang" id="lang1" value="1"><label for="lang1">Java</label>
  <input type="checkbox" name="lang" id="lang2" value="2"><label for="lang2">C#</label>
  <input type="checkbox" name="lang" id="lang3" value="3"><label for="lang3">TypeScript</label>
  <input type="checkbox" name="lang" id="lang4" value="4"><label for="lang4">Kotlin</label>
  <input type="checkbox" name="lang" id="lang5" value="5"><label for="lang5">Ruby</label>
  <HR>

  <label>idが重複したdisplay:noneとstyleなしのinput</label>
  <BR>
  <input type="text" name="input1-1" id="input1" size="10" maxlength="20" style="display:none" value="input1-1">
  <input type="text" name="input1-2" id="input1" size="5" maxlength="20" value="input1-2">
  <BR> 
  <input type="text" name="input2-1" id="input2" size="20" maxlength="20" value="input2-1">
  <input type="text" name="input2-2" id="input2" size="6" maxlength="20" style="display:none" value="input2-2">
  <HR>

  <label>idが重複したvisibility:hiddenとstyleなしのinput</label>
  <BR>
  <input type="text" name="input3-1" id="input3" size="30" maxlength="20" style="visibility:hidden" value="input3-1">
  <input type="text" name="input3-2" id="input3" size="7" maxlength="20" value="input3-2">
  <BR> 
  <input type="text" name="input4-1" id="input4" size="40" maxlength="20" value="input4-1">
  <input type="text" name="input4-2" id="input4" size="8" maxlength="20" style="visibility:hidden" value="input3-3">
  <HR>

  <label>非表示のbutton1-1とbutton1-2</label>
  <BR>
  <input type="button" id="button1" name="button1-1" value="button1-1" style="display:none" onclick="setClickButton('button1-1')">
  <input type="button" id="button1" name="button1-2" value="button1-2" onclick="setClickButton('button1-2')">
  <BR>

  <label>visibility:hiddenのdivの内部にbutton2</label>
  <div style="visibility:hidden>
    <input type="button" id="button2" value="button2" onclick="setClickButton('button2')">
  </div>
  <label>クリックしたボタンを表示する領域</label><div id="clickButtonResult" name="div1Name">dummy</div>
  <HR>

</body>
</html>

プロジェクトの作成

IntelliJ IDEAを起動してプロジェクトを作成します。
プロジェクト作成1.png

プロジェクト作成2.png

NameとLocationは適当に変更ください。
プロジェクト作成3.png

「Finish」でプロジェクトが作成されますのでbuild.gradleを以下の内容に変更します。

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.4.32'
}

group 'kotlin.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'

    testImplementation 'com.codeborne:selenide:5.20.1'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.1'
    testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.7.1'
}

test {
    useJUnitPlatform()
}

初めの一歩目のクラス作成

パッケージとKotlin Classの作成

src/test/kotlinを選択して右クリックメニューから[New]-[Package]をクリックします。
クラス作成1.png

sampleを入力しリターンボタンを押下します。

sampleを選択して右クリックメニューから[New]-[Kotlin Class/File]をクリックします。
クラス作成2.png

KotlinSelenideSampleを入力しリターンボタンを押下します。

作成したKotlinSelenideSample.ktが表示されますので、内容を以下ように変更します。

package sample

import com.codeborne.selenide.Condition.*
import com.codeborne.selenide.Configuration
import com.codeborne.selenide.Selenide
import com.codeborne.selenide.SelenideElement
import com.codeborne.selenide.WebDriverRunner
import com.codeborne.selenide.ex.InvalidStateException
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
import org.openqa.selenium.By
import org.openqa.selenium.JavascriptExecutor

class KotlinSelenideSample {
    companion object {
        @JvmStatic
        @BeforeAll
        fun initialize() {
            //BeforeAllでChromeをブラウザにセット
            Configuration.browser = WebDriverRunner.CHROME;
        }
    }

    @BeforeEach
    fun beforeEach() {
        //各テストで処理対象は同一のtarget.htmlなのでBeforeEachでオープン
        //fileプロトコルでC:/example/target.htmlを対象としています。
        Selenide.open("file:///C:/example/target.html")
    }

    @Test
    fun testDiv1() {
        //id指定でdiv1のtextを検証
        assertEquals("1段目", selectById("div1").text())

        //class指定でdiv1のtextを検証
        assertEquals("1段目", selectByClassName("div1Class").text())

        //name指定でdiv1のtextを検証
        assertEquals("1段目", selectByName("div1Name").text())

        //cssSelector指定でdiv1のtextを検証
        assertEquals("1段目", selectByCssSelector("#div1").text())

        //div[id='div1']のcssSelectorでも検索可能
        assertEquals("1段目", selectByCssSelector("div[id='div1']").text())
    }

    private fun selectByName(tagName: String): SelenideElement {
        return Selenide.`$`(By.name(tagName))
    }

    private fun selectByClassName(className: String): SelenideElement {
        return Selenide.`$`(By.className(className))
    }

    private fun selectByCssSelector(cssSelector: String): SelenideElement {
        return Selenide.`$`(By.cssSelector(cssSelector))
    }

    private fun selectById(id: String): SelenideElement {
        return Selenide.`$`(By.id(id))
    }
}

初めの一歩目のクラスの説明

ブラウザはChromeを利用します。この処理はBeforeAllで行っています。
target.htmlはC:/example/target.htmlに存在している前提となります。
以降で記載するテストは全てtarget.htmlを対象として動作しますのでBeforeEachでオープンしています。

testDiv1の前に
selectByName、selectByClassName、selectByCssSelector、selectByIdを説明させていただきます。
と言っても、こう書けば単一要素をName、ClassName、CssSelector、Idで検索できるんだな!、と見るだけで理解していただけると思います。単一要素の検索はとてもよく利用しますので予めprivateメソッドを定義した方がいろいろスッキリすると思います。まあ個人的な感覚なので多数派でないかもしれないですが・・・

testDiv1ですが

<div id="div1" class="div1Class" name="div1Name">1段目</div>

の要素のtextをそれぞれ

id='div1'指定で検索

        assertEquals("1段目", selectById("div1").text())

ClassName='div1Classdiv1'指定で検索

        assertEquals("1段目", selectByClassName("div1Class").text())

Name='div1Name'指定で検索

        assertEquals("1段目", selectByName("div1Name").text())

cssSelector=div[id='div1']指定で検索

        assertEquals("1段目", selectByCssSelector("div[id='div1']").text())

して検証しています。

おいおい、何て痛いコード書いてるんだ!!、って突っ込まれてもしょうがない代物です。
プロダクトコードにはこんなテストではダメダメです。

実際のプロダクトコードの処理では描画が完了するまでに遅延があります。この書き方だと期待するtextになっていない状態で検証が行われる可能性が高いです。まずは第一歩目なので分かりやすさを優先しています。

初めの二歩目

第一歩目では遅延を考慮しておらず、再現性が低いが、理解しやすいコードとしておりましたが、ここからはより実践的なコードを説明させていただきます。

id='divRoot'を対象としたテスト

第一歩目では単一要素の取得のみでしたが、Selenide.$$(By)で複数要素を取得可能です。同様に、第一歩目では単一要素のtextの取得の例でしたが、SelenideElement#text()はそのままの記法で自分自身も含めて配下の要素のtextをまとめて取得可能です。

divRootを対象にした検証
    @Test
    fun testDivRoot() {
        //id=divRootの配下のdivのElementsCollectionを取得
        val divRootChildren = Selenide.`$$`(By.cssSelector("#divRoot > div"))

        //divRootChildrenのサイズを検証
        divRootChildren.shouldHaveSize(3)

        //id指定でdivRootのtext(配下も含む)を検証 遅延未考慮版
        assertEquals("1段目\n2段目\n3段目", selectById("divRoot").text())

        //id指定でdivRootのtext(配下も含む)を検証 遅延考慮版
        selectById("divRoot").shouldHave(exactText("1段目\n2段目\n3段目"))

        //2段目を含むかの検証
        selectById("divRoot").shouldHave(text("2段目"))
    }
        //divRootChildrenのサイズを検証
        divRootChildren.shouldHaveSize(3)

で取得したdivRootChildren:ElementsCollectionのサイズが3であることを検証しています。

shouldHaveSizeはcom.codeborne.selenide.Configuration.timeout(デフォルト:4000msec)まで
com.codeborne.selenide.Configuration.pollingInterval(デフォルト:200msec)間隔でElementsCollectionのサイズが期待値になる事を検証してくれます。

次はid='divRoot'を条件にtext(その配下も含めて)を取得し期待値と比較しています。

        //id指定でdivRootのtext(配下も含む)を検証 遅延未考慮版
        assertEquals("1段目\n2段目\n3段目", selectById("divRoot").text())

divRootは以下のようになっています。
divRoot.png
まだこの処理は遅延は考慮していない記載方法となっていますので、次に遅延を考慮した検証を行っています。

遅延考慮版
        //id指定でdivRootのtext(配下も含む)を検証 遅延考慮版
        selectById("divRoot").shouldHave(exactText("1段目\n2段目\n3段目"))

でshouldHaveを利用して、exactText:完全一致テキスト比較を行っています。

shouldHaveは肯定の条件指定です。shouldHaveを含んで同じように
肯定系の3種のshould、shouldBe、shouldHave
否定形の3種のshouldNot、shouldNotBe、shouldNotHaveが利用可能です。
動作としてはどれを指定しても同じとなりますので、”common english phrase”としてしっくりくるものを選択いただければと思います。

タイムアウトはshouldHaveSizeと同様の仕様となります。

最後に部分一致の検証を行っています。
exactTextの代わりにtextを利用することで部分一致の検証が可能となります。

部分一致の検証
        //2段目を含むかの検証
        selectById("divRoot").shouldHave(text("2段目"))

id='addButton'を対象としたテスト

addButtonをクリックすると遅延なしにid='div4'をdivRootの配下の最後に追加します。
delayを入れていないだけで、遅延なしとの表現は厳密には違うのですが・・・

addButtonのonclickイベントハンドラ
  function clickAddButton(){
    addDiv4Action(0);
  }

  function addDiv4Action(delay){
    let divRoot = document.getElementById("divRoot");
    // 要素の追加
    let div4 = document.createElement("div");
    div4.id = "div4"; 
    let text1 = document.createTextNode("4段目");
    div4.appendChild(text1);
    window.setTimeout( function() { divRoot.appendChild(div4); }, delay );
  }

addButtonクリック後のHTMLは以下のようになります。
div4追加後.png

テストは以下の通りです。

testAddButton
    @Test
    fun testAddButton() {
        //追加ボタンをクリック
        selectByCssSelector("#addButton").click()

        //単純な処理なので速攻追加されるので遅延を考慮しなくても検証は成功する
        assertEquals("4段目", selectByCssSelector("#div4").text())

        //あるべき姿としては処理遅延があるのでshouldHaveで検証しないとテストの再現性は低い
        selectByCssSelector("#div4").shouldHave(exactText("4段目"))
    }

新しい内容としては、取得したSelenideElementのclickを呼び出してクリックする。
ぐらいです。

div4のtextの検証(遅延未考慮と遅延考慮)を行っていますが、addButtonのonclickでは遅延しないのでどちらも成功します。

id='delayAddButton'を対象としたテスト

delayAddButtonをクリックすると5秒待ってからid='div4'をdivRootの配下の最後に追加します。
5秒なのでcom.codeborne.selenide.Configuration.timeout(デフォルト:4000msec)のままでは検証が失敗します。

delayAddButtonのonclickイベントハンドラ
  function clickDelayAddButton(){
    addDiv4Action(5000);
  }

  function addDiv4Action(delay){
    let divRoot = document.getElementById("divRoot");
    // 要素の追加
    let div4 = document.createElement("div");
    div4.id = "div4"; 
    let text1 = document.createTextNode("4段目");
    div4.appendChild(text1);
    window.setTimeout( function() { divRoot.appendChild(div4); }, delay );
  }

テストは以下の通りです。

    @Test
    fun testDelayAddButton() {
        //遅延追加ボタンをクリック
        selectByCssSelector("#delayAddButton").click()

        //clickDelayAddButtonで遅延させているので遅延を考慮していない検証は失敗する。
        //assertEquals("4段目", selectByCssSelector("#div4").text())

        //clickDelayAddButtonで5秒遅延させているので4秒のタイムアウトでも失敗する。
        //selectByCssSelector("#div4").shouldBe(exactText("4段目"))

        //タイムアウトを6秒に変更すると成功する。
        Configuration.timeout = 6000
        selectByCssSelector("#div4").shouldBe(exactText("4段目"))
    }

コメントに記載している通りなのですが、clickDelayAddButtonの遅延があるのでConfiguration.timeoutを変更しないと検証が成功しません。ここでは6秒に変更しています。

id='deleteButton'を対象としたテスト

次は、div4の削除ボタンのテストです。

ボタンクリック時の動作は、div4の遅延なし削除となります。

deleteButtonのonclickイベントハンドラ
  function clickDeleteButton(){
    deleteDiv4ActionButton(0)
  }

  function deleteDiv4ActionButton(delay){
    // 要素の削除
    let divRoot = document.getElementById("divRoot");
    let div4 = document.getElementById("div4");
    if (div4){
      window.setTimeout( function() { divRoot.removeChild(divRoot.lastChild); }, delay );
    }
  }

テストは以下の通りです。addButtonをクリックし、deleteButtonをクリックし、div4が消えていることを確認しているだけとなります。

    @Test
    fun testDeleteButton() {
        //追加ボタンをクリック
        selectByCssSelector("#addButton").click()

        //削除ボタンをクリック
        selectByCssSelector("#deleteButton").click()

        //div4の非存在確認
        assertEquals(0, Selenide.`$$`(By.cssSelector("#div4")).size)
    }

id='delayDeleteButton'を対象としたテスト

次は、div4の遅延削除ボタンのテストです。

ボタンクリック時の動作は、div4の遅延削除版となります。

delayDeleteButtonのonclickイベントハンドラ
  function clickDelayDeleteButton(){
    deleteDiv4ActionButton(5000)
  }

  function deleteDiv4ActionButton(delay){
    // 要素の削除
    let divRoot = document.getElementById("divRoot");
    let div4 = document.getElementById("div4");
    if (div4){
      window.setTimeout( function() { divRoot.removeChild(divRoot.lastChild); }, delay );
    }
  }

テストは以下の通りです。

    @Test
    fun testDeleteDelayDiv4() {
        //追加ボタンをクリック
        selectByCssSelector("#addButton").click()

        //遅延削除ボタンをクリック
        selectByCssSelector("#delayDeleteButton").click()

        //div4の非存在確認 5秒遅延させているので4秒のタイムアウトでは失敗する。
        //Selenide.`$$`(By.cssSelector("#div4")).shouldHaveSize(0)

        //clickDelayDeleteButtonで5秒遅延させているので4秒のタイムアウトでは失敗するので6秒に変更
        Configuration.timeout = 6000
        //div4の非存在確認
        Selenide.`$$`(By.cssSelector("#div4")).shouldHaveSize(0)
    }

新しい内容は何もないです。clickDelayDeleteButtonで5秒の遅延があるので、Configuration.timeout = 6000として
shouldHaveSize(0)で非存在確認を行っています。

id='hideButton'を対象としたテスト

次は、div4の非表示ボタンのテストです。
ボタンクリック時の動作は、div4の非表示(style="display: none;"に変更)となります。

hideButtonのonclickイベントハンドラ
  function clickHideButton(){
    let div4 = document.getElementById("div4");
    if (div4){
      div4.style.display ="none";
    }
  }

テストは以下の通りです。

    @Test
    fun testHideDiv4() {
        //追加ボタンをクリック
        selectByCssSelector("#addButton").click()

        //非表示ボタンをクリック
        selectByCssSelector("#hideButton").click()

        //div4の非表示確認 XPAHでstyle='display: none;を指定して検索し要素が1つ存在することを検証
        Selenide.`$$`(By.ByXPath("//*[@id=\"div4\"][@style='display: none;']")).shouldHaveSize(1)
    }

非表示になった事を検証するために
XPATH=//*[@id="div4"][@style='display: none;']で要素の存在確認を行っています。
XPATHは細かい条件を指定して検証が行えますので便利です。ただこれを乱発するとコード量が多くなるし、変更にも弱くなるので気を付けないといけないです。

input type='text'を対象としたテスト

対象のHTMLは以下の通りです。

  <label>名前:</label>
  <input type="text" name="nameText" size="30" maxlength="20"></td>

テストは以下の通りです。

    @Test
    fun testName() {
        val nameElement = selectByName("nameText")
        nameElement.sendKeys("名前です。")
        assertEquals("名前です。", nameElement.value)

        //処理遅延を考慮した場合の検証
        nameElement.shouldBe(value("名前です"))
    }

byNameで検索しsendKeysで文字列を入力し、入力できたことを検証しています。
sendKeysでセットされるのはtextではなくvalueとなりますので
shouldBe(value("名前です"))
と記載する必要があります。遅延を考慮した検証も同様となります。

selectを対象としたテスト

対象のHTMLは以下の通りです。

  <label>血液型:</label>
  <select name="blood">
    <option value="A">A型</option>
    <option value="B">B型</option>
    <option value="O">O型</option>
    <option value="AB">AB型</option>
  </select>
  <HR>

テストは以下の通りです。

testBloodSelect
    @Test
    fun testBloodSelect() {
        //value="B"で選択
        selectByName("blood").selectOptionByValue("B")
        assertEquals("B", selectByName("blood").selectedValue)
        assertEquals("B型", selectByName("blood").selectedText)

        //option="O型"で選択
        selectByName("blood").selectOption("O型")
        assertEquals("O", selectByName("blood").selectedValue)
        assertEquals("O型", selectByName("blood").selectedText)

        //option="O型"が選択されていることを検証
        assertTrue(selectByCssSelector("body > select > option:nth-child(3)").isSelected)

        //option="O型"以外が選択されていないことを検証
        for(notSelectedChildIndex in listOf(1, 2, 4)) {
            assertFalse(selectByCssSelector("body > select > option:nth-child($notSelectedChildIndex)").isSelected)
        }

        //処理遅延を考慮した場合の検証
        selectByCssSelector("body > select > option:nth-child(3)").shouldBe(selected)
        for(notSelectedChildIndex in listOf(1, 2, 4)) {
            selectByCssSelector("body > select > option:nth-child($notSelectedChildIndex)").shouldNotBe(selected)
        }
    }

セレクトボックスは、
selectOptionByValue(value)で値指定での選択
selectOption(option)でオプション指定での選択が可能です。

選択されていることの検証は

        //option="O型"が選択されていることを検証
        selectByCssSelector("body > select > option:nth-child(3)").shouldBe(selected)

のようにshouldBe(selected)で行います。

選択されていないことの検証は

        //A型が選択されていないことの検証
        selectByCssSelector("body > select > option:nth-child(1)").shouldNotBe(selected)

のようにshouldNotBe(selected)で行います。

radioを対象としたテスト

対象のHTMLは以下の通りです。

  <label>学年:</label>
  <input type="radio" name="grade" id="gradle1" value="1"><label for="gradle1">1年生</label>
  <input type="radio" name="grade" id="gradle2" value="2"><label for="gradle2">2年生</label>
  <input type="radio" name="grade" id="gradle3" value="3"><label for="gradle3">3年生</label>
  <HR>
testGradeRadio
    @Test
    fun testGradeRadio() {
        selectByName("grade").selectRadio("3")
        selectByCssSelector("#gradle1").shouldNotBe(selected)
        selectByCssSelector("#gradle2").shouldNotBe(selected)
        selectByCssSelector("#gradle3").shouldBe(selected)
    }

ラジオボタンはselectRadio(value)で選択可能です。
値の検証はセレクトボックスと同じとなります。それにしても、id="gradle1"って・・・、ほんと「Gradle Kotlin DSL」に移行してググること多くて、手が勝手に・・・

checkboxを対象としたテスト

対象のHTMLは以下の通りです。

  <label>よく利用する言語:</label>
  <input type="checkbox" name="lang" id="lang1" value="1"><label for="lang1">Java</label>
  <input type="checkbox" name="lang" id="lang2" value="2"><label for="lang2">C#</label>
  <input type="checkbox" name="lang" id="lang3" value="3"><label for="lang3">TypeScript</label>
  <input type="checkbox" name="lang" id="lang4" value="4"><label for="lang4">Kotlin</label>
  <input type="checkbox" name="lang" id="lang5" value="5"><label for="lang5">Ruby</label>
    @Test
    fun tsetHobbyCheckbox() {
        selectByCssSelector("#lang3").isSelected = true
        selectByCssSelector("#lang3").shouldBe(selected)

        for(notSelectedLangIndex in listOf(1, 2, 4, 5)) {
            selectByCssSelector("#lang$notSelectedLangIndex").shouldNotBe(selected)
        }
    }

チェックボックスはisSelectedの値を指定することで選択・非選択を設定できます。
値の検証はセレクトボックスと同じとなります。

初めの3歩目

次は同じid or CssSelectorの要素で非表示の物が存在するときの動作の説明となります。

display:noneの扱い

対象のHTMLは以下の通りです。

  <label>idが重複したdisplay:noneとstyleなしのinput</label>
  <BR>
  <input type="text" name="input1-1" id="input1" size="10" maxlength="20" style="display:none" value="input1-1">
  <input type="text" name="input1-2" id="input1" size="5" maxlength="20" value="input1-2">
  <BR> 
  <input type="text" name="input2-1" id="input2" size="20" maxlength="20" value="input2-1">
  <input type="text" name="input2-2" id="input2" size="6" maxlength="20" style="display:none" value="input2-2">

テストは以下の通りです。
この例であればCssSelectorで重複しない値を指定できるのですが、重複の条件を作り出すために#idをあえて指定しています。

testDisplayNoneInput
    @Test
    fun testDisplayNoneInput() {
        //CssSelectorが同一の要素の単一取得はstyle="display:none"の要素も含んで最初に出現するものを返却
        //#input1の場合はstyle="display: none"の要素が先なのでname=input1-1となる
        val input1Element = selectByCssSelector("#input1")
        assertEquals("input1-1", input1Element.attr("name"))

        //本筋とは関係ないですが、shouldHaveで属性=nameの値がinput1-1であるとの検証も可能です。
        input1Element.shouldHave(attribute("name", "input1-1"))

        //#input2の場合はstyle未指定の要素が先なのでname=input2-1
        val input2Element = selectByCssSelector("#input2")
        assertEquals("input2-1", input2Element.attr("name"))

        //表示されている要素に入力したいときはElementsCollectionを取得してisDisplayedな要素に入力
        val input1List = Selenide.`$$`(By.cssSelector("#input1"))
        for(currentInput1Element in input1List) {
            if(currentInput1Element.isDisplayed) {
                currentInput1Element.clear()
                currentInput1Element.sendKeys("input1NewValue")
            }
        }

        //name=input1-1の値が変更されていないこと
        assertEquals("input1-1", selectByName("input1-1").value)
        //name=input1-2の値がinput1NewValueに変更さていること
        assertEquals("input1NewValue", selectByName("input1-2").value)
    }

新しい内容としては、SelenideElement#attr(attributeName:String)とshouldHave(attribute(attributeName:String, attributeValue:String))ぐらいでしょうか。
見たまんまですが、SelenideElement#attrは属性名を指定して属性値を取得できます。
shouldHaveでattribute利用も説明なしで理解いただけると思います。
表示・非表示に関わらず最初に出現する要素がselectByCssSelectorでは返却されますので、input1Elementのnameはinput1-1となります。

表示されている要素だけを対象としたい場合は、XPATHでstyleを指定してもよいのですが

        //表示されている要素に入力したいときはElementsCollectionを取得してisDisplayedな要素に入力
        val input1List = Selenide.`$$`(By.cssSelector("#input1"))
        for(currentInput1Element in input1List) {
            if(currentInput1Element.isDisplayed) {
                currentInput1Element.clear()
                currentInput1Element.sendKeys("input1NewValue")
            }
        }

のようにElementsCollectionをループしてisDisplayedな要素を絞り込む。
との実装も可能となります。

visibility:hiddenの扱い

対象のHTMLは以下の通りです。

  <label>idが重複したvisibility:hiddenとstyleなしのinput</label>
  <BR>
  <input type="text" name="input3-1" id="input3" size="30" maxlength="20" style="visibility:hidden" value="input3-1">
  <input type="text" name="input3-2" id="input3" size="7" maxlength="20" value="input3-2">
  <BR> 
  <input type="text" name="input4-1" id="input4" size="40" maxlength="20" value="input4-1">
  <input type="text" name="input4-2" id="input4" size="8" maxlength="20" style="visibility:hidden" value="input3-3">

テストは以下の通りです。
display:noneと同じ動作となりますので、説明は省略させていただきます。

testVisibilityHiddenInput
    @Test
    fun testVisibilityHiddenInput() {
        //style="visibility:hidden"もstyle="display:none"と同様
        val input3Element = selectByCssSelector("#input3")
        assertEquals("30", input3Element.attr("size"))

        val input4Element = selectByCssSelector("#input4")
        assertEquals("40", input4Element.attr("size"))

        val input3List = Selenide.`$$`(By.cssSelector("#input3"))
        for(currentInput3Element in input3List) {
            if(currentInput3Element.isDisplayed) {
                currentInput3Element.clear()
                currentInput3Element.sendKeys("input3NewValue")
            }
        }
        assertEquals("input3-1", selectByName("input3-1").value)
        assertEquals("input3NewValue", selectByName("input3-2").value)
    }

初めの4歩目

次は非表示のボタンの説明となります。

重複したCssSelectorを指定したときの動作

対象のHTMLは以下の通りです。

  <label>非表示のbutton1-1とbutton1-2</label>
  <BR>
  <input type="button" id="button1" name="button1-1" value="button1-1" style="display:none" onclick="setClickButton('button1-1')">
  <input type="button" id="button1" name="button1-2" value="button1-2" onclick="setClickButton('button1-2')">
  <label>クリックしたボタンを表示する領域</label><div id="clickButtonResult" name="div1Name">dummy</div>

ボタン押下時の処理は

  function setClickButton(buttonName){
    var buttonResult = document.getElementById('clickButtonResult');
    buttonResult.textContent = buttonName;
  }

となりますので、ボタンクリック後にclickButtonResultのテキストを検証することで、正しく押されたことが判別可能としています。

テストは以下の通りです。

testVisibilityHiddenInput
    @Test
    fun testDisplayNoneButton() {
        /*name="button1-1"は非表示なのでクリックするとElement should be visible or transparent: visible or have css value opacity=0 {#button1}
         と怒られる*/
        try {
            selectByCssSelector("#button1").click()
            fail("button1-1は非表示なのでクリックするとUIAssertionErrorがスローされるべき")
        }catch (e: UIAssertionError) {
            e.stackTrace
        }

        //JavascriptExecutorを利用すればname="button1-1"をクリック可能
        val driver = WebDriverRunner.getWebDriver()
        val executor = driver as JavascriptExecutor
        val element = driver.findElement(By.cssSelector("#button1"))
        executor.executeScript("arguments[0].click()", element)
        assertEquals("button1-1", selectByCssSelector("#clickButtonResult").text)

        //表示されている要素をクリックしたい場合はisDisplayedの要素を特定してクリック
        var clickCount = 0
        val button1List = Selenide.`$$`(By.cssSelector("#button1"))
        for(currentButton1Element in button1List) {
            if(currentButton1Element.isDisplayed) {
                currentButton1Element.click()
                clickCount++
            }
        }

        //ちゃんと押せているか確認
        assertEquals("button1-2", selectByCssSelector("#clickButtonResult").text)
        assertEquals(1, clickCount)
    }

ここでのポイントは、非表示のボタンでもJavascriptExecutorからJavaScriptを利用すればクリック可能なところとなります。
押せない!、操作できない!!、って時にはJavaScript!!!、はSeleniumerには常識だと言えます。

CssSelectorが重複しているボタンの表示さているものをクリックする場合は、ElementsCollectionをループしてisDisplayedなボタンをクリックする。
との方法も可能ですが、たいていの場合、押したいボタンじゃないボタンが発火すると思います。

非表示の要素の配下に存在するボタンの動作

対象のHTMLは以下の通りです。

  <label>visibility:hiddenのdivの内部にbutton2</label>
  <div style="visibility:hidden>
    <input type="button" id="button2" value="button2" onclick="setClickButton('button2')">
  </div>
  <label>クリックしたボタンを表示する領域</label><div id="clickButtonResult" name="div1Name">dummy</div>

テストは以下の通りです。

testVisibilityHiddenInput
    @Test
    fun testClickInterceptedButton() {
        try {
            /*非表示のdivの内部のボタンをクリックするとInvalid element state: element click intercepted:
             Element <div style="visibility:hidden>...<input type=" button"="" id="button2" value="button2"
              onclick="setClickButton('button2')">
              との例外が発生する*/
            selectByCssSelector("#button2").click()
            fail("非表示のdivの内部なのでInvalidStateExceptionがスローされるべき")
        }catch (e: InvalidStateException) {
            e.stackTrace
        }

        //このパターンもJavascriptExecutorでクリック可能
        val driver = WebDriverRunner.getWebDriver()
        val executor = driver as JavascriptExecutor
        val element = driver.findElement(By.cssSelector("#button2"))
        executor.executeScript("arguments[0].click()", element)

        assertEquals("button2", selectByCssSelector("#clickButtonResult").text)
    }

ここでのポイントは、selectByCssSelector("#button2").click()でクリックすると
”Invalid element state: element click intercepted:”
と怒られることとなります。

親要素を表示するようにしてからクリックする。
との方法もありますが、プロダクトコードはdivを「今、表示してます。待ってください!」状態の場合のパターンである可能性が高いですので、親要素が表示状態になることを検証したあとクリックする。
との方針でも良いのですが、その処理を書く時間がもったいないのでJavascriptExecutorでクリックが現実的な解と考えます。
まあこれは好みの問題なので、一概に言えることではないですが・・・

付録

「Chrome Developer Tools」でCssSelectorとXPATHを取得する方法

「Chrome Developer Tools」を普段から利用されている方には不要な説明ですが・・・、
入門と銘打っておりますので、「Chrome Developer Tools」でCssSelectorとXPATHを取得する方法を説明させていただきます。

と言っても、対象の要素を選択して右クリックメニューの「検証」を押下して、右クリックメニューの「Copy」から「Copy selector」や「Copy XPath」をクリックするとクリップボードに値が保存される。ただそれだけですが・・・

CssSelectorの取得に関しては、Selenideの操作処理(Kotlin)を自動生成してみる。
を作成いたしましたので、こちらもご覧いただければ幸いです。

4
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
8