ブラウザの操作

待機

Webブラウザとテストシナリオアプリは、その構造上常に非同期で動作しています。例えば、URLを指定したページジャンプでは、WebDriver の”get” APIを使用しますが、ページの完全なロードを待つことなくAPIは完了します。従って、ジャンプした直後に表示されるであろう要素を検索した場合、ページのロードが完了する前であれば失敗しますし、完了していたら成功します。 テストシナリオは、こうした非同期性を常に意識して適切なタイミングで処理を行う必要があり、そのために様々な待機機能が用意されています。

一定時間の待機

この例は1000ミリ秒待機します。

await driver.sleep(1000);

要素の待機

この例はxpath変数で指定される要素が見つかるまで、最大1000ミリ秒待機します。

await driver.wait(until.elementLocated(By.XPath(xpath)),1000);

URLの待機

正規表現で示されるURLが表示されるまで、最大1000ミリ秒待機します。

await driver.wait(
        until.urlMatches(/^http.*\/demo\/reg\.html$/),1000);

JavaScriptの変数の待機

以下の例では、appDataというJavaScript変数の内容が”sample”になるまで最大10秒待機しています。この処理は、executeScriptメソッドによりWebブラウザ内に転送されるJavaScript(return appData=="sample"')の戻り値により待機の終了を決定することで実現しています。
従って、変数の値だけではなく任意の処理により待機の完了を判定することが可能であり、ページのロード時に動的に画面を構成するような場合で、その完了を判断するようなケースで有用です。

const getAppData = () =>
    driver.executeScript('return appData=="sample"');

await driver.wait(() => getAppData(), 10000);

alertの待機

alertが表示されるまで、最大1000ミリ秒待機します。

await driver.wait(until.alertIsPresent(), 1000);

上記以外にも様々な待機の方法が用意されていますので、詳細はSeleniumのドキュメントを参照してください。

要素の検索

HTML要素の検索は、id属性、XPath、cssセレクタなどJavaScriptによるページ操作と同様の手法で行うことができます。
どの検索方法が適しているのかはHTMLの構造によりますが、id属性が正しく使用されている場合はid属性を利用すると容易です。

ここでは、サンプルのindex.htmlページから「トップメニュー」と表示されている部分の検索を例として説明します。

確認するページをPC上のブラウザ(ここではChromeブラウザ)で表示し「デベロッパーツール」を表示します。

右側ペインの「Elements」タブを選択して、赤丸の選択アイコンをクリックすると、左側ペインのHTMLページ上で確認したい要素にマウスカーソルを重ねると、右側ペインに対応するエレメントが選択表示されます。

右ペインのエレメント上でコンテキストメニューを表示し、「Copy」> 「Copy XPath」を選択すると、該当エレメントを指すXPath文字列がクリップボードにコピーされます。

コピーしたXPathでエレメントを検索し、文字列を取得するためには以下のように記述します。
24行目でコピーしたXPathを使用しています。

const {Builder,Capabilities,Key,until,By}
                    = require("selenium-webdriver");
let driver = null;
try {
    console.log("capabilitiesの設定");
    const capabilities = new Capabilities()
        .setBrowserName("safari+G.O");

    console.log("WebDriverへ接続");
    driver = new Builder()
        .usingServer("http://192.168.1.147:7000/wd/hub")
        .withCapabilities(capabilities)
        .build();

    console.log("テスト対象アプリのindex.htmlをロード");
    await driver.get(
        "http://dev01.nexaweb.co.jp/GHOST_Operator/demo/index.html");

    console.log("index.htmlのロード完了を1秒待つ");
    await driver.sleep(1000);

    console.log("コピーしたXPathでelementを検索する");
    const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));

    console.log("elementのテキストを取得する");
    const text = await elm.getText();

    console.log("結果=<"+text+">");
} catch (e) {
    console.log(e);
} finally {
    if(driver !== null)
        await driver.quit();
}

実行結果は以下のようになります。

capabilitiesの設定WebDriverへ接続
テスト対象アプリのindex.htmlをロード
index.htmlのロード完了を1秒待つ
コピーしたXPathでelementを検索する
elementのテキストを取得する
結果=<トップメニュー>

マウス操作

マウスの操作は、対象とするHTML要素の表示座標からの相対位置への移動、左右ボタンのクリック、ホイールの操作を行うことができます。

要素のクリック

この例は、XPathで検索した要素の中心をクリックします。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
await elm.click();

要素の表領域中心以外をクリックする場合、actionsオブジェクトを生成し、要素中心からの相対位置を指定して移動した後クリック操作を行います。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const actions = driver.actions({bridge: true});
await actions
.move({origin: elm, x: 10, y: 20})
.click()
.perform();

マウスの右クリックにはcontextClickを使用します。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const actions = driver.actions({bridge: true});
await actions
.move({origin: elm})
.contextClick()
.perform();

マウスカーソルの移動

この例は、XPathで検索した要素の中心へマウスカーソルを移動します。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const actions = driver.actions({bridge: true});
await actions
.move({origin: elm})
.perform();

表示要素の右下へ移動するためには、要素の表範囲を取得し中心からの相対位置を指定します。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const rect = await elm.getRect();
const actions = driver.actions({bridge: true});
await actions
.move({origin: elm, x:rect.width/2, y:rect.height/2})
.perform();

キーボード操作

入力項目などへの文字列入力と、ショートカットキーなど文字入力以外のキー操作を行うことができます。

一般的なキー入力操作はWebElement.sendKeysメソッドを使用します。
通常、sendKeysメソッドを呼び出すと、対象となるHTML要素に対して自動的にキーボードフォーカスが与えられキー入力が実行されますが、GHOST Operatorを使用した場合では対象とするHTML要素は考慮されず、自動的にフォーカスを与える動作はありません。その時点でキーボードフォーカスを持つオブジェクトに対して作用することに注意してください。

テキスト入力

console.log("「お名前」入力欄の表示を待つ");
const nameElm = await driver.wait(
        until.elementLocated(By.id("name1", 3000)));
console.log("「お名前」入力欄をクリック");
await nameElm.click();  // クリックで明示的にキーボードフォーカスを与える
console.log("「お名前」入力欄にYamada Tarouを入力");
await nameElm.sendKeys("Yamada Tarou");

制御キー

Enterキーや矢印キーなど文字にならない制御用キーでもsendKeysで入力することができます。通常使用されるキーは、Keyオブジェクトのメンバとして定義されており、EnterキーはKey.ENTER、左矢印キーはKey.LEFTで指定できます。
定義されているキーはSeleniumのドキュメント Enumeration Keyを参照してください。

await nameElm.sendKeys(
        Key.COMMAND,"a",Key.COMMAND,Key.BACK_SPACE);
await nameElm.sendKeys("Nakamura", Key.TAB);

モディファイアキーについて
sendKeysメソッドでは、ShiftキーやCommandキー等のモディファイアキーを使用した場合、キーの押下状態はトグル動作となり、最初の出現時に押して次の出現時に放す動作となります。つまり、上記の例では最初のKey.COMMANDでCommandキーを押した状態となり、続く”a”で全選択、その後のKey.COMMANDでCommandキーが離されます。
また、sendKeysの終了時にすべてのモディファイアキーを離す操作が実行されます。従って、Key.COMMANDが一つだけ指定され明示的に離す操作がない場合でも、sendKeysの終了時にはCommandキーは離された状態となります。

クリップボード操作

コピーは文字列を選択してからCommand+C、ペーストは貼り付け位置にキャレットを移動してからCommand+Vで操作しますが、GHOST Operatorを利用するとこのような文字入力以外のキーボード操作を再現することが可能です。この例では、名前の入力に続きTABキーでメールアドレス欄にフォーカスを移動してYamada.Tarou@nexaweb.comを入力し、Command+aで全選択、Command+cでコピーを行い、Tabキーでメールアドレス確認欄にフォーカス移動してからCommand+vでペーストしています。

const elm= await driver.findElement(By.id("name1"));
await elm.click();
await driver.sleep(100);
await elm.sendKeys("[HanjaMode On]yamada",
    Key.SPACE,Key.RETURN," ");
await elm.sendKeys("tarou",
    Key.SPACE,Key.RETURN,"[HanjaMode Off]");
await elm.sendKeys(Key.TAB);
await elm.sendKeys("Tarou.Yamada@nexaweb.com");
await elm.sendKeys(Key.COMMAND,"ac",Key.COMMAND, Key.TAB);
await elm.sendKeys(Key.COMMAND, "v");

上記例では、実際には名前入力欄、メールアドレス入力欄、メールアドレス確認欄の3つのフィールドに文字入力を行っていますが、すべての入力欄でnameElm変数を使用しています。このような記述では一般的なWebDriverでは誤動作しますが、GHOST Operatorはキー入力はHID EmulatorによりBluetoothキーボードの入力として処理されるため常にキーボードフォーカスを持つオブジェクトに作用し、APIを呼び出すオブジェクトは意味を持ちません。
逆に、検索した要素を対象に文字入力する場合でも、単に検索した要素を保持する変数のsendKeysメソッドを呼び出してもその項目に入力することはできず、TABキーやマウスクリックにより明示的にキーボードフォーカスを与える必要がある事に注意してください。

IMEを使用したテキスト入力

GHOST Operatorを利用した場合、独自拡張されたキーシーケンスによりIMEモードを切り替えることができ、”[HanjaMode On]”でIMEはONに、”[HanjaMode Off]”でIMEはOFFになります。また、IMEによる変換操作中に発火するイベントも正確に再現されまするため、読み仮名の自動入力などでも自動テストで実行することが可能です。
しかし、IMEの変換結果は辞書の学習により変動するため、常に同じ変換結果を期待する自動テストの観点からは、不都合となることが多いです。

こうした問題を抑止するため、テストシナリオでは変換対象の仮名文字と変換結果の漢字を1対1となるように注意する必要があります。
例えば、「やまだ」は「山田」に対応し、「たろう」は「太郎」に対応するなどは問題となり難いですが、読み仮名の入力などで「やまだ」を変換せずに「やまだ」で確定する場合と「山田」で確定する場合が混在したりすると期待した変換結果が得られない場合があります。また、iOSの設定でIMEの様々な便利機能の設定ができますが、「自動修正」や「ライブ変換」などが有効になっていると変換結果が予測できなくなるため、すべてOFFに設定することをお勧めします。
また、iPadの性質としてシステム負荷が高い場合にキー入力を取りこぼすことがあり、特にIMEの処理や文字の選択操作などによる負荷が後続のキー入力を阻害するような挙動を見せることがあります。このような挙動がある場合、driver.sleep()などで少し間を開けると正常動作する事があります。

以下の例ではnameElmのクリック後、フォーカス移動を待つために100msecのsleepを挿入しています。

const elm = await driver.findElement(By.id("name1"));
await elm.click();
await driver.sleep(100);
await elm.sendKeys(
        "[HanjaMode On]yamada",Key.SPACE,Key.RETURN," ");
await elm.sendKeys(
        "tarou",Key.SPACE,Key.RETURN,"[HanjaMode Off]");

キーストロークの操作

sendKeysメソッドは、文字列の入力のために使用されますが、単にキーの押し離しを行いたい場合には、keyDown, keyUpを使用します。sendKeysがWebElementオブジェクトのメソッドであるのに対し、keyDown, keyUpはWebDriverのメソッドであり、特定のHTML要素には関連しません。

もちろん、keyDown, keyUpを用いてもテキスト入力は可能ですが、「文字の入力」ではなく「キーの押し離し」のための機能であり、sendKeysのように文字の入力に特化した便利な機能はありません。
具体的には、キーボードに物理的に配置されているボタンを操作するだけですので、ボタンの存在しない操作を行うことはできません。従って、”a”や”1”は押すことができますが、”A”や”!”というボタンは存在しないので押すことはできませんし、sendKeysで利用可能なGHOST Operatorの拡張文字である[Home]や[HanjaMode On]なども利用できません。
また、押したボタンは明示的に離す処理も必要となり、keyDown(“a”)には対になるkeyUp(“a”)の記述が必ず必要となります。

モディファイアキーの動作もsendKeysとは異なり、明示的なkeyUpを実行するまで押されたままになります。
例えば以下のようなケースです。

const elm = await driver.findElement(By.id("name1"));
await elm.click();
const actions = driver.actions({bridge: true});
await actions
    .keyDown(Key.SHIFT)
    .keyDown("a")
    .keyUp("a")
    .perform();
elm.sendKeys("bc");
elm.sendKeys("def");

上記でelmに入力される文字は”ABCdef”となります。
これは、5行目で押したSHIFTキーは9行目でも押されたままとなり、sendkeysの終了時のモディファイアキーを開放する動作により離されるためです。

また、例えば以下のようなケースでは

const elm = await driver.findElement(By.id("name1"));
await elm.click();
const actions = driver.actions({bridge: true});
await actions
    .keyDown(Key.SHIFT)
    .keyDown("a")
    .keyUp("a")
    .perform();
elm.sendKeys("BC");
elm.sendKeys("def");

上記でelmに入力される文字は”Abcdef”となります。
これは、5行目で押したSHIFTキーは9行目でも押されたままとなりますが、snedKeysで大文字”B”の入力により自動的にSHIFTキーが押された結果、逆にトグル動作によりSHIFTキーは離されることになり小文字”b”が入力れるためです。

Actions.でもsendKeysメソッドが定義されており、文字列の入力に利用することもできます。

const elm = await driver.findElement(By.id("name1"));
await elm.click();
const actions = driver.actions({bridge: true});
await actions
    .keyDown(Key.SHIFT)
    .sendKeys("ab")
    .perform();
elm.sendKeys("C");
elm.sendKeys("def");

ActionsのsendKeysはWebElement.sendKeysと同じメソッド名でも動作が異なることに注意が必要です。
キーストロークの操作は本来Actions.keyDownとkeyUpで記述しますが、これらのメソッドはひとつのキーを示す引数しか指定できない為、連続する文字列のキー操作は非常に多くの記述が必要となってしまいます。Actions.sendKeysメソッドはこうした問題に対して簡略化して記述できるようになっており、指定文字列は内部的にActions.keyDownとActions.keyUpに分解して実行されます。
従って、WebElement.sendKeysのようにモディファイアキーがトグル動作になることはなく、Actions.sendKeys(Key.SHIFT,”a”)は、keyDown(Key.SHIFT), keyUp(Key.SHIFT), keyDown(“a”), keyUp(“a”)に展開されます。
Actions.sendKeys("A")などのモディファイアキーが必要なキーシーケンスでは、keyDown(Key.SHIFT), keyDown(“a”), keyUp(“a”), keyUp(Key.SHIFT)に展開されることを期待しますが、現在のJavaScript用WebDriverライブラリ(2021年2月現在では4.0.0-beta.1)では単純に keyDown(“A”), keyUp(“A”)に展開されるため、GHOST OperatorのWebDriver Serverでは存在しないキーと見なされ動作しないことに気を付けてください。

マウスとキーボードの複合操作

SHIFTキーを押しながらクリックなどの複合操作は、actionsを利用して行います。
以下の例のようにモディファイアキーは押した状態を維持してマウス操作が可能ですが、一般キー(”a”,”b”,”c”などのような)はkeyDownの直後にkeyUpが必要であり、キーを押したままマウス操作を行うことはできません。

const elm = await driver.findElement(By.id("MemoTextarea"));
const rect = await elm.getRect();
const actions = driver.actions({bridge: true});
await actions
    .keyDown(Key.SHIFT)
    .move({origin: elm, x: 10, y: 20})
    .click()
    .move({origin: elm, x: 200, y: 20})
    .keyUp(Key.SHIFT)
    .perform();

状態の取得

テスト結果の合否判定では、現在のページがどのような状態となっているかを把握することが必要です。
WebDriverAPIでは、HTML要素の状態について詳細を把握するための機能を整えています。

ID

HTML要素のIDを取得します。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const value = await elm.getId();
console.log("id="+value );

TagName

HTML要素のタグ名を取得します。
const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const value = await elm.getTagName();
console.log("tagName="+value );

Displayed

HTML要素が表示状態にあるのか調べます。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const value = await elm.isDisplayed();
console.log("isDisplayed="+value );

Enabled

HTML要素が活性状態にあるのか調べます。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const value = await elm.isEnabled();
console.log("isEnabled="+value );

Selected

HTML要素が選択状態にあるのか調べます。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const value = await elm.isSelected();
console.log("isSelected="+value );

Rect

HTML要素の表領域を取得します。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const value = await elm.getRect();
console.log(
    "rect x="+value.x+" y="+value.y+
    " width="+value.width+" height="+value.height);

CSS

HTML要素に現在適応されているCSSスタイルの値を取得します。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const value = await elm.getCssValue("font-family");
console.log("font-family="+value );

Text

HTML要素とその子孫に現在表示されているテキスト(innerText)を取得します。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const value = await elm.getText();
console.log("text="+value );

Attribute

HTML要素の現在の属性値を取得します。

const elm = await driver.findElement(
        By.xpath("/html/body/div[1]/header/div/h1"));
const value = await elm.getAttribute("class");
console.log("class="+value );

Screenshot

現在表示されているページのスクリーンキャプチャ画像を取得します。
WebDriverの仕様では、WebDriver.takeScreenshotでページ全体を、WebElement.takeScreenshotで指定要素内をキャプチャした画像を返しますが、GHOST operatorではWebElement.takeScreenshotはサポートされず、ページ全体のキャプチャのみ取得できます。
また、GHOST Operatorでは、その構造上の制約によりHTML構造より現在表示されているであろう画像イメージを算出します。従って、厳密にスクリーンに表示されているイメージと一致することはありません。

const fs = require('fs');
const path = require('path');

// takeScreenshot()はbase64エンコードされたPNG形式の画像を戻します。
const img_b64 = await driver.takeScreenshot();
// base64エンコードをデコードしてバイナリに変換
const img_bin = Buffer.from(img_b64, "base64");
// 画像保存の準備
const dirName = "images";
const fileName = "snapshot";
const fn = path.normalize(path.format({
        dir: dirName,
        name: fileName,
        ext: ".png"
    }));
// ディレクトリ作成
const fnDir = path.dirname(fn);
if (!fs.existsSync(fnDir))
    fs.mkdirSync(fnDir,{recursive:true});
// 画像をファイルに保存
fs.writeFileSync(fn, img_bin);