How to control time for capybara / phantomjs tests - ruby-on-rails

I want to test that some deadlines are getting displayed to users correctly in different timezones and at different times of day. My tests are using capybara+rspec+phantomjs.
I am passing a block to Timecop.travel(datetime) and the code in the test within that block is getting the mocked datetime correctly, but it looks like PhantomJS / the mocked browser are not getting the mocked time.
Is there any known way to get PhantomJS to work with Timecop? Or other ways to mock out or manipulate time for testing purposes?
Here's a simple example to illustrate what I mean.
time_spec.rb:
it "should show the Time travel date" do
# current date is 2017-01-24
Date.today.should == Date.parse("2017-01-24")
Timecop.travel( Time.parse("2001-01-01 01:01") ) {
sign_in(user)
visit "/#{user.username}"
Date.today.should == Date.parse("2001-01-01")
page.should have_text("Today is 2001-01-01")
page.should have_text("Javascript says 2001-01-01")
}
end
user.html.erb:
<p>Today is <%= Time.now.iso8601 %></p>
<script>
var now = moment().format()
$('p').append("<p>Javascript says "+now+"</p>")
</script>
output of running the test:
Failures:
1) Dashboard should show the time travel date
Failure/Error: page.should have_text("Javascript says 2001-01-01")
expected to find text "Javascript says 2001-01-01" in
"Today is 2001-01-01T01:01:00-08:00 Javascript says 2017-01-24T12:36:02-08:00"
# ./spec/features/time_spec.rb:67:in `block (3 levels) in <top (required)>'
# /gems/ruby-2.2.0/gems/timecop-0.8.0/lib/timecop/timecop.rb:147:in `travel'
# /gems/ruby-2.2.0/gems/timecop-0.8.0/lib/timecop/timecop.rb:121:in `send_travel'
# /gems/ruby-2.2.0/gems/timecop-0.8.0/lib/timecop/timecop.rb:62:in `travel'
# ./spec/features/time_spec.rb:59:in `block (2 levels) in <top (required)>'

As you've discovered TimeCop only adjusts the servers time, but the browser uses system time. There are a number of JS libraries to allow for time faking, that all work by mocking the JS Date Class. One I have used successfully is Sinon.JS which I implemented in my _head.html.haml file by doing
- if defined?(Timecop) && Timecop.top_stack_item
= javascript_include_tag "testing/sinon-1.17.3.js"
- unix_millis = (Time.now.to_f * 1000.0).to_i
:javascript
sinon.useFakeTimers(#{unix_millis});
before requiring any other JS. This will set the browser time in any page rendered to whatever Timecop is set to.

Related

Capybara + Selenium-webdriver + RSpec file fixtures + SSR giving Net::ReadTimeout

I'm noticing a strange issue that I haven't been able to solve for a few days.
I have a Rails 5 API server with system tests using RSpec and Capybara + Selenium-webdriver driving headless Chrome.
I'm using Capybara.app_host = 'http://localhost:4200' to make the tests hit a separate development server which is running an Ember front-end. The Ember front-end looks at the user agent to know to then send requests to the Rails API test database.
All the tests run fine except for ones which use RSpec file fixtures.
Here's one spec that is failing:
describe 'the affiliate program', :vcr, type: :system do
fixtures :all
before do
Capybara.session_name = :affiliate
visit('/')
signup_and_verify_email(signup_intent: :seller)
visit_affiliate_settings
end
it 'can use the affiliate page' do
affiliate_token = page.text[/Your affiliate token is \b(.+?)\b/i, 1]
expect(affiliate_token).to be_present
# When a referral signs up.
Capybara.session_name = :referral
visit("?client=#{affiliate_token}")
signup_and_verify_email(signup_intent: :member)
refresh
# It can track the referral.
Capybara.session_name = :affiliate
refresh
expect(page).to have_selector('.referral-row', count: 1)
# When a referral makes a purchase.
Capybara.session_name = :referral
find('[href="/videos"]').click
find('.price-area .coin-usd-amount', match: :first).click
find('.cart-dropdown-body .checkout-button').click
find('.checkout-button').click
wait_for { find('.countdown-timer') }
order = Order.last
order.force_complete_payment!
Rake::Task['affiliate_referral:update_amounts_earned'].invoke
# It can track the earnings.
Capybara.session_name = :affiliate
refresh
amount = (order.price * AffiliateReferral::COMMISSION_PERCENTAGE).floor.to_f
amount_in_dom = find('.referral-amount-earned', match: :first).text.gsub(/[^\d\.]/, '').to_f * 100
expect(amount).to equal(amount_in_dom)
end
end
This will fail maybe 99% of the time. There is the odd case where it passes. I can get my test suite to eventually pass by running it on a loop for a day.
I ended up upgrading all versions to the latest (Node 10, latest Ember, latest Rails) but the issue persists.
I can post a sample repo that reproduces the issue later. I just wanted to get this posted in case anyone has encountered the issue.
Here's a typical stack trace when the timeout happens:
1.1) Failure/Error: page.evaluate_script('window.location.reload()')
Net::ReadTimeout:
Net::ReadTimeout
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/webmock-3.3.0/lib/webmock/http_lib_adapters/net_http.rb:97:in `block in request'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/webmock-3.3.0/lib/webmock/http_lib_adapters/net_http.rb:110:in `block in request'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/webmock-3.3.0/lib/webmock/http_lib_adapters/net_http.rb:109:in `request'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/selenium-webdriver-3.14.0/lib/selenium/webdriver/remote/http/default.rb:121:in `response_for'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/selenium-webdriver-3.14.0/lib/selenium/webdriver/remote/http/default.rb:76:in `request'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/selenium-webdriver-3.14.0/lib/selenium/webdriver/remote/http/common.rb:62:in `call'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/selenium-webdriver-3.14.0/lib/selenium/webdriver/remote/bridge.rb:164:in `execute'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/selenium-webdriver-3.14.0/lib/selenium/webdriver/remote/oss/bridge.rb:584:in `execute'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/selenium-webdriver-3.14.0/lib/selenium/webdriver/remote/oss/bridge.rb:267:in `execute_script'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/selenium-webdriver-3.14.0/lib/selenium/webdriver/common/driver.rb:211:in `execute_script'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/capybara-3.8.2/lib/capybara/selenium/driver.rb:84:in `execute_script'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/capybara-3.8.2/lib/capybara/selenium/driver.rb:88:in `evaluate_script'
# /home/mhluska/.rvm/gems/ruby-2.5.1/gems/capybara-3.8.2/lib/capybara/session.rb:575:in `evaluate_script'
# ./spec/support/selenium.rb:48:in `refresh'
# ./spec/support/pages.rb:70:in `signup_and_verify_email'
# ./spec/system/payment_spec.rb:43:in `block (3 levels) in <top (required)>'
I should point out it doesn't always happen with page.evaluate_script('window.location.reload()'). It can happen with something benign like visit('/').
Edit: I tried disabling Ember FastBoot (server-side rendering) using the DISABLE_FASTBOOT env variable and suddenly all tests pass. I'm thinking that somehow the RSpec fixtures are causing Ember FastBoot to not finish rendering in some cases. This certainly lines up with dropped connections I've occasionally seen in production logs.
I've been experimenting with the client code and it may be due to my use of FastBoot's deferRendering call.
Edit: I'm using the following versions:
ember-cli: 3.1.3
ember-data: 3.0.2
rails: 5.2.1
rspec: 3.8.0
capybara: 3.8.2
selenium-webdriver: 3.14.0
google chrome: 69.0.3497.100 (Official Build) (64-bit)
Edit: I'm using this somewhat flaky Node/Express library fastboot-app-server to do server-side rendering. I've discovered that it sometimes strips important response headers (Content-Type and Content-Encoding). I'm wondering if this is contributing to the issue.
Edit: I added a strict Content Security Policy to make sure there are no external requests running during the test suite that could be causing the Net::ReadTimeout.
I inspect the Chrome network tab at the point when it locks up and it seems to be loading nothing. Manually refreshing the browser allows the tests to pick up and continue running. How strange.
I've spent a couple weeks on this now and it may be time to give up on Selenium tests.
I upgraded to Chrome 70 and chromedriver 2.43. It didn't seem to make a difference.
I tried using the rspec-retry gem to force a refresh when the timeout occurs but the gem seems to fail to catch the timeout exception.
I've inspected the raw request to chromedriver where things hang. It looks like it's always POST http://127.0.0.1/session/<session id>/refresh. I tried refreshing in an alternate way: visit(page.current_path) which seems to fix things!
I finally got my test suite to pass by switching page.driver.browser.navigate.refresh to visit(page.current_path).
I know it's an ugly hack but it's the only thing I could find to get things working (see my various attempts in the question edits).
I looked at the request to chromedriver that was causing the timeouts each time: POST http://127.0.0.1/session/<session id>/refresh. I can only guess that it's some kind of issue with chromedriver. Perhaps incidentally, it only hangs when multiple chromedriver instances are active (which happens when multiple Capybara sessions are being used).
Edit: I needed to account for query params as well:
def refresh
query = URI.parse(page.current_url).query
path = page.current_path
path += "?#{query}" if query.present?
visit(path)
end
I tried just doing visit(page.current_url) but that was giving timeouts as well.

Stub browser time and time zone with Capybara

I have a JavaScript component (e.g. a date picker) that heavily depends on -
The current system time
The current system time zone
In Ruby and Capybara it's possible to stub any time with the help of libraries such as Timecop.
Is it also possible to stub these values in the headless browser that Capybara controls?
Thanks!
Edit: Here's an example of how Ruby is stubbed but Capybara's browser still uses the system time
before do
now = Time.zone.parse("Apr 15, 2018 12:00PM")
Timecop.freeze(now)
visit root_path
binding.pry
end
> Time.zone.now
=> Sun, 15 Apr 2018 12:00:00 UTC +00:00
> page.evaluate_script("new Date();")
=> "2018-03-27T04:15:44Z"
As you've discovered, Timecop only affects the time in the tests and application under test. The browser is run as a separate process and completely unaffected by Timecop. Because of that you need to stub/mock the time in the browser as well using one of many JS libraries designed to do that. The one I generally use is sinon - http://sinonjs.org/ - , which I conditionally install in the pages head using something like
- if defined?(Timecop) && Timecop.top_stack_item
= javascript_include_tag "sinon.js" # adjust based on where you're loading sinon from
- unix_millis = (Time.now.to_f * 1000.0).to_i
:javascript
sinon.useFakeTimers(#{unix_millis});
That should work in a haml template (adjust if using erb) and would install and mock the browsers time whenever a page is visited while Timecop is being used to mock the apps time.
I know that the question is a bit old, but we had the same request and found the following solution to work with rails 6:
context 'different timezones between back-end and front-end' do
it 'shows the event timestamp according to front-end timezone' do
# Arrange
previous_timezone = Time.zone
previous_timezone_env = ENV['TZ']
server_timezone = "Europe/Copenhagen"
browser_timezone = "America/Godthab"
Time.zone = server_timezone
ENV['TZ'] = browser_timezone
Capybara.using_session(browser_timezone) do
freeze_time do
# Act
# ... Code here
# Assert
server_clock = Time.zone.now.strftime('%H:%M')
client_clock = Time.zone.now.in_time_zone(browser_timezone).strftime('%H:%M')
expect(page).not_to have_content(server_clock)
expect(page).to have_content(client_clock)
end
end
# (restore)
Time.zone = previous_timezone
ENV['TZ'] = previous_timezone_env
end
end
This helps me for usage in pair with zonebie gem
RSpec.configure do |config|
config.before(:suite) do
ENV['TZ'] = Time.zone.tzinfo.name
# ...
end
end

"Errno::EMFILE: Too many open files" with local images on create

On create of an object in Rails, I want to automatically assign it a stock image from the assets directory which can be overwritten later by a user.
As a result, I execute the following private method upon creation of the object:
def save_stock_image
image_path = Dir.glob(<list-of-images-from-directory>).sample
File.open(image_path) do |file|
self.image = file
self.save!
end
end
However, after 6 RSpec tests, I begin to receive the following error:
Failure/Error: let(:object) { create(:object) }
Errno::EMFILE:
Too many open files - /tmp/16020130822-36578-q8j9v9.jpg
# ./app/models/object.rb:502:in `block in save_stock_image'
# ./app/models/object.rb:501:in `open'
# ./app/models/object.rb:501:in `save_stock_image'
# ./spec/controllers/object_controller_spec.rb:318:in `block (3 levels) in <top (required)>'
# ./spec/controllers/object_controller_spec.rb:344:in `block (4 levels) in <top (required)>'
The above error is on ~40 of 60 tests. I've looked at a few SO questions, as well as https://github.com/thoughtbot/paperclip/issues/1122 and https://github.com/thoughtbot/paperclip/issues/1000. The closest answer I could find was to ensure the file descriptor was closing. Before I used File.open in the block, I explicitly closed the file with file.close - this didn't work either.
Something obvious that I'm doing wrong? Is there a better way to accomplish what I'm trying to do?
UPDATE
It looks like it has something to do with the temporary files which Paperclip creates before they are uploaded to S3. Is there something with closing those tempfiles that I'm missing?
Just ran into this myself. It looks like the master branch has a fix. See my comments here:
https://github.com/thoughtbot/paperclip/issues/1326?source=cc
If this is a development/test environment and you want a quick resolution.
Try to identify resque process id, kill it and restart the resque server.
Additionally you may try the below
Redis.current.client.reconnect
$redis = Redis.current
Just ran into this and the latest code didn't help me. So, I delegated the work of closing those temp files to the OS by spawning a child process:
def save_stock_image
ActiveRecord::Base.connection.disconnect!
Proces.fork do
image_path = Dir.glob(<list-of-images-from-directory>).sample
File.open(image_path) do |file|
self.image = file
self.save!
end
end
Process.wait
ActiveRecord::Base.establish_connection
end
Also, consider placing a timeout on the Process.wait like suggested here: Waiting for Ruby child pid to exit

Rails + Capybara: clicking link with evaluate_script freezes webdriver

I run the following in my js: true request spec:
page.evaluate_script("$('#sign-up').click();")
That opens the modal successfully. However, the webdriver freezes at that point, regardless of what comes next in the spec. After a long pause, I get:
Failure/Error:
Timeout::Error:
Timeout::Error
# ./spec/requests/my_spec.rb:14:in `block (3 levels) in <top (required)>'
I want to use evaluate_script instead of 'click_on' in this case, because there is no href attribute on that particular link (click_on doesn't work). How do I get it to work without timing out?
It's due to a bug in Selenium. Found the answer here: https://groups.google.com/forum/?fromgroups=#!topic/ruby-capybara/YcZwyPdMJFU
It doesn't hang when replacing page.evaluate_script with:
page.driver.browser.execute_script

why do tests fail at the prompt and pass in the rails console?

well - to answer my own question (in retrospect) - 'watch your six'; meaning that settings, definitions and tiny 'patches' from the past may sneak up on you!
In my case an environment variable which helps me default-scope all my models, was the culprit!
This this gist documents me trying to prove that the exact same test passes in the rails console, when startet with
OX_ID=1 RAILS_ENV=test r c
that fails when run of the mill with rake spec, providing this output:
Running spec/models/user_spec.rb
....F
Failures:
1) User should have 1 role through user_group
Failure/Error: permission = Factory.create( :roleable, authorization: "all", ox: #ox, role: role )
ActiveRecord::RecordInvalid:
Validering fejlede: Ox skal udfyldes
# ./spec/factories/factories.rb:28:in `block (3 levels) in <top (required)>'
# ./spec/models/user_spec.rb:60:in `block (2 levels) in <top (required)>'
Finished in 2.01 seconds
5 examples, 1 failure
Failed examples:
rspec ./spec/models/user_spec.rb:56 # User should have 1 role through user_group
Being a total newcomer to Rspec testing, and not really that comfortable with the entire Rails Stack which BTW keeps getting taller and taller, I totally missed that my ENV["OX_ID"] on the command line, did not survive into the test environment (of cause, why/how should/could it?)
To take away from this
I finally added
y user.roles.to_sql
to my test (letting me see what kind of SQL Rails compiles) - and when I saw the output I did not believe my own eyes!
SELECT `roles`.* FROM `roles` INNER JOIN `roleables` ON `roles`.`id` = `roleables`.`role_id`
INNER JOIN `user_groups` ON `roleables`.`roleable_id` = `user_groups`.`id` AND `roleables`.`roleable_type`
= 'UserGroup' INNER JOIN `user_groups_users` ON `user_groups`.`id` = `user_groups_users`.`user_group_id`
WHERE `user_groups_users`.`user_id` = 3 AND (roles.ox_id=NULL) AND (roleables.ox_id=NULL)
AND (user_groups.ox_id=NULL)
The (roles.ox_id=NULL) gave it all away! I was expecting it to say roles.ox_id=1 and here it was!
You are running rspec/shoulda code in the console, in the development environment, where the libraries are not loaded. I'm not exactly sure how you would go about doing that, but if you wanted to do a quick test in the console, you can just print out:
user.user_groups[0].roles.count == 1
Keeping it simple may be better in this case. Hope that helps.

Resources