#はじめに
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の内部に存在するボタン
も含んでおります。
##操作対象の動作イメージ
##操作対象の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を起動してプロジェクトを作成します。
「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]をクリックします。
sampleを入力しリターンボタンを押下します。
sampleを選択して右クリックメニューから[New]-[Kotlin Class/File]をクリックします。
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をまとめて取得可能です。
@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は以下のようになっています。
まだこの処理は遅延は考慮していない記載方法となっていますので、次に遅延を考慮した検証を行っています。
//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を入れていないだけで、遅延なしとの表現は厳密には違うのですが・・・
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は以下のようになります。
テストは以下の通りです。
@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)のままでは検証が失敗します。
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の遅延なし削除となります。
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の遅延削除版となります。
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;"に変更)となります。
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>
テストは以下の通りです。
@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>
@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をあえて指定しています。
@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と同じ動作となりますので、説明は省略させていただきます。
@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のテキストを検証することで、正しく押されたことが判別可能としています。
テストは以下の通りです。
@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>
テストは以下の通りです。
@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)を自動生成してみる。
を作成いたしましたので、こちらもご覧いただければ幸いです。