Seleniumの薄っすい話4:俺と非公式バインディング

この記事は[Selenium/Appium Advent Calendar 2015](http://qiita.com/advent-calendar/2015/selenium)の4日目の記事です。

薄っすい記事も4日目です！こんにちは！
JavaかC#かRubyかPythonかJavaScriptですか！！
Perlは死にますか！！！ PHPはどうですか！！！
そうですか。

# 非公式バインディングの存在

Seleniumを利用するための言語バインディングについては「公式」「非公式」と呼ばれがちなくくりがありますが、これは単にSeleniumの開発主体チームが手当てしているか手が回っていないかというだけのくくりです。
公式が良くて非公式が悪いとか、そういうのはないです。

* 公式バインディングがある言語
  * Java
  * C#
  * Ruby
  * Python
  * JavaScript
* 非公式バインディングがある言語・公式サイトで紹介あり
  * Perl
  * PHP
  * Haskell
  * Objective-C
  * R
  * Dart
  * Tcl
* 非公式バインディングがある言語・公式サイトで紹介なし
  * [Smalltalk](http://ss3.gemstone.com/ss/Parasol.html)
  * [Bash](https://github.com/jfriesse/selenium-bash)（Selenium RCのみ対応）

# なんでこんなに対応言語多いの？

「Seleniumに対応する」と言っても大きく次の2つの段階があって、第1段階を実装するだけでもとりあえず対応した感あるからじゃないですかね〜

1. Selenium ServerとRESTfulな通信をするだけの、RemoteWebDriverのみ実装する。ブラウザーとの直接のやりとりはSelenium Serverから後段にまかせる。
2. ブラウザーと直接なんだかんだするFirefoxDriverやChromeDriverなども実装する。

![unofficial01.png](https://qiita-image-store.s3.amazonaws.com/0/33963/e1d65d09-e4cb-67d1-c580-6b6826b00c9f.png)

前述の非公式バインディングを一通り見ていると、第1段階でとどめているためにSelenium Serverの併用が必須になっているケースが多いように思います。

つまり、**HTTPリクエストを投げられる処理系なら、とりあえず何でもSeleniumに対応できる**ということですし、もしそういう直接的なAPIが無くても**cURLがあればなんとかなる**んだろ？

# 本題はここからだ！！！

つまりBashでcURL叩いてるだけでBashバインディングを名乗って良いということですねヒャッハーーーーーー！！！！！！１１１

# Bashバインディングやってみよう

では、WebDriver APIのごくごく基本的な操作を、シェルスクリプトしばりでやってみましょう。
Selenium Serverは[公式サイト](http://www.seleniumhq.org/download/)からダウンロードして、次のように実行しておきましょう。

```bash
$ java -jar selenium-server-standalone-2.48.2.jar
```

今回作ってみるのは次の処理です。

1. Selenium Serverに接続する
2. あるURLにアクセスさせる
3. DOMのidを指定して、画面要素を得る
4. 得た画面要素に対してクリック操作をさせる
5. 得た画面要素に対してキー操作をさせる
6. スクリーンショットを撮る
7. Selenium Serverへの接続を解除する

## Selenium Serverに接続する

まず、Selenium Serverへのセッションを作りましょう。
`/session`に、CapabilitiesのJSON付きでPOSTします。最低限、`desiredCapabilities`に`browserName`の指定が必要です。ここではサンプルとしてFirefoxを起動させます。

```bash
$ RESPONSE=$(curl --request "POST" \
    --data '{"desiredCapabilities":{"browserName":"firefox"},"requiredCapabilities":{}}' \
    "http://localhost:4444/wd/hub/session")
```

実行すると、Firefoxが起動します。
この処理を関数にするなら`New-RemoteWebDriver`という名前にでもすればいいでしょう。
成功すれば、セッションID（`sessionId`）をはじめとした、諸々の情報が含まれたレスポンスを得られます。

```json:レスポンス
{
    "status" : 0,
    "sessionId" : "e476a7ac-454f-4bfe-9fcd-e6434684c8fc",
    "state" : null,
    "value" : {
        "platform" : "MAC",
        "javascriptEnabled" : true,
        "acceptSslCerts" : true,
        "browserName" : "firefox",
        "rotatable" : false,
        "locationContextEnabled" : true,
        "webdriver.remote.sessionid" : "e476a7ac-454f-4bfe-9fcd-e6434684c8fc",
        "version" : "42.0",
        "databaseEnabled" : true,
        "cssSelectorsEnabled" : true,
        "handlesAlerts" : true,
        "webStorageEnabled" : true,
        "nativeEvents" : false,
        "applicationCacheEnabled" : true,
        "takesScreenshot" : true
    },
    "class" : "org.openqa.selenium.remote.Response",
    "hCode" : 600786023
}
```

このセッションIDは、この後のリクエストでずっと使い回すものです。パースして保存しましょう。JSONパーサーが無くてもsedがあればなんとかなるだろ！ つべこべ言うな！！！！

```bash
$ SESSION_ID=$(echo ${RESPONSE} | \
    sed -e 's/^.*"sessionId":"\([^"]*\)".*/\1/g')
```

## あるURLにアクセスさせる

ここでは、サンプルとして[日本Seleniumユーザーコミュニティが提供するサンプルページ](http://example.selenium.jp/reserveApp/)にアクセスさせてみましょう。
`/session/SESSION_ID/url`に、URLを記載したJSON付きでPOSTします。

```bash
$ RESPONSE=$(curl --request "POST" \
    --data '{"url":"http://example.selenium.jp/reserveApp/"}' \
    "http://localhost:4444/wd/hub/session/"${SESSION_ID}"/url")
```

この処理を関数にするなら`WebDriver-Get`という名前にでもすればいいでしょう。
実行して成功すると、指定したURLにアクセスして、成功した状態を表すレスポンスを得られます。

```json:レスポンス
{
    "status" : 0,
    "sessionId" : "e476a7ac-454f-4bfe-9fcd-e6434684c8fc",
    "state" : "success",
    "value" : null,
    "class" : "org.openqa.selenium.remote.Response",
    "hCode" : 1697607286
}
```

## DOMのidを指定して、画面要素を得る

サンプルページのうち、「お名前」のテキストフィールド要素への参照を取得してみましょう。
`/session/SESSION_ID/element`に、ロケーターの種類 `using`と、ロケーター文字列 `value`を記載したJSON付きでPOSTします。

```bash
$ RESPONSE=$(curl --request "POST" \
    --data '{"using":"id","value":"guestname"}' \
    "http://localhost:4444/wd/hub/session/"${SESSION_ID}"/element")
```

この処理を関数にするなら`WebDriver-FindElement-ById`という名前にでもすればいいでしょう。
実行して成功すると、要素への参照を示す一時的なID（`ELEMENT`）を含むレスポンスを得られます。

```json:レスポンス
{
    "status" : 0,
    "sessionId" : "e476a7ac-454f-4bfe-9fcd-e6434684c8fc",
    "state" : "success",
    "value" : {
        "ELEMENT" : "0"
    },
    "class" : "org.openqa.selenium.remote.Response",
    "hCode" : 1200638256
}
```

この要素のIDは、このページ内のリクエストでずっと使い回せるものです。パースして保存しましょう。JSONパーサーが無くてもsedがあれば（略）

```bash
$ ELEMENT_ID=$(echo ${RESPONSE} | \
    sed -e 's/^.*"ELEMENT":"\([^"]*\)".*$/\1/g')
```

## 得た画面要素に対してクリック操作をさせる

「お名前」のテキストフィールド要素への参照を取得できましたので、この要素をクリックしてみましょう。
`/session/SESSION_ID/element/ELEMENT_ID/click`に、JSON無しでPOSTします。

```bash
$ RESPONSE=$(curl --request "POST" \
    "http://localhost:4444/wd/hub/session/"${SESSION_ID}"/element/"${ELEMENT_ID}"/click")
```

この処理を関数にするなら`WebElement-Click`という名前にでもすればいいでしょう。
実行して成功すると、テキストフィールドがクリックされて、成功した状態を表すレスポンスを得られます。

```json:レスポンス
{
    "status" : 0,
    "sessionId" : "e476a7ac-454f-4bfe-9fcd-e6434684c8fc",
    "state" : "success",
    "value" : null,
    "class" : "org.openqa.selenium.remote.Response",
    "hCode" : 1761586552
}
```

## 得た画面要素に対してキー操作をさせる

「お名前」のテキストフィールド要素に対してキー操作することで、文字を入力させましょう。
`/session/SESSION_ID/element/ELEMENT_ID/value`に、入力する値の配列のJSON付きでPOSTします。
なお、私は新人の時に先輩からBashの素晴らしさについてバッチリ教育されましたので、多少の思想の偏りはしょうがないと思います。

```bash
$ RESPONSE=$(curl --request "POST" \
    --data '{"value":["all your shell are belong to bash."]}' \
    "http://localhost:4444/wd/hub/session/"${SESSION_ID}"/element/"${ELEMENT_ID}"/value")
```

この処理を関数にするなら`WebElement-SendKeys`という名前にでもすればいいでしょう。
実行して成功すると、テキストフィールドに一種のプロパガンダが入力されて、成功した状態を表すレスポンスを得られます。

```json:レスポンス
{
    "status" : 0,
    "sessionId" : "e476a7ac-454f-4bfe-9fcd-e6434684c8fc",
    "state" : "success",
    "value" : null,
    "class" : "org.openqa.selenium.remote.Response",
    "hCode" : 574322827
}
```

## スクリーンショットを撮る

では、記念にスクリーンショットを撮りましょう。
`/session/SESSION_ID/screenshot`をGETします。

```bash
$ RESPONSE=$(curl --request "GET" \
    "http://localhost:4444/wd/hub/session/"${SESSION_ID}"/screenshot")
```

この処理を関数にするなら`TakeScreenshot-GetScreenshotAs`という名前にでもすればいいでしょう。
実行して成功すると、`value`にBASE64エンコードされたPNG画像のデータが入ったクソでかいレスポンスを得られます。
あまりデカイので、次の例では`[ ... base64 value ... ]`と略記します。

```json:レスポンス
{
    "status" : 0,
    "sessionId" : "e476a7ac-454f-4bfe-9fcd-e6434684c8fc",
    "state" : "success",
    "value" : "[ ... base64 value ... ]",
    "class" : "org.openqa.selenium.remote.Response",
    "hCode" : 1542510082
}
```

じゃあ`value`をbase64コマンドに食わせればPNG画像出力できるよね！！！！

```bash
$ echo ${RESPONSE} | \
    sed -e 's/^.*"value":"\([^"]*\)".*$/\1/g' | \
    tr -d '\\' | \
    tr -d '\n' | \
    base64 --decode --output sample.png
```

## Selenium Serverへの接続を解除する

最後に、接続を解除するために、セッションIDつきでDELETEしましょう。

```bash
$ RESPONSE=$(curl --request "DELETE" \
    "http://localhost:4444/wd/hub/session/"${SESSION_ID})
```

この処理を関数にするなら`WebDriver-Quit`という名前にでもすればいいでしょう。
実行して成功すると、ブラウザーが閉じて、成功した状態を表すレスポンスを得られます。

```json:レスポンス
{
    "state" : "success",
    "sessionId" : "3161aabc-115e-43aa-ab94-65e549002ab7",
    "hCode" : 492200972,
    "value" : null,
    "class" : "org.openqa.selenium.remote.Response",
    "status" : 0
}
```

Bashでできちゃいました！！！！ フゥーーーーー！！！！！

# 薄っすい話なので以上です。

この記事を書くにあたって、Microsoftが当時公開していたWebDriver APIの練習台「Test Drive - WebDriver : Microsoft Edge Dev」が非常に役立ちました。

あと、冒頭で紹介しました[selenium-bash](https://github.com/jfriesse/selenium-bash)は、Makefileの中身がSelenium RCのXSLTを読み込んでいるだけという変態性を見せており私はまだまだ修行が足りないなと思いました。

次回はmeganetaaanです。よろしくお願いします。
[Selenium/Appium Advent Calendar 2015](http://qiita.com/advent-calendar/2015/selenium)はまだまだ薄っすいのも濃ゆいのも募集中です！