手を動かしながら学ぶSeleniumWebDriverの仕組み

SeleniumWebDriverを使えば、findElementやclickなど直感的なメソッドを並べるだけでブラウザの操作を自動化することができます。
（実運用するにはちゃんと設計しないといけないのはもちろんですが。）

一方で、SeleniumWebDrierがどうやってブラウザを動かしているのか、を意識することは普段ありません。

そこで、普段書いている自動テストスクリプトから一歩進んで、SeleniumWebDriverが何やっているのかを、手を動かしながら把握してみましょう。

（間違ってたらぜひ教えてください。）

## 準備するもの

- GoogleChrome
- ↑のバージョンに対応したchromedriver.exe

別のブラウザでも基本一緒ですが、今回はChromeを使います。

## WebDriverの仕組み概要

各ブラウザ向けのdriverは、API仕様（＝WebDriver Protocol）に従って作られています。

![スライド1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/1bdf2587-171b-16fa-3263-e89e716736c6.png)

driverはHTTPリクエストを受け付けて、その内容に応じた操作指示をブラウザに送るのですが、このときの仕様がWebDriver Protocolです。仕様が存在することによって、ユーザはChrome向けの自動テストとFirefox向けの自動テストを書き分けて・・・といったことをせず、ひとつの自動テストスクリプトで複数の種類のブラウザを動かすことができます。

自動テストスクリプトの中のclickなどのメソッドが実行されると、各言語向けのバインディングによってHTTPリクエストに変換されてdriverに送られます。するとdriverがリクエストに対応した操作をブラウザに対して行う、という仕組みです。

ここまでの説明で完全に理解した場合は続き読まなくて大丈夫です。

## 実際に動かしてみる

ここからは、上に書いた概要を体感するため、手を動かしていきましょう。

### 1. chromedriver.exeを起動する

まず最初に、普段は直接実行することがないchromedriver.exeを起動します。

すると、コマンドラインが立ち上がって、以下のように表示されます。

``` zsh
Starting ChromeDriver 80.0.3987.106 (f68069574609230cf9b635cd784cfb1bf81bb53a-refs/branch-heads/3987@{#882}) on port 9515
Only local connections are allowed.
Please protect ports used by ChromeDriver and related test frameworks to prevent access by malicious code.
```

このとき、ChromeDriverのバージョンや、その後に続く英数字などは場合によって異なりますので、読み替えてください。

ここで気にしていただきたいのは、エラー等が出ていないことと、 `port 9515`のところです。

chromedriverの場合デフォルトのポートが9515になっています。もし異なるポート番号が表示されていたらその番号をどこかに控えておいてください。

いまから、ここで起動したchromedriverに対しHTTPリクエストを送ってブラウザを操作していこうと思います。

### 2. セッションの作成

まず、ブラウザを操作するために新たなセッションを作成します。

コマンドラインから以下のコマンドを実行してリクエストを投げてみましょう。

``` zsh
curl -X POST -H 'Content-Type: application/json' -d '{ "capabilities": {} }' localhost:9515/session
```

成功すると、Chromeが起動して空のタブが表示されます。

そして、コマンドラインには以下のJSONが表示されます。ここでは見やすいように整形していますが、コマンドラインには整形されずずらずらと出てきます。

``` json
{
    "value": {
        "capabilities": {
            "acceptInsecureCerts": false,
            "browserName": "chrome",
            "browserVersion": "80.0.3987.116",
            "chrome": {
                "chromedriverVersion": "80.0.3987.106 (f68069574609230cf9b635cd784cfb1bf81bb53a-refs/branch-heads/3987@{#882})",
                "userDataDir": "/var/folders/0g/y1wlfywj13v09ydx309dg7tw0000gn/T/.com.google.Chrome.Lt6AVt"
            },
            "goog:chromeOptions": {
                "debuggerAddress": "localhost:62900"
            },
            "networkConnectionEnabled": false,
            "pageLoadStrategy": "normal",
            "platformName": "mac os x",
            "proxy": {},
            "setWindowRect": true,
            "strictFileInteractability": false,
            "timeouts": {
                "implicit": 0,
                "pageLoad": 300000,
                "script": 30000
            },
            "unhandledPromptBehavior": "dismiss and notify"
        },
        "sessionId": "ffb44eac88be0f2882e4741946433852"
    }
}
```

platformNameや、その他細かい英数字などは環境によって異なります。

ここで気にする必要があるのは、最後のsessionIdです。この後も使うので、どこかにコピーしましょう。

### 3. URLを開く

次に、先程起動したChromeに対して、指定したURLを開くためのリクエストを送ります。

普段WebDriverを使う際の

``` python
driver.get("http://google.com")
```

的な操作に相当します。

今回は以下のコマンドで、Qiitaを開いてみましょう。

``` zsh
curl -X POST -H 'Content-Type: application/json' -d '{"url": "https://qiita.com/"}' localhost:9515/session/ここに先程メモしたsessionIdを入力/url
```

実行すると、Qiitaのトップページが開きます。

![Qiita.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/c60198d8-efc5-bc4b-efae-08fde20cc7ae.jpeg)

そして、コマンドラインには以下のようなJSONが出力されます。

``` zsh
{"value":null}
```

ここでは特に値が返ってくるわけではありません。

### 4. 要素のクリックを行う

ページが開けたら、あとはそのページ内で操作をしていくわけですが、今回は一番よく使うであろうクリックを行ってみましょう。

まず、クリックしたい対象の要素＝普段find elementする対象の要素のUUID（Universally Unique IDentifier）というものを取得します。ここでは「ユーザ登録」ボタンを対象にUUIDを取得してみましょう。

<img width="322" alt="スクリーンショット 2020-02-20 10.28.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/bfaf57b0-e81f-f9b2-db76-dc04ca0d71a4.png">

``` zsh
curl -X POST -H 'Content-Type: application/json' -d'{ "using": "css selector", "value": "#globalHeader > div > div.st-Header_end > a.st-Header_signupButton" }' localhost:9515/session/ここに先程メモしたsessionIdを入力/element
```

すると、以下のようなJSONが返ってきます。

``` JSON
{
    "value": {
        "element-6066-11e4-a52e-4f735466cecf": "ef3915f5-5570-4d4c-b13a-41d785578484"
    }
}
```

ここで、"using": "css selector"というところでロケータを設定していますが、使えるものは以下の６つです。

State | Keyword
--- | ---
CSS selector | "css selector"
Link text selector | "link text"
Partial link text selector | "partial link text"
Tag name | "tag name"
XPath selector | "xpath"

via [12.2.1 Locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies)

返ってきたJSONを見ると、今回クリックしたい「ユーザ登録」のUUIDが"ef3915f5-5570-4d4c-b13a-41d785578484"だとわかるので、これを使ってクリックさせます。

``` zsh
curl -X POST -H 'Content-Type: application/json' -d'{}' localhost:9515/session/ここに先程メモしたsessionIdを入力/element/ef3915f5-5570-4d4c-b13a-41d785578484/click
```

これで、ブラウザでユーザ登録がクリックされました。

このときも、返ってくるJSONはvalue:nullです。

``` zsh
{"value":null}
```

### 5. その他の操作

ここまでで要素のクリックができました。この先の他の操作は、基本的にクリックと同じです。

やりたい操作に応じて、HTTPリクエストが異なります。

どんなリクエストを送ればよいかは、W3CのWebDriverの項に詳細が書かれているので、こちらを見てみてください。

参照：[6.5 Endpoints](https://www.w3.org/TR/webdriver/#endpoints)

## 言語バインディングの中身も見てみよう

たとえばRubyのバインディングの中だと、こんな形で書かれています。

[selenium/commands.rb at master · SeleniumHQ/selenium](https://github.com/SeleniumHQ/selenium/blob/master/rb/lib/selenium/webdriver/remote/commands.rb)

普段使っているメソッドと、HTTPリクエストとの対応が読み取れます。もちろん実際には、findElementがそのままリクエストに変換されているわけではなく、さらにいくつかの処理が行われています。

が、基本中でやっていることがわかってきたのではないでしょうか。

## おわりに

普段はWebDriverの各言語向けバインディングがやっていることを実際手でやってみることで、SeleniumWebDriverの仕組みの理解が少し進んだのではないかと思います。

ここがわかれば、自作言語に対するオレオレ言語バインディングを作ることも可能です。夢が広がりますね。

## 参考

- [WebDriver](https://www.w3.org/TR/webdriver/)