Madogiwa Blog

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

E2EテストでJavaScript上の時間を固定するgem `travel_to_javascript`を作りました💎✨

JavaScriptの処理で時間に依存してたりする実装を行っているとE2Eのときに時間を固定したいときが稀によくあるのですが、 travel_toを使っても結局はサーバーサイド上の時刻しか固定出来ません😢

というわけでJavaScript上の時刻を固定するtravel_to_javascriptというgemを作ってみました💎

github.com

中身は前に記事にしたHelperです😅

madogiwa0124.hatenablog.com

使い方

まずは下記のような形でGemfileに追記してbundle installしてください📦

gem 'travel_to_javascript'

あとは任意のfeature spec内でrequire 'travel_to_javascript'を実行してinclude TravelToJavascriptしてください。

そうするとtravel_to_javascriptが使用出来るようになり、Capybara::Sessionのオブジェクトと固定したい時間を引数で渡すと、 block内のJavaScript上の現在時刻を引数で渡した時間で固定出来ます🕛

Rspecのサンプルは下記のような感じです。

require 'spec_helper'
require 'travel_to_javascript'

RSpec.describe 'SampleFeatureSpec', type: :feature do
  include TravelToJavascript

  it 'sample spec' do
   # NOTE: Use a JavaScript enabled driver.
    page = Capybara::Session.new(:headless_chrome, TestApp)
    travel_to_javascript(page, DateTime.parse('2000-01-01 1:11:11.111+9:00')) do
      page.execute_script('console.error(Date.now(), new Date())')
      pp page.driver.browser.manage.logs.get(:browser).map(&:message)
      # locks time by args in block.
      # => ["console-api 2:32 946656671111 Sat Jan 01 2000 01:11:11 GMT+0900"]
    end
    page.execute_script('console.error(Date.now(), new Date())')
    pp page.driver.browser.manage.logs.get(:browser).map(&:message)
    # restore time outside block.
    # => ["console-api 2:32 1586652460142 Sun Apr 12 2020 09:47:40 GMT+0900"]
  end
end

Minitestでも使えます。

require 'test_helper'
require 'travel_to_javascript'

class SampleFeatureTest < Minitest::Test
  include TravelToJavascript

  def test_sample
     # NOTE: Use a JavaScript enabled driver.
    page = Capybara::Session.new(:headless_chrome, TestApp)
    travel_to_javascript(page, DateTime.parse('2000-01-01 1:11:11.111+9:00')) do
      page.execute_script('console.error(Date.now(), new Date())')
      pp page.driver.browser.manage.logs.get(:browser).map(&:message)
      # locks time by args in block.
      # => ["console-api 2:32 946656671111 Sat Jan 01 2000 01:11:11 GMT+0900"]
    end
    page.execute_script('console.error(Date.now(), new Date())')
    pp page.driver.browser.manage.logs.get(:browser).map(&:message)
    # restore time outside block.
    # => ["console-api 2:32 1586652460142 Sun Apr 12 2020 09:47:40 GMT+0900"]
  end
end

仕組み

このGemの仕組みは渡された時間をiso8601形式に変換してJavaScript上のDateDate.nowを渡された時間を返すようにオーバーライドします。 ※このときにJavaScript上に引数を渡していた場合は固定せずに、そのままの時間を返すようにしています。

その後yieldでblockに渡された処理を実行して、時間を元に戻しています🕛

  def travel_to_javascript(page, datetime)
    page.execute_script time_stop_javascript(datetime)
    yield
    page.execute_script time_undo_javsctipt
  end

  def time_stop_javascript(rb_datetime)
    <<~JS
      originDate = Date;
      Date = #{time_stop_js_function_for_date(rb_datetime)};
      Date.now = #{time_stop_js_function_for_date_now(rb_datetime)};
    JS
  end

  def time_stop_js_function_for_date(rb_datetime)
    <<~JS
      function (datetime) {
        if (datetime) {
          return new originDate(datetime);
        } else {
          return new originDate("#{rb_datetime.iso8601(6)}");
        }
      }
    JS
  end

  def time_stop_js_function_for_date_now(rb_datetime)
    <<~JS
      function (datetime) {
        if (datetime) {
          return new originDate(datetime).getTime();
        } else {
          return new originDate("#{rb_datetime.iso8601(6)}").getTime();
        }
      }
    JS

travel_to_javascript/travel_to_javascript.rb at master · Madogiwa0124/travel_to_javascript · GitHub

出来ないこと

下記のような場合にはこのgemだと対応出来ません😢

  • travel_to_javascriptの中でvisit等をしてページ跨ぎで時間を止めるようなことはページがリロードされたタイミングでJavaScriptもリロードされてしまうので出来ません。

  • ページ読み込み時にJavaScript側で判定するような場合にはexecute_scriptの実行前に判定が行われる可能性があるので効かなそうです。

おわりに

今回は以前に作ったHelperをgemとして使いやすくしてみました💎対応出来るケースはちょっと限られているかもですが、JavaScript上で時間を固定したいときには使えるかもしれないです🙇‍♂️