SpectronからElectronアプリケーションのMenuを操作する

この記事は Electron Advent Calendar 2016 の12日目の記事です。

SpectronはElectronアプリケーションのためのE2Eテストツールです。

electron.atom.io

SpectronはElectronのChrome Driverを通じてアプリケーションの操作を実行できるのですが、メニューの操作には現状対応していません。(詳しくは後述します。)

そこでspectron-fake-menuというSpectronからメニューの操作ができるnpmを作成しました。

github.com

使い方

https://github.com/joe-re/spectron-fake-menu/tree/master/example にサンプルアプリケーションがあります。

ちょっと変なアプリですが、メニューからカウントのインクリメント、デクリメントをするカウンターです。

f:id:joe-re:20161212070307p:plain

テストコードでは、インクリメントとデクリメントをspectron-fake-menuを使って操作し、成功していることを検証しています。

以下のように、SpectronのApplicationクラスのインスタンスをapplyメソッドに渡すことで、spectron-fake-menuのセットアップは完了します。

const Application = require('spectron').Application;
const fakeMenu = require('spectron-fake-menu');
const app = new Application({ path: electron, args: [ path.join(__dirname, '.') ] });

fakeMenu.apply(app); // apply fake menu

その後は、fakeMenu.clickMenuメソッドでMenuのクリックイベントを発火させることができます。clickMenuメソッドの引数にはラベルの文字列を渡します。

const fakeMenu = require('spectron-fake-menu');
fakeMenu.clickMenu('Count', 'Increment'); // trigger 'Count -> Increment'
fakeMenu.clickMenu('Count', 'Decrement'); // trigger 'Count -> Decrement'

そもそもなぜSpectronからMenuの操作ができないのか

以下のIssueがあります。

Testing menu interaction · Issue #21 · electron/spectron · GitHub

require('electron').remote.Menu.getApplicationMenu() API does not serialize to JSON so it can't currently be fetched via Spectron.

MenuのAPIJSONにserializeされていないので、process間通信が必要なSpectronでは操作が難しいというのがその理由のようです。 同じ様に、dialogモジュールの操作も出来ません。

Can't interact with dialog · Issue #23 · electron/spectron · GitHub

アプリケーションの操作をChrome Driverを通じて行うのが基本理念のSpectronでは、これらのnativeなAPIを必要とする操作に対するサポートはまだ不十分、というのが実情です。

spectron-fake-menuではどうしているのか

Mocking electron modules · Issue #94 · electron/spectron · GitHub

のIssueで紹介されている方法なのですが、Electronは--requireオプションで、Electronの起動直前にスクリプトを差し込むことができます。

これを利用して、Mainプロセスにspectron-fake-menu用のイベントを受け取る口を仕込んでいます。

require('electron').ipcMain.on('SPECTRON_FAKE_MENU/SEND', (_e, labels) => {
  const item = findItem(require('electron').Menu.getApplicationMenu().items, labels);
  item.click();
});

あとは簡単で、clickMenuメソッドでそのイベントを中継してMainプロセスに通知すれば良いだけです。

function clickMenu(...labels) {
  _app.electron.ipcRenderer.send('SPECTRON_FAKE_MENU/SEND', labels);
}

実際、この一連の処理のコードは30行程度しかありません。

つらいところ: メニューの操作は非同期処理になる

Exampleのテストコードは以下のようになっています。

function waitForChangeCount(app, count) {
  return new Promise((resolve, _reject) => {
    // Since the click event is asynchronous processing in the native layer, wait for the count chang on DOM.
    // If the expected change does not occur, the test will fail with a timeout.
    const timer = setInterval(() => {
      app.client.getText('#count').then((text) => {
        if (text === count) {
          clearInterval(timer);
          resolve();
        }
      });
    }, 1000);
  });
}

desctibe('decrement', function() {
  // ...省略
  it('decrement count', () => {
    return app.client.waitForExist('#count')
      .then(() => {
        fakeMenu.clickMenu('Count', 'Decrement');
        return waitForChangeCount(app, '-1');
      })
      .then(() => assert.ok(true));
  });

waitForChangeCountメソッドの処理にとてもつらい感が現れています。 clickMenuメソッドでメニューのクリックを発火してから画面上にその変化が現れるまでが非同期なので、タイマーをセットしてDOMの変化を監視しています。

nativeなレイヤーでのclickイベントのやり取りがそもそも非同期なので、外部から処理を差し挟んで同期APIにするのが非常に難しいです。(どなたかアイディアあれば教えてください。)

dialogモジュールのテスト時のモックも同じ理屈でできる

File -> Saveをクリックしたらdialogを表示する、みたいな操作は結構あると思います。 用意されているAPIのみでテストを書くとすると、現状ではSpectronでこのケースのE2Eテストを書くのは難しいです。

しかし同じ理屈で、preloadスクリプト内でモックオブジェクトをセットしてしまえば、dialogモジュールの処理のモックも可能です。

例えば、以下の処理はdialogモジュールのshowSaveDialogメソッドを置き換えています。

const nativeRequire = require;
require = function(moduleName) {
  if (moduleName === 'electron') {
    const electron = nativeRequire('electron');
    electron.dialog.showSaveDialog = mockShowSaveDialog;
    return electron;
  }
  return nativeRequire(moduleName);
};

当然ですが、やればやるだけE2Eとは何だったのか...みたいなことになってどんどん信頼性が落ちていくので、用法用量は守りましょう。

まとめると

Rendererプロセスからの操作の方がテストは格段に楽です。

でもどうしてもMenuからの操作をテストしたい...という方は是非使ってみてください。

エンジニア立ち居振舞い: 発言にブレーキをかけない

お題「エンジニア立ち居振舞い」

僕自身が普段から意識的にやっているのは発言にブレーキをかけないことだ。

具体的にはslackのpublicな部屋で騒ぐ。 作業中にハマってしまっていかんともし難い時や、クソコードを見つけて怒りが湧いてきた時、などなど。

ハマってる内容を呟いておくと、以前にも同じような経験をした人が反応してくれて一瞬で解決に導かれるかもしれない。

クソコードに対して怒りをぶつけていると、同じように共感した人が現れてやっていきをお互い高められて、いい感じにタスク化できるかもしれない。クソコードは見ているだけでイライラしてくるので、吐き出すだけでも気持ちが宥められる効果がある。

時には実は騒いでいた対象はクソコードではなくて、僕の実力が足りていないがゆえにそう見えてしまっていただけのこともある。それはそれで良くて、一時僕は恥をかくかもしれないけど、引き換えによって得られる学びは比較にならない。

発言にブレーキをかけない、と言うと他人への配慮を欠く発言をしても良い、みたいな印象を受けてしまうかもしれないけど、それは全く違う。ここで大事なのは遠慮しないということと、publicな場で発言しにくい空気を作らないということだ。

人間のパターン認識力はめちゃめちゃ高度なので、内容だけ見ればある程度攻撃性を伴ってしまう発言でも、思いやりを含んだコンテキストや言葉尻であれば、受け手はそれを感じ取ることができる。 コードを憎んで人を憎まずというのと似ている。

という気持ちを持っていて、人は憎みたくないんだけど、悪いコードは悪いと言っていかないとお互いの成長は望めないと思う。

こういう自分の発言に反応があると嬉しいので、他の人の発言もなるべく反応するようにしている。なんでもかんでも反応していると作業時間がなくなってしまうので、なかなか線引きは難しい。自分がある程度近しいコンテキストを持てる内容ならば、という感じ。線引きを迷っているな??と自覚した場合には、ブレーキをかけずに発言するようにしている。分からなければ分からないと発言する。無視してしまうよりは良い。議論が盛り上がっていると、自然と人が集まってきて、そのうち有識者が現れるみたいなことも期待できる。

もしチームに入ったばかりの人が困惑しているようなら、詳しそうな人にメンションしたりもする。チームに入ったばかりの人は、誰がどこに詳しいかが分からないので、そういうフォローが大事だと思う。

こういうものはpublicな場でやることが大事だと思っていて、closedな場でやってしまうとせっかく得られたハマりポイントの知見やクソコードに対する怒りが共有されない。勿体無い。

とはいえTPOは大事で、サポートやセールスも含めた全体連絡用の部屋でクソコードの話題を振るのは違う。

いろいろなコンテキストごとに部屋があるとすごくやりやすくて、今の会社はslackの部屋を誰もが好きに作れるので恵まれてるなー、と思う。(部屋が大量に生まれすぎる問題もあるので、バランスは難しいですね。)

思いやりのあるマサカリを投げ合っていきましょう。

Frontend Meetup vol.1 で革命と秩序とSPAという発表をしてきた

FiNCさん主催のFrontend Meetupというイベントで革命と秩序とSPAという発表をしてきた。

connpass.com

有志の方がまとめを書いてくださっているので、そちらもぜひ。

www.chirashiura.com

qiita.com

僕の発表資料はこちら。

speakerdeck.com

主催のFiNCさん、ありがとうございました。

内容

フロントエンドのパラダイムシフトとfreeeの関わり方と、今のスタックであるReact + FluxUtils + Frowtypeについてのお話。 どちらかといえば後半のFlowtypeの適用についての方に力を入れてお話しした。

個人的な感覚もあるけど、Flowtypeはここ1年で大分扱いやすくなっている。当初はReactのAPIの型定義が揃っていなかったりしたけど、現状では型定義のないAPIは見当たらない。 意味のわからないエラーに遭遇することもほとんど無くなった。個人的な慣れもあるかもしれない。

なぜTypeScriptでなくてFlowtypeなのか、という点については単純にbabelでトランスパイルする環境がすでにあるので、それを刷新してまでTypeScriptに寄せるよりも導入が楽だからというのが一番大きい。 当初からデフォルトがnon-nullableであったとか、個人的な好みもあるけれど、TypeScriptにも2.0から--strictNullChecksが入ったし、そういう部分での優位性は失われつつある。

TypeScript 2.0 · TypeScript

Flowtypeの世界はあくまで型アノテーションであり、TypeScriptは言語であるという根本的な違いはあるけれど、少なくとも静的型を提供するという目的においては両者とも同じ方向を向いている。

資料でも触れているけど、僕はjsには絶対に型が必要だ!という強硬派ではない。だけどチーム開発においては型があることの利点が大きいと感じている。

Fluxなりのアーキテクチャによりレイヤーを設けられたフロントエンドの世界において、Flowtypeを導入するだけで型安全を手に入れられるのか、というとそんな訳はなくて、それなりに書き方を工夫する必要がある。(具体例は資料に書いています)

発表は試行錯誤した結果だけど、もっといい書き方もあるかもしれない。知見のある方と是非意見交換したい。よろしくお願いします。

宣伝

というわけで、freeeでは革命と秩序に積極的なフロントエンドエンジニアを鋭意募集しています。

jobs.jobvite.com

共に世界をぶっ壊して再構成して秩序を取り戻しましょう。よろしくお願いします。