Testing with selenium javascript binding #2

mocha のテストをすべて終えると最後に結果が表示される。こんなふうに:

basic test -- 2 tests, 50.00%
--------------------

1 passing (2s)
1 failing

1) basic test should get the title of test frame page:

AssertionError: 'wasavi test frame' == 'wasavi test frame?'
+ expected - actual

-wasavi test frame
+wasavi test frame?

at Object.eq (src/wd-tests/src/all-tests.js:359:10)
at Context. (src/wd-tests/src/basic-test.js:11:10)
at Generator.next ()
at pump (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:3221:25)
at callNext (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:3207:7)
at ManagedPromise.invokeCallback_ (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:1366:14)
at TaskQueue.execute_ (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:2970:14)
at TaskQueue.executeNext_ (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:2953:27)
at asyncRun (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:2813:27)
at /home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:676:7
at process._tickCallback (internal/process/next_tick.js:103:7)
From: Task: basic test should get the title of test frame page
at Context.ret (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/testing/index.js:185:10)
at /home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/testing/index.js:104:5
at ManagedPromise.invokeCallback_ (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:1366:14)
at TaskQueue.execute_ (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:2970:14)
at TaskQueue.executeNext_ (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:2953:27)
at asyncRun (/home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:2813:27)
at /home/akahuku/.nvm/versions/node/v7.2.1/lib/node_modules/selenium-webdriver/lib/promise.js:676:7
at process._tickCallback (internal/process/next_tick.js:103:7)

見てのとおり殆どがスタックトレースの情報なのだ。こんなのいらないよ…。いや役に立つときはあるにはあるだろうけど、少なくとも mocha や selenium 内部のスタックトレースは普通はいらない。

そんなわけで、アサートを単に assert.equal() を呼ぶのではなく、

try {
assert.equal(actual, expected);
}
catch (ex) {
ex.stack = ex.stack.replace(/* cute regexp */, '');
throw ex;
}

なんてコードが考えられる。これはこれですごく嫌らしいコードなのだが(stack プロパティが書き換えられていいの?)、これでやってみてもスタックトレースは空にならない。上記の [cci]From: Task:[/cci] で始まる行以降の部分は、すでに selenium が stack に追記済みなのである。なので、stack を操作して再度 throw してもそれを selenium がまたいじるので、スタックトレースは空にならない。selenium にそのへんを何とかするオプションがあるのかはまだ調べてない。

ということで、とりあえずのワークアラウンドは
mocha --timeout=60000 --reporter=almost-min src/wd-tests/src/all-tests.js | sed -e '/^\\s*at\\s*/d' -e '/^\\s*From:\\s*Task:/d'
てな感じに、すべてのテストが完了した後に sed で削ぎ落とすことくらいしか思いつかない。これだと、mocha ならではの色とりどりのテスト結果ではなくなってしまうが、まあ個人的にはあれはちょっと華美すぎだと思うので、これはこれでいいかな…。

Testing with selenium javascript binding

wasaviの機能テストは今javaで書いているのだがjavaでなければならない理由は別にない。なぜjavaを選択したのかといえば、かつてはwasaviのビルドにantを使っていたのでそのへんの相性だとか、Seleniumのコード例として割とjavaが多く検索に引っかかるとか、公式のAPIリファレンスがjavadocだったからとか、その程度の理由だ。

しかし機能テストの個数が増えてくるとjavaだと困ったことにコンパイルにもそれなりに時間がかかってくるわけでなかなかストレスフルだ。それ以前にテストが全体的に遅い。ブラウザを起動する速度すら遅い。これを期に、スクリプト言語に移行したい。wasavi場合それ自体をJavascriptで書いているのだから、Javascriptに移行するのが自然だ。なによりJavascriptへのバインディングはSeleniumが公式でメンテしているのであんしんあんぜんということだ。ちなみに実はビルドの際に呼び出されるちまちましたスクリプト群も以前はRubyで書いていたのだが全部Javascriptに書きなおした。

ということでJavascriptによるテストコードの基盤を書いているのだが。基盤とはなんのこっちゃというと、各々の記述されたテストはページ上のtextarea、あるいは時にはdiv要素に対してwasaviが起動していることを前提としている。なのでテストの前段階でCtrl+Enterを送出してwasaviを起動させ、実際に起動したことを確認するコード、後段階でwasaviを終了させるコードを挟む必要があるのだ。そういったことをするために、基本的には


@test
public void testFoo () {
startWasavi();
// test body
;
closeWasavi();
}

なんてふうにテスト自体の先頭の末尾に何がしかの処理を挟むことになる。しかしこれはめんどくさいわけで、もうちょっと賢く特定の処理を割りこませたい。

となると次の案としては当然setUp()とtearDown()を定義してそこでwasaviを開いたり閉じたりすればよいのである。ここまでは実に普通だ。めんどくさいのはここからだ。

テストの中には、textarea要素ではなくcontenteditableなdiv要素とか、あるいはセクション・パラグラフ・センテンスをテストするために予めそのための初期本文が必要であるとか、初期化のタイプにもいくつかのバリエーションがある。そのへんもテスト本体に手を入れることなく1箇所で面倒を見たい。たとえばセクションに対するテストであればtestSectionなんとか、ってメソッド名なので、それを利用すればいい。あるいはアノテーションでもまあいい。しかし…困ったことにsetUp()にはそういったテスト自体のメタ情報は渡されないのであった。そのかわり、JUnitの場合TestWatcherというクラスがあってテストスイートの進行具合を観測することができるので、これを使う。

さて、これがJUnitからmochaへ移行するとどうなるか。setUp()とtearDown()は、beforeEach()とafterEach()という同機能が用意されているので単にそれを使えばいい。しかしやはり困ったことにこれらのメソッドにもテスト自体のメタ情報は渡されないのであった。というわけでTestWatcherの代替品を探さないといけないのだが、ない。

無理やり代替品を2つ考えてみると、まずReporterというものがありこれがテスト結果を表示する。これが内包するリスナはテストランナーから送出される各種イベントを受信して、その中にはテストのメタ情報が含まれる。なのでそれを使ってテストスイートへリダイレクトする。これのconsは好きなReporterを使えなくなるという点。

もうひとつは、テストの定義にit()を使うのでそれをラップして、

var testNames = [];
var originalIt = it;
it = function (should, fn) {
testName.push(should);
return originalIt.apply(undefined, arguments);
};

あとは beforeEach()内で[cci]testNames[/cci]から順々に表明を取り出せばそれは個々のテストを区別できる。これのconsは、必ずしもテストがit()の呼び出し順で実行されるとは限らないという点。

というわけでどちらもイマイチな点がある。うーむ。

 * * *

もうひとつ。JavascriptのSeleniumバインディングは、ほぼすべてのAPIがPromiseを返す仕様なのだ。だから、Javaであれば同期的に

WebElement el = driver.findElement(By.id("foobar"));
el.click();

などと何も考えずに書いていたコードは

driver
.findElement(By.id('foobar'))
.then(el => {el.click()});

というような感じになる。やることがシーケンシャルなのであれば.then()の羅列でいいのだがしかし分岐やループが入ってくるととたんに面倒くさくなってくる。それで、WebDriver.promise オブジェクトが用意されていてこれがジェネレータを引数に取り、ジェネレータがyieldしなくなるまで呼び出し続け、最後にresolve()するというナイスな関数consume()を持っているので

WebDriver.promise.consume(function*() {
var el = yield driver.findElement(By.id('foobar'));
yield el.click();
});

てな具合に同期的っぽく書ける。というかこれを使わないととてもじゃないけど書いてられない。気をつけないといけないことが一つあり、WebDriver.promiseはdeprecateである。なので将来的にはこれはasync/await版にしないといけない。

The TV program time table #3

1 クール(12〜13話)分の一挙放送、といった番組は番組表においても縦方向に非常に長い面積を占める。その枠の先頭部分にタイトルとサムネイルを描画するわけなのだが残りの部分は全くの空白なので、始まって数時間もすると何を放送しているのか分からないただの枠が番組表に残るだけになる。これを何とかしたい。

これを何とかするのは、近年よく広告に使われる sticky positioning だ。sticky というのは日本語で言えばネバネバだ。ページをスクロールした時にできるだけビュー内にとどまろうと粘っこい動作をするアレである。現実には広告によく使われるが、本来 sticky な動作をして嬉しいのは上下に長い表のヘッダ部分などである。あべアニで言えばそれは正にチャンネル名の部分で、既にそういう動作をするように仕掛けてある。同じことを、それぞれの番組枠にも適用すればいい。

ところでこの sticky positioning というのは CSS3 の規格でまだ策定中の段階であり、現時点では Firefox を除くブラウザにはまだ実装されていない。なので javascript による Polyfill を使うことになる。そういうわけであべアニでは StickyFill というライブラリを使っている。ところが残念なことに、このライブラリに対してそれぞれの番組枠を sticky にするように指示しても上手くいかないのであった。上手くいかない上にスクロールもかなり重くなる。

Polyfill でやるべきことは

  • window の scroll イベントをリスンして
  • sticky 動作の対象となっている要素を走査し、その領域内にビューの上端が含まれていたら要素を position:fixed にする。このとき、その親要素の領域内に収まることを優先しつつ、要素の内容がビューに収まるように位置を微調整する。それから、領域の周辺の要素の位置を動かさないためにダミーの置換要素を生成したりも必要ならする

ということにつきる。しかしやることは単純だが、重い。まず scroll イベントは相当な頻度で発生する。それから要素を走査する際にその配置情報を逐一取得するのも重い。StickyFill は汎用的に使えるようにするためにその辺を素直にやっているので重い。

そういうわけで仕方ないので番組枠の sticky 動作についてはページに最適化しつつ自前で書いた。たとえばいったん計算したら使いまわせるもの(margin や border のサイズ)は積極的にキャッシュしたり、そもそも sticky 動作させる必要のない番組枠(内容の高さが枠の高さ以上あるもの)は積極的に除外したりだ。それでもブラウザがスムーズスクロールする設定にされていると若干描画がバタつく。早くブラウザでネイティブサポートされて欲しい。

ついでに言うとネイティブサポートの際は stuck 状態かどうかを取れる CSS の擬似クラスを定義してくれるととても助かるのだが。そうすると stuck したときに限り box-shadow をかけるなどの小細工ができる。

Talk about news #2

PhoneticNews は一種の RSS リーダーと考えることができるが(ただし NHK ニュースに関しては公式サイトが内部的に利用している、限りなく RSS に近い独自形式の XML を利用している)、任意のアドレスのフィードを利用できるわけではないので用途はかなり限定的だ。

なぜフィードを絞っているのかというと、発話の際のトラフィックの問題だ。PhoneticNews は発話データを得るのに VoiceText の API を利用している。この API のエンドポイントに文字列を POST すると、それを読み上げた発話の aac データが返ってくる。

この VoiceText へのネットワークアクセスは PhoneticNews をインストールした Chrome がそれぞれ個別に行うため、任意のフィードを登録できるようにすると VoiceText 側の負荷が結構なものになるかもしれないのである。まあ、勝手な想像なのですが。

API のリファレンスには呼び出しに関する回数や頻度の制限などは記述されていないので、どこまで許されるのか実際わからないのであった。うーんお問合せしたほうがいいのかなー。

いずれにしてもできうることを考えると、

  • 各 Chrome から個別に VoiceText にアクセスさせることをやめて、ここのサーバを経由させる。ここのサーバが代理的に VoiceText へのやり取りを行う: ここのサーバへの負荷はどうするのかという話になるが、現状でここのサーバは CloudFlare のお世話になっているのでたぶん負荷の面では大丈夫
  • 発話データを Chrome 自身の tts で生成する: ただし、日本語の発話データは OSX か、Windows10 以降じゃないと取得できない、らしい

あたりになると思う。はてさて。

Talk about news

PhoneticNews という Chrome エクステンションを前書いたのだが、いくつか既知のバグがあり、特に Chrome の起動時に二重に読み上げが行われる現象がある。この機会に探ってみた。

このエクステンションは定期的に NHK のニュースを読み込み、それを読み上げる。そのためにバックグラウンドで定期的に読み込み処理、読み上げ処理を走らせることが動作の核になる。一般的にそれを実現するには setTimeout や setInterval を使うのだが、Chrome のエクステンションの場合はバックグラウンドページの構造について Event Pages という別のアプローチがあり、PhoneticNews もそれを使っている。しかしこれがなかなかに癖があり、正しくそれに対応していなかったのがバグの原因になっている。

エクステンションのバックグラウンドが Event Pages のルールに従うか否かで変わる最大の物は、つまりバックグラウンドページが勝手にアンロードされるか否かということだ。バックグラウンドがアイドル状態になると即アンロードされる。アンロードというのは Chrome ブラウザの環境下においては、エクステンションに割り当てられたプロセスを終了するという意味だ。つまり Event Pages に対応すると、タスクマネージャを開いた時ずらっと Chrome が並ぶのを幾ばくか減らすことができるというわけだ。これはもちろんその分のメモリを開放するという意味もあるが、特にモバイル機器において不要な負荷を減らしバッテリーの寿命を伸ばすという効果があるのだと思う。

さて Event Pages では setTimeout/setInterval の代わりに chrome.alarms を使う。これにアラームを登録すると指定したタイミングにバックグラウンドがロードされ、次に chrome.runtime.onStartup に登録したイベントハンドラが呼ばれ、最後にアラームに登録したイベントハンドラが呼ばれる。

このアラームが特徴的なのは、登録が Chrome ブラウザに対して永続的であるということだ。定期的に発生するアラームは、エクステンションのインストール時にただ 1 度登録すればいいのである。PhoneticNews ではそれを知らず、アラームの登録をエクステンションのスタートアップ時に毎回行っていた。これが起動時に二重に読み上げが行われていた原因だ。

その他、従来は NHK ニュースだけに限っていたのを他のニュース配信社もいくつか含めるようにした。
phoneticnews-options
これ以外に、例えば共同通信や時事通信あたりも入れようかなと思ったのだが、なぜか日本の配信社は RSS フィードを公開していない、あるいはこっそりとしか公開していないところが多い。

アイコン押下時のポップアップでは最後に読み込んだ 1 件のニュースだけを表示していたのだが、最大 10 件にまで拡大した。
phoneticnews-popup

fixed: GRADIUS

gradius-on-edge

手持ちのプロジェクトをいろいろ整理しているのだが、ずいぶん前に作ったグラディウスのソースを読んでたら RequestAnimationFrame の使い方を勘違いしたまま数年経過してたようなのでとりあえず直してみた。若干スクロールがスムーズになった気のせいがするかもしれない以外は特に動作は変わっていない。というよりも、各種データをどこからどうやって生成したのかきれいさっぱり忘れているのでソースを近視眼的にいじるくらいしかもうできないのだが。

その他、Windows 10 の edge で動かないという話があったので


edge でも動作するよう確認。

edge 自身は…将来に期待という感じ。今夏の Windows 10 のアップデートで拡張機能の実装を含めて良くなるそうなので、そのときになったら wasavi のテストもしてみたい。

Optimize a loading of assets

赤福プラスの話。

赤福プラス 3.x はふたばの画像掲示板を開いた時に DOMContentLoaded のタイミングで一旦ドキュメントをすべて書き換える。これはなかなかアグレッシブな動作で、3.x 未満の赤福プラスや、おそらくは他の、ブラウザ上で動作するふたば閲覧エクステンションはあくまでふたばがネイティブで送出するコンテンツに何かを「付け足す」ようになっていると思う。赤福プラス 3.x はそうではなく、完全に書き換える。元のドキュメントからメタな中間 XML ドキュメントを生成し、それを XSLT によって HTML に変換・再構築し、元のドキュメントに上書きする。

さて DOMContentLoaded というのは定義としては DOM の構造を構築し終わった段階で発火するイベントで、load イベントに比べてかなり早い段階で発生する。しかし、これはドキュメントに付随するスタイルシートやスクリプトや画像や iframe で指定されるサブフレームのドキュメントをまだ読んでいない段階というわけでは *ない*。もしかしたら並行して読んでいる途中かもしれないし、そうでないかもしれない。特に最新のブラウザほど、並行動作をしている割合が高い。

このブラウザの動作は、赤福プラスの仕様とは相容れない。もしその手のアセットを読み込むとしても、それは赤福プラスが管理する文書の下で読み込まれるべきで、その前段階でネットワークアクセスが発生するのは全て無駄になるため、好ましくない。

そういうわけで、その辺りを最適化するために以前から Presto Opera では外部の script 要素の動作をブロックしたりしていた。似たようなことは Chrome の WebRequest API でもできるわけで、やってみた。

やることは、赤福プラスがページを再構築している間、そのページから発生するふたば外へのリクエストをすべてブロックすることである。そのために、

  • バックエンドの main.js にタブ ID をキーとするハッシュ initializingTabIds を用意する
  • WebRequest API によりすべてのリクエストをリスンする
  • ふたばの画像掲示板の URL がアクセスされたら、そのタブ ID を initializingTabIds に記録する
  • その他の何かがアクセスされた場合、それが属するタブ ID が initializingTabIds に含まれていれば、リクエストをキャンセルする
  • 赤福プラスのフロントエンドがページを再構築を完了したら、バックエンドへ initialized メッセージを投げる
  • バックエンドが initialized メッセージを受け取ったら、そのタブ ID を intializingTabIds から削除する

という処理を加えた。

ところで WebRequest を使うには manifest.json の permissions に “webrequest” および対象となる URL パターンを含める必要がある。今回の場合は URL は http://*/* および https://*/*/ というかなり広範囲に及ぶ強いパーミッションを必要とする。その場合 webstore での審査もけっこう時間がかかるようだ。昨日の午後提出して今審査が通った。

ちなみに Firefox 版はそういう最適化をしていないので読み込みが一番遅い。気が向いたらがんばる。

Assign to a register

エディタを開いた時に特定のレジスタに常に任意の内容が入っているようにするには、vim では vimrc に

:let &z="foobar"

みたいにすると思う。この手のことは、wasavi ではまだできない(レジスタの内容自体は wasavi を閉じるときにエクステンションのバックエンドへ永続化されるので結果的に件のツイートの要件はすでに満たしている)。

できないというのは、vim の [cci]let[/cci] 文は、ex コマンドというよりは vimscript のいわゆる assignment statement で(ただしヘルプ上では expression command ということになっている)、wasavi にはまだスクリプティングの機能がないということだ。

もしその手の機能を設けるとしたら、ex コマンドに [cci]script[/cci] を新設して

:script wasavi.registers["z"] = "foobar";


:script << END wasavi.registers["z"] = "foobar"; END

な感じになると思う(スクリプトの文法がこうなるかどうかは未定だ)。vim のようにスクリプトの構造が ex コマンドのスーパーセットになっているような設計には、多分しない。

実は以前に script コマンドを実装しかけたことはあったのだが

  • wasavi の動作のうち、バックエンドにメッセージを送信して返事を待つ箇所は非同期だ。したがって、スクリプトも単にシーケンシャルに実行するわけにはいかない。つまり

    a = wasavi.registers["*"];
    print a;

    というようにはいかない。

    wasav.registers.get("*").then(function (content) {
    print a;
    });

    のような形式にせざるを得ない。すべてこの形式だとお気楽なスクリプトの範疇を超えている。同期か非同期かで文法を使い分けるとしてもそれも煩雑すぎる
  • スクリプトが例えば javascript であると定義するとそれを実行するのは eval 文になるが、それだとできることが多すぎる。何でもできすぎると動作の保証もセキュリティ面での安全性の保証もできない
  • 一方で完全に動作を制御可能なスクリプトインタープリタを実装し、その管理下でスクリプトを動かすとなるとコードサイズと実行速度の危険が危ない

などなど問題がありすぎてこりゃ時期尚早だということで棚上げになっている。どうしたものか。

Options page

前の記事で触れたように、かつては Chrome のエクステンションに関するオプションページは、1つのタブを消費するタイプだった。そのためのリファレンス。

それが、最新のエクステンションの仕様では chrome://extensions のページにオーバーレイされる形で表示するようになっている。そのためのリファレンス。オプションページの構造自体は変わっていないのだが、表示される場所が違う。この場所の違いを吸収するため、[cci]chrome.runtime.openOptionsPage()[/cci] という API が新設されていて、manifest.json に記述されているオプションページの設定に従ってふさわしいそれを開くようになっている。

オーバーレイ表示といってもどうということはなく、要するに iframe みたいなものだ(実際には Shadow DOM や object 要素などを駆使したナウいものになっている)。ということは、やろうと思えば [cci]chrome.runtime.openOptionsPage()[/cci] を読んだ時、その時点のカレントタブにオーバーレイ表示することもできるはずだ。その方が無駄なタブ移動等々が少なくて混乱することはないのだが、実際はこの API を呼ぶと 必ず chrome://extensions を開いた上で(すでにそれを開いている場合はそれをアクティブにした上で)オーバーレイ表示する。これはまあきっと、偽装をさせないための制限なのだろう。

そういうわけで wasavi についても新しいオプションページで開くようにしてみたのだが、別にこれと言って新しくなったメリットはないわけで、リバートしてしまった。

wasavi and SSL

ペンディングになっていた件。つまり wasavi が参照する、appsweets.net 上のアドレスを http 化したいな、という件。そういったアドレスは 2 種類あって:

  • http://wasavi.appsweets.net/ – app モードのプレースホルダとなる URL
  • http://appsweets.net/authorized.html – オンラインストレージに接続した際のコールバック URL

これらを https 化するために、cloudflare の助けを借りることにした。cloudflare というのは CDN サービスだ。cloudflare 側にアカウントを作り諸々設定した上で appsweets.net ドメインに割り当てていたネームサーバを cloudflare が提供するそれに書き換える。そうすると appsweets.net を正引きすると今までは直接 xrea のサーバ s278 を指していたわけだが、cloudflare 内のサーバを指すようになる。このサーバがプロクシとして動作し、ブラウザ – cloudflare プロクシ – s278 という形でデータが流れる。このときメディアファイルについては cloudflare 側で適当にキャッシュされたりするのでその分 s278 側の転送量が抑えられる……等々のメリットがある。

そして SSL まわりだが、不思議なことに cloudflare に登録したドメインに SSL の証明書が即与えられる。従ってブラウザと cloudflare プロクシ間の接続については https でアクセスできるようになる。この時に使用するドメインはもちろん appsweets.net なので、結果的に appsweets.net が https でアクセスできるようになる。ちなみにここまで完全に無料である。なんか騙されたみたいな話だが、実際にこうなる。

そういうわけで、上記の 2 アドレスについても https を使えるようになる。

デメリットとしては appsweets.net の内容を cloudflare に 100% 預ける形で仲介させているので、悪意の有無を問わず、そちらで変な操作をされることが絶対にないとはいえない。SSL 化されるのはあくまでブラウザと cloudflare 間であって、cloudflare と appsweets.net 間は http でやりとりされる。従ってすごくセキュアというわけではない。ブラウザと cloudflare 間の SSL 接続はバーチャルホスト名ベースの割と新しい仕組み(SNI: Server Name Indication)を利用していて、古めの OS/ブラウザからはアクセスできない(たとえば、Presto Opera 12.17 ではアクセスできない)