Madogiwa Blog

主に技術系の学習メモに使っていきます。

Rspec、Capybaraを使ったfeature specでJavaScript側でも指定した時間で固定できるHelperを作った

今回はタイトルどおり、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.todayDateTime.nowTime.nowをスタブしているだけだからです。

rails/time_helpers.rb at b9ca94caea2ca6a6cc09abaffaad67b447134079 · rails/rails · GitHub

JavaScriptnew 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_javascriptpageと固定する時間のオブジェクトを引数に渡して実行すると、ブロックの中は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を使ってDateDate.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

参考

qiita.com

techracho.bpsinc.jp