今回はタイトルどおり、Rspec、Capybaraを使ったfeature specでJavaScript側の時間を操作するhelperを作ってみたので考慮漏れとかあるかもですが書きます。
↓コードだけみたい人はこちら
rspecを使ったfeature specでjavascriptの時間を指定時間に固定するhelper · GitHub
作った背景
Railsで「公開して1時間以内の場合は記事にNEWのラベルを表示」する等の機能を検証するときにActiveSupport::Testing::TimeHelpers#travel_to
を使って下記のような感じで時間を止めて検証すると思います。
require 'rails_helper' RSpec.feature '記事一覧画面', type: :feature do describe '記事のリスト表示', js: true do before { create(:post, publish_at: Time.current.noon) } context '公開して1時間以内の場合' do bafore do trabel_to Time.current.noon visit posts_path end it 'NEWが表示されること' do expect(page).to have_content "NEW" end end end
単純なRailsだけの世界であれば、上記テストは問題ないのですがJavaScript側で現在時間をもとにNEWのラベルを表示している場合に昼の12時~13時以外の時間にテストが走ると落ちてしまいます・・・!
それはなぜかというとActiveSupport::Testing::TimeHelpers#travel_to
は、Railsの世界でDate.today
、DateTime.now
、Time.now
をスタブしているだけだからです。
rails/time_helpers.rb at b9ca94caea2ca6a6cc09abaffaad67b447134079 · rails/rails · GitHub
JavaScriptのnew Date()
やDate.now()
には何も影響を与えないので、travel_to
しても意味が無いんですね。。。
こういうケースに対応したいというのが作った背景です。
どうやって使うか
下記のようにrequire 'helpers/travel_to_javascript'
を実行後にtravel_to_javascript
が使用できるようになります。
require 'rails_helper' require 'helpers/travel_to_javascript' RSpec.feature '記事一覧画面', type: :feature do describe '記事のリスト表示', js: true do before { create(:post, publish_at: Time.current.noon) } context '公開して1時間以内の場合' do bafore do trabel_to Time.current.noon visit posts_path end it 'NEWが表示されること' do # travel_to_javascriptのブロック内は本日の12時で時間が固定される。 travel_to_javascript(page, Time.current.noon) do page.execute_script("console.error('now1:', new Date())") page.execute_script("console.error('now2:', Date.now())") pp page.driver.browser.manage.logs.get(:browser).map(&:message) #=> ["console-api 0:32 \"now1:\" Thu Nov 11 2010 12:00:00 GMT+0900 (日本標準時)", # "console-api 0:32 \"now2:\" 1289444400000"] expect(page).to have_content "NEW" end end end end
travel_to_javascript
にpage
と固定する時間のオブジェクトを引数に渡して実行すると、ブロックの中はJavaScript側の時間が固定されます。
どうやって時間を固定しているか
travel_to_javascript
のコードは下記のとおりです。
module TravelToJavascript def travel_to_javascript(page, datetime) page.execute_script time_stop_javascript(datetime) yield page.execute_script time_undo_javsctipt end
やっていることは、下記の3つです。
execute_script
を使ってDate
とDate.now
を引数で渡された日時を返すようにオーバーライド- ブロックで渡された処理を実行
- オーバーライドを戻す
日時生成処理のオーバーライドでは下記のようなJavaScriptを実行しています。もともとの処理で引数を与えている箇所についてはtravel_to_javascript
の引数で指定した日時ではなくJavaScript側でもともと指定されていた日時を返却するようにしています。
originDate = Date; Date = function (datetime) { if (datetime) { return new originDate(datetime); } else { return new originDate("2010-11-11T12:00:00+09:00"); } } ; Date.now = function (datetime) { if (datetime) { return new originDate(datetime).getTime(); } else { return new originDate("2010-11-11T12:00:00+09:00").getTime(); } } ;
originDate = Date;
を実行しているのは、「オーバーライドを戻す」をする際に下記のJavaScriptを実行するためです。
'Date = originDate;'
詳しいコードは下記を見てみてください。
rspecを使ったfeature specでjavascriptの時間を指定時間に固定するhelper · GitHub
出来ないこと
JavaScriptのあらゆるケースで時間を操作することは現状できなさそうです。下記のようなケースには対応できなさそう。。。
travel_to_javascript
の中でvisit等をしてページ跨ぎで時間を止めるようなことはページがリロードされたタイミングでJavaScriptもリロードされてしまうので出来ません。- ページ読み込み時にJavaScript側で判定するような場合には
execute_script
の実行前に判定が行われる可能性があるので効かなそうです。
ページ読み込み時にDate
等をオーバーライドするコードを一番先にロードするようなことができれば行けそうだけど、Capybaraのpageオブジェクト周りのコードを読むとどうにか出来たりするんだろうか。。。🤔
現状では。travel_to_javascript
に渡したブロック内でページのリロード等が行われなければ時間を固定するというだけのものです🙇♂️
おわりに
時間が関係するテストコードでRails以外が関わってくる場合に時間をどう扱うかって非常に難しいですよね・・・。 今回はJavaScript側の時間操作をサポートするようなヘルパーを作ってみましたが、CUIのツール等だと実行環境側の時間等、いろいろと考慮しないと他にもありそうです。
あとはRspecのhelperまわりの作り方とか、あんまりわかってなかったので勉強になりました。
今回作ったものが少しでも役に立てばなと思いますm( )m