buffering key input

キー入力周りを書き直した。

wasavi は document に対して keydown、keypress 両方をリスンする。keydown では機能キー(esc、tab、backspace、enter、pageup、pagedown、home、end、insert、delete、F1 ~ F12 の入力を見る。keypress では文字を表すキーを見る。

機能キーは ctrl および shift 併用の入力は、併用しない入力とは別個のキー入力として扱う(が、:map に定義する展開パターンではまだモディファイアキーに対応してない)。一方文字キーは、特定の キーと同時に ctrl が押されていたらコントロールコードの入力とみなすほかは、ctrl も shift も無視する……これは、特に shift 併用した際にキーボードレイアウトがらみで判断が難しいからだ。難しいというより、無理だと思う。あくまで入力された文字そのものだけをみてどのコマンドにディスパッチするかを決める。

また、スムーズスクロール中のキー入力を今までは単に捨てていたのを、キューにバッファするようにした。スクロールが完了した直後にキューをすべて掃き出す。これによりテストがちょっと楽になる。

 * * *

ところで amo を覗いてみたら、wasavi の full review は Queue Position: 45 of 106 だった。おーけっこう進んでるー!

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 してるのかはわからないが、いつ順番来るのこれ?

reply for gradius, behind

去年の 8 月に javascript、canvas、audio などを活用した html5 版 gradius を某掲示板にそっと公開したところ、なんか知らないがあれよあれよと拡散した。1 年経過した今、html5 gradius でググると、ブログでもそれなりに話題になっていたようなので 1 年越しに適当に返答してみる。

キーコンフィグの説明がPC88/X1版っぽくてニヤリとしますね。

17 才とちょっとのヤングなので偶然の一致です。88/X1 版はおろか msx 版も X68000 版も知りませんったら知りません。

マシン性能のせいかちょっともっさりしていましたが、
確かにグラディウスがでした。
公開しているのも元のグラディウスがを作っていたコナミのようです。

残念ながら違います。

個々のキャラクター(スプライト)をなめらかに動かすことと、方向キーとショットという入力に対してリアルタイムにレスポンスするようにプログラミングするのは、けっこう大変なノウハウを必要とするわけですが

そんな大したノウハウは必要ないです。

(はてブのウェジェットが貼ってある+ソースのクレジットから察するに作者は日本人??)

うーんそもそも海外の人にグラディウスってあんまり知名度高くないんじゃないですかね……?

HTML5版のグラディウス。 JavaScriptは全くの素人なのだけど、コードはすごく洗練されてると思う。 マジックナンバーが多くて難読ではあるのだけど。

javascript のコードの質としてはふつーだと思います。

システム開発でもゲームプログラマーのような能力が求められてくる。人材が二極化していくのではなかろうか?

よく分かりませんが、プログラマの質についての危機感のようなものは伝わってきました。

>HTML5で完全再現されたグラディウスとはなんだったのか
今やってみたが、回転処理が滑らか過ぎてニセモノ。

そもそも完全再現してないし、目指してもないんですが……。

ビッグコア出てくるときに何かSEが入るけど 原版でもあったっけ?

オリジナルにはないです。もったいないのでつけました。

マリオやらグラディウスやらは著作権的にどーなのかと突っ込みたいが。

はっはっはー(笑ってごまかす)。

そういうことが可能なように作られてんだから別にHTML5が凄いとは思わない

これはその通りです。25 年前のハードウェアでやってたことをブラウザで再現できるようになったねやったね、とそれだけのことですね。

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 できるようだ。どうしてそういう動作をするのかはよく分からない。