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 の該当部分はこうなった:































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

brushing up the tests

wasavi の自動テストは、jsUnit で行っているが、いくつか問題が無いわけではない。過去の記事のとおり、クリップボードを参照する場合など、wasavi の ex コマンドは条件によっては非同期で行われる。しかし jsUnit のテストの仕組みでは非同期的に実行される機能をテストするのは難しい。いやそもそもそのレベルになると “unit” test の域を超えて機能テストになっているのでこれは別に jsUnit の問題ではない。

機能テストのレベルで自動テストするにはどうするか。その方面で有名なのは Selenium だ。これを使ってみることにしよう。

Selenium は java 製のアプリケーションで、ブラウザに対する操作を機械的にエミュレートすることでテストを行う。IE/Firefox/Safari/Chrome と主要なブラウザはサポートされている。そしてなんということか、Opera もサポートされている。現在、操作のエミュレーションの仕組みで区別された Selenium RC (Remote Control) と Selenium Webdriver の 2 種類があるが、前者は公式に deprecated であり、Webdriver 版が最新だそうなのでこちらを使うことにする。ちなみに Opera 向け Webdriver や、Ruby 版の自動テストツールである Watir への対応などは Opera 社自らが手がけているみたい。

さてテストは基本的には java で記述し、コンパイルし、実行するが、その API は他の言語へのバインディングも提供されている。しかし wasavi のビルドですでに ant を使っているので、素直に java で書くことにしよう。ant のタスクに junit そのものがあるので、単にそれを呼ぶだけで済む。

ゴール
Selenium を用いた自動的な機能テストを行えるようにする。テストは
$ ant runtest
のように ant のターゲットの 1 つにする。

必要なファイル
http://seleniumhq.org/download/ からダウンロードしてくる。さまざまなファイルがあるが、

  • Selenium IDE: Firefox で動くプラグインで、ブラウザの操作をインタラクティブに記録していろんな言語によるテストスクリプトにエクスポートできるとか。今回は関係ない
  • Selenium Server: テストを行うマシンを別に誂える場合にサーバ機能を担当するアプリケーション、だと思うけど今回は関係ない。ちょっと勘違いしていた。これも必要。
  • The Internet Explorer Driver Server: 字の通り IE でテストする際に必要なもの。32bit 版と 64bit 版があるので Selenium 本体とは別になっているのでしょう、たぶん。もちろん今回は関係ない
  • Selenium Client Drivers: ローカルマシンでテストする際に必要な Selenium の本体。今回必要なのはその java 版なのでそれを落とす
  • Third Party Browser Drivers: Opera 版や Chrome 版の Webdriver。Selenium 本体を落とせばこれらの個別の Webdriver を含んでいるようなので、たぶん落とす必要はないと思う。ただし各ドライバが個別にアップデートされることはありえるのでそのときは必要かも
  • その他のファイル: とりあえず今回は関係ない

また、JUnit の jar も必要なのでもらってくる。github にさまざまなファイルが置いてあるが、最新の snapshot はとりあえず避けて junit-4.10.jar を使うことにする。

ファイルの配置

+ wasavi
+ wd-tests
+ src
+ dst
+ libs
+ selenium-2.25.0
+ junit

こんな感じで配置。テストは src の下に置き、.class は dst の下に置く。

build.xml への追加















こんな感じで追加。とりあえず独立したターゲットにしたが、ビルドプロセスの間に行うようにしても良いだろう。

テストを書く

import org.junit.*;
import static org.junit.Assert.*;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class WasaviTestSuite {
private static WebDriver driver;

@BeforeClass
public static void beforeClass () {
driver = new FirefoxDriver();
}

@AfterClass
public static void afterClass () {
if (driver != null) {
driver.quit();
driver = null;
}
}

@Test
public void foobar () {
assertTrue("TestExample", true);
}
}

とりあえず Firefox を起動させてみる。あと、実際にはテストは複数ファイルに分かれるのでテストスイートにしないといけない。なんか junit4 になったらいろいろ変わりすぎてて困る。

とりあえずここまで。

yet another vi clone #3

textarea 要素を vi として振る舞わせるエクステンション。ただし、現在はメンテされていないものが多い。特に Firefox は 3.6 あたりで軒並み古いエクステンションを切り捨てているようなので、公開した当時のままのエクステンションは分断されている。

いずれも textarea に対して直接キーボードイベントハンドラを仕掛けて、自前の制御を行っている。また、ex コマンドに類するものはほとんど実装されていない。