requested full review

Firefox の Add on は preliminary review と full review があって、前者はセキュリティに関する点など最低限の部分的なレビューをした後、限定的に公開される。preliminary review に通ってから 10 日ほどの待機期間みたいなのがあって、それが過ぎると full review を申請できるようになる。

というわけで、full review を申請した。

それにしても 8 月 17 日に preliminary review 申請、29 日に通過、9 月 6 日に full review 受付……ってなかなかのんびりしたペースだなあ。そして、full review 待ちのキューには現在 132 個の Add on が溜まっている(wasavi の位置は 133 of 134)。full review ひとつに付き 10 日くらいかかる。

何人がかりで full review してるのかはわからないが、いつ順番来るのこれ?

asynchronous madness

Selenium でテストすると keypress イベントが発生したりしなかったりする件。

原因は wasavi 側にあった。G とはつまり指定の行へカーソルを移動させる vi コマンドだ。このとき、スクロールが発生するのだが(実際のビューのスクロール量が 0 であっても)、[cci]:set smooth[/cci] の状態だと、スクロールは非同期に行われる。すっかり忘れていた。スクロールが完了するまで、keydown 時に preventDefault() される。つまり、keypress は発生しない。

そして、G に後続するテスト用コマンドはまさにスクロールが完了するまでの狭間のタイミングに送信されていたのであった。こりゃーちゃんとテストできるわけないよ。

というわけで、G を送信した後はコマンドの実行が完了するまで待つことにして解決。

で、とりあえず、jsunit 版の基本編集のテスト editing.js の 51 テストを移植したのだけど。javascript のコードを java に移すというのは、メロンパンの中をくりぬいてメロンを詰め込むようななんともいえない気持ちになりますな。

to open a hole #5

keypress が正しく生成されない件。

Wasavi.send("1G1|y3lp");

といったコードでテストすると、”G” 以降のストロークについて keydown イベントは発生するものの、keypress が一切発生しなくなる。chromedriver_win_23.0.1240.0 で確認した。

これを、

Wasavi.send("gg1|y3lp");

にすると、期待通りのイベントが発生する。となるとやはり “G” が悪いのか。”G” に付随して、shift キーのプレスとリリースもエミュレートされるのだが、この辺が怪しい……? しかし興味深いことに、”|” も shift キー押下イベントが生成されるのだが、それによって keypress が脱落することはない。そうすると関係ないか。

issues に似たバグがないこともないのだけど、解決している様子はない。

実際にストロークを送信する部分は

private void sendStrokes (CharSequence[] strokes) {
Actions act = new Actions(driver);
for (CharSequence s: strokes) {
act.sendKeys(s);
}
act.perform();
}

という感じ。もしかしてこの辺に問題があるのだろうか。もしかしたらまとめてじゃなく細切れで perform() すれば sendKeys() の内部状態(があるとして)がそのたび初期化されてうまく行くかも:

for (CharSequence s: strokes) {
for (int i = 0, goal = s.length(); i < goal; i++) { Actions act = new Actions(driver); act.sendKeys(s.subSequence(i, i + 1)); act.perform(); } }

結果: 変わらず。Selenium 内の問題ではなく chromedriver.exe の中の問題なのか?

ちなみに Opera や Firefox ではどうなのかというと、Opera では textarea に対する sendKeys() 自体が変。"hello!" とかは自動入力できるのだが、Keys.CONTROL とか Keys.INSERT とかは無視される。つまり wasavi を起動させられない。Firefox は textarea に対しては sendKeys() は上手く動いて、wasavi を起動させられるのだが、起動した wasavi の iframe に対して sendKeys() が行われない。つまり wasavi を操作できない。

というわけで、結局まともにブラウザを操作できるのが、(うちの環境では)Chrome だけなのだ。うーん動かし方が悪いのか……?

とにかく Chrome が最後の砦なので、何とか正確にキーストロークを生成してもらわないと困る。

to open a hole #4

Selenium からブラウザに対して任意の javascript を実行させるのに、executeScript() が用意されている。このメソッドは、引数で指定された javascritpt コード をブラウザ上で匿名関数の中身として実行する。

javascript コードが、”return なんちゃら;” であった場合はどうなるのか? その点も抜かりはない、ということになっている。
http://selenium.googlecode.com/svn/trunk/docs/api/java/org/openqa/selenium/JavascriptExecutor.html
返された何者かの型によって、よきに計らってくれる。

ところでブラウザ上で実行される javascript が返す何かが JSON を文字列化した文字列であった場合。なんか一切文書化されていないが(ChromeDriver 特有の動作なのかもしれない)、自動的に Map ……というか google collections library の TransformedEntiresMap に変換されるような感じがする。

これはなかなかに余計なお世話なのではないか。wasavi の状態を返すのに、それを JSON 化した文字列を返している。文字列を返したのなら、java 側でも文字列として現れてほしい。Selenium 側ではやはりそれを JSON オブジェクト化した上で扱うつもりでコードを書いたら、まったくまるで動かないので悩んでしまったぞ。

しかたがないので、ブラウザ側の javascript を経由するのではなく直接 wasavi を保持する iframe の 属性を java 側から読むようにした。直接読むと、:quit 近辺のテストあたりが影響を受ける(状態を取得しようとした時点ですでに wasavi は消滅している)のでなかなか困るのだが。

to open a hole #3

Selenum は、実際に人がブラウジングする過程をなぞるのでブラウザの実動作を対象にテストできるというのがウリなのだと思うけど、ちょっと不思議な動作がある。

起動した wasavi に対してキーストロークを送る。

public void testPasteCharsForward () {
Wasavi.send("i", "foobar\nfoobar", Keys.ESCAPE);

Wasavi.send("1G1|y3lp", Keys.ESCAPE);
assertEquals("ffoooobar\nfoobar", Wasavi.getValue());
}

これを Chrome に対して動かす。Chrome 版 wasavi ではキー入力イベントは keydown と keypress 両方処理している。文字を示すキーは keypress で、機能を示すキーは keydown で扱う。

ところで実際に実行してみると 2 つめの Wasavi.send() で

keydown 27 (U+001B)
keydown 80 P (U+0050)
keydown 76 L (U+004C)
keydown 51 3 (U+0033)
keydown 89 Y (U+0059)
keydown 220 (U+00DC)
keydown 16 (Shift)
keydown 49 1 (U+0031)
keypress 71 G (U+0000)
keydown 71 G (U+0047)
keydown 16 (Shift)
keypress 49 1 (U+0000)
keydown 49 1 (U+0031)

こんな感じのキー入力がなされる(行はイベント種別、キーコード、対応する文字(あれば)、keyIdentifier の値。いちばん上が最新)。これ、変だ。”G” の入力以降、文字を示すキーに対応する keypress が発生していない。したがってテストも失敗する。

これは一体?

to open a hole #2

結局昨日の記事とはぜんぜん違う方法になりつつある。

まず wasavi 側は、コマンドが終了するたびにエージェント(wasavi が寄生する textarea 要素が属する文書に対して挿入される content script)にそれを通知する。通知の際に wasavi の状態も併せて送りつける。

エージェントでは、文書上の wasavi の実体である iframe の適当な属性に、wasavi の状態を json 文字列化した上で格納する。

テストページでは、呼び出されるたび iframe に格納された wasavi の状態を単に返す関数 getWasaviState() を定義する。

Selenium 側では、wasavi へキーストロークを送信した直後、コマンドが終了するのを待ち、終了したら getWasaviSatate() を呼び出し、結果を取り込む。

という流れ。

 * * *

全然関係ないけど Thinkpad X1 carbon が発売開始された。いいなー欲しいなー誰か買ってくれないかな。

to open a hole

wasavi.js の動作テストを行う際、起動中の wasavi の状態を外から取得する必要がある。

そのために、wasavi はグローバルオブジェクトに対して Wasavi オブジェクトを公開している。それには Wasavi.value とか Wasavi.state、あるいは Wasavi.registers() といったものが用意されていて、外から状態を得ることができる。

これが可能なのは、jsUnit でテストするときは wasavi.js をページに属するスクリプトとして扱っているからだ。つまりそのコンテキストでのグローバルオブジェクトは window であって、外から見るのに何の制限もない。

さてテストの方法を Selenium を利用したものに移行しようとしているのだが、こちらの場合は実際にエクステンションとして動作する wasavi に対してテストを行う。この状態では、上記の Wasavi オブジェクトは外から見えないし、また見えては困る。しかしテストを行うためにこれに例外を設ける必要がある。

テストページとして、http://wasavi.appsweets.net/test_frame.html を用意している。Selenium はこのページを自動的にブラウザに開かせ、すべてのテストを行う。Selenium、このページ上のスクリプト、wasavi の 3 つの要素が連携して、制限されたパスを通して wasavi の状態を Selenium へ伝達したい。

まず Selenium 側。Selenium 側はつまり java である。Selenium の api に executeScript() というものがあるので、これを利用する。このメソッドはブラウザに対して任意の javascript を同期的に実行させ、値を返す。スクリプトはブラウザ上では匿名関数の中身として実行される。

JavascriptExecutor js = (JavascriptExecutor)driver;
Object result = js.executeScript("return getWasaviProperty('value');");

次にテストページ上のスクリプト。ここから起動中の wasavi へ何かしらの操作をし、何かしらの状態を取得することになる。wasavi の実態は iframe であり、テストページと同じドメインに存在する。しかし、いずれのブラウザでも wasavi.js は isolated world に存在しているので、単純に iframe#contentWindow を覗けばいいというふうにはいかない。

そこで、テストページから wasavi へ postMessage() することにする。また、wasavi 側からの返答を受けるために message イベントハンドラを登録しておく。

var buffer;
function getWasaviProperty (name) {
$('wasavi_frame').postMessage(name, 'http://wasavi.appsweets.net');
return buffer;
}
window.addEventListener('message', function (e) {
buffer = e.data;
}, false);

最後に wasavi 側。テストページからのメッセージを受け、値を返す。

window.addEventListener('message', function (e) {
e.source.postMessage(Wasavi[e.data], e.origin);
}, false);

なお上記コードはエラー処理とか省いている。特に origin の判定とかは実使用では絶対に必要なので注意。

というわけでとりあえず Chrome で動かしてみたのだけど、前にも書いたような気がするが、どうもやはり、ページ上に属する javascript と content script の間では cross messaging すらできないような。具体的には、wasavi 側でメッセージを受けるところまではいくのだけど、MessageEvent#source が undefined なのだ。

困ったー。

 * * *

iframe#contentWindow に対してではなく、MessageChannel に対してなら postMessage できるようだ。どうしてそういう動作をするのかはよく分からない。

brushing up the tests #3: ChromeDriver instantiation

ということでテストを書き始める。正確に言うとテストそのものは jsUnit でやってたものを変換するだけなので、土台を書く。

chrome へのオプションを指定しつつ ChromeDriver を生成するのに、

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
:
DesiredCapabilities cap = DesiredCapabilities.chrome();
cap.setCapabikity("chrome.switches", Arrays.asList("--load-extension=/path/to/extension"));
driver = new ChromeDriver(cap);

と書いたのだが、ChromeDriver を上記のようなコンストラクタで生成するのは deprecated だよ、と怒られる。
これは

import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.chrome.ChromeDriver;
:
ChromeDriverService service = ChromeDriverService.createDefaultService();
ChromeOptions options = new ChromeOptions();
options.addArguments("--load-extension=/path/to/extension");
driver = new ChromeDriver(service, options);

と書くのがナウいらしい。
参考: https://groups.google.com/forum/?fromgroups=#!topic/selenium-users/-shBLr8WTq0
ありがとうありがとう。

brushing up the tests #2

とりあえず単に Firefox を起動させることはできたので、次に Opera を起動させてみる。

まず classpath に OperaDriver を追加する:

テストファイルで OperaDriver を参照するために import を追加する:

import com.opera.core.systems.OperaDriver;

とりあえずこれだけでいいはず……と起動してみたら、Opera が起動しない。wiki を見ると現在の OperaDriver は、opera.exe の場所を割と決めうちの場所からしか探さないようだ。うちの Opera は C:\Program Files\Opera\current\opera.exe にいるのであった。

これは前述の wiki に書いてあるとおり、DesiredCapabilities オブジェクトに設定を突っ込めば上書きできるので、そうした。これでとりあえず Opera 自体は起動するようになったが、自動的にリモートデバッガが開いて何かをしようとしているところで接続できない云々というエラーを吐いて終了してしまう。どういうことなの。

ここはどうも勘違いしていたようだ。先の記事で Selenium Server は今回はいらないと書いたが、必要のようだ。Firefox で試したら普通に起動したので惑わされた。selenium-server-standalone-2.25.0.jar をダウンロードして、あらかじめ

$ java -jar selenium-server-standalone-2.25.0.jar

などとしてサーバを起動させておくおく必要がある。ただ、せっかく ant で一本化しているのだからサーバの起動と終了も自動的に行わせたい。そこで
http://wiki.openqa.org/display/SRC/Selenium-RC+and+Continuous+Integration
を参考に build.xml にいろいろ追加した。これで Opera も起動した。

ということで Opera でのテスト準備も整った。次は Chrome で、ChromeDriver は
https://code.google.com/p/chromedriver/downloads/list
からダウンロードしてくるわけなのだが。zip が揃っていてとりあえず最新の持ってくればそれに jar が入ってるのかな? と期待したらなんと zip の中身は chromedriver.exe ファイル 1 つなのであった。うん……!? 何かのインストーラなの?

と思いつつ実行してみるとコマンドプロンプト的ウィンドウが開きサーバ的動作を始めたようなログが出てきた。はあ、Chrome 場合はこれをサーバとして使うのね。Chrome にまつわるエトセトラは
http://code.google.com/p/selenium/wiki/ChromeDriver
にまとまっているが、それによると ChromeDriver は子プロセスでサーバを起動するモード、すでに起動してあるサーバと http でやりとりするモード、そして chrome 専用のサーバを起動させるモードと 3 形態の使い方があるようだ。chromedriver.exe は %PATH% が通っている場所に置くか、webdriver.chrome.driver システムプロパティにパスを書いておく必要がある。

とりあえず chromedriver.exe を使う方法で行ってみる。

というわけで、なんとか 3 ブラウザを起動させるところには達した。build.xml の該当部分はこうなった:































あとはテストの骨組みに注力することになる。