poltergeist (PhantomJS) でFacebookのJavascript SDKによるログインをテストする方法

[jonleighton/poltergeist](https://github.com/jonleighton/poltergeist "jonleighton/poltergeist")というgemを使えば、capybaraのバックエンドとしてPhantomJSが使えます。

最近のpoltergeistにはポップアップのウインドウを扱うためのAPIが追加されてまして、これを上手く使えば、今までかなり難問だったJS SDK経由のFacebookログインがcapybaraとPhantomJS経由で実現できるようになります。

(注: 2013/7/24時点で、まだ正式リリースされていない機能を利用しているため、poltergeistはgithubのmasterブランチを利用する必要があります)

まずspec_helperを少し弄ります。

```rb:spec_helper.rb
require "capybara/poltergeist"

Capybara.register_driver :poltergeist do |app|
  Capybara::Poltergeist::Driver.new(app, phantomjs_options: ["--web-security=false"])
end

def find_available_port_for_test
  server = TCPServer.new('127.0.0.1', 0)
  server.addr[1]
ensure
  server.close if server
end

port = find_available_port_for_test
Capybara.app_host = "http://local.facebookapp.jp:#{port}" # Facebookアプリに登録したドメインにしておく
Capybara.server_port = port
Capybara.javascript_driver = :poltergeist
```

app_hostを書き換える事で、capybaraが接続しに行くURLのホスト部分を書き換える事ができます。
Facebookに登録したドメインでアクセスしないとセキュリティによってログインが弾かれます。
app_hostは接続先のURLを書き換えるだけなので、このホスト名でアクセス出来るようにhostsを設定してローカルホストに解決されるようにしておかないと、テストサーバーまでアクセスがこなくなってしまいます。
また、PhantomJSのオプションとしてweb-securityをオフにしておかないと、外部ドメインへのアクセスが弾かれてログインできなくなります。

続いてspecのコードです。

```rb:facebook_login_spec.rb
visit root_path
Timeout.timeout(3) do # Facebook SDKの初期化待ち
  loop do
    break if page.evaluate_script("window.fbApiInit")
    sleep(0.1)
  end
end
find_link("Login with Facebook").trigger("click")

sleep(0.1)

fb_popup = page.driver.window_handles.last
page.within_window fb_popup do
  fill_in "email", :with => "facebook_email@email.tst" # Facebookアプリ テストユーザーのメールアドレス
  fill_in "pass", :with => "my_pass" # Facebookアプリ テストユーザーのパスワード
  click_button "ログイン"
end
```

Facebook SDKの読み込み

```html:
<div id="fb-root"></div>
<script>
// Additional JS functions here
window.fbAsyncInit = function() {
  FB.init({
    appId      : '#{::Settings.facebook_app_id}', // App ID
    channelUrl : '//#{request.host_with_port}/channel.html', // Channel File
    status     : true, // check login status
    cookie     : true, // enable cookies to allow the server to access the session
    xfbml      : true  // parse XFBML
  });

  // Additional init code here
  FB.getLoginStatus(function(response){
      window.fbApiInit = true;
  });
};

// Load the SDK Asynchronously
(function(d){
   var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
   if (d.getElementById(id)) {return;}
   js = d.createElement('script'); js.id = id; js.async = true;
   js.src = "//connect.facebook.net/ja_JP/all" + ".js";
   ref.parentNode.insertBefore(js, ref);
 }(document));
</script>
```

Facebook SDKは非同期読み込みなので、読み込み完了後に`window.fApiInitl`をtrueにしてpoltergeist側から初期化状況をチェックできるようにしておきます。
ここでちゃんと待ちを入れないと、初期化完了前に処理が進んで画面遷移が上手くいきません。

その後、肝になるのが`page.driver.window_handles`です。
ウインドウがポップアップするとここにウインドウハンドラが追加され、within_windowメソッドにハンドラを渡す事で、ポップアップウインドウにスコープを移す事が出来ます。
このAPIを利用してFacebook SDKが開いたログインウインドウにパラメーターを入力します。

この時、Facebookのアプリケーション設定からテストユーザーを作成してパスワードを設定しておくと、そのユーザーでちゃんとログインする事が出来ます。
なので、実際にFacebookにアクセスして正しいログインシーケンスを踏んだ結果をコールバックする事が出来ます。

一つ、注意点があって要求パーミッション次第ではログインボタンを押しても、権限要求画面に遷移する場合があります。その場合はOKを押して先に進む必要があります。
ただ、これ同じユーザーを使い回す場合、次回以降は必要無くなるので、初回は手動でログインしてパーミッションの承認を済ませてから実行するのが良いと思います。

もしおかしいなと思ったら、`binding.pry`で一旦止めてpry上で対話的に操作するのがオススメです。
pry上で`save_screenshot`メソッド等を活用すると、PhantomJSの状況を逐次確認しながら、対話的にブラウザを制御する事ができます。

実際にFacebookにアクセスできる環境が無いと実行できないため、頻繁に実行するには問題ありそうですが、スモークテストや、重要なシナリオテストなんかをcapybaraで実行する際には活用できそうです。

ただ、どうもJSのSDKとphantomJSが余り相性が良くないようで、読み込みの際のブラウザの状態次第でphantomJSがクラッシュしたりします。
ここさえ何とかなればいいんだけど…。