-
-
Notifications
You must be signed in to change notification settings - Fork 1
HOWTO dev selenium_vs_phantomjs
We are choosing Selenium/Headless Chrome as webdriver used for rendering Capybara feature specs over PhantomJS/Poltergeist, due to a more precise rendering and fidelity when the results are compared with an actual browser.
Also, testing with Headless Chrome on Selenium has been possible for a while now, and this allows even for multiple batches of tests to be run at once.
To overcome Selenium's webdriver current inability to edit or mess with the requests headers, in some occasions we use a custom Rack Middleware that once inserted in the Rack app-stack is able to patch-in any headers (such as Authorization
) just before the driver takes in.
This allows us to emulate a more plenty range of browser behavior in all the feature specs.
- Using Selenium with Capybara
- Headless feature specs with Chrome using Selenium
- GitLab decides to move to Selenium/Headless Chrome for testing
- Selenium vs. PhantomJS (a little bit outdated, but still valid)
- PhantomJS
- Selenium
- Rack
PhantomJS with Poltergeist as Capybara Javascript webdriver has typically the advantage of being more versatile and offers, as example, the ability to edit/update/patch or retrieve easily the requests headers (which Selenium webdriver doesn't).
But, also, it has the drawback of being quite approximate during the rendering phase having several known bugs and issues that arise especially when using most of the current CSS/HTML/JavaScript techniques.
These rendering glitches may cause the inability for Capybara to find or click on elements due to the fact of being rendered as partially hidden or behind other DOM nodes.
PhantomJS used to be the de-facto way to go for automated testing because of it being the only way to do actual Headless browser testing. This has radically changed since Chrome version 65+, as well in all the other & latest Firefox versions.
Moreover, PhantomJS development has been suspended by its own author until further notice.
On the other side, Selenium offers excellent rendering but as a webdriver it doesn't allow request.headers
editing or retrieval by design choice.
(A questionable decision, but that is not going to change, according to it's authors.)
Although interacting with just "what is visible to the end-user" is a correct stand point, in several use-cases (*), having the ability "to mess with the request headers" is a plus even if it's just for a background operation preparing the actual use-case of the feature spec. So, it is sometimes indeed a limit from our point of view.
We've come around this issue by using a simple Rack Middleware to inject "what it is needed" into the request header, thus adopting Selenium for good.
(*) - (such as, for instance, injecting a valid JWT Authorization bearer)
To configure Selenium as webdriver for Capybara, do the following:
- Add to the
:test
group of yourGemfile
and update your bundle:
gem 'capybara'
gem 'selenium-webdriver' # support for Chrome
gem 'geckodriver-helper' # support for Firefox
gem 'webdrivers', '~> 4.0', require: false # tasks for drivers update
- Edit your Capybara configuration file (or create one, for example as
<APP_ROOT>/features/support/capybara.rb
) and add any driver customization you may need, like the ones below:
Capybara.register_driver(:headless_firefox) do |app|
browser_options = Selenium::WebDriver::Firefox::Options.new
browser_options.args << '--headless'
Capybara::Selenium::Driver.new( app, browser: :firefox, options: browser_options )
end
Capybara.register_driver(:headless_chrome) do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
chromeOptions: { args: %w[headless disable-gpu] }
)
Capybara::Selenium::Driver.new(app, browser: :chrome, desired_capabilities: capabilities)
end
# [Steve A.] (Selenium webdriver Firefox/headless currently has no real
# support for mobileEmulation, so no "headless_firefox_mobile" driver
# customization here.)
Capybara.register_driver(:headless_chrome_mobile) do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
chromeOptions: {
args: %w[headless disable-gpu],
mobileEmulation: { deviceName: "iPhone 6" }
}
)
Capybara::Selenium::Driver.new( app, browser: :chrome, desired_capabilities: capabilities )
end
# Select one driver that has to be used for the tests.
# Chose a symbol either from the defaults
# (see: https://www.rubydoc.info/gems/capybara#Using_Capybara_with_RSpec)
# or use one of the registered custom versions above:
JS_DRIVER = :headless_chrome
# JS_DRIVER = :headless_chrome_mobile
# JS_DRIVER = :headless_firefox
# ...
Capybara.default_driver = JS_DRIVER
Capybara.javascript_driver = JS_DRIVER
Capybara.current_driver = JS_DRIVER
By changing the value of Capybara.current_driver
it's possible to change the current driver even at runtime.
At design time, each defined symbol corresponds to a valid tag that can be used to change the current driver.
The example above has defined 3 tags: @headless_chrome
, @headless_chrome_mobile
& @headless_firefox
.
By using one of these tags before any feature scenario you can force-change the current driver used accordingly.
In order to be able to add/edit the request headers, do the following:
-
Copy & save the rack middleware below as
<APP_ROOT>/middleware/custom_header_patcher.rb
-
Add the following initializer as
<APP_ROOT>/config/initializers/custom_header_patcher.rb
:if Rails.env.test? # Insert the CustomHeaderPatcher on top of the Rack stack: Rails.application.config.middleware.insert_before( 0, CustomHeaderPatcher ) end
...And that is all.
The CustomHeaderPatcher
class below allows to add or replace any request.header
by setting the desired value as an ENV
variable just before or even during the feature spec execution.
The Middleware searches for any ENV
entries that start with the constant text string stored in CustomHeaderPatcher::ENV_KEY_PREFIX
.
If it finds any header with the prefix, it removes the prefix substituting it with HTTP_
while giving it as value the one stored in the ENV
entry.
This will add a HTTP_AUTHORIZATION
header to the request, using as value my_jwt_value
:
ENV["#{ CustomHeaderPatcher::ENV_KEY_PREFIX }Authorization"] = my_jwt_value
The ENV variable once found is not consumed by the Middleware: this patched header value will remain stored inside the ENV
binding as long as the context lives (typically just 1 scenario).
=begin
= CustomHeaderPatcher
- version: 1.0
- author: Steve A.
Custom (& dumb) Rack middleware that allows to
inject/patch any request header via ENV values.
=end
class CustomHeaderPatcher
# Each desired ENV key will have to be prefixed by this text
# in order to be considered as a processable Header to be
# patched into the list of current request Heaxers.
ENV_KEY_PREFIX = "HEADER_PATCH_".freeze unless defined?(ENV_KEY_PREFIX)
# Init the Rack middleware app
def initialize(app)
@app = app
end
# Perform the call thread-safely on a +dup+ instance
def call(env)
Rails.logger.debug("-" * 80)
Rails.logger.debug("*** CustomHeaderPatcher ***".center(80))
dup._call env
end
# Direct call (non-thread-safe)
def _call(env)
Rails.logger.debug("-" * 80)
# Check if there's any custom-patch header
# set inside the ENV variable:
if ENV && ENV.respond_to?(:keys)
possible_keys = ENV.keys.select { |key| key =~ /^#{ ENV_KEY_PREFIX }/ }
possible_headers = ENV.select { |key, _value| possible_keys.include?(key) }
# Extract actual header keys to be added
# & log the resulting Hash:
possible_headers.each do |key, value|
actual_key = "HTTP_#{ key.split(ENV_KEY_PREFIX)[1].upcase }"
Rails.logger.debug("Processing '#{key}' => '#{actual_key}'...")
# Patch the extracted header:
env[actual_key] = value
end
end
Rails.logger.debug("-" * 80)
# Pass-through call:
@status, @headers, @response = @app.call(env)
# Return the result:
[@status, @headers, @response]
end
end