Capybara


Configuration


Capybara.register_driver :selenium_chrome do |app|
  options = Selenium::WebDriver::Chrome::Options.new
  options.add_argument("--headless=new")
  Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
end

Capybara.javascript_driver = :selenium_chrome

The above shows how to configure the JavaScript feature tests to use a headless Google Chrome to automate the browser. Headless means that the web browser will be running in the background. Sometimes I use the term headed to refer to a web browser that has an open GUI, but I may have made that word up.

Example: debugging step definitions


# Saves a screenshot to tmp/capybara
Then "screenshot" do
  @i ||= 0
  @i += 1
  page.save_screenshot("screenshot#{@i}.png", full: true)
  puts "Page screenshot saved to screenshot#{@i}.png"
end

Then "show JavaScript console" do
  puts page.driver.browser.logs.get(:browser)
end

# rubocop:disable Lint/Debugger
Then "pry" do
  binding.pry
end
# rubocop:enable Lint/Debugger

This is some debugging code used to debug test code. I'm not a big believer in test code test code (code being used to test test code). The ||= is a null-coalescing assignment that assigns the instance variable here @i to 0 if @i is nil. It saves screenshots to anki_books/tmp/capybara.

This is just a random file I had in my development working directory for Anki Books in tmp/capybara called screenshot1.png. It is just showing a screenshot from when I was debugging a failing test (probably from weeks ago). It is considered by Git to be an ignored file (see .gitignore) so you will not get it if you clone the repo. I think that when I added the HTML attribute required to some <input> elements with type "text" that does client-side validation of the form. The test was failing because it was no longer possible to submit the form using a web browser (that's not made specifically for malicious actions). This is good because it prevents user frustrations and helps not congest the entire Internet (kids are trying to play video games, adults are trying to video call).


As an experiment to demonstrate the dependency on these debugging steps to Selenium WebDriver, I added Then screenshot to a feature test that had the @javascript tag, removed that tag, and executed the test:

$ bundle exec cucumber features/tests/books/dragging_basic_notes_between_articles.feature
Using the default profile...
# Anki Books, a note-taking app to organize knowledge,
# is licensed under the GNU Affero General Public License, version 3
# Copyright (C) 2023 Kyle Rego
Feature: Creating an article

  Background: Adding an article to a book                                                    # features/tests/books/dragging_basic_notes_between_articles.feature:7
    Given there is a user "test_user", email "test@example.com", and password "1234asdf!!!!" # features/step_definitions/user_steps.rb:16
    And the user "test_user" has a book called "My first book"                               # features/step_definitions/books_steps.rb:7
    Then screenshot                                                                          # features/step_definitions/debugging_steps.rb:8
      Capybara::Driver::Base#save_screenshot (Capybara::NotSupportedByDriverError)
      <internal:kernel>:90:in `tap'
      ./features/step_definitions/debugging_steps.rb:11:in `"screenshot"'
      features/tests/books/dragging_basic_notes_between_articles.feature:10:in `screenshot'
    And the book "My first book" has the article "test article 1"                            # features/step_definitions/article_steps.rb:10
    And the article "test article 1" has 2 basic notes                                       # features/step_definitions/article_steps.rb:15
    And the book "My first book" has the article "test article 2"                            # features/step_definitions/article_steps.rb:10
    And the article "test article 2" has 2 basic notes                                       # features/step_definitions/article_steps.rb:15
    And I am logged in as the user "test_user" with password: "1234asdf!!!!"                 # features/step_definitions/user_steps.rb:7
    And I click the "Books" link                                                             # features/step_definitions/shared/link_steps.rb:7
    And I click the last "My first book" link                                                # features/step_definitions/shared/link_steps.rb:12
    And I click the "My first book" link                                                     # features/step_definitions/shared/link_steps.rb:7

With :rack_test driver that does not attempt to execute JavaScript, capybara throws an exception called Capybara::NotSupportedByDriver.

Aside on Ruby constants


If the identifier in a Ruby program starts with a capital letter, it is a constant. Looking at Capybara::NotSupportedByDriver, Capybara is a module being used as a namespace for a class called NotSupportedByDriver. Modules and classes in Ruby are constants. It is common for a gem to namespace all of its API inside a module with a name closely related to the gem name. For example, the Anki Record gem uses a namespace AnkiRecord. If we do a quick git clone git@github.com:teamcapybara/capybara.git, cd capybara, and code . to open it in Visual Studio Code, we can see some related code in capybara.rb:

# (... means I omitted code)
module Capybara
  ...
  class NotSupportedByDriverError < CapybaraError; end
  ...
end

Example: a step definition that uses a regular expression


Regular expressions are pretty hard so I will try to sprinkle some examples throughout. I wouldn't spend too long trying to understand this if it makes no sense.

This step definition from capybara_steps.rb in capybara is similar to an example you will also find in Anki Books:

When(/^I visit the (?:root|home) page$/) do
  visit('/')
end

Regular expressions in Ruby can be created using the Regexp.new constructor and a few different literal syntaxes (/pat/ and %r{pat} are examples of Ruby regex literals). The ^ matches the beginning of the line, and $ matches the end of the line. This is different from using \A and \z which match the start and end of a Ruby string, respectively. The distinction could be important because $ could be exploited as an attack vector (not here though, this step definition that is just part of test code would never be an attack point on any app). The ?: in the parentheses has the effect of making the parentheses be only used for grouping, and not capturing. This just relates to how the parentheses has multiple purposes in regular expressions and here it's only being used to group this part where the | is saying it can match either root or home. This regular expression would match either of these strings: "I visit the root page" and "I visit the home page."

Article notes

What is the optional argument given to the RSpec it method to switch to the Capybara.javascript_driver (:selenium by default)?
Does the Capybara DSL vary with the specific browser and headless driver being used?
What is the fast, but limited driver that is the default used by Capybara and does not support JavaScript?
What is the line of code to make Capybara use :selenium as the default driver instead of :rack_test?
Previous Next