Protractor + WebDriver で楽天銀行と Gmail にログインして入出金明細を保存
上記のリポジトリに Pull Request を出させてもらった。それがこちら。
- (Gmail等に) IMAP でログインしてメールを監視
- 楽天銀行にログインしてワンタイムパスワードのメールを送信
- ワンタイムパスワードが書かれたメールが来るまで browser.wait() + protractor.promise.defer() *1 で待つ
- ワンタイムパスワードでログインして入出金明細を TSV ファイルとして保存 *2
みたいなことしてます。
正直 Protractor のデバッグが一番大変だった。 変な場所で expect() してて全然関係ないテストでコケたりしてた。 デバッグは正直勘で進めてしまったのでよくない。 ちゃんと詳細なログとか取れる方法があるかもっと調べればよかった。
Windows 対応
ついでに既存の npm run-scripts の Windows 対応もした。
本文終わり。以下は調べたことです。
実装中に調べたこと
参考にしたページ
- まだmechanizeで消耗してるの? WebDriverで銀行をスクレイピング(ProtractorとWebdriverIOを例に) - 詩と創作・思索のひろば
- Node.js の IMAP 用 ライブラリ inbox
iconv を Windows でビルドするための環境構築
- SE奮闘記: IMAPでGmailを監視して、新着メールがあったらPHPを実行
- node.js - NodeJS - Error installing with NPM - Stack Overflow
- Windows users are not happy. · Issue #629 · nodejs/node-gyp · GitHub
この2つのサイトがとっかかりとしてはとても助かったんだけど、一つまずい箇所がある。
それは {stream}.on('data', ...)
に来たバッファから文字列を検索してるけど、data
の時に渡されるデータは細切れなので、
iconv の変換時に不正なバイトとしてエラーになる可能性がある (なった)。
あと潜在的に検索文字列の途中で切れてたら検索に引っかからない。
なのでこのようにした。
stream.on("data", function(chunk) { body += chunk; }); stream.on("end", function() { body = converter.convert(body).toString(); // FIXME: body にはヘッダ部も含まれているため RFC822 に則ってちゃんとパースする? if (/ワンタイムキー[ ]*[::][ ]*([a-zA-Z0-9]+)/.test(body)) { var otKey = RegExp.$1; console.log('ワンタイムキーを本文から取得成功:' + otKey); deferred.fulfill(otKey); } else { console.log('ワンタイムキーを本文から取得失敗'); deferred.reject(); } });
- nodejs-guidelines/windows-environment.md at master · Microsoft/nodejs-guidelines · GitHub
- 上にリンクいっぱい書いたけど、結局公式のこのガイド通りにやったらいけた。
以下は init.cmd の Node 関連の内容。
rem # Node set PATH=C:\Program Files\nodejs;%USERPROFILE%\AppData\Roaming\npm;%PATH% rem # ================ Prerequisites for Node ================ rem # https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md rem # Visual Studio rem # set PATH=C:\Program Files\Microsoft SQL Server\110\Tools\Binn\;%PATH% set PATH=C:\Program Files (x86)\Windows Kits\8.1\Windows Performance Toolkit\;%USERPROFILE%\.dnx\bin;C:\Program Files\Microsoft DNX\Dnvm\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;%PATH% rem # Python 2.7 set PATH=C:\Python27;C:\Python27\Scripts;%PATH% rem # ================ Prerequisites for Node ================
browser.wait()
- How to use browser.wait() to wait for URL to change? · Issue #610 · angular/protractor · GitHub
- Protractor - end to end testing for AngularJS
- browser.wait() に Promise を返す関数を渡すと resolve() されるまで待ってくれる (Promise をそのまま渡してもいいのかな?)。
- Protractor blocking wait | Emmett Pickerel's
- protractor/toc.md at master · angular/protractor · GitHub
- Protractor - end to end testing for AngularJS, protractor/timeouts.md at master · angular/protractor · GitHub
- Jasmine, WebDriver, Protractor それぞれにタイムアウトがある
- Protractor - end to end testing for AngularJS
If your page does manual bootstrap Protractor will not be able to load your page using browser.get. Instead, use the base webdriver instance - browser.driver.get. This means that Protractor does not know when your page is fully loaded, and you may need to add a wait statement to make sure your tests avoid race conditions.
(2段階認証を使っている場合) アプリパスワードを生成
2段階認証を使っている場合はパスワードをそのまま入力したのではログインできない。 アプリパスワードというアプリ固有のパスワードを発行する必要がある。
node.js - Authentication issue with node imap and node mail lister - Stack Overflow
https://support.google.com/accounts/answer/6010255
この設定は、2 段階認証プロセスが有効になっているアカウントでは利用できません。こうしたアカウントで安全性の低いアプリにアクセスするには、アプリケーション固有のパスワードが必要です。 ヘルプ
発行されたパスワードをそのままアカウントのパスワードとして渡せば認証される。
人は天啓を得ないと一日以上に渡る自己学習をすることは難しい
気がする。
例えば「セキュリティのこと勉強しよう!」とか「数学を勉強しなおそう!」とか思って継続的に続くことってあんまりない。 この例は毎日の仕事や趣味とかでやってることと全く違うことだから尚更なんだけど。 だから数時間でできることを継続的にやれるようなスケジューリングとか試みを考えるべき、というまぁなんか当たり前の結論になった。 正直これまでの自分は割と意地になって頭でっかちに根詰めるタイプだったので違うやり方を考えたいなーと思っている。 ただプログラミングに関しては割とのんびりやってこれたので、まぁ興味の赴くままにやりたいことやればいいんじゃねってことになる (プログラミングって言うと範囲広いけど)。 もちろん仕事じゃないなら別にそれでいいんだけど、なんか他に自分をうまくコントロールできる頭の良さそうな方法ないかなーみたいな淡い期待を抱いてなんとなく考えたりしてみてる (この文章がすでに頭悪そうなことは置いておく)。
数時間でできるとはちょっと違うけど、なんとなく印象に残ってるのが誰かの記事で毎日1つのサイトを作るってやつだった。 単純なのでもいいので、必ず毎日作ると書いてあったけど、今考えると「それ残業とか緊急対応しない会社・仕事じゃないと難しいな…」という感想しかなかった。かなしい。 確か外国の翻訳記事だったので、なんとなく残業はない前提なんだろうな…とか。
まぁさすがに1日は無理でも1週間だったら土日に作れるので*1、やっぱスケジューリング大事だなー (とは分かってはいるけどノウハウがない…)。 色んな人に継続的に続けてる習慣を聞いてみたい。
*1:幸い今の現場ではまだ休日出勤はしてない
Angular で「Error: 10 $digest() iterations reached. Aborting!」というエラーが出る時の対処法
最近 Angular で調べたことを週末に記事にして上げるサイクルになりつつある。良い傾向。
あと説明する時につっかからずに説明できたりするので、自分で調べてまとめて人に教える重要さをひしひしと感じる… なので最近は「書かなくても分かるやろ」ってとこを手抜きせず、あえて言うなら冗長に書いたりしてる。
本題
Angular は監視(バインド)している変数の値が変更された場合*1、他の値が変わるか毎回ループを回してチェックする。これを digest loop と呼びます。
冒頭のエラーメッセージは digest loop を10回回したけど毎回値が変わっていて収束しなさそうだし、キリがないのでやめますという意味 (ざっくり)。
こんな場合に出る
- ランダム値を返している場合
- インスタンスが毎回違う場合
対処法
いくつかあります。
- 関数ではなく変数にして $watch() により変更を伝播させる。
- 違うインスタンスを返している場合は計算した結果を変数に保存してキャッシュしておき、キャッシュがあり再計算の必要がない場合はそれを返すなどして、同じインスタンスを返すようにする (バッドプラクティスなのでやるべきではない)。
- One-time binding を使う
良くある一例として、ng-repeat の in ...
の部分では関数を使うべきではありません (1 のリンクを参照)。
もちろん関数で前述したようにキャッシュして常に同じインスタンスを返すようにすればエラーは起こらなくなるが、関数の中で無理矢理キャッシュして解決するのではなく、計算(関数)の終わりに計算結果を変数(モデル)にセットして、Angular 側に再計算させ変更を伝播させるようにすべきです。
つまり関数はキャッシュなどの不必要な副作用を持つべきではありません。 不必要というのは、変更を伝播させるために必ず計算結果を変数(モデル)にセットする必要があるからです。
キャッシュを使ってもべき等性(何回実行しても同じ結果が返ってくること)が保たれるなら問題ありません。 しかしバグを引き起こしやすくなるのであまり使うべきではないです。 Angular アプリのパフォーマンスを改善したければ Chrome の Angular 用拡張機能を使ってボトルネックを見つけ出す方が正道です。
しかしランダム値を返すなど、どうしてもべき等性を仕様的に担保できない場合もあります。 そういう場合は3番の One-time binding が有効です。
要点を箇条書きにすると以下の通りです。
- 関数はべき等にする
- 変更の伝播は Angular にやらせる
- 関数がランダム値を返すなどしてべき等性を保てないなら上で挙げた対処法を試す
所感
React 使ってる人からしたら結論は当たり前だと思うだろうし Angular 1 こわいと思われそう… (だと思ってるけど実際どうなんだろう)
実際自分も Angular 1 の digest loop は度々悩まされてるので、digest loop の無いフレームワーク使えたらなぁと思う。 というかそもそも digest loop が双方向バインディングを実現する(ちょっと無理矢理な)方法なので、一方向にして Flux アーキテクチャを導入できたらとは思う。
ただ、React だけだと Flux の強制力はないので、またさらに他のフレームワークが必要となって、そうすると学習コストとかで敬遠される確率が高まる (実際は Angular のが面倒難しくても)。
あとフルスタックな技術は検索しやすい。
そういう意味でフルスタックという点では Java と同じく多いに助かってます。 業務寄りではないけど業務向けな感じ*2のフレームワークという印象。
参考リンク
- ポケモンのタイプ相性を確認できるウェブアプリケーションを作りました - 詩と創作・思索のひろば
- $watch() を使う場合のサンプルコード
- javascript - Angular - Error: 10 $digest() iterations reached. Aborting - Stack Overflow
- 関数で毎回ランダムな値を返しているせいで digest loop が終わらないケース
- angularjs - angular Infinite $digest Loop in ng-repeat - Stack Overflow
- AngularJSのOne-time Bindingを使ってパフォーマンス改善をしよう | 株式会社LIG
- One-time binding の説明