Selenium 使いのための PhantomJS 解説


この記事は、[Selenium Advent Calendar 2013](http://www.adventar.org/calendars/128)の8日目の記事です。

[Web+DB PRESS vol.77]()の特集1「スマートフォンテスト最前線」の第3章で、様々なSeleniumのDriverの紹介をしました。
4章の中でPhantomJSも取り上げることになっていたため、3章ではあえて取り扱いませんでした。
そこで、本記事では、Selenium使いのためのPhantomJS解説と銘打って、PhantomJSDriverの紹介とそのTipsを解説します。

## PhantomJS とは

PhantomJS は、ヘッドレスなWebKitベースのブラウザで、JavaScriptの実行も可能です。ヘッドレスなブラウザを操作するために、PhantomJSはJavaScriptのインターフェースを提供しています。またそれをラップした様々な関連ライブラリがあります。

## GhostDriver

PhantomJSをラップして、JSON Wire ProtocolをしゃべれるようにしたのがGhostDriverです。
GhostDriverは1.8からPhantomJS本体に取り込まれました。
phantomjsの起動オプションで、`-w`か`--wd`か`--webdriver`のオプションで、Seleniumのサーバのポートを指定することができます。

JSON Wire Protocolさえしゃべってもらえれば、SeleniumのRemoteDriverクライアントを使ってどうにでもできるのがSeleniumのよいところです。
もちろん、RemoteDriverでも問題はないのですが、Selenium公式にPhantomJSDriverがあるのでそれを使った方が簡単です。

以下はRubyの selenium-webdriver での例となるのですが、事前にphantomjsを必要がなく、また、開いているポートをよしなに探してくれるので、同時にスクリプトが走ったらとかそういうことを気にしなくてすみます。

```ruby:before
# このスクリプトの前にGhostDriverを 8080番で起動しておく
# phantomjs -w 8080 

require "selenium/webdriver"
driver = ::Selenium::WebDriver.for(:remote, url: "http://localhost:8080")
driver.get "http://www.google.com"
```

```ruby:after
require "selenium/webdriver"
driver = ::Selenium::WebDriver.for(:phantomjs)
driver.get "http://www.google.com"
```

## GhostDriverの制約

当然ですが、GhostDriverはJSON Wire Protocolを利用するのでその定義されているAPI以上のことはできません。
例えば、PhantomJSでは、`customHeaders`により任意のHTTPヘッダーを用いてリクエストすることができるのですが、JSON Wire Protocolにはそれを実現可能なインターフェースがないためその機能を利用することができません。

ただ、Seleniumの良いところはクロスブラウザでのテストがそれなりに簡単に実現できることなので、このようなブラウザ独自の機能が利用できなくても大きな問題はないはずです。
どうしてもそのようなブラウザ独自の機能が使いたいのであれば、そもそもSeleniumを選択する意味がないと思います。

### Alert、Confirm が処理できない

それよりも問題なのが、GhostDriverでAlert系のAPIが全く実装されていないことです。
https://github.com/detro/ghostdriver/issues/20

あまり詳しくは追っていないのですが、PhantomJSではalertのダイアログを出してしまうと、JavaScriptがブロックされてしまうため、実際にダイアログを表示したりせずに、onAlertとかのcallbackを予め設定することで、擬似的にダイアログの処理を行うというようになっているようです。

一方で、SeleniumのAlert系のAPIが期待することは、ダイアログがいままさに開かれて止まっているブラウザに対して何らかの操作をすることです。
通常のSeleniumでは、alertに対して以下のようなコードを書きます。
何らかの操作をした結果、表示されているalertに対して情報を取得したり、操作したりするというような形になります。

```ruby:普通のブラウザの場合
driver.find_element(:css, "#hoge").click
driver.switch_to.alert.text
driver.switch_to.alert.accept
```

しかし、PhantomJSのやり方では、clickする前にonAlertの登録をしないとだめです。

現在この問題のワークアラウンドとして、PhantomJSでのonAlertで処理するように、以下のようにwindow.alertやwindow.confirmを置き換える方法があります。
普通のブラウザに対する操作と同等のコードは以下のようになります。

```ruby:PhantomJSの場合
driver.execute_script <<_JS_
(function () {
    var lastAlert = undefined;
    window.alert = function (message) {
        lastAlert = message;
    };
    window.getLastAlert = function () {
        var result = lastAlert;
        lastAlert = undefined;
        return result;
    };
}());
_JS_
driver.find_element(:css, "#hoge").click
driver.execute_script("return window.getLastAlert()")
```

したがって、PhantomJSと他のブラウザを使い分けることがあり、その中でalertを扱う場合は、現在利用しているブラウザに応じて処理を切り替える必要があります。
例えば、以下のようなメソッドを定義しておいて、それを使うようにしておくといいと思います。

```ruby
def with_alert(&block)
  if phantomjs?
    # phantomjs の場合
  else
     # 普通のブラウザの場合
  end
end

with_alert do
  find_element(:css, "#hoge").click
end
```

## まとめ

Seleniumから簡単にヘッドレスブラウザであるPhantomJSを利用できるようになったのは、特にCI環境でSeleniumのテストを回す際には非常に便利なのでぜひとも活用してください。

最後に紹介したGhostDriverのalert問題ですが、RESTfulなAPIであるJSON Wire Protocolでの解決はRESTfulであるば故に若干難しそうです。
また、PhantomJS側での対応もブロックすることができないために簡単ではなさそうです。
この問題はしばらく解決されなさそうな予感がしますので今後とも注意が必要だと思います。

明日は、[@geotana](https://twitter.com/geotana)さんです。懇親会の席で突然ネタが降ってきたSelenium体操が見られるという噂です。
また、[Selenium Advent Calendar 2013](http://www.adventar.org/calendars/128)はまだまだ募集中ですので、ぜひとも参加をよろしくおねがいします！